search_conversations
Search past conversations and project discussions using full-text queries. Find mentions of topics, decisions, or code changes with support for AND, OR, NOT, and exact phrases.
Instructions
Search conversations using full-text search. Defaults to the current project. Set allProjects=true to search across all projects, or pass a specific projectId. Use this when the user asks "did we discuss X before?" or references past work, decisions, or code changes.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search query (supports FTS5: AND, OR, NOT, "exact phrases") | |
| projectId | No | Search a specific project (defaults to current) | |
| allProjects | No | Search across ALL projects instead of just the current one | |
| limit | No | Max results (default 10) |
Implementation Reference
- server/src/mcp/tools.js:250-310 (handler)The main tool handler for 'search_conversations'. Registers the tool with server.tool(), defines the Zod schema (query, projectId, allProjects, limit), calls searchDb.search() with the resolved project scope, formats results as structured JSON with session info and snippets, and returns them as text content. On error, returns an error response.
// ─── search_conversations ──────────────────────────────────────── server.tool( 'search_conversations', 'Search conversations using full-text search. Defaults to the current project. Set allProjects=true to search across all projects, or pass a specific projectId. Use this when the user asks "did we discuss X before?" or references past work, decisions, or code changes.', { query: z.string().describe('Search query (supports FTS5: AND, OR, NOT, "exact phrases")'), projectId: z.string().optional().describe('Search a specific project (defaults to current)'), allProjects: z.boolean().default(false).optional().describe('Search across ALL projects instead of just the current one'), limit: z.number().min(1).max(50).default(10).optional().describe('Max results (default 10)'), }, async ({ query, projectId, allProjects = false, limit = 10 }) => { try { const { searchDb, currentProjectId } = await getServices() // Determine search scope const searchProjectId = allProjects ? undefined : (projectId || currentProjectId) const scopeLabel = allProjects ? 'all projects' : `project "${searchProjectId}"` const results = await searchDb.search({ query, projectId: searchProjectId, limit, offset: 0, }) if (!results.hits || results.hits.length === 0) { return { content: [{ type: 'text', text: `No results found for "${query}" in ${scopeLabel}. ${!allProjects ? 'Try allProjects=true to search everywhere.' : ''}`, }], } } const formatted = results.hits.map(hit => ({ project: hit.projectName, session: hit.sessionTitle || hit.sessionId, sessionId: hit.sessionId, projectId: hit.projectId, role: hit.role, snippet: hit.snippet, timestamp: hit.timestamp, score: hit.score, })) return { content: [{ type: 'text', text: `Found ${results.total} results for "${query}" in ${scopeLabel} (showing ${formatted.length}):\n\n${JSON.stringify(formatted, null, 2)}`, }], } } catch (error) { return { content: [{ type: 'text', text: `Search error: ${error.message}` }], isError: true, } } } ) - server/src/mcp/tools.js:250-259 (schema)Zod schema for search_conversations input: 'query' (required string with FTS5 support), 'projectId' (optional string), 'allProjects' (optional boolean, default false), 'limit' (optional number 1-50, default 10).
// ─── search_conversations ──────────────────────────────────────── server.tool( 'search_conversations', 'Search conversations using full-text search. Defaults to the current project. Set allProjects=true to search across all projects, or pass a specific projectId. Use this when the user asks "did we discuss X before?" or references past work, decisions, or code changes.', { query: z.string().describe('Search query (supports FTS5: AND, OR, NOT, "exact phrases")'), projectId: z.string().optional().describe('Search a specific project (defaults to current)'), allProjects: z.boolean().default(false).optional().describe('Search across ALL projects instead of just the current one'), limit: z.number().min(1).max(50).default(10).optional().describe('Max results (default 10)'), }, - server/src/mcp/index.js:106-106 (registration)The tool is registered via registerTools(server, getServices) call in the main MCP server entry point (index.js). This wires the tools.js module into the McpServer instance.
registerTools(server, getServices) - The SearchDatabase.search() method that performs the actual FTS5 full-text search on the SQLite messages_fts table. Supports filtering by projectId, role, date range, and returns BM25-relevance-scored results with snippets.
async search({ query, projectId, role, from, to, limit = 50, offset = 0 }) { // Escape FTS5 query to handle terms like "ETL" that could be interpreted as column names const escapedQuery = this.escapeFTS5Query(query); let searchSQL = ` SELECT project_id, project_name, session_id, session_title, message_id, role, snippet(messages_fts, 6, '<mark>', '</mark>', '...', 64) as snippet, timestamp, file_path, line_number, template, bm25(messages_fts) as relevance_score FROM messages_fts WHERE messages_fts MATCH ? ` const params = [escapedQuery] // Add filters if (projectId) { searchSQL += ` AND project_id = ?` params.push(projectId) } if (role) { searchSQL += ` AND role = ?` params.push(role) } if (from) { searchSQL += ` AND datetime(timestamp) >= datetime(?)` params.push(from) } if (to) { searchSQL += ` AND datetime(timestamp) <= datetime(?)` params.push(to) } // Order by relevance searchSQL += ` ORDER BY bm25(messages_fts) LIMIT ? OFFSET ?` params.push(limit, offset) const results = await this.db.all(searchSQL, params) // Get total count let countSQL = `SELECT COUNT(*) as total FROM messages_fts WHERE messages_fts MATCH ?` const countParams = [escapedQuery] if (projectId) { countSQL += ` AND project_id = ?` countParams.push(projectId) } if (role) { countSQL += ` AND role = ?` countParams.push(role) } const countResult = await this.db.get(countSQL, countParams) return { hits: results.map(row => ({ projectId: row.project_id, projectName: row.project_name, sessionId: row.session_id, sessionTitle: row.session_title, messageId: row.message_id, role: row.role, snippet: row.snippet.replace(/<mark>/g, '**').replace(/<\/mark>/g, '**'), // Convert to markdown timestamp: row.timestamp, score: Math.abs(row.relevance_score), // BM25 scores are negative line: row.line_number, template: row.template })), total: countResult.total, query, filters: { projectId, role, from, to } } } - bin/claudex-mcp.js:37-37 (registration)CLI help text listing 'search_conversations' as one of the available tools, confirming the tool is exposed via the MCP server.
search_conversations Full-text search across conversations