# Directory Structure ``` ├── .gitignore ├── Dockerfile ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── index.ts │ ├── mcp.ts │ ├── tools │ │ ├── braveSearch.ts │ │ ├── deepResearch.ts │ │ └── sequentialThinking.ts │ ├── types │ │ └── mcp-sdk.d.ts │ ├── types.ts │ ├── utils │ │ ├── analysis.ts │ │ ├── question.ts │ │ ├── search.ts │ │ └── synthesis.ts │ └── websocket │ └── server.ts ├── test-server.sh └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Dependencies node_modules/ # Build dist/ # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? # OS .DS_Store Thumbs.db # Thrash /downloads ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # OpenDeepSearch An open-source alternative to Perplexity Deep Research using the Model Context Protocol (MCP). ## Overview OpenDeepSearch is a powerful research tool that performs comprehensive, in-depth research on complex topics. It combines the structured thinking approach of Sequential Thinking with the search capabilities of Brave Search to provide detailed, well-sourced research reports. ## Features - **Comprehensive Research**: Breaks down complex questions into manageable sub-questions - **Iterative Search**: Performs multiple searches to gather diverse information - **Intelligent Analysis**: Analyzes search results to extract relevant information - **Synthesis**: Combines findings into a coherent, well-structured report - **Citations**: Includes sources for all information in the report - **MCP Integration**: Seamlessly integrates with Claude Desktop, Cursor, and other MCP clients - **WebSockets**: Supports integration with Smithery and other MCP clients - **Publication**: Allows publishing the research tool on the Smithery platform for easy access ## Installation ### Prerequisites - Node.js 16 or higher - A Brave Search API key (get one at [https://brave.com/search/api/](https://brave.com/search/api/)) ### NPM Installation ```bash npm install -g open-deep-research ``` ### Running with NPX ```bash BRAVE_API_KEY=your_api_key npx open-deep-research ``` ### Local Installation ```bash # Clone the repository git clone https://github.com/tositon/open-deep-research.git cd open-deep-research # Install dependencies npm install # Build the project npm run build # Run with Brave Search API BRAVE_API_KEY=your_api_key npm start ``` ### Installation via Smithery ```bash # Install for Claude npx @smithery/cli install open-deep-research --client claude # Install for Cursor npx @smithery/cli install open-deep-research --client cursor ``` When installing via Smithery, you will be prompted to enter a Brave Search API key. ## Usage ### With Claude Desktop Add the following to your Claude Desktop configuration: ```json { "mcpServers": { "open-deep-research": { "command": "npx", "args": [ "-y", "open-deep-research" ], "env": { "BRAVE_API_KEY": "your_api_key_here" } } } } ``` ### With Cursor In Cursor, you can add the MCP server with: ``` claude mcp add "open-deep-research" npx open-deep-research ``` Make sure to set the `BRAVE_API_KEY` environment variable before running Cursor. ### Example Queries - "What are the latest developments in quantum computing?" - "Compare and contrast different approaches to climate change mitigation" - "Explain the history and impact of the Renaissance on European art" - "What are the pros and cons of different renewable energy sources?" ## How It Works 1. **Question Analysis**: The system analyzes the main question and breaks it down into sub-questions 2. **Iterative Search**: For each sub-question, the system performs searches using Brave Search API 3. **Result Analysis**: The system analyzes the search results to extract relevant information 4. **Synthesis**: The system combines the findings into a coherent report 5. **Citation**: All information is properly cited with sources ## Development ### Setup ```bash git clone https://github.com/tositon/open-deep-research.git cd open-deep-research npm install ``` ### Build ```bash npm run build ``` ### Run in Development Mode ```bash BRAVE_API_KEY=your_api_key npm run dev ``` ## Testing ### Testing with MCP Inspector Для тестирования MCP сервера можно использовать MCP Inspector, который предоставляет удобный интерфейс для взаимодействия с инструментами: ```bash # Установка и запуск MCP Inspector npx @modelcontextprotocol/inspector # Запуск сервера в другом терминале BRAVE_API_KEY=your_api_key npm start ``` После запуска Inspector, откройте браузер и перейдите по адресу http://localhost:5173. Подключитесь к WebSocket серверу, используя URL `ws://localhost:3000`. ### Примеры запросов для тестирования инструментов В интерфейсе MCP Inspector вы можете выбрать инструмент и настроить параметры запроса: #### Тестирование Brave Web Search ```json { "query": "latest quantum computing advancements", "count": 5 } ``` #### Тестирование Sequential Thinking ```json { "thought": "Начинаю анализ проблемы глобального потепления", "thoughtNumber": 1, "totalThoughts": 5, "nextThoughtNeeded": true } ``` #### Тестирование Deep Research ```json { "query": "Сравнение различных источников возобновляемой энергии", "action": "start", "maxSubQuestions": 3 } ``` ### Testing with Claude or Cursor После установки сервера через Smithery или локально, вы можете использовать его с Claude Desktop или Cursor, выбрав соответствующий MCP сервер в настройках. ## Publishing on Smithery To publish the server on the Smithery platform: 1. Ensure the repository is hosted on GitHub and is public 2. Register on the [Smithery](https://smithery.ai/) platform 3. Authenticate via GitHub to connect with the repository 4. Go to the "Deployments" tab on the server page 5. Click the "Deploy on Smithery" button 6. Follow the deployment setup instructions After publishing, users can install the server using the Smithery CLI: ```bash npx @smithery/cli install open-deep-research --client claude ``` ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the MIT License - see the LICENSE file for details. ## Acknowledgments - Inspired by Perplexity Deep Research - Built on the Model Context Protocol - Uses Sequential Thinking approach for structured research - Powered by Brave Search API ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "NodeNext", "moduleResolution": "NodeNext", "esModuleInterop": true, "strict": true, "outDir": "dist", "declaration": true, "sourceMap": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /src/types/mcp-sdk.d.ts: -------------------------------------------------------------------------------- ```typescript declare module '@modelcontextprotocol/sdk/server/mcp.js' { export class McpServer { constructor(options: { name: string; version: string; description: string; }); tool( name: string, description: string, paramsSchema: any, handler: (params: any) => Promise<any> ): void; connect(transport: any): Promise<void>; } } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "open-deep-research", "version": "0.1.0", "description": "An open-source alternative to Perplexity Deep Research using MCP protocol", "main": "dist/index.js", "type": "module", "bin": { "open-deep-research": "dist/index.js" }, "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts", "lint": "eslint src --ext .ts", "format": "prettier --write \"src/**/*.ts\"", "prepublishOnly": "npm run build", "test": "./test-server.sh", "clean": "rm -rf dist", "rebuild": "npm run clean && npm run build" }, "keywords": [ "mcp", "deep-research", "perplexity", "ai", "search", "research", "cursor" ], "author": "", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.7.0", "axios": "^1.8.3", "chalk": "^5.4.1", "dotenv": "^16.4.7", "uuid": "^9.0.1", "ws": "^8.18.1" }, "devDependencies": { "@types/node": "^20.10.0", "@types/uuid": "^9.0.7", "@types/ws": "^8.18.0", "eslint": "^8.54.0", "prettier": "^3.1.0", "tsx": "^4.6.0", "typescript": "^5.3.2" }, "engines": { "node": ">=16.0.0" }, "repository": { "type": "git", "url": "https://github.com/tositon/OpenDeepSearch.git" }, "bugs": { "url": "https://github.com/tositon/OpenDeepSearch/issues" }, "homepage": "https://github.com/tositon/OpenDeepSearch#readme" } ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript /** * Types and interfaces for OpenDeepSearch */ // Type of research step export enum ResearchStepType { QUESTION_ANALYSIS = 'question_analysis', SEARCH = 'search', RESULT_ANALYSIS = 'result_analysis', SYNTHESIS = 'synthesis', FOLLOW_UP = 'follow_up' } // Status of research export enum ResearchStatus { PLANNING = 'planning', SEARCHING = 'searching', ANALYZING = 'analyzing', SYNTHESIZING = 'synthesizing', COMPLETED = 'completed' } // Data for one research step export interface ResearchStep { id: string; type: ResearchStepType; content: string; timestamp: number; metadata?: Record<string, any>; } // Sub-question export interface SubQuestion { id: string; question: string; status: 'pending' | 'in-progress' | 'completed'; searchResults?: SearchResult[]; analysis?: string; } // Search result export interface SearchResult { title: string; description: string; url: string; relevance?: number; // Relevance score from 0 to 1 } // Complete research data export interface ResearchData { id: string; question: string; subQuestions: SubQuestion[]; steps: ResearchStep[]; status: ResearchStatus; report?: string; startTime: number; endTime?: number; } // Options for the server export interface DeepResearchOptions { braveApiKey: string; maxSubQuestions?: number; // Maximum number of sub-questions maxSearchesPerQuestion?: number; // Maximum number of searches per sub-question maxTotalSteps?: number; // Maximum number of research steps timeout?: number; // Timeout in milliseconds } ``` -------------------------------------------------------------------------------- /src/mcp.ts: -------------------------------------------------------------------------------- ```typescript /** * Type definitions for Model Context Protocol (MCP) * Since we don't have access to the official SDK, we'll define our own types */ /** * MCP Tool Definition */ export interface MCPToolDefinition { name: string; description: string; parameters: { type: string; properties: Record<string, any>; required?: string[]; }; } /** * MCP Tool Response */ export interface MCPToolResponse { status: 'success' | 'error'; result?: any; error?: string; } /** * MCP Tool Interface */ export interface MCPTool { getDefinition(): MCPToolDefinition; execute(params: any): Promise<MCPToolResponse>; } /** * MCP Server Options */ export interface MCPServerOptions { name: string; version: string; description: string; } /** * MCP Server */ export class MCPServer { private options: MCPServerOptions; private tools: Map<string, MCPTool> = new Map(); constructor(options: MCPServerOptions) { this.options = options; } /** * Register a tool with the server * @param tool The tool to register */ registerTool(tool: MCPTool): void { const definition = tool.getDefinition(); this.tools.set(definition.name, tool); } /** * Start the MCP server * This is a simplified implementation that doesn't actually start a server * In a real implementation, this would start a WebSocket server */ async start(): Promise<void> { // In a real implementation, this would start a WebSocket server // For now, we'll just log that the server is starting console.log(`Starting MCP server: ${this.options.name} v${this.options.version}`); console.log(`Description: ${this.options.description}`); console.log(`Registered tools: ${Array.from(this.tools.keys()).join(', ')}`); } /** * Stop the MCP server */ async stop(): Promise<void> { // In a real implementation, this would stop the WebSocket server console.log(`Stopping MCP server: ${this.options.name}`); } } ``` -------------------------------------------------------------------------------- /src/utils/question.ts: -------------------------------------------------------------------------------- ```typescript /** * Utilities for working with questions */ // Если в будущем будут добавлены импорты, они должны включать расширение .js // import { SomeType } from '../types.js'; /** * Analyzes the main question and breaks it down into sub-questions * @param question The main question * @returns Array of sub-questions */ export async function analyzeQuestion(question: string): Promise<string[]> { // In a real implementation, this could use an LLM for question decomposition // For the prototype, we'll use a simple algorithm // Remove question marks and split by "and", "or", commas const cleanQuestion = question.replace(/\?/g, '').trim(); // Look for keywords that might indicate a compound question const conjunctions = ['and', 'or', 'versus', 'vs', 'compared to', 'differences between']; let hasConjunction = false; for (const conj of conjunctions) { if (cleanQuestion.toLowerCase().includes(conj)) { hasConjunction = true; break; } } // If the question contains conjunctions, break it down if (hasConjunction) { // Simple heuristic for breaking down the question // In a real implementation, this would be more sophisticated const parts = cleanQuestion.split(/\s+(?:and|or|versus|vs|compared to)\s+/i); if (parts.length > 1) { // Form sub-questions based on parts return parts.map(part => `${part.trim()}?`); } } // If we couldn't break it down by conjunctions, create sub-questions by key aspects // This is a simplified version, a real implementation would need a more complex algorithm const aspects = [ 'what is', 'how does', 'why is', 'when was', 'where is', 'definition', 'history', 'examples', 'advantages', 'disadvantages' ]; const subQuestions = []; // Add the main question subQuestions.push(question); // Add sub-questions by aspects const mainTopic = extractMainTopic(cleanQuestion); if (mainTopic) { // Add several sub-questions on different aspects subQuestions.push(`What is ${mainTopic}?`); subQuestions.push(`What are the key features of ${mainTopic}?`); subQuestions.push(`What are the applications of ${mainTopic}?`); } // Remove duplicates and return unique sub-questions return Array.from(new Set(subQuestions)); } /** * Extracts the main topic from a question * @param question The question * @returns The main topic or null if it couldn't be determined */ function extractMainTopic(question: string): string | null { // Remove question words at the beginning const withoutQuestionWords = question .replace(/^(what|who|when|where|why|how|is|are|do|does|did|can|could|would|should|will)\s+/i, '') .trim(); // If the question starts with "the", "a", "an", remove the article const withoutArticles = withoutQuestionWords .replace(/^(the|a|an)\s+/i, '') .trim(); // If there's anything left, return it as the main topic return withoutArticles.length > 0 ? withoutArticles : null; } ``` -------------------------------------------------------------------------------- /src/utils/search.ts: -------------------------------------------------------------------------------- ```typescript /** * Utilities for performing searches using Brave Search API */ import axios from 'axios'; import { SearchResult } from '../types.js'; /** * Performs a search using the Brave Search API * @param query The search query * @param apiKey The Brave Search API key * @param count The number of results to return (max 20) * @returns Array of search results */ export async function performSearch( query: string, apiKey: string, count: number = 10 ): Promise<SearchResult[]> { if (!query) { throw new Error('Search query is required'); } if (!apiKey) { throw new Error('Brave Search API key is required'); } // Limit count to maximum of 20 results const limitedCount = Math.min(count, 20); try { // Construct the API request URL const url = `https://api.search.brave.com/res/v1/web/search`; // Make the API request const response = await axios.get(url, { headers: { 'Accept': 'application/json', 'Accept-Encoding': 'gzip', 'X-Subscription-Token': apiKey }, params: { q: query, count: limitedCount } }); // Check if the response is valid if (!response.data || !response.data.web || !response.data.web.results) { return []; } // Parse the results const results: SearchResult[] = response.data.web.results.map((result: any) => { return { title: result.title || '', url: result.url || '', description: result.description || '', relevance: calculateRelevance(query, result.title, result.description) }; }); return results; } catch (error) { console.error('Error performing search:', error); throw new Error(`Failed to perform search: ${error instanceof Error ? error.message : String(error)}`); } } /** * Calculates the relevance score of a search result * @param query The search query * @param title The result title * @param description The result description * @returns A relevance score between 0 and 1 */ function calculateRelevance(query: string, title: string, description: string): number { // Normalize the query and result text const normalizedQuery = query.toLowerCase(); const normalizedTitle = title.toLowerCase(); const normalizedDescription = description.toLowerCase(); // Split the query into words const queryWords = normalizedQuery.split(/\s+/).filter(word => word.length > 2); // Count matches in title (weighted higher) let titleMatches = 0; for (const word of queryWords) { if (normalizedTitle.includes(word)) { titleMatches++; } } // Count matches in description let descriptionMatches = 0; for (const word of queryWords) { if (normalizedDescription.includes(word)) { descriptionMatches++; } } // Calculate relevance score (title matches weighted 3x) const maxPossibleScore = queryWords.length * 4; // 3 for title + 1 for description const actualScore = (titleMatches * 3) + descriptionMatches; // Return normalized score between 0 and 1 return maxPossibleScore > 0 ? actualScore / maxPossibleScore : 0; } ``` -------------------------------------------------------------------------------- /src/utils/analysis.ts: -------------------------------------------------------------------------------- ```typescript /** * Utilities for analyzing search results */ import { SearchResult } from '../types.js'; /** * Analyzes search results and extracts key information * @param results Array of search results * @param query The original search query * @returns Analysis report */ export async function analyzeResults( results: SearchResult[], query: string ): Promise<string> { if (!results || results.length === 0) { return `No results found for query: "${query}"`; } // Sort results by relevance const sortedResults = [...results].sort((a, b) => { const relevanceA = a.relevance ?? 0; const relevanceB = b.relevance ?? 0; return relevanceB - relevanceA; }); // Take the top 5 most relevant results const topResults = sortedResults.slice(0, 5); // Extract key information from each result const analysisPoints = topResults.map((result, index) => { const keyInfo = extractKeyInformation(result, query); return `${index + 1}. ${result.title}\n ${keyInfo}\n Source: ${result.url}`; }); // Format the analysis report const report = ` Analysis for query: "${query}" Key Information: ${analysisPoints.join('\n\n')} This analysis is based on the top ${topResults.length} most relevant results. `; return report; } /** * Extracts key information from a search result * @param result The search result * @param query The original search query * @returns Extracted key information */ function extractKeyInformation(result: SearchResult, query: string): string { // In a real implementation, this would use NLP techniques // For the prototype, we'll use a simple approach // Get the most relevant sentences from the description const relevantSentences = selectRelevantSentences(result.description, query, 2); return relevantSentences.join(' '); } /** * Selects the most relevant sentences from a text * @param text The text to analyze * @param query The search query * @param maxSentences Maximum number of sentences to return * @returns Array of relevant sentences */ function selectRelevantSentences(text: string, query: string, maxSentences: number): string[] { // Split text into sentences const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); if (sentences.length === 0) { return [text]; } // Calculate relevance score for each sentence const scoredSentences = sentences.map(sentence => { const score = calculateSentenceRelevance(sentence, query); return { sentence, score }; }); // Sort by relevance score scoredSentences.sort((a, b) => b.score - a.score); // Return the top N sentences return scoredSentences.slice(0, maxSentences).map(s => s.sentence.trim()); } /** * Calculates the relevance of a sentence to a query * @param sentence The sentence * @param query The query * @returns A relevance score */ function calculateSentenceRelevance(sentence: string, query: string): number { const normalizedSentence = sentence.toLowerCase(); const normalizedQuery = query.toLowerCase(); // Split query into words const queryWords = normalizedQuery.split(/\s+/).filter(word => !isStopWord(word) && word.length > 2); // Count how many query words appear in the sentence let matchCount = 0; for (const word of queryWords) { if (normalizedSentence.includes(word)) { matchCount++; } } // Calculate score based on match percentage and sentence length const matchPercentage = queryWords.length > 0 ? matchCount / queryWords.length : 0; const lengthFactor = 1 - Math.min(Math.abs(normalizedSentence.length - 100) / 100, 0.5); return matchPercentage * 0.7 + lengthFactor * 0.3; } /** * Checks if a word is a common stop word * @param word The word to check * @returns True if it's a stop word */ function isStopWord(word: string): boolean { const stopWords = [ 'a', 'an', 'the', 'and', 'or', 'but', 'if', 'because', 'as', 'what', 'which', 'this', 'that', 'these', 'those', 'then', 'just', 'so', 'than', 'such', 'both', 'through', 'about', 'for', 'is', 'of', 'while', 'during', 'to', 'from', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will', 'just', 'don', 'should', 'now' ]; return stopWords.includes(word.toLowerCase()); } ``` -------------------------------------------------------------------------------- /src/utils/synthesis.ts: -------------------------------------------------------------------------------- ```typescript /** * Utilities for synthesizing research results into a coherent report */ import { SubQuestion } from '../types.js'; /** * Synthesizes research results into a coherent report * @param mainQuestion The main research question * @param subQuestions Array of sub-questions with their analysis * @returns Synthesized research report */ export async function synthesizeReport( mainQuestion: string, subQuestions: SubQuestion[] ): Promise<string> { if (!subQuestions || subQuestions.length === 0) { return `No research data available for question: "${mainQuestion}"`; } // Filter completed sub-questions const completedSubQuestions = subQuestions.filter(sq => sq.status === 'completed' && sq.analysis); if (completedSubQuestions.length === 0) { return `Research is still in progress for question: "${mainQuestion}"`; } // Generate introduction const introduction = generateIntroduction(mainQuestion); // Generate sections for each sub-question const sections = completedSubQuestions.map(sq => { return generateSection(sq.question, sq.analysis || '', sq.searchResults || []); }); // Generate conclusion const conclusion = generateConclusion(mainQuestion, completedSubQuestions); // Generate sources section const sources = generateSources(completedSubQuestions); // Combine all parts into a complete report const report = ` # Research Report: ${mainQuestion} ## Introduction ${introduction} ${sections.join('\n\n')} ## Conclusion ${conclusion} ## Sources ${sources} `; return report; } /** * Generates the introduction section * @param mainQuestion The main research question * @returns Introduction text */ function generateIntroduction(mainQuestion: string): string { return `This report presents a comprehensive analysis of the question: "${mainQuestion}". The research was conducted using multiple sources and approaches to provide a thorough understanding of the topic.`; } /** * Generates a section for a sub-question * @param question The sub-question * @param analysis The analysis text * @param searchResults The search results * @returns Formatted section text */ function generateSection( question: string, analysis: string, searchResults: { title: string; url: string }[] ): string { // Extract a section title from the question const sectionTitle = question.replace(/\?/g, '').trim(); // Format the section return `## ${sectionTitle} ${analysis} ${generateCitations(analysis, searchResults)}`; } /** * Generates citation markers for the analysis text * @param analysis The analysis text * @param searchResults The search results * @returns Text with citation markers */ function generateCitations( analysis: string, searchResults: { title: string; url: string }[] ): string { // In a real implementation, this would use NLP to match text to sources // For the prototype, we'll use a simple approach if (!searchResults || searchResults.length === 0) { return ''; } // Create citation notes const citations = searchResults.map((result, index) => { return `[${index + 1}] ${result.title} - ${result.url}`; }); return `**Sources:**\n${citations.join('\n')}`; } /** * Generates the conclusion section * @param mainQuestion The main research question * @param subQuestions The sub-questions with analysis * @returns Conclusion text */ function generateConclusion( mainQuestion: string, subQuestions: SubQuestion[] ): string { return `This research has explored various aspects of ${extractMainTopic(mainQuestion)}. The findings from different sub-questions provide a comprehensive understanding of the topic. The research was based on ${subQuestions.length} sub-questions and utilized multiple sources to ensure accuracy and depth.`; } /** * Generates the sources section * @param subQuestions The sub-questions with search results * @returns Formatted sources text */ function generateSources(subQuestions: SubQuestion[]): string { // Collect all unique sources const allSources = new Map<string, { title: string; url: string }>(); subQuestions.forEach(sq => { if (sq.searchResults) { sq.searchResults.forEach(result => { if (!allSources.has(result.url)) { allSources.set(result.url, { title: result.title, url: result.url }); } }); } }); // Format the sources const sourcesList = Array.from(allSources.values()).map((source, index) => { return `${index + 1}. ${source.title} - ${source.url}`; }); return sourcesList.join('\n'); } /** * Extracts the main topic from a question * @param question The question * @returns The main topic */ function extractMainTopic(question: string): string { // Remove question words at the beginning const withoutQuestionWords = question .replace(/^(what|who|when|where|why|how|is|are|do|does|did|can|could|would|should|will)\s+/i, '') .trim(); // If the question starts with "the", "a", "an", remove the article const withoutArticles = withoutQuestionWords .replace(/^(the|a|an)\s+/i, '') .trim(); // If there's anything left, return it as the main topic return withoutArticles.length > 0 ? withoutArticles : question; } ``` -------------------------------------------------------------------------------- /src/tools/braveSearch.ts: -------------------------------------------------------------------------------- ```typescript /** * MCP tool for Brave Search */ import { MCPTool, MCPToolDefinition, MCPToolResponse } from '../mcp.js'; import { performSearch } from '../utils/search.js'; /** * Brave Web Search tool for MCP */ export class BraveWebSearchTool implements MCPTool { private apiKey: string; constructor(apiKey: string) { this.apiKey = apiKey; } /** * Get the tool definition */ getDefinition(): MCPToolDefinition { return { name: 'brave_web_search', description: 'Performs a web search using the Brave Search API, ideal for general queries, news, articles, and online content. Use this for broad information gathering, recent events, or when you need diverse web sources. Supports pagination, content filtering, and freshness controls. Maximum 20 results per request, with offset for pagination. ', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query (max 400 chars, 50 words)' }, count: { type: 'number', description: 'Number of results (1-20, default 10)', default: 10 }, offset: { type: 'number', description: 'Pagination offset (max 9, default 0)', default: 0 } }, required: ['query'] } }; } /** * Execute the tool * @param params Tool parameters * @returns Tool response */ async execute(params: any): Promise<MCPToolResponse> { try { // Validate parameters if (!params.query) { return { status: 'error', error: 'Search query is required' }; } // Limit query length const query = params.query.slice(0, 400); // Set count with default and limits const count = Math.min(Math.max(params.count || 10, 1), 20); // Set offset with default and limits const offset = Math.min(Math.max(params.offset || 0, 0), 9); // Perform the search const results = await performSearch(query, this.apiKey, count); // Format the response return { status: 'success', result: { query, count: results.length, offset, results: results.map(result => ({ title: result.title, description: result.description, url: result.url, relevance: result.relevance })) } }; } catch (error) { console.error('Error in Brave Web Search tool:', error); return { status: 'error', error: error instanceof Error ? error.message : String(error) }; } } } /** * Brave Local Search tool for MCP */ export class BraveLocalSearchTool implements MCPTool { private apiKey: string; constructor(apiKey: string) { this.apiKey = apiKey; } /** * Get the tool definition */ getDefinition(): MCPToolDefinition { return { name: 'brave_local_search', description: 'Searches for local businesses and places using Brave\'s Local Search API. Best for queries related to physical locations, businesses, restaurants, services, etc. Returns detailed information including:\n- Business names and addresses\n- Ratings and review counts\n- Phone numbers and opening hours\nUse this when the query implies \'near me\' or mentions specific locations. Automatically falls back to web search if no local results are found.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Local search query (e.g. \'pizza near Central Park\')' }, count: { type: 'number', description: 'Number of results (1-20, default 5)', default: 5 } }, required: ['query'] } }; } /** * Execute the tool * @param params Tool parameters * @returns Tool response */ async execute(params: any): Promise<MCPToolResponse> { try { // Validate parameters if (!params.query) { return { status: 'error', error: 'Search query is required' }; } // Limit query length const query = params.query.slice(0, 400); // Set count with default and limits const count = Math.min(Math.max(params.count || 5, 1), 20); // For now, we'll use the web search API since we don't have direct access to local search // In a real implementation, this would use a different endpoint const results = await performSearch(`${query} near me`, this.apiKey, count); // Format the response return { status: 'success', result: { query, count: results.length, results: results.map(result => ({ title: result.title, description: result.description, url: result.url, relevance: result.relevance })) } }; } catch (error) { console.error('Error in Brave Local Search tool:', error); return { status: 'error', error: error instanceof Error ? error.message : String(error) }; } } } ``` -------------------------------------------------------------------------------- /src/tools/sequentialThinking.ts: -------------------------------------------------------------------------------- ```typescript /** * MCP tool for Sequential Thinking */ import { MCPTool, MCPToolDefinition, MCPToolResponse } from '../mcp.js'; import { v4 as uuidv4 } from 'uuid'; /** * Sequential Thinking tool for MCP */ export class SequentialThinkingTool implements MCPTool { private thoughts: Map<string, any[]> = new Map(); /** * Get the tool definition */ getDefinition(): MCPToolDefinition { return { name: 'sequentialthinking', description: 'A detailed tool for dynamic and reflective problem-solving through thoughts.\nThis tool helps analyze problems through a flexible thinking process that can adapt and evolve.\nEach thought can build on, question, or revise previous insights as understanding deepens.\n\nWhen to use this tool:\n- Breaking down complex problems into steps\n- Planning and design with room for revision\n- Analysis that might need course correction\n- Problems where the full scope might not be clear initially\n- Problems that require a multi-step solution\n- Tasks that need to maintain context over multiple steps\n- Situations where irrelevant information needs to be filtered out\n\nKey features:\n- You can adjust total_thoughts up or down as you progress\n- You can question or revise previous thoughts\n- You can add more thoughts even after reaching what seemed like the end\n- You can express uncertainty and explore alternative approaches\n- Not every thought needs to build linearly - you can branch or backtrack\n- Generates a solution hypothesis\n- Verifies the hypothesis based on the Chain of Thought steps\n- Repeats the process until satisfied\n- Provides a correct answer\n\nParameters explained:\n- thought: Your current thinking step, which can include:\n* Regular analytical steps\n* Revisions of previous thoughts\n* Questions about previous decisions\n* Realizations about needing more analysis\n* Changes in approach\n* Hypothesis generation\n* Hypothesis verification\n- next_thought_needed: True if you need more thinking, even if at what seemed like the end\n- thought_number: Current number in sequence (can go beyond initial total if needed)\n- total_thoughts: Current estimate of thoughts needed (can be adjusted up/down)\n- is_revision: A boolean indicating if this thought revises previous thinking\n- revises_thought: If is_revision is true, which thought number is being reconsidered\n- branch_from_thought: If branching, which thought number is the branching point\n- branch_id: Identifier for the current branch (if any)\n- needs_more_thoughts: If reaching end but realizing more thoughts needed\n\nYou should:\n1. Start with an initial estimate of needed thoughts, but be ready to adjust\n2. Feel free to question or revise previous thoughts\n3. Don\'t hesitate to add more thoughts if needed, even at the "end"\n4. Express uncertainty when present\n5. Mark thoughts that revise previous thinking or branch into new paths\n6. Ignore information that is irrelevant to the current step\n7. Generate a solution hypothesis when appropriate\n8. Verify the hypothesis based on the Chain of Thought steps\n9. Repeat the process until satisfied with the solution\n10. Provide a single, ideally correct answer as the final output\n11. Only set next_thought_needed to false when truly done and a satisfactory answer is reached', parameters: { type: 'object', properties: { thought: { type: 'string', description: 'Your current thinking step' }, nextThoughtNeeded: { type: 'boolean', description: 'Whether another thought step is needed' }, thoughtNumber: { type: 'integer', description: 'Current thought number', minimum: 1 }, totalThoughts: { type: 'integer', description: 'Estimated total thoughts needed', minimum: 1 }, isRevision: { type: 'boolean', description: 'Whether this revises previous thinking' }, revisesThought: { type: 'integer', description: 'Which thought is being reconsidered', minimum: 1 }, branchFromThought: { type: 'integer', description: 'Branching point thought number', minimum: 1 }, branchId: { type: 'string', description: 'Branch identifier' }, needsMoreThoughts: { type: 'boolean', description: 'If more thoughts are needed' } }, required: ['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts'] } }; } /** * Execute the tool * @param params Tool parameters * @returns Tool response */ async execute(params: any): Promise<MCPToolResponse> { try { // Validate parameters if (!params.thought) { return { status: 'error', error: 'Thought content is required' }; } if (params.thoughtNumber === undefined || params.thoughtNumber < 1) { return { status: 'error', error: 'Valid thought number is required' }; } if (params.totalThoughts === undefined || params.totalThoughts < 1) { return { status: 'error', error: 'Valid total thoughts is required' }; } // Generate a session ID if this is the first thought let sessionId = ''; if (params.thoughtNumber === 1) { sessionId = uuidv4(); this.thoughts.set(sessionId, []); } else { // Find the session ID from previous thoughts for (const [id, thoughts] of this.thoughts.entries()) { if (thoughts.length > 0 && thoughts.some(t => t.thoughtNumber === params.thoughtNumber - 1)) { sessionId = id; break; } } if (!sessionId) { // If we can't find a previous session, create a new one sessionId = uuidv4(); this.thoughts.set(sessionId, []); } } // Store the thought const thought = { thoughtNumber: params.thoughtNumber, totalThoughts: params.totalThoughts, content: params.thought, nextThoughtNeeded: params.nextThoughtNeeded, isRevision: params.isRevision || false, revisesThought: params.revisesThought, branchFromThought: params.branchFromThought, branchId: params.branchId, needsMoreThoughts: params.needsMoreThoughts || false, timestamp: Date.now() }; const thoughts = this.thoughts.get(sessionId) || []; thoughts.push(thought); this.thoughts.set(sessionId, thoughts); // Format the response return { status: 'success', result: { sessionId, thoughtNumber: params.thoughtNumber, totalThoughts: params.totalThoughts, nextThoughtNeeded: params.nextThoughtNeeded, previousThoughts: thoughts .filter(t => t.thoughtNumber < params.thoughtNumber) .map(t => ({ thoughtNumber: t.thoughtNumber, content: t.content, isRevision: t.isRevision, revisesThought: t.revisesThought })) } }; } catch (error) { console.error('Error in Sequential Thinking tool:', error); return { status: 'error', error: error instanceof Error ? error.message : String(error) }; } } } ``` -------------------------------------------------------------------------------- /src/tools/deepResearch.ts: -------------------------------------------------------------------------------- ```typescript /** * MCP tool for Deep Research * Combines Sequential Thinking and Brave Search for comprehensive research */ import { MCPTool, MCPToolDefinition, MCPToolResponse } from '../mcp.js'; import { v4 as uuidv4 } from 'uuid'; import { analyzeQuestion } from '../utils/question.js'; import { performSearch } from '../utils/search.js'; import { analyzeResults } from '../utils/analysis.js'; import { synthesizeReport } from '../utils/synthesis.js'; import { ResearchData, ResearchStatus, ResearchStep, ResearchStepType, SubQuestion } from '../types.js'; /** * Deep Research tool for MCP */ export class DeepResearchTool implements MCPTool { private apiKey: string; private researches: Map<string, ResearchData> = new Map(); constructor(apiKey: string) { this.apiKey = apiKey; } /** * Get the tool definition */ getDefinition(): MCPToolDefinition { return { name: 'deep_research', description: 'Performs comprehensive, in-depth research on complex topics. Combines Sequential Thinking with Brave Search to provide detailed, well-sourced research reports. Ideal for academic research, complex questions, and topics requiring synthesis of multiple sources.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'The research question or topic' }, maxSubQuestions: { type: 'number', description: 'Maximum number of sub-questions to generate (default: 5)', default: 5 }, maxSearchesPerQuestion: { type: 'number', description: 'Maximum number of searches per sub-question (default: 2)', default: 2 }, researchId: { type: 'string', description: 'ID of an existing research to continue or retrieve' }, action: { type: 'string', description: 'Action to perform: "start", "status", "continue", or "report"', default: 'start' } }, required: ['query'] } }; } /** * Execute the tool * @param params Tool parameters * @returns Tool response */ async execute(params: any): Promise<MCPToolResponse> { try { // Validate parameters if (!params.query && !params.researchId) { return { status: 'error', error: 'Either query or researchId is required' }; } // Determine the action const action = params.action || 'start'; // Handle different actions switch (action) { case 'start': return this.startResearch(params); case 'status': return this.getResearchStatus(params); case 'continue': return this.continueResearch(params); case 'report': return this.generateReport(params); default: return { status: 'error', error: `Invalid action: ${action}` }; } } catch (error) { console.error('Error in Deep Research tool:', error); return { status: 'error', error: error instanceof Error ? error.message : String(error) }; } } /** * Start a new research * @param params Tool parameters * @returns Tool response */ private async startResearch(params: any): Promise<MCPToolResponse> { // Generate a research ID const researchId = uuidv4(); // Create a new research data object const research: ResearchData = { id: researchId, question: params.query, subQuestions: [], steps: [], status: ResearchStatus.PLANNING, startTime: Date.now() }; // Store the research this.researches.set(researchId, research); // Add the first step - question analysis const step: ResearchStep = { id: uuidv4(), type: ResearchStepType.QUESTION_ANALYSIS, content: `Analyzing question: "${params.query}"`, timestamp: Date.now() }; research.steps.push(step); // Analyze the question to generate sub-questions const subQuestions = await analyzeQuestion(params.query); // Limit the number of sub-questions const maxSubQuestions = Math.min(params.maxSubQuestions || 5, 10); const limitedSubQuestions = subQuestions.slice(0, maxSubQuestions); // Create sub-question objects research.subQuestions = limitedSubQuestions.map(question => ({ id: uuidv4(), question, status: 'pending' })); // Update the step with the sub-questions step.content = `Analyzed question: "${params.query}"\nGenerated ${research.subQuestions.length} sub-questions:\n${research.subQuestions.map(sq => `- ${sq.question}`).join('\n')}`; // Update the research status research.status = ResearchStatus.SEARCHING; // Return the response return { status: 'success', result: { researchId, question: params.query, subQuestions: research.subQuestions.map(sq => sq.question), status: research.status } }; } /** * Get the status of a research * @param params Tool parameters * @returns Tool response */ private getResearchStatus(params: any): Promise<MCPToolResponse> { // Get the research ID const researchId = params.researchId; // Check if the research exists if (!researchId || !this.researches.has(researchId)) { return Promise.resolve({ status: 'error', error: `Research not found: ${researchId}` }); } // Get the research const research = this.researches.get(researchId)!; // Return the status return Promise.resolve({ status: 'success', result: { researchId, question: research.question, status: research.status, subQuestions: research.subQuestions.map(sq => ({ question: sq.question, status: sq.status })), steps: research.steps.length, startTime: research.startTime, endTime: research.endTime } }); } /** * Continue a research * @param params Tool parameters * @returns Tool response */ private async continueResearch(params: any): Promise<MCPToolResponse> { // Get the research ID const researchId = params.researchId; // Check if the research exists if (!researchId || !this.researches.has(researchId)) { return { status: 'error', error: `Research not found: ${researchId}` }; } // Get the research const research = this.researches.get(researchId)!; // Check if the research is already completed if (research.status === ResearchStatus.COMPLETED) { return { status: 'success', result: { researchId, question: research.question, status: research.status, message: 'Research is already completed' } }; } // Find the next pending sub-question const nextSubQuestion = research.subQuestions.find(sq => sq.status === 'pending'); // If there are no more pending sub-questions, synthesize the results if (!nextSubQuestion) { return this.synthesizeResults(research); } // Mark the sub-question as in-progress nextSubQuestion.status = 'in-progress'; // Add a search step const searchStep: ResearchStep = { id: uuidv4(), type: ResearchStepType.SEARCH, content: `Searching for: "${nextSubQuestion.question}"`, timestamp: Date.now() }; research.steps.push(searchStep); // Perform the search const searchResults = await performSearch(nextSubQuestion.question, this.apiKey, 10); // Store the search results nextSubQuestion.searchResults = searchResults; // Update the search step searchStep.content = `Searched for: "${nextSubQuestion.question}"\nFound ${searchResults.length} results`; // Add an analysis step const analysisStep: ResearchStep = { id: uuidv4(), type: ResearchStepType.RESULT_ANALYSIS, content: `Analyzing results for: "${nextSubQuestion.question}"`, timestamp: Date.now() }; research.steps.push(analysisStep); // Analyze the results const analysis = await analyzeResults(searchResults, nextSubQuestion.question); // Store the analysis nextSubQuestion.analysis = analysis; // Update the analysis step analysisStep.content = `Analyzed results for: "${nextSubQuestion.question}"`; // Mark the sub-question as completed nextSubQuestion.status = 'completed'; // Return the response return { status: 'success', result: { researchId, question: research.question, subQuestionCompleted: nextSubQuestion.question, remainingSubQuestions: research.subQuestions.filter(sq => sq.status === 'pending').length, status: research.status } }; } /** * Synthesize the results of a research * @param research The research data * @returns Tool response */ private async synthesizeResults(research: ResearchData): Promise<MCPToolResponse> { // Add a synthesis step const synthesisStep: ResearchStep = { id: uuidv4(), type: ResearchStepType.SYNTHESIS, content: 'Synthesizing research results', timestamp: Date.now() }; research.steps.push(synthesisStep); // Update the research status research.status = ResearchStatus.SYNTHESIZING; // Synthesize the report const report = await synthesizeReport(research.question, research.subQuestions); // Store the report research.report = report; // Update the synthesis step synthesisStep.content = 'Synthesized research results'; // Update the research status and end time research.status = ResearchStatus.COMPLETED; research.endTime = Date.now(); // Return the response return { status: 'success', result: { researchId: research.id, question: research.question, status: research.status, message: 'Research completed', reportPreview: report.substring(0, 500) + '...' } }; } /** * Generate a report for a research * @param params Tool parameters * @returns Tool response */ private generateReport(params: any): Promise<MCPToolResponse> { // Get the research ID const researchId = params.researchId; // Check if the research exists if (!researchId || !this.researches.has(researchId)) { return Promise.resolve({ status: 'error', error: `Research not found: ${researchId}` }); } // Get the research const research = this.researches.get(researchId)!; // Check if the research is completed if (research.status !== ResearchStatus.COMPLETED) { return Promise.resolve({ status: 'error', error: 'Research is not yet completed' }); } // Return the report return Promise.resolve({ status: 'success', result: { researchId, question: research.question, report: research.report, subQuestions: research.subQuestions.length, steps: research.steps.length, startTime: research.startTime, endTime: research.endTime, duration: (research.endTime! - research.startTime) / 1000 } }); } } ```