# 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: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build 5 | dist/ 6 | 7 | # Environment variables 8 | .env 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # OS 27 | .DS_Store 28 | Thumbs.db 29 | 30 | # Thrash 31 | /downloads ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # OpenDeepSearch 2 | 3 | An open-source alternative to Perplexity Deep Research using the Model Context Protocol (MCP). 4 | 5 | ## Overview 6 | 7 | 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. 8 | 9 | ## Features 10 | 11 | - **Comprehensive Research**: Breaks down complex questions into manageable sub-questions 12 | - **Iterative Search**: Performs multiple searches to gather diverse information 13 | - **Intelligent Analysis**: Analyzes search results to extract relevant information 14 | - **Synthesis**: Combines findings into a coherent, well-structured report 15 | - **Citations**: Includes sources for all information in the report 16 | - **MCP Integration**: Seamlessly integrates with Claude Desktop, Cursor, and other MCP clients 17 | - **WebSockets**: Supports integration with Smithery and other MCP clients 18 | - **Publication**: Allows publishing the research tool on the Smithery platform for easy access 19 | 20 | ## Installation 21 | 22 | ### Prerequisites 23 | 24 | - Node.js 16 or higher 25 | - A Brave Search API key (get one at [https://brave.com/search/api/](https://brave.com/search/api/)) 26 | 27 | ### NPM Installation 28 | 29 | ```bash 30 | npm install -g open-deep-research 31 | ``` 32 | 33 | ### Running with NPX 34 | 35 | ```bash 36 | BRAVE_API_KEY=your_api_key npx open-deep-research 37 | ``` 38 | 39 | ### Local Installation 40 | 41 | ```bash 42 | # Clone the repository 43 | git clone https://github.com/tositon/open-deep-research.git 44 | cd open-deep-research 45 | 46 | # Install dependencies 47 | npm install 48 | 49 | # Build the project 50 | npm run build 51 | 52 | # Run with Brave Search API 53 | BRAVE_API_KEY=your_api_key npm start 54 | ``` 55 | 56 | ### Installation via Smithery 57 | 58 | ```bash 59 | # Install for Claude 60 | npx @smithery/cli install open-deep-research --client claude 61 | 62 | # Install for Cursor 63 | npx @smithery/cli install open-deep-research --client cursor 64 | ``` 65 | 66 | When installing via Smithery, you will be prompted to enter a Brave Search API key. 67 | 68 | ## Usage 69 | 70 | ### With Claude Desktop 71 | 72 | Add the following to your Claude Desktop configuration: 73 | 74 | ```json 75 | { 76 | "mcpServers": { 77 | "open-deep-research": { 78 | "command": "npx", 79 | "args": [ 80 | "-y", 81 | "open-deep-research" 82 | ], 83 | "env": { 84 | "BRAVE_API_KEY": "your_api_key_here" 85 | } 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | ### With Cursor 92 | 93 | In Cursor, you can add the MCP server with: 94 | 95 | ``` 96 | claude mcp add "open-deep-research" npx open-deep-research 97 | ``` 98 | 99 | Make sure to set the `BRAVE_API_KEY` environment variable before running Cursor. 100 | 101 | ### Example Queries 102 | 103 | - "What are the latest developments in quantum computing?" 104 | - "Compare and contrast different approaches to climate change mitigation" 105 | - "Explain the history and impact of the Renaissance on European art" 106 | - "What are the pros and cons of different renewable energy sources?" 107 | 108 | ## How It Works 109 | 110 | 1. **Question Analysis**: The system analyzes the main question and breaks it down into sub-questions 111 | 2. **Iterative Search**: For each sub-question, the system performs searches using Brave Search API 112 | 3. **Result Analysis**: The system analyzes the search results to extract relevant information 113 | 4. **Synthesis**: The system combines the findings into a coherent report 114 | 5. **Citation**: All information is properly cited with sources 115 | 116 | ## Development 117 | 118 | ### Setup 119 | 120 | ```bash 121 | git clone https://github.com/tositon/open-deep-research.git 122 | cd open-deep-research 123 | npm install 124 | ``` 125 | 126 | ### Build 127 | 128 | ```bash 129 | npm run build 130 | ``` 131 | 132 | ### Run in Development Mode 133 | 134 | ```bash 135 | BRAVE_API_KEY=your_api_key npm run dev 136 | ``` 137 | 138 | ## Testing 139 | 140 | ### Testing with MCP Inspector 141 | 142 | Для тестирования MCP сервера можно использовать MCP Inspector, который предоставляет удобный интерфейс для взаимодействия с инструментами: 143 | 144 | ```bash 145 | # Установка и запуск MCP Inspector 146 | npx @modelcontextprotocol/inspector 147 | 148 | # Запуск сервера в другом терминале 149 | BRAVE_API_KEY=your_api_key npm start 150 | ``` 151 | 152 | После запуска Inspector, откройте браузер и перейдите по адресу http://localhost:5173. Подключитесь к WebSocket серверу, используя URL `ws://localhost:3000`. 153 | 154 | ### Примеры запросов для тестирования инструментов 155 | 156 | В интерфейсе MCP Inspector вы можете выбрать инструмент и настроить параметры запроса: 157 | 158 | #### Тестирование Brave Web Search 159 | 160 | ```json 161 | { 162 | "query": "latest quantum computing advancements", 163 | "count": 5 164 | } 165 | ``` 166 | 167 | #### Тестирование Sequential Thinking 168 | 169 | ```json 170 | { 171 | "thought": "Начинаю анализ проблемы глобального потепления", 172 | "thoughtNumber": 1, 173 | "totalThoughts": 5, 174 | "nextThoughtNeeded": true 175 | } 176 | ``` 177 | 178 | #### Тестирование Deep Research 179 | 180 | ```json 181 | { 182 | "query": "Сравнение различных источников возобновляемой энергии", 183 | "action": "start", 184 | "maxSubQuestions": 3 185 | } 186 | ``` 187 | 188 | ### Testing with Claude or Cursor 189 | 190 | После установки сервера через Smithery или локально, вы можете использовать его с Claude Desktop или Cursor, выбрав соответствующий MCP сервер в настройках. 191 | 192 | ## Publishing on Smithery 193 | 194 | To publish the server on the Smithery platform: 195 | 196 | 1. Ensure the repository is hosted on GitHub and is public 197 | 2. Register on the [Smithery](https://smithery.ai/) platform 198 | 3. Authenticate via GitHub to connect with the repository 199 | 4. Go to the "Deployments" tab on the server page 200 | 5. Click the "Deploy on Smithery" button 201 | 6. Follow the deployment setup instructions 202 | 203 | After publishing, users can install the server using the Smithery CLI: 204 | 205 | ```bash 206 | npx @smithery/cli install open-deep-research --client claude 207 | ``` 208 | 209 | ## Contributing 210 | 211 | Contributions are welcome! Please feel free to submit a Pull Request. 212 | 213 | ## License 214 | 215 | This project is licensed under the MIT License - see the LICENSE file for details. 216 | 217 | ## Acknowledgments 218 | 219 | - Inspired by Perplexity Deep Research 220 | - Built on the Model Context Protocol 221 | - Uses Sequential Thinking approach for structured research 222 | - Powered by Brave Search API ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "strict": true, 8 | "outDir": "dist", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist"] 16 | } ``` -------------------------------------------------------------------------------- /src/types/mcp-sdk.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | declare module '@modelcontextprotocol/sdk/server/mcp.js' { 2 | export class McpServer { 3 | constructor(options: { 4 | name: string; 5 | version: string; 6 | description: string; 7 | }); 8 | 9 | tool( 10 | name: string, 11 | description: string, 12 | paramsSchema: any, 13 | handler: (params: any) => Promise<any> 14 | ): void; 15 | 16 | connect(transport: any): Promise<void>; 17 | } 18 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "open-deep-research", 3 | "version": "0.1.0", 4 | "description": "An open-source alternative to Perplexity Deep Research using MCP protocol", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "open-deep-research": "dist/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "start": "node dist/index.js", 13 | "dev": "tsx src/index.ts", 14 | "lint": "eslint src --ext .ts", 15 | "format": "prettier --write \"src/**/*.ts\"", 16 | "prepublishOnly": "npm run build", 17 | "test": "./test-server.sh", 18 | "clean": "rm -rf dist", 19 | "rebuild": "npm run clean && npm run build" 20 | }, 21 | "keywords": [ 22 | "mcp", 23 | "deep-research", 24 | "perplexity", 25 | "ai", 26 | "search", 27 | "research", 28 | "cursor" 29 | ], 30 | "author": "", 31 | "license": "MIT", 32 | "dependencies": { 33 | "@modelcontextprotocol/sdk": "^1.7.0", 34 | "axios": "^1.8.3", 35 | "chalk": "^5.4.1", 36 | "dotenv": "^16.4.7", 37 | "uuid": "^9.0.1", 38 | "ws": "^8.18.1" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^20.10.0", 42 | "@types/uuid": "^9.0.7", 43 | "@types/ws": "^8.18.0", 44 | "eslint": "^8.54.0", 45 | "prettier": "^3.1.0", 46 | "tsx": "^4.6.0", 47 | "typescript": "^5.3.2" 48 | }, 49 | "engines": { 50 | "node": ">=16.0.0" 51 | }, 52 | "repository": { 53 | "type": "git", 54 | "url": "https://github.com/tositon/OpenDeepSearch.git" 55 | }, 56 | "bugs": { 57 | "url": "https://github.com/tositon/OpenDeepSearch/issues" 58 | }, 59 | "homepage": "https://github.com/tositon/OpenDeepSearch#readme" 60 | } 61 | ``` -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Types and interfaces for OpenDeepSearch 3 | */ 4 | 5 | // Type of research step 6 | export enum ResearchStepType { 7 | QUESTION_ANALYSIS = 'question_analysis', 8 | SEARCH = 'search', 9 | RESULT_ANALYSIS = 'result_analysis', 10 | SYNTHESIS = 'synthesis', 11 | FOLLOW_UP = 'follow_up' 12 | } 13 | 14 | // Status of research 15 | export enum ResearchStatus { 16 | PLANNING = 'planning', 17 | SEARCHING = 'searching', 18 | ANALYZING = 'analyzing', 19 | SYNTHESIZING = 'synthesizing', 20 | COMPLETED = 'completed' 21 | } 22 | 23 | // Data for one research step 24 | export interface ResearchStep { 25 | id: string; 26 | type: ResearchStepType; 27 | content: string; 28 | timestamp: number; 29 | metadata?: Record<string, any>; 30 | } 31 | 32 | // Sub-question 33 | export interface SubQuestion { 34 | id: string; 35 | question: string; 36 | status: 'pending' | 'in-progress' | 'completed'; 37 | searchResults?: SearchResult[]; 38 | analysis?: string; 39 | } 40 | 41 | // Search result 42 | export interface SearchResult { 43 | title: string; 44 | description: string; 45 | url: string; 46 | relevance?: number; // Relevance score from 0 to 1 47 | } 48 | 49 | // Complete research data 50 | export interface ResearchData { 51 | id: string; 52 | question: string; 53 | subQuestions: SubQuestion[]; 54 | steps: ResearchStep[]; 55 | status: ResearchStatus; 56 | report?: string; 57 | startTime: number; 58 | endTime?: number; 59 | } 60 | 61 | // Options for the server 62 | export interface DeepResearchOptions { 63 | braveApiKey: string; 64 | maxSubQuestions?: number; // Maximum number of sub-questions 65 | maxSearchesPerQuestion?: number; // Maximum number of searches per sub-question 66 | maxTotalSteps?: number; // Maximum number of research steps 67 | timeout?: number; // Timeout in milliseconds 68 | } ``` -------------------------------------------------------------------------------- /src/mcp.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Type definitions for Model Context Protocol (MCP) 3 | * Since we don't have access to the official SDK, we'll define our own types 4 | */ 5 | 6 | /** 7 | * MCP Tool Definition 8 | */ 9 | export interface MCPToolDefinition { 10 | name: string; 11 | description: string; 12 | parameters: { 13 | type: string; 14 | properties: Record<string, any>; 15 | required?: string[]; 16 | }; 17 | } 18 | 19 | /** 20 | * MCP Tool Response 21 | */ 22 | export interface MCPToolResponse { 23 | status: 'success' | 'error'; 24 | result?: any; 25 | error?: string; 26 | } 27 | 28 | /** 29 | * MCP Tool Interface 30 | */ 31 | export interface MCPTool { 32 | getDefinition(): MCPToolDefinition; 33 | execute(params: any): Promise<MCPToolResponse>; 34 | } 35 | 36 | /** 37 | * MCP Server Options 38 | */ 39 | export interface MCPServerOptions { 40 | name: string; 41 | version: string; 42 | description: string; 43 | } 44 | 45 | /** 46 | * MCP Server 47 | */ 48 | export class MCPServer { 49 | private options: MCPServerOptions; 50 | private tools: Map<string, MCPTool> = new Map(); 51 | 52 | constructor(options: MCPServerOptions) { 53 | this.options = options; 54 | } 55 | 56 | /** 57 | * Register a tool with the server 58 | * @param tool The tool to register 59 | */ 60 | registerTool(tool: MCPTool): void { 61 | const definition = tool.getDefinition(); 62 | this.tools.set(definition.name, tool); 63 | } 64 | 65 | /** 66 | * Start the MCP server 67 | * This is a simplified implementation that doesn't actually start a server 68 | * In a real implementation, this would start a WebSocket server 69 | */ 70 | async start(): Promise<void> { 71 | // In a real implementation, this would start a WebSocket server 72 | // For now, we'll just log that the server is starting 73 | console.log(`Starting MCP server: ${this.options.name} v${this.options.version}`); 74 | console.log(`Description: ${this.options.description}`); 75 | console.log(`Registered tools: ${Array.from(this.tools.keys()).join(', ')}`); 76 | } 77 | 78 | /** 79 | * Stop the MCP server 80 | */ 81 | async stop(): Promise<void> { 82 | // In a real implementation, this would stop the WebSocket server 83 | console.log(`Stopping MCP server: ${this.options.name}`); 84 | } 85 | } ``` -------------------------------------------------------------------------------- /src/utils/question.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utilities for working with questions 3 | */ 4 | 5 | // Если в будущем будут добавлены импорты, они должны включать расширение .js 6 | // import { SomeType } from '../types.js'; 7 | 8 | /** 9 | * Analyzes the main question and breaks it down into sub-questions 10 | * @param question The main question 11 | * @returns Array of sub-questions 12 | */ 13 | export async function analyzeQuestion(question: string): Promise<string[]> { 14 | // In a real implementation, this could use an LLM for question decomposition 15 | // For the prototype, we'll use a simple algorithm 16 | 17 | // Remove question marks and split by "and", "or", commas 18 | const cleanQuestion = question.replace(/\?/g, '').trim(); 19 | 20 | // Look for keywords that might indicate a compound question 21 | const conjunctions = ['and', 'or', 'versus', 'vs', 'compared to', 'differences between']; 22 | let hasConjunction = false; 23 | 24 | for (const conj of conjunctions) { 25 | if (cleanQuestion.toLowerCase().includes(conj)) { 26 | hasConjunction = true; 27 | break; 28 | } 29 | } 30 | 31 | // If the question contains conjunctions, break it down 32 | if (hasConjunction) { 33 | // Simple heuristic for breaking down the question 34 | // In a real implementation, this would be more sophisticated 35 | const parts = cleanQuestion.split(/\s+(?:and|or|versus|vs|compared to)\s+/i); 36 | 37 | if (parts.length > 1) { 38 | // Form sub-questions based on parts 39 | return parts.map(part => `${part.trim()}?`); 40 | } 41 | } 42 | 43 | // If we couldn't break it down by conjunctions, create sub-questions by key aspects 44 | // This is a simplified version, a real implementation would need a more complex algorithm 45 | const aspects = [ 46 | 'what is', 'how does', 'why is', 'when was', 'where is', 47 | 'definition', 'history', 'examples', 'advantages', 'disadvantages' 48 | ]; 49 | 50 | const subQuestions = []; 51 | 52 | // Add the main question 53 | subQuestions.push(question); 54 | 55 | // Add sub-questions by aspects 56 | const mainTopic = extractMainTopic(cleanQuestion); 57 | if (mainTopic) { 58 | // Add several sub-questions on different aspects 59 | subQuestions.push(`What is ${mainTopic}?`); 60 | subQuestions.push(`What are the key features of ${mainTopic}?`); 61 | subQuestions.push(`What are the applications of ${mainTopic}?`); 62 | } 63 | 64 | // Remove duplicates and return unique sub-questions 65 | return Array.from(new Set(subQuestions)); 66 | } 67 | 68 | /** 69 | * Extracts the main topic from a question 70 | * @param question The question 71 | * @returns The main topic or null if it couldn't be determined 72 | */ 73 | function extractMainTopic(question: string): string | null { 74 | // Remove question words at the beginning 75 | const withoutQuestionWords = question 76 | .replace(/^(what|who|when|where|why|how|is|are|do|does|did|can|could|would|should|will)\s+/i, '') 77 | .trim(); 78 | 79 | // If the question starts with "the", "a", "an", remove the article 80 | const withoutArticles = withoutQuestionWords 81 | .replace(/^(the|a|an)\s+/i, '') 82 | .trim(); 83 | 84 | // If there's anything left, return it as the main topic 85 | return withoutArticles.length > 0 ? withoutArticles : null; 86 | } ``` -------------------------------------------------------------------------------- /src/utils/search.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utilities for performing searches using Brave Search API 3 | */ 4 | 5 | import axios from 'axios'; 6 | import { SearchResult } from '../types.js'; 7 | 8 | /** 9 | * Performs a search using the Brave Search API 10 | * @param query The search query 11 | * @param apiKey The Brave Search API key 12 | * @param count The number of results to return (max 20) 13 | * @returns Array of search results 14 | */ 15 | export async function performSearch( 16 | query: string, 17 | apiKey: string, 18 | count: number = 10 19 | ): Promise<SearchResult[]> { 20 | if (!query) { 21 | throw new Error('Search query is required'); 22 | } 23 | 24 | if (!apiKey) { 25 | throw new Error('Brave Search API key is required'); 26 | } 27 | 28 | // Limit count to maximum of 20 results 29 | const limitedCount = Math.min(count, 20); 30 | 31 | try { 32 | // Construct the API request URL 33 | const url = `https://api.search.brave.com/res/v1/web/search`; 34 | 35 | // Make the API request 36 | const response = await axios.get(url, { 37 | headers: { 38 | 'Accept': 'application/json', 39 | 'Accept-Encoding': 'gzip', 40 | 'X-Subscription-Token': apiKey 41 | }, 42 | params: { 43 | q: query, 44 | count: limitedCount 45 | } 46 | }); 47 | 48 | // Check if the response is valid 49 | if (!response.data || !response.data.web || !response.data.web.results) { 50 | return []; 51 | } 52 | 53 | // Parse the results 54 | const results: SearchResult[] = response.data.web.results.map((result: any) => { 55 | return { 56 | title: result.title || '', 57 | url: result.url || '', 58 | description: result.description || '', 59 | relevance: calculateRelevance(query, result.title, result.description) 60 | }; 61 | }); 62 | 63 | return results; 64 | } catch (error) { 65 | console.error('Error performing search:', error); 66 | throw new Error(`Failed to perform search: ${error instanceof Error ? error.message : String(error)}`); 67 | } 68 | } 69 | 70 | /** 71 | * Calculates the relevance score of a search result 72 | * @param query The search query 73 | * @param title The result title 74 | * @param description The result description 75 | * @returns A relevance score between 0 and 1 76 | */ 77 | function calculateRelevance(query: string, title: string, description: string): number { 78 | // Normalize the query and result text 79 | const normalizedQuery = query.toLowerCase(); 80 | const normalizedTitle = title.toLowerCase(); 81 | const normalizedDescription = description.toLowerCase(); 82 | 83 | // Split the query into words 84 | const queryWords = normalizedQuery.split(/\s+/).filter(word => word.length > 2); 85 | 86 | // Count matches in title (weighted higher) 87 | let titleMatches = 0; 88 | for (const word of queryWords) { 89 | if (normalizedTitle.includes(word)) { 90 | titleMatches++; 91 | } 92 | } 93 | 94 | // Count matches in description 95 | let descriptionMatches = 0; 96 | for (const word of queryWords) { 97 | if (normalizedDescription.includes(word)) { 98 | descriptionMatches++; 99 | } 100 | } 101 | 102 | // Calculate relevance score (title matches weighted 3x) 103 | const maxPossibleScore = queryWords.length * 4; // 3 for title + 1 for description 104 | const actualScore = (titleMatches * 3) + descriptionMatches; 105 | 106 | // Return normalized score between 0 and 1 107 | return maxPossibleScore > 0 ? actualScore / maxPossibleScore : 0; 108 | } ``` -------------------------------------------------------------------------------- /src/utils/analysis.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utilities for analyzing search results 3 | */ 4 | 5 | import { SearchResult } from '../types.js'; 6 | 7 | /** 8 | * Analyzes search results and extracts key information 9 | * @param results Array of search results 10 | * @param query The original search query 11 | * @returns Analysis report 12 | */ 13 | export async function analyzeResults( 14 | results: SearchResult[], 15 | query: string 16 | ): Promise<string> { 17 | if (!results || results.length === 0) { 18 | return `No results found for query: "${query}"`; 19 | } 20 | 21 | // Sort results by relevance 22 | const sortedResults = [...results].sort((a, b) => { 23 | const relevanceA = a.relevance ?? 0; 24 | const relevanceB = b.relevance ?? 0; 25 | return relevanceB - relevanceA; 26 | }); 27 | 28 | // Take the top 5 most relevant results 29 | const topResults = sortedResults.slice(0, 5); 30 | 31 | // Extract key information from each result 32 | const analysisPoints = topResults.map((result, index) => { 33 | const keyInfo = extractKeyInformation(result, query); 34 | return `${index + 1}. ${result.title}\n ${keyInfo}\n Source: ${result.url}`; 35 | }); 36 | 37 | // Format the analysis report 38 | const report = ` 39 | Analysis for query: "${query}" 40 | 41 | Key Information: 42 | ${analysisPoints.join('\n\n')} 43 | 44 | This analysis is based on the top ${topResults.length} most relevant results. 45 | `; 46 | 47 | return report; 48 | } 49 | 50 | /** 51 | * Extracts key information from a search result 52 | * @param result The search result 53 | * @param query The original search query 54 | * @returns Extracted key information 55 | */ 56 | function extractKeyInformation(result: SearchResult, query: string): string { 57 | // In a real implementation, this would use NLP techniques 58 | // For the prototype, we'll use a simple approach 59 | 60 | // Get the most relevant sentences from the description 61 | const relevantSentences = selectRelevantSentences(result.description, query, 2); 62 | 63 | return relevantSentences.join(' '); 64 | } 65 | 66 | /** 67 | * Selects the most relevant sentences from a text 68 | * @param text The text to analyze 69 | * @param query The search query 70 | * @param maxSentences Maximum number of sentences to return 71 | * @returns Array of relevant sentences 72 | */ 73 | function selectRelevantSentences(text: string, query: string, maxSentences: number): string[] { 74 | // Split text into sentences 75 | const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); 76 | 77 | if (sentences.length === 0) { 78 | return [text]; 79 | } 80 | 81 | // Calculate relevance score for each sentence 82 | const scoredSentences = sentences.map(sentence => { 83 | const score = calculateSentenceRelevance(sentence, query); 84 | return { sentence, score }; 85 | }); 86 | 87 | // Sort by relevance score 88 | scoredSentences.sort((a, b) => b.score - a.score); 89 | 90 | // Return the top N sentences 91 | return scoredSentences.slice(0, maxSentences).map(s => s.sentence.trim()); 92 | } 93 | 94 | /** 95 | * Calculates the relevance of a sentence to a query 96 | * @param sentence The sentence 97 | * @param query The query 98 | * @returns A relevance score 99 | */ 100 | function calculateSentenceRelevance(sentence: string, query: string): number { 101 | const normalizedSentence = sentence.toLowerCase(); 102 | const normalizedQuery = query.toLowerCase(); 103 | 104 | // Split query into words 105 | const queryWords = normalizedQuery.split(/\s+/).filter(word => !isStopWord(word) && word.length > 2); 106 | 107 | // Count how many query words appear in the sentence 108 | let matchCount = 0; 109 | for (const word of queryWords) { 110 | if (normalizedSentence.includes(word)) { 111 | matchCount++; 112 | } 113 | } 114 | 115 | // Calculate score based on match percentage and sentence length 116 | const matchPercentage = queryWords.length > 0 ? matchCount / queryWords.length : 0; 117 | const lengthFactor = 1 - Math.min(Math.abs(normalizedSentence.length - 100) / 100, 0.5); 118 | 119 | return matchPercentage * 0.7 + lengthFactor * 0.3; 120 | } 121 | 122 | /** 123 | * Checks if a word is a common stop word 124 | * @param word The word to check 125 | * @returns True if it's a stop word 126 | */ 127 | function isStopWord(word: string): boolean { 128 | const stopWords = [ 129 | 'a', 'an', 'the', 'and', 'or', 'but', 'if', 'because', 'as', 'what', 130 | 'which', 'this', 'that', 'these', 'those', 'then', 'just', 'so', 'than', 131 | 'such', 'both', 'through', 'about', 'for', 'is', 'of', 'while', 'during', 132 | 'to', 'from', 'in', 'out', 'on', 'off', 'over', 'under', 'again', 'further', 133 | 'then', 'once', 'here', 'there', 'when', 'where', 'why', 'how', 'all', 'any', 134 | 'both', 'each', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'nor', 135 | 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 136 | 'will', 'just', 'don', 'should', 'now' 137 | ]; 138 | 139 | return stopWords.includes(word.toLowerCase()); 140 | } ``` -------------------------------------------------------------------------------- /src/utils/synthesis.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Utilities for synthesizing research results into a coherent report 3 | */ 4 | 5 | import { SubQuestion } from '../types.js'; 6 | 7 | /** 8 | * Synthesizes research results into a coherent report 9 | * @param mainQuestion The main research question 10 | * @param subQuestions Array of sub-questions with their analysis 11 | * @returns Synthesized research report 12 | */ 13 | export async function synthesizeReport( 14 | mainQuestion: string, 15 | subQuestions: SubQuestion[] 16 | ): Promise<string> { 17 | if (!subQuestions || subQuestions.length === 0) { 18 | return `No research data available for question: "${mainQuestion}"`; 19 | } 20 | 21 | // Filter completed sub-questions 22 | const completedSubQuestions = subQuestions.filter(sq => sq.status === 'completed' && sq.analysis); 23 | 24 | if (completedSubQuestions.length === 0) { 25 | return `Research is still in progress for question: "${mainQuestion}"`; 26 | } 27 | 28 | // Generate introduction 29 | const introduction = generateIntroduction(mainQuestion); 30 | 31 | // Generate sections for each sub-question 32 | const sections = completedSubQuestions.map(sq => { 33 | return generateSection(sq.question, sq.analysis || '', sq.searchResults || []); 34 | }); 35 | 36 | // Generate conclusion 37 | const conclusion = generateConclusion(mainQuestion, completedSubQuestions); 38 | 39 | // Generate sources section 40 | const sources = generateSources(completedSubQuestions); 41 | 42 | // Combine all parts into a complete report 43 | const report = ` 44 | # Research Report: ${mainQuestion} 45 | 46 | ## Introduction 47 | ${introduction} 48 | 49 | ${sections.join('\n\n')} 50 | 51 | ## Conclusion 52 | ${conclusion} 53 | 54 | ## Sources 55 | ${sources} 56 | `; 57 | 58 | return report; 59 | } 60 | 61 | /** 62 | * Generates the introduction section 63 | * @param mainQuestion The main research question 64 | * @returns Introduction text 65 | */ 66 | function generateIntroduction(mainQuestion: string): string { 67 | return `This report presents a comprehensive analysis of the question: "${mainQuestion}". 68 | The research was conducted using multiple sources and approaches to provide a thorough understanding of the topic.`; 69 | } 70 | 71 | /** 72 | * Generates a section for a sub-question 73 | * @param question The sub-question 74 | * @param analysis The analysis text 75 | * @param searchResults The search results 76 | * @returns Formatted section text 77 | */ 78 | function generateSection( 79 | question: string, 80 | analysis: string, 81 | searchResults: { title: string; url: string }[] 82 | ): string { 83 | // Extract a section title from the question 84 | const sectionTitle = question.replace(/\?/g, '').trim(); 85 | 86 | // Format the section 87 | return `## ${sectionTitle} 88 | 89 | ${analysis} 90 | 91 | ${generateCitations(analysis, searchResults)}`; 92 | } 93 | 94 | /** 95 | * Generates citation markers for the analysis text 96 | * @param analysis The analysis text 97 | * @param searchResults The search results 98 | * @returns Text with citation markers 99 | */ 100 | function generateCitations( 101 | analysis: string, 102 | searchResults: { title: string; url: string }[] 103 | ): string { 104 | // In a real implementation, this would use NLP to match text to sources 105 | // For the prototype, we'll use a simple approach 106 | 107 | if (!searchResults || searchResults.length === 0) { 108 | return ''; 109 | } 110 | 111 | // Create citation notes 112 | const citations = searchResults.map((result, index) => { 113 | return `[${index + 1}] ${result.title} - ${result.url}`; 114 | }); 115 | 116 | return `**Sources:**\n${citations.join('\n')}`; 117 | } 118 | 119 | /** 120 | * Generates the conclusion section 121 | * @param mainQuestion The main research question 122 | * @param subQuestions The sub-questions with analysis 123 | * @returns Conclusion text 124 | */ 125 | function generateConclusion( 126 | mainQuestion: string, 127 | subQuestions: SubQuestion[] 128 | ): string { 129 | return `This research has explored various aspects of ${extractMainTopic(mainQuestion)}. 130 | The findings from different sub-questions provide a comprehensive understanding of the topic. 131 | The research was based on ${subQuestions.length} sub-questions and utilized multiple sources to ensure accuracy and depth.`; 132 | } 133 | 134 | /** 135 | * Generates the sources section 136 | * @param subQuestions The sub-questions with search results 137 | * @returns Formatted sources text 138 | */ 139 | function generateSources(subQuestions: SubQuestion[]): string { 140 | // Collect all unique sources 141 | const allSources = new Map<string, { title: string; url: string }>(); 142 | 143 | subQuestions.forEach(sq => { 144 | if (sq.searchResults) { 145 | sq.searchResults.forEach(result => { 146 | if (!allSources.has(result.url)) { 147 | allSources.set(result.url, { title: result.title, url: result.url }); 148 | } 149 | }); 150 | } 151 | }); 152 | 153 | // Format the sources 154 | const sourcesList = Array.from(allSources.values()).map((source, index) => { 155 | return `${index + 1}. ${source.title} - ${source.url}`; 156 | }); 157 | 158 | return sourcesList.join('\n'); 159 | } 160 | 161 | /** 162 | * Extracts the main topic from a question 163 | * @param question The question 164 | * @returns The main topic 165 | */ 166 | function extractMainTopic(question: string): string { 167 | // Remove question words at the beginning 168 | const withoutQuestionWords = question 169 | .replace(/^(what|who|when|where|why|how|is|are|do|does|did|can|could|would|should|will)\s+/i, '') 170 | .trim(); 171 | 172 | // If the question starts with "the", "a", "an", remove the article 173 | const withoutArticles = withoutQuestionWords 174 | .replace(/^(the|a|an)\s+/i, '') 175 | .trim(); 176 | 177 | // If there's anything left, return it as the main topic 178 | return withoutArticles.length > 0 ? withoutArticles : question; 179 | } ``` -------------------------------------------------------------------------------- /src/tools/braveSearch.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * MCP tool for Brave Search 3 | */ 4 | 5 | import { MCPTool, MCPToolDefinition, MCPToolResponse } from '../mcp.js'; 6 | import { performSearch } from '../utils/search.js'; 7 | 8 | /** 9 | * Brave Web Search tool for MCP 10 | */ 11 | export class BraveWebSearchTool implements MCPTool { 12 | private apiKey: string; 13 | 14 | constructor(apiKey: string) { 15 | this.apiKey = apiKey; 16 | } 17 | 18 | /** 19 | * Get the tool definition 20 | */ 21 | getDefinition(): MCPToolDefinition { 22 | return { 23 | name: 'brave_web_search', 24 | 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. ', 25 | parameters: { 26 | type: 'object', 27 | properties: { 28 | query: { 29 | type: 'string', 30 | description: 'Search query (max 400 chars, 50 words)' 31 | }, 32 | count: { 33 | type: 'number', 34 | description: 'Number of results (1-20, default 10)', 35 | default: 10 36 | }, 37 | offset: { 38 | type: 'number', 39 | description: 'Pagination offset (max 9, default 0)', 40 | default: 0 41 | } 42 | }, 43 | required: ['query'] 44 | } 45 | }; 46 | } 47 | 48 | /** 49 | * Execute the tool 50 | * @param params Tool parameters 51 | * @returns Tool response 52 | */ 53 | async execute(params: any): Promise<MCPToolResponse> { 54 | try { 55 | // Validate parameters 56 | if (!params.query) { 57 | return { 58 | status: 'error', 59 | error: 'Search query is required' 60 | }; 61 | } 62 | 63 | // Limit query length 64 | const query = params.query.slice(0, 400); 65 | 66 | // Set count with default and limits 67 | const count = Math.min(Math.max(params.count || 10, 1), 20); 68 | 69 | // Set offset with default and limits 70 | const offset = Math.min(Math.max(params.offset || 0, 0), 9); 71 | 72 | // Perform the search 73 | const results = await performSearch(query, this.apiKey, count); 74 | 75 | // Format the response 76 | return { 77 | status: 'success', 78 | result: { 79 | query, 80 | count: results.length, 81 | offset, 82 | results: results.map(result => ({ 83 | title: result.title, 84 | description: result.description, 85 | url: result.url, 86 | relevance: result.relevance 87 | })) 88 | } 89 | }; 90 | } catch (error) { 91 | console.error('Error in Brave Web Search tool:', error); 92 | return { 93 | status: 'error', 94 | error: error instanceof Error ? error.message : String(error) 95 | }; 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Brave Local Search tool for MCP 102 | */ 103 | export class BraveLocalSearchTool implements MCPTool { 104 | private apiKey: string; 105 | 106 | constructor(apiKey: string) { 107 | this.apiKey = apiKey; 108 | } 109 | 110 | /** 111 | * Get the tool definition 112 | */ 113 | getDefinition(): MCPToolDefinition { 114 | return { 115 | name: 'brave_local_search', 116 | 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.', 117 | parameters: { 118 | type: 'object', 119 | properties: { 120 | query: { 121 | type: 'string', 122 | description: 'Local search query (e.g. \'pizza near Central Park\')' 123 | }, 124 | count: { 125 | type: 'number', 126 | description: 'Number of results (1-20, default 5)', 127 | default: 5 128 | } 129 | }, 130 | required: ['query'] 131 | } 132 | }; 133 | } 134 | 135 | /** 136 | * Execute the tool 137 | * @param params Tool parameters 138 | * @returns Tool response 139 | */ 140 | async execute(params: any): Promise<MCPToolResponse> { 141 | try { 142 | // Validate parameters 143 | if (!params.query) { 144 | return { 145 | status: 'error', 146 | error: 'Search query is required' 147 | }; 148 | } 149 | 150 | // Limit query length 151 | const query = params.query.slice(0, 400); 152 | 153 | // Set count with default and limits 154 | const count = Math.min(Math.max(params.count || 5, 1), 20); 155 | 156 | // For now, we'll use the web search API since we don't have direct access to local search 157 | // In a real implementation, this would use a different endpoint 158 | const results = await performSearch(`${query} near me`, this.apiKey, count); 159 | 160 | // Format the response 161 | return { 162 | status: 'success', 163 | result: { 164 | query, 165 | count: results.length, 166 | results: results.map(result => ({ 167 | title: result.title, 168 | description: result.description, 169 | url: result.url, 170 | relevance: result.relevance 171 | })) 172 | } 173 | }; 174 | } catch (error) { 175 | console.error('Error in Brave Local Search tool:', error); 176 | return { 177 | status: 'error', 178 | error: error instanceof Error ? error.message : String(error) 179 | }; 180 | } 181 | } 182 | } ``` -------------------------------------------------------------------------------- /src/tools/sequentialThinking.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * MCP tool for Sequential Thinking 3 | */ 4 | 5 | import { MCPTool, MCPToolDefinition, MCPToolResponse } from '../mcp.js'; 6 | import { v4 as uuidv4 } from 'uuid'; 7 | 8 | /** 9 | * Sequential Thinking tool for MCP 10 | */ 11 | export class SequentialThinkingTool implements MCPTool { 12 | private thoughts: Map<string, any[]> = new Map(); 13 | 14 | /** 15 | * Get the tool definition 16 | */ 17 | getDefinition(): MCPToolDefinition { 18 | return { 19 | name: 'sequentialthinking', 20 | 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', 21 | parameters: { 22 | type: 'object', 23 | properties: { 24 | thought: { 25 | type: 'string', 26 | description: 'Your current thinking step' 27 | }, 28 | nextThoughtNeeded: { 29 | type: 'boolean', 30 | description: 'Whether another thought step is needed' 31 | }, 32 | thoughtNumber: { 33 | type: 'integer', 34 | description: 'Current thought number', 35 | minimum: 1 36 | }, 37 | totalThoughts: { 38 | type: 'integer', 39 | description: 'Estimated total thoughts needed', 40 | minimum: 1 41 | }, 42 | isRevision: { 43 | type: 'boolean', 44 | description: 'Whether this revises previous thinking' 45 | }, 46 | revisesThought: { 47 | type: 'integer', 48 | description: 'Which thought is being reconsidered', 49 | minimum: 1 50 | }, 51 | branchFromThought: { 52 | type: 'integer', 53 | description: 'Branching point thought number', 54 | minimum: 1 55 | }, 56 | branchId: { 57 | type: 'string', 58 | description: 'Branch identifier' 59 | }, 60 | needsMoreThoughts: { 61 | type: 'boolean', 62 | description: 'If more thoughts are needed' 63 | } 64 | }, 65 | required: ['thought', 'nextThoughtNeeded', 'thoughtNumber', 'totalThoughts'] 66 | } 67 | }; 68 | } 69 | 70 | /** 71 | * Execute the tool 72 | * @param params Tool parameters 73 | * @returns Tool response 74 | */ 75 | async execute(params: any): Promise<MCPToolResponse> { 76 | try { 77 | // Validate parameters 78 | if (!params.thought) { 79 | return { 80 | status: 'error', 81 | error: 'Thought content is required' 82 | }; 83 | } 84 | 85 | if (params.thoughtNumber === undefined || params.thoughtNumber < 1) { 86 | return { 87 | status: 'error', 88 | error: 'Valid thought number is required' 89 | }; 90 | } 91 | 92 | if (params.totalThoughts === undefined || params.totalThoughts < 1) { 93 | return { 94 | status: 'error', 95 | error: 'Valid total thoughts is required' 96 | }; 97 | } 98 | 99 | // Generate a session ID if this is the first thought 100 | let sessionId = ''; 101 | if (params.thoughtNumber === 1) { 102 | sessionId = uuidv4(); 103 | this.thoughts.set(sessionId, []); 104 | } else { 105 | // Find the session ID from previous thoughts 106 | for (const [id, thoughts] of this.thoughts.entries()) { 107 | if (thoughts.length > 0 && thoughts.some(t => t.thoughtNumber === params.thoughtNumber - 1)) { 108 | sessionId = id; 109 | break; 110 | } 111 | } 112 | 113 | if (!sessionId) { 114 | // If we can't find a previous session, create a new one 115 | sessionId = uuidv4(); 116 | this.thoughts.set(sessionId, []); 117 | } 118 | } 119 | 120 | // Store the thought 121 | const thought = { 122 | thoughtNumber: params.thoughtNumber, 123 | totalThoughts: params.totalThoughts, 124 | content: params.thought, 125 | nextThoughtNeeded: params.nextThoughtNeeded, 126 | isRevision: params.isRevision || false, 127 | revisesThought: params.revisesThought, 128 | branchFromThought: params.branchFromThought, 129 | branchId: params.branchId, 130 | needsMoreThoughts: params.needsMoreThoughts || false, 131 | timestamp: Date.now() 132 | }; 133 | 134 | const thoughts = this.thoughts.get(sessionId) || []; 135 | thoughts.push(thought); 136 | this.thoughts.set(sessionId, thoughts); 137 | 138 | // Format the response 139 | return { 140 | status: 'success', 141 | result: { 142 | sessionId, 143 | thoughtNumber: params.thoughtNumber, 144 | totalThoughts: params.totalThoughts, 145 | nextThoughtNeeded: params.nextThoughtNeeded, 146 | previousThoughts: thoughts 147 | .filter(t => t.thoughtNumber < params.thoughtNumber) 148 | .map(t => ({ 149 | thoughtNumber: t.thoughtNumber, 150 | content: t.content, 151 | isRevision: t.isRevision, 152 | revisesThought: t.revisesThought 153 | })) 154 | } 155 | }; 156 | } catch (error) { 157 | console.error('Error in Sequential Thinking tool:', error); 158 | return { 159 | status: 'error', 160 | error: error instanceof Error ? error.message : String(error) 161 | }; 162 | } 163 | } 164 | } ``` -------------------------------------------------------------------------------- /src/tools/deepResearch.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * MCP tool for Deep Research 3 | * Combines Sequential Thinking and Brave Search for comprehensive research 4 | */ 5 | 6 | import { MCPTool, MCPToolDefinition, MCPToolResponse } from '../mcp.js'; 7 | import { v4 as uuidv4 } from 'uuid'; 8 | import { analyzeQuestion } from '../utils/question.js'; 9 | import { performSearch } from '../utils/search.js'; 10 | import { analyzeResults } from '../utils/analysis.js'; 11 | import { synthesizeReport } from '../utils/synthesis.js'; 12 | import { ResearchData, ResearchStatus, ResearchStep, ResearchStepType, SubQuestion } from '../types.js'; 13 | 14 | /** 15 | * Deep Research tool for MCP 16 | */ 17 | export class DeepResearchTool implements MCPTool { 18 | private apiKey: string; 19 | private researches: Map<string, ResearchData> = new Map(); 20 | 21 | constructor(apiKey: string) { 22 | this.apiKey = apiKey; 23 | } 24 | 25 | /** 26 | * Get the tool definition 27 | */ 28 | getDefinition(): MCPToolDefinition { 29 | return { 30 | name: 'deep_research', 31 | 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.', 32 | parameters: { 33 | type: 'object', 34 | properties: { 35 | query: { 36 | type: 'string', 37 | description: 'The research question or topic' 38 | }, 39 | maxSubQuestions: { 40 | type: 'number', 41 | description: 'Maximum number of sub-questions to generate (default: 5)', 42 | default: 5 43 | }, 44 | maxSearchesPerQuestion: { 45 | type: 'number', 46 | description: 'Maximum number of searches per sub-question (default: 2)', 47 | default: 2 48 | }, 49 | researchId: { 50 | type: 'string', 51 | description: 'ID of an existing research to continue or retrieve' 52 | }, 53 | action: { 54 | type: 'string', 55 | description: 'Action to perform: "start", "status", "continue", or "report"', 56 | default: 'start' 57 | } 58 | }, 59 | required: ['query'] 60 | } 61 | }; 62 | } 63 | 64 | /** 65 | * Execute the tool 66 | * @param params Tool parameters 67 | * @returns Tool response 68 | */ 69 | async execute(params: any): Promise<MCPToolResponse> { 70 | try { 71 | // Validate parameters 72 | if (!params.query && !params.researchId) { 73 | return { 74 | status: 'error', 75 | error: 'Either query or researchId is required' 76 | }; 77 | } 78 | 79 | // Determine the action 80 | const action = params.action || 'start'; 81 | 82 | // Handle different actions 83 | switch (action) { 84 | case 'start': 85 | return this.startResearch(params); 86 | case 'status': 87 | return this.getResearchStatus(params); 88 | case 'continue': 89 | return this.continueResearch(params); 90 | case 'report': 91 | return this.generateReport(params); 92 | default: 93 | return { 94 | status: 'error', 95 | error: `Invalid action: ${action}` 96 | }; 97 | } 98 | } catch (error) { 99 | console.error('Error in Deep Research tool:', error); 100 | return { 101 | status: 'error', 102 | error: error instanceof Error ? error.message : String(error) 103 | }; 104 | } 105 | } 106 | 107 | /** 108 | * Start a new research 109 | * @param params Tool parameters 110 | * @returns Tool response 111 | */ 112 | private async startResearch(params: any): Promise<MCPToolResponse> { 113 | // Generate a research ID 114 | const researchId = uuidv4(); 115 | 116 | // Create a new research data object 117 | const research: ResearchData = { 118 | id: researchId, 119 | question: params.query, 120 | subQuestions: [], 121 | steps: [], 122 | status: ResearchStatus.PLANNING, 123 | startTime: Date.now() 124 | }; 125 | 126 | // Store the research 127 | this.researches.set(researchId, research); 128 | 129 | // Add the first step - question analysis 130 | const step: ResearchStep = { 131 | id: uuidv4(), 132 | type: ResearchStepType.QUESTION_ANALYSIS, 133 | content: `Analyzing question: "${params.query}"`, 134 | timestamp: Date.now() 135 | }; 136 | 137 | research.steps.push(step); 138 | 139 | // Analyze the question to generate sub-questions 140 | const subQuestions = await analyzeQuestion(params.query); 141 | 142 | // Limit the number of sub-questions 143 | const maxSubQuestions = Math.min(params.maxSubQuestions || 5, 10); 144 | const limitedSubQuestions = subQuestions.slice(0, maxSubQuestions); 145 | 146 | // Create sub-question objects 147 | research.subQuestions = limitedSubQuestions.map(question => ({ 148 | id: uuidv4(), 149 | question, 150 | status: 'pending' 151 | })); 152 | 153 | // Update the step with the sub-questions 154 | step.content = `Analyzed question: "${params.query}"\nGenerated ${research.subQuestions.length} sub-questions:\n${research.subQuestions.map(sq => `- ${sq.question}`).join('\n')}`; 155 | 156 | // Update the research status 157 | research.status = ResearchStatus.SEARCHING; 158 | 159 | // Return the response 160 | return { 161 | status: 'success', 162 | result: { 163 | researchId, 164 | question: params.query, 165 | subQuestions: research.subQuestions.map(sq => sq.question), 166 | status: research.status 167 | } 168 | }; 169 | } 170 | 171 | /** 172 | * Get the status of a research 173 | * @param params Tool parameters 174 | * @returns Tool response 175 | */ 176 | private getResearchStatus(params: any): Promise<MCPToolResponse> { 177 | // Get the research ID 178 | const researchId = params.researchId; 179 | 180 | // Check if the research exists 181 | if (!researchId || !this.researches.has(researchId)) { 182 | return Promise.resolve({ 183 | status: 'error', 184 | error: `Research not found: ${researchId}` 185 | }); 186 | } 187 | 188 | // Get the research 189 | const research = this.researches.get(researchId)!; 190 | 191 | // Return the status 192 | return Promise.resolve({ 193 | status: 'success', 194 | result: { 195 | researchId, 196 | question: research.question, 197 | status: research.status, 198 | subQuestions: research.subQuestions.map(sq => ({ 199 | question: sq.question, 200 | status: sq.status 201 | })), 202 | steps: research.steps.length, 203 | startTime: research.startTime, 204 | endTime: research.endTime 205 | } 206 | }); 207 | } 208 | 209 | /** 210 | * Continue a research 211 | * @param params Tool parameters 212 | * @returns Tool response 213 | */ 214 | private async continueResearch(params: any): Promise<MCPToolResponse> { 215 | // Get the research ID 216 | const researchId = params.researchId; 217 | 218 | // Check if the research exists 219 | if (!researchId || !this.researches.has(researchId)) { 220 | return { 221 | status: 'error', 222 | error: `Research not found: ${researchId}` 223 | }; 224 | } 225 | 226 | // Get the research 227 | const research = this.researches.get(researchId)!; 228 | 229 | // Check if the research is already completed 230 | if (research.status === ResearchStatus.COMPLETED) { 231 | return { 232 | status: 'success', 233 | result: { 234 | researchId, 235 | question: research.question, 236 | status: research.status, 237 | message: 'Research is already completed' 238 | } 239 | }; 240 | } 241 | 242 | // Find the next pending sub-question 243 | const nextSubQuestion = research.subQuestions.find(sq => sq.status === 'pending'); 244 | 245 | // If there are no more pending sub-questions, synthesize the results 246 | if (!nextSubQuestion) { 247 | return this.synthesizeResults(research); 248 | } 249 | 250 | // Mark the sub-question as in-progress 251 | nextSubQuestion.status = 'in-progress'; 252 | 253 | // Add a search step 254 | const searchStep: ResearchStep = { 255 | id: uuidv4(), 256 | type: ResearchStepType.SEARCH, 257 | content: `Searching for: "${nextSubQuestion.question}"`, 258 | timestamp: Date.now() 259 | }; 260 | 261 | research.steps.push(searchStep); 262 | 263 | // Perform the search 264 | const searchResults = await performSearch(nextSubQuestion.question, this.apiKey, 10); 265 | 266 | // Store the search results 267 | nextSubQuestion.searchResults = searchResults; 268 | 269 | // Update the search step 270 | searchStep.content = `Searched for: "${nextSubQuestion.question}"\nFound ${searchResults.length} results`; 271 | 272 | // Add an analysis step 273 | const analysisStep: ResearchStep = { 274 | id: uuidv4(), 275 | type: ResearchStepType.RESULT_ANALYSIS, 276 | content: `Analyzing results for: "${nextSubQuestion.question}"`, 277 | timestamp: Date.now() 278 | }; 279 | 280 | research.steps.push(analysisStep); 281 | 282 | // Analyze the results 283 | const analysis = await analyzeResults(searchResults, nextSubQuestion.question); 284 | 285 | // Store the analysis 286 | nextSubQuestion.analysis = analysis; 287 | 288 | // Update the analysis step 289 | analysisStep.content = `Analyzed results for: "${nextSubQuestion.question}"`; 290 | 291 | // Mark the sub-question as completed 292 | nextSubQuestion.status = 'completed'; 293 | 294 | // Return the response 295 | return { 296 | status: 'success', 297 | result: { 298 | researchId, 299 | question: research.question, 300 | subQuestionCompleted: nextSubQuestion.question, 301 | remainingSubQuestions: research.subQuestions.filter(sq => sq.status === 'pending').length, 302 | status: research.status 303 | } 304 | }; 305 | } 306 | 307 | /** 308 | * Synthesize the results of a research 309 | * @param research The research data 310 | * @returns Tool response 311 | */ 312 | private async synthesizeResults(research: ResearchData): Promise<MCPToolResponse> { 313 | // Add a synthesis step 314 | const synthesisStep: ResearchStep = { 315 | id: uuidv4(), 316 | type: ResearchStepType.SYNTHESIS, 317 | content: 'Synthesizing research results', 318 | timestamp: Date.now() 319 | }; 320 | 321 | research.steps.push(synthesisStep); 322 | 323 | // Update the research status 324 | research.status = ResearchStatus.SYNTHESIZING; 325 | 326 | // Synthesize the report 327 | const report = await synthesizeReport(research.question, research.subQuestions); 328 | 329 | // Store the report 330 | research.report = report; 331 | 332 | // Update the synthesis step 333 | synthesisStep.content = 'Synthesized research results'; 334 | 335 | // Update the research status and end time 336 | research.status = ResearchStatus.COMPLETED; 337 | research.endTime = Date.now(); 338 | 339 | // Return the response 340 | return { 341 | status: 'success', 342 | result: { 343 | researchId: research.id, 344 | question: research.question, 345 | status: research.status, 346 | message: 'Research completed', 347 | reportPreview: report.substring(0, 500) + '...' 348 | } 349 | }; 350 | } 351 | 352 | /** 353 | * Generate a report for a research 354 | * @param params Tool parameters 355 | * @returns Tool response 356 | */ 357 | private generateReport(params: any): Promise<MCPToolResponse> { 358 | // Get the research ID 359 | const researchId = params.researchId; 360 | 361 | // Check if the research exists 362 | if (!researchId || !this.researches.has(researchId)) { 363 | return Promise.resolve({ 364 | status: 'error', 365 | error: `Research not found: ${researchId}` 366 | }); 367 | } 368 | 369 | // Get the research 370 | const research = this.researches.get(researchId)!; 371 | 372 | // Check if the research is completed 373 | if (research.status !== ResearchStatus.COMPLETED) { 374 | return Promise.resolve({ 375 | status: 'error', 376 | error: 'Research is not yet completed' 377 | }); 378 | } 379 | 380 | // Return the report 381 | return Promise.resolve({ 382 | status: 'success', 383 | result: { 384 | researchId, 385 | question: research.question, 386 | report: research.report, 387 | subQuestions: research.subQuestions.length, 388 | steps: research.steps.length, 389 | startTime: research.startTime, 390 | endTime: research.endTime, 391 | duration: (research.endTime! - research.startTime) / 1000 392 | } 393 | }); 394 | } 395 | } ```