search
Search across files with full-text keyword queries. Get ranked matches including excerpts and tags for instant snippet previews.
Instructions
Full-text (SQLite FTS5) keyword search across files. Returns ranked matches with inline match_excerpt and title_highlight (no follow-up read_file needed for snippets) plus tags, est_tokens, size_bytes, and aggregate total_est_tokens. Read-only; no side effects, auth, or rate limits. FTS is tokenised: it WILL miss URLs, hyphenated terms, and partial substrings — fall back to regex_search for those. project_id: null searches only the KB; omit the field to span everything; tags[] requires ALL listed tags to match. For prompt-ready bundled bodies use bundle_search.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Search query | |
| project_id | No | Filter by project ID. Pass null to search ONLY Knowledge Base files. | |
| tags | No | Filter by tags (all must match) | |
| favorite | No | Filter by favorite status |
Implementation Reference
- apps/mcp/src/index.ts:582-604 (registration)MCP server tool registration for 'search' — defines the tool name, description, Zod schema for inputs (query, project_id, tags, favorite), and handler that calls the core search function.
server.tool( "search", "Full-text (SQLite FTS5) keyword search across files. Returns ranked matches with inline match_excerpt and title_highlight (no follow-up `read_file` needed for snippets) plus tags, est_tokens, size_bytes, and aggregate `total_est_tokens`. Read-only; no side effects, auth, or rate limits. FTS is tokenised: it WILL miss URLs, hyphenated terms, and partial substrings — fall back to `regex_search` for those. `project_id: null` searches only the KB; omit the field to span everything; `tags[]` requires ALL listed tags to match. For prompt-ready bundled bodies use `bundle_search`.", { query: z.string().describe("Search query"), project_id: z.number().nullable().optional().describe("Filter by project ID. Pass null to search ONLY Knowledge Base files."), tags: z.array(z.string()).optional().describe("Filter by tags (all must match)"), favorite: z.boolean().optional().describe("Filter by favorite status"), }, async ({ query, project_id, tags, favorite }) => { const filters: any = { query }; if (project_id !== undefined) filters.project_id = project_id; if (tags !== undefined) filters.tags = tags; if (favorite !== undefined) filters.favorite = favorite; const result = search(filters); const annotated = attachTags(result.map(annotateTokens)); const total_est_tokens = annotated.reduce((s, f) => s + (f.est_tokens ?? 0), 0); return { content: [{ type: "text", text: JSON.stringify({ matches: annotated, total_est_tokens }, null, 2) }], }; } ); - Core 'search' function — executes FTS5 full-text query against SQLite with optional filters for project_id, tags, and favorite. Returns ranked file records with match excerpts and title highlights.
export function search(filters: SearchFilters): FileRecordWithRank[] { const db = getDatabase(); // snippet(fts_index, 1, ...) targets the `content` column (column 1; title // is column 0). Markers are agent-parsable triple-bracket pairs that are // unlikely to collide with real markdown content. let sql = ` SELECT files.*, fts_index.rank, snippet(fts_index, 1, '<<<', '>>>', '…', 16) AS match_excerpt, highlight(fts_index, 0, '<<<', '>>>') AS title_highlight FROM fts_index JOIN files ON files.id = fts_index.rowid WHERE fts_index MATCH ? `; const params: any[] = [filters.query]; if (filters.project_id !== undefined) { sql += " AND files.project_id = ?"; params.push(filters.project_id); } if (filters.favorite !== undefined && filters.favorite) { sql += " AND EXISTS (SELECT 1 FROM favorites WHERE favorites.file_id = files.id)"; } if (filters.tags !== undefined && filters.tags.length > 0) { // All tags must be present (AND condition) for (const tag of filters.tags) { sql += ` AND EXISTS ( SELECT 1 FROM file_tags JOIN tags ON tags.id = file_tags.tag_id WHERE file_tags.file_id = files.id AND tags.name = ? )`; params.push(tag); } } sql += " ORDER BY rank LIMIT 50"; const stmt = db.prepare(sql); return stmt.all(...params) as FileRecordWithRank[]; } - packages/core/src/types.ts:64-69 (schema)SearchFilters interface defining the schema for the search function's filters: query (string), project_id (optional number), tags (optional string array), favorite (optional boolean).
export interface SearchFilters { query: string; project_id?: number; tags?: string[]; favorite?: boolean; } - packages/core/src/index.ts:6-8 (registration)Core package exports — re-exports the 'search' function from the metadata module.
export { addTags, removeTags, setFavorite, search, registerProject, unregisterProject, discoverFiles, listTags, listProjects, - bundleSearch function — calls the core 'search' function (line 104) then bundles matched file contents into a prompt-ready format.
export async function bundleSearch( filters: SearchFilters, opts: BundleOptions = {} ): Promise<BundleResult> { const format: BundleFormat = opts.format ?? "xml"; const max_tokens = opts.max_tokens ?? 50000; const hits = search(filters); const included: BundleIncludedItem[] = []; const skipped: BundleSkippedItem[] = []; const docs: DocFields[] = [];