search_conversations
Search Cursor chat content using exact text matching to find discussions containing specific technical terms, error messages, or code patterns.
Instructions
Searches through Cursor chat content using exact text matching (NOT semantic search) to find relevant discussions. WARNING: For project-specific searches, use list_conversations with projectPath instead of this tool! This tool is for searching message content, not project filtering.
WHEN TO USE THIS TOOL:
Searching for specific technical terms in message content (e.g., "useState", "async/await")
Finding conversations mentioning specific error messages
Searching for code patterns or function names
WHEN NOT TO USE THIS TOOL:
❌ DON'T use query="project-name" - use list_conversations with projectPath instead
❌ DON'T search for project names in message content
❌ DON'T use this for project-specific filtering
Search methods (all use exact/literal text matching):
Simple text matching: Use query parameter for literal string matching (e.g., "react hooks")
Multi-keyword: Use keywords array with keywordOperator for exact matching
LIKE patterns: Advanced pattern matching with SQL wildcards (% = any chars, _ = single char)
Date range: Filter by message timestamps (YYYY-MM-DD format)
IMPORTANT: When using date filters, call get_system_info first to know today's date.
Examples: likePattern="%useState(%" for function calls, keywords=["typescript","interface"] with AND operator.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Exact text matching - searches for literal string occurrences in MESSAGE CONTENT (e.g., "react hooks", "useState", "error message"). ❌ DON'T use for project names - use list_conversations with projectPath instead! | |
| keywords | No | Array of keywords for exact text matching - use with keywordOperator to find conversations with specific combinations | |
| keywordOperator | No | How to combine keywords: "AND" = all keywords must be present, "OR" = any keyword can be present | OR |
| likePattern | No | SQL LIKE pattern for advanced searches - use % for any characters, _ for single character. Examples: "%useState(%" for function calls, "%.tsx%" for file types | |
| startDate | No | Start date for search (YYYY-MM-DD). Note: Timestamps may be unreliable. | |
| endDate | No | End date for search (YYYY-MM-DD). Note: Timestamps may be unreliable. | |
| searchType | No | Focus search on specific content types. Use "project" for project-specific searches that leverage file path context. | all |
| maxResults | No | Maximum number of conversations to return | |
| includeCode | No | Include code blocks in search results | |
| outputMode | No | Output format: "json" for formatted JSON (default), "compact-json" for minified JSON | json |
Implementation Reference
- src/tools/conversation-tools.ts:570-855 (handler)Core handler function implementing the search_conversations tool. Handles input validation, database connection, various search modes (query, keywords, LIKE patterns, project search), date filtering, relevance scoring, and formats conversation summaries with match details.export async function searchConversations(input: SearchConversationsInput): Promise<SearchConversationsOutput> { const validatedInput = searchConversationsSchema.parse(input); const dbPath = process.env.CURSOR_DB_PATH || detectCursorDatabasePath(); const reader = new CursorDatabaseReader({ dbPath }); try { await reader.connect(); // Determine the search query for display purposes const displayQuery = validatedInput.query || (validatedInput.keywords ? validatedInput.keywords.join(` ${validatedInput.keywordOperator} `) : '') || validatedInput.likePattern || 'advanced search'; if (validatedInput.projectSearch && validatedInput.query) { // Handle project search (existing logic) const searchOptions = { fuzzyMatch: validatedInput.fuzzyMatch, includePartialPaths: validatedInput.includePartialPaths, includeFileContent: validatedInput.includeFileContent, minRelevanceScore: validatedInput.minRelevanceScore, orderBy: validatedInput.orderBy, limit: validatedInput.maxResults }; const conversationIds = await reader.getConversationIds({ format: validatedInput.format, projectPath: validatedInput.query }); const conversations = []; const matchTypeDistribution = { exactPath: 0, partialPath: 0, filePath: 0, fuzzy: 0 }; let totalConversationsScanned = 0; let totalRelevanceScore = 0; for (const composerId of conversationIds.slice(0, validatedInput.maxResults * 2)) { try { totalConversationsScanned++; const conversation = await reader.getConversationById(composerId); if (!conversation) continue; const format = conversation.hasOwnProperty('_v') ? 'modern' : 'legacy'; if (format === 'modern') { const modernConv = conversation as any; const headers = modernConv.fullConversationHeadersOnly || []; for (const header of headers.slice(0, 5)) { try { const bubbleMessage = await reader.getBubbleMessage(modernConv.composerId, header.bubbleId); if (bubbleMessage) { (conversation as any).resolvedMessages = (conversation as any).resolvedMessages || []; (conversation as any).resolvedMessages.push(bubbleMessage); } } catch (error) { continue; } } } const relevanceResult = calculateEnhancedProjectRelevance( conversation, validatedInput.query, { fuzzyMatch: validatedInput.fuzzyMatch || false, includePartialPaths: validatedInput.includePartialPaths || false, includeFileContent: validatedInput.includeFileContent || false } ); if (relevanceResult.score >= (validatedInput.minRelevanceScore || 0.1)) { const summary = await reader.getConversationSummary(composerId, { includeFirstMessage: true, maxFirstMessageLength: 150 }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles || [], attachedFolders: summary.attachedFolders || [], firstMessage: summary.firstMessage, size: summary.conversationSize, relevanceScore: relevanceResult.score, matchDetails: relevanceResult.details }); totalRelevanceScore += relevanceResult.score; if (relevanceResult.details.exactPathMatch) matchTypeDistribution.exactPath++; if (relevanceResult.details.partialPathMatch) matchTypeDistribution.partialPath++; if (relevanceResult.details.filePathMatch) matchTypeDistribution.filePath++; if (relevanceResult.details.fuzzyMatch) matchTypeDistribution.fuzzy++; } } } catch (error) { continue; } } if (validatedInput.orderBy === 'relevance') { conversations.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0)); } return { conversations: conversations.slice(0, validatedInput.maxResults), totalResults: conversations.length, query: displayQuery, searchOptions: { includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType, format: validatedInput.format, highlightMatches: validatedInput.highlightMatches, projectSearch: validatedInput.projectSearch, fuzzyMatch: validatedInput.fuzzyMatch, includePartialPaths: validatedInput.includePartialPaths, includeFileContent: validatedInput.includeFileContent, minRelevanceScore: validatedInput.minRelevanceScore, orderBy: validatedInput.orderBy }, debugInfo: { totalConversationsScanned, averageRelevanceScore: totalConversationsScanned > 0 ? totalRelevanceScore / totalConversationsScanned : 0, matchTypeDistribution } }; } else { const hasSearchCriteria = (validatedInput.query && validatedInput.query.trim() !== '' && validatedInput.query.trim() !== '?') || validatedInput.keywords || validatedInput.likePattern; if (!hasSearchCriteria && (validatedInput.startDate || validatedInput.endDate)) { // Date-only search: get all conversations and filter by date const allConversationIds = await reader.getConversationIds({ format: validatedInput.format }); const conversations = []; for (const composerId of allConversationIds.slice(0, validatedInput.maxResults * 2)) { try { const conversation = await reader.getConversationById(composerId); if (!conversation) continue; // Apply date filtering const hasValidDate = checkConversationDateRange( conversation, validatedInput.startDate, validatedInput.endDate ); if (!hasValidDate) continue; const summary = await reader.getConversationSummary(composerId, { includeFirstMessage: true, maxFirstMessageLength: 150, includeTitle: true, includeAIGeneratedSummary: true }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles || [], attachedFolders: summary.attachedFolders || [], firstMessage: summary.firstMessage, title: summary.title, aiGeneratedSummary: summary.aiGeneratedSummary, size: summary.conversationSize }); if (conversations.length >= validatedInput.maxResults) break; } } catch (error) { console.error(`Failed to process conversation ${composerId}:`, error); } } return { conversations, totalResults: conversations.length, query: displayQuery, searchOptions: { includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType, format: validatedInput.format, highlightMatches: validatedInput.highlightMatches } }; } // Handle enhanced search with keywords, LIKE patterns, or simple query const searchResults = await reader.searchConversationsEnhanced({ query: validatedInput.query, keywords: validatedInput.keywords, keywordOperator: validatedInput.keywordOperator, likePattern: validatedInput.likePattern, includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType === 'project' ? 'all' : validatedInput.searchType, format: validatedInput.format, startDate: validatedInput.startDate, endDate: validatedInput.endDate }); // Convert search results to conversation summaries for consistency const conversations = []; for (const result of searchResults) { try { // Apply date filtering if specified (post-query filtering due to unreliable timestamps) if (validatedInput.startDate || validatedInput.endDate) { const conversation = await reader.getConversationById(result.composerId); if (!conversation) continue; const hasValidDate = checkConversationDateRange( conversation, validatedInput.startDate, validatedInput.endDate ); if (!hasValidDate) continue; } const summary = await reader.getConversationSummary(result.composerId, { includeFirstMessage: true, maxFirstMessageLength: 150, includeTitle: true, includeAIGeneratedSummary: true }); if (summary) { conversations.push({ composerId: summary.composerId, format: summary.format, messageCount: summary.messageCount, hasCodeBlocks: summary.hasCodeBlocks, relevantFiles: summary.relevantFiles || [], attachedFolders: summary.attachedFolders || [], firstMessage: summary.firstMessage, title: summary.title, aiGeneratedSummary: summary.aiGeneratedSummary, size: summary.conversationSize }); } } catch (error) { console.error(`Failed to get summary for conversation ${result.composerId}:`, error); } } return { conversations, totalResults: conversations.length, query: displayQuery, searchOptions: { includeCode: validatedInput.includeCode, contextLines: validatedInput.contextLines, maxResults: validatedInput.maxResults, searchBubbles: validatedInput.searchBubbles, searchType: validatedInput.searchType, format: validatedInput.format, highlightMatches: validatedInput.highlightMatches } }; } } finally { reader.close(); } }
- src/server.ts:178-233 (registration)MCP server registration of the 'search_conversations' tool, defining description, Zod input schema, and thin wrapper handler that prepares input and calls the core searchConversations function.server.tool( 'search_conversations', 'Searches through Cursor chat content using exact text matching (NOT semantic search) to find relevant discussions. **WARNING: For project-specific searches, use list_conversations with projectPath instead of this tool!** This tool is for searching message content, not project filtering.\n\n**WHEN TO USE THIS TOOL:**\n- Searching for specific technical terms in message content (e.g., "useState", "async/await")\n- Finding conversations mentioning specific error messages\n- Searching for code patterns or function names\n\n**WHEN NOT TO USE THIS TOOL:**\n- ❌ DON\'T use query="project-name" - use list_conversations with projectPath instead\n- ❌ DON\'T search for project names in message content\n- ❌ DON\'T use this for project-specific filtering\n\nSearch methods (all use exact/literal text matching):\n1. Simple text matching: Use query parameter for literal string matching (e.g., "react hooks")\n2. Multi-keyword: Use keywords array with keywordOperator for exact matching\n3. LIKE patterns: Advanced pattern matching with SQL wildcards (% = any chars, _ = single char)\n4. Date range: Filter by message timestamps (YYYY-MM-DD format)\n\nIMPORTANT: When using date filters, call get_system_info first to know today\'s date.\n\nExamples: likePattern="%useState(%" for function calls, keywords=["typescript","interface"] with AND operator.', { query: z.string().optional().describe('Exact text matching - searches for literal string occurrences in MESSAGE CONTENT (e.g., "react hooks", "useState", "error message"). ❌ DON\'T use for project names - use list_conversations with projectPath instead!'), keywords: z.array(z.string().min(1)).optional().describe('Array of keywords for exact text matching - use with keywordOperator to find conversations with specific combinations'), keywordOperator: z.enum(['AND', 'OR']).optional().default('OR').describe('How to combine keywords: "AND" = all keywords must be present, "OR" = any keyword can be present'), likePattern: z.string().optional().describe('SQL LIKE pattern for advanced searches - use % for any characters, _ for single character. Examples: "%useState(%" for function calls, "%.tsx%" for file types'), startDate: z.string().optional().describe('Start date for search (YYYY-MM-DD). Note: Timestamps may be unreliable.'), endDate: z.string().optional().describe('End date for search (YYYY-MM-DD). Note: Timestamps may be unreliable.'), searchType: z.enum(['all', 'project', 'files', 'code']).optional().default('all').describe('Focus search on specific content types. Use "project" for project-specific searches that leverage file path context.'), maxResults: z.number().min(1).max(50).optional().default(10).describe('Maximum number of conversations to return'), includeCode: z.boolean().optional().default(true).describe('Include code blocks in search results'), outputMode: z.enum(['json', 'compact-json']).optional().default('json').describe('Output format: "json" for formatted JSON (default), "compact-json" for minified JSON') }, async (input) => { try { const hasSearchCriteria = (input.query && input.query.trim() !== '' && input.query.trim() !== '?') || input.keywords || input.likePattern; const hasDateFilter = input.startDate || input.endDate; const hasOtherFilters = input.searchType !== 'all'; if (!hasSearchCriteria && !hasDateFilter && !hasOtherFilters) { throw new Error('At least one search criteria (query, keywords, likePattern), date filter (startDate, endDate), or search type filter must be provided'); } const fullInput = { ...input, contextLines: 2, searchBubbles: true, format: 'both' as const, highlightMatches: true, projectSearch: input.searchType === 'project', fuzzyMatch: input.searchType === 'project', includePartialPaths: input.searchType === 'project', includeFileContent: false, minRelevanceScore: 0.1, orderBy: 'recency' as const }; const result = await searchConversations(fullInput); return { content: [{ type: 'text', text: formatResponse(result, input.outputMode) }] }; } catch (error) { return { content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}` }] }; } } );
- Zod schema for validating inputs to the searchConversations function, supporting query, multi-keyword, LIKE patterns, date ranges, and various search options.export const searchConversationsSchema = z.object({ // Simple query (existing - backward compatible) query: z.string().optional(), // Multi-keyword search keywords: z.array(z.string().min(1)).optional(), keywordOperator: z.enum(['AND', 'OR']).optional().default('OR'), // LIKE pattern search (database-level) likePattern: z.string().optional(), // Date filtering startDate: z.string().optional(), endDate: z.string().optional(), // Existing options includeCode: z.boolean().optional().default(true), contextLines: z.number().min(0).max(10).optional().default(2), maxResults: z.number().min(1).max(100).optional().default(10), searchBubbles: z.boolean().optional().default(true), searchType: z.enum(['all', 'summarization', 'code', 'files', 'project']).optional().default('all'), format: z.enum(['legacy', 'modern', 'both']).optional().default('both'), highlightMatches: z.boolean().optional().default(true), projectSearch: z.boolean().optional().default(false), fuzzyMatch: z.boolean().optional().default(false), includePartialPaths: z.boolean().optional().default(true), includeFileContent: z.boolean().optional().default(false), minRelevanceScore: z.number().min(0).max(1).optional().default(0.1), orderBy: z.enum(['relevance', 'recency']).optional().default('relevance') }).refine( (data) => { const hasSearchCriteria = (data.query && data.query.trim() !== '' && data.query.trim() !== '?') || data.keywords || data.likePattern; const hasDateFilter = data.startDate || data.endDate; const hasOtherFilters = data.searchType !== 'all'; return hasSearchCriteria || hasDateFilter || hasOtherFilters; }, { message: "At least one search criteria (query, keywords, likePattern), date filter (startDate, endDate), or search type filter must be provided" } );
- Helper function for computing relevance scores during project-based searches, handling exact/partial/fuzzy matches on paths, files, folders, and content.function calculateEnhancedProjectRelevance( conversation: any, projectQuery: string, options: { fuzzyMatch: boolean; includePartialPaths: boolean; includeFileContent: boolean; } ): { score: number; details: { exactPathMatch: boolean; partialPathMatch: boolean; filePathMatch: boolean; fuzzyMatch: boolean; matchedPaths: string[]; matchedFiles: string[]; }; } { let score = 0; const details = { exactPathMatch: false, partialPathMatch: false, filePathMatch: false, fuzzyMatch: false, matchedPaths: [] as string[], matchedFiles: [] as string[] }; const queryLower = projectQuery.toLowerCase(); const queryParts = queryLower.split(/[-_\s]+/); // Split on common separators // Helper function for fuzzy matching const fuzzyMatch = (text: string, query: string): number => { const textLower = text.toLowerCase(); // Exact match if (textLower.includes(query)) return 10; // Check if all query parts are present const allPartsPresent = queryParts.every(part => textLower.includes(part)); if (allPartsPresent) return 8; // Check for partial matches const partialMatches = queryParts.filter(part => textLower.includes(part)).length; if (partialMatches > 0) return (partialMatches / queryParts.length) * 6; // Levenshtein-like similarity for very fuzzy matching const similarity = calculateSimilarity(textLower, query); if (similarity > 0.6) return similarity * 4; return 0; }; // Helper function to process files and folders const processFiles = (files: string[], scoreMultiplier: number = 1) => { if (!files || !Array.isArray(files)) return; for (const file of files) { if (typeof file === 'string') { const fileName = file.split('/').pop() || file; const filePath = file.toLowerCase(); const fileNameLower = fileName.toLowerCase(); // Check if file path contains project query if (filePath.includes(queryLower)) { score += 10 * scoreMultiplier; details.filePathMatch = true; details.matchedFiles.push(file); } // Check file name else if (fileNameLower.includes(queryLower)) { score += 8 * scoreMultiplier; details.filePathMatch = true; details.matchedFiles.push(file); } // Fuzzy match on file paths else if (options.fuzzyMatch) { const fuzzyScore = Math.max( fuzzyMatch(file, queryLower), fuzzyMatch(fileName, queryLower) ); if (fuzzyScore > 0) { score += fuzzyScore * 0.5 * scoreMultiplier; // Lower weight for file matches details.fuzzyMatch = true; details.matchedFiles.push(file); } } } } }; const processFolders = (folders: string[], scoreMultiplier: number = 1) => { if (!folders || !Array.isArray(folders)) return; for (const folder of folders) { if (typeof folder === 'string') { const folderName = folder.split('/').pop() || folder; // Get last part of path const folderLower = folder.toLowerCase(); // Exact path match if (folderLower === queryLower || folderName.toLowerCase() === queryLower) { score += 20 * scoreMultiplier; details.exactPathMatch = true; details.matchedPaths.push(folder); } // Partial path match else if (options.includePartialPaths && (folderLower.includes(queryLower) || folderName.toLowerCase().includes(queryLower))) { score += 15 * scoreMultiplier; details.partialPathMatch = true; details.matchedPaths.push(folder); } // Fuzzy match else if (options.fuzzyMatch) { const fuzzyScore = Math.max( fuzzyMatch(folder, queryLower), fuzzyMatch(folderName, queryLower) ); if (fuzzyScore > 0) { score += fuzzyScore * scoreMultiplier; details.fuzzyMatch = true; details.matchedPaths.push(folder); } } } } }; // Check top-level attachedFoldersNew and relevantFiles (legacy format) processFolders(conversation.attachedFoldersNew); processFiles(conversation.relevantFiles); // Check legacy conversation messages if (conversation.conversation && Array.isArray(conversation.conversation)) { for (const message of conversation.conversation) { processFolders(message.attachedFoldersNew, 0.8); processFiles(message.relevantFiles, 0.8); // Check message content if enabled if (options.includeFileContent && message.text && typeof message.text === 'string') { const textLower = message.text.toLowerCase(); if (textLower.includes(queryLower)) { score += 2; // Lower weight for content matches } } } } // Check modern format messages (this is the key fix!) if (conversation.messages && Array.isArray(conversation.messages)) { for (const message of conversation.messages) { processFolders(message.attachedFolders, 0.8); processFiles(message.relevantFiles, 0.8); // Check message content if enabled if (options.includeFileContent && message.text && typeof message.text === 'string') { const textLower = message.text.toLowerCase(); if (textLower.includes(queryLower)) { score += 2; // Lower weight for content matches } } } } // Check modern format bubbles for additional context if (conversation._v && conversation.bubbles && Array.isArray(conversation.bubbles)) { for (const bubble of conversation.bubbles) { processFolders(bubble.attachedFoldersNew, 0.5); processFiles(bubble.relevantFiles, 0.5); } } return { score: Math.max(score, 0), details }; }
- Helper function for date range filtering on conversations by checking message timestamps.function checkConversationDateRange(conversation: any, startDate?: string, endDate?: string): boolean { if (!startDate && !endDate) return true; const start = startDate ? new Date(startDate) : new Date('1970-01-01'); const end = endDate ? new Date(endDate) : new Date(); // Check if conversation is legacy or modern format const isLegacy = conversation.conversation && Array.isArray(conversation.conversation); if (isLegacy) { // Legacy format: check timestamps in conversation.conversation array for (const message of conversation.conversation) { if (message.timestamp) { const messageDate = new Date(message.timestamp); if (messageDate >= start && messageDate <= end) { return true; } } } } else { // Modern format: would need to resolve bubble messages to check timestamps // For now, return true to include all modern conversations when date filtering // since resolving all bubble messages would be too expensive return true; } // If no valid timestamps found, include the conversation return true; }