search_tiddlers
Search TiddlyWiki tiddlers using filter expressions, semantic similarity, or a hybrid combination to find entries by tag, date, title, or conceptual meaning. Returns matching tiddlers with metadata and optional text content.
Instructions
Search tiddlers using filter syntax, semantic similarity, or both. Supports filter-based queries (e.g., by tag, date, title), semantic/conceptual search, and hybrid combinations. Returns matching tiddlers with metadata and optionally text content.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| semantic | No | Natural language semantic search query (e.g., "times I felt anxious about parenting", "entries about work stress"). Finds conceptually related entries even without exact keyword matches. | |
| filter | No | TiddlyWiki filter expression (e.g., "[tag[Journal]prefix[2025-11]]" for November 2025 journal entries, "[title[2025-11-12]]" for specific entry). Can be used alone for filter-based search, or combined with semantic for hybrid search. | |
| includeText | No | Include text content in results (default: false). Set to true to get full tiddler content. | |
| offset | No | Number of results to skip for pagination (default: 0). Only applies to filter-based search. | |
| limit | No | Maximum number of results to return (default: 10 for semantic search, unlimited for filter-only, max: 100). Use for pagination to avoid response size limits. |
Implementation Reference
- src/tools/search-tiddlers.ts:65-236 (handler)Main handler function for the search_tiddlers tool. Supports three modes: filter-only (pure TiddlyWiki filter expressions), semantic-only (similarity search), and hybrid (filter results re-ranked by semantic similarity). Handles token counting, pagination, and response size validation.
export async function handleSearchTiddlers( args: unknown, deps: ToolDependencies ): Promise<ToolResult> { const input = SearchTiddlersInput.parse(args); const includeText = input.includeText ?? false; const hasSemantic = input.semantic !== undefined; const hasFilter = input.filter !== undefined; // Filter-only mode if (hasFilter && !hasSemantic) { const offset = input.offset ?? 0; const limit = input.limit; const filter = input.filter!; const results = await queryTiddlers(filter, includeText, offset, limit); // Validate response size const sizeError = validateResponseSize(results, filter, includeText); if (sizeError) { return { content: [{ type: 'text', text: sizeError }], isError: true, }; } return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], }; } // Semantic mode (with optional filter) if (hasSemantic) { const { embeddingsDB, ollamaClient, syncWorker } = deps; // Check if embeddings infrastructure is available if (!embeddingsDB || !ollamaClient) { return { content: [ { type: 'text', text: JSON.stringify( { error: 'Semantic search is not available', reason: 'Embeddings database or Ollama client not initialized', suggestion: 'Check server logs for initialization errors', }, null, 2 ), }, ], isError: true, }; } // Check if any tiddlers have been indexed const indexedCount = embeddingsDB.getIndexedTiddlersCount(); if (indexedCount === 0) { return { content: [ { type: 'text', text: JSON.stringify( { error: 'No tiddlers have been indexed yet', suggestion: 'The sync worker is still indexing entries. Please wait a few minutes and try again.', status: syncWorker?.getStatus() || 'unknown', }, null, 2 ), }, ], isError: true, }; } // Generate embedding for the query with search_query prefix const semantic = input.semantic!; const queryEmbedding = await ollamaClient.generateQueryEmbedding(semantic); // Search for similar entries const limit = input.limit || 10; const results = embeddingsDB.searchSimilar(queryEmbedding, limit); // Apply optional TiddlyWiki filter for hybrid search let filteredResults = results; if (hasFilter) { const filter = input.filter!; const filterMatches = await queryTiddlers(filter, false); const filterTitles = new Set(filterMatches.map((t) => t.title)); filteredResults = results.filter((r) => filterTitles.has(r.tiddler_title)); } // Fetch full tiddlers if includeText is true const formattedResults = await Promise.all( filteredResults.map(async (r) => { const result: Record<string, unknown> = { tiddler_title: r.tiddler_title, chunk_id: r.chunk_id, similarity_score: (1 - r.distance).toFixed(4), // Convert distance to similarity created: r.created, modified: r.modified, tags: r.tags, }; // Fetch full tiddler text if requested if (includeText) { const fullTiddler = await getTiddler(r.tiddler_title); if (fullTiddler) { result.text = fullTiddler.text; result.type = fullTiddler.type; } } return result; }) ); // Validate response size const responseJson = JSON.stringify(formattedResults, null, 2); const tokenCount = countTokens(responseJson); if (tokenCount > MAX_RESPONSE_TOKENS) { const avgTokensPerItem = tokenCount / formattedResults.length; const suggestedLimit = Math.floor(MAX_RESPONSE_TOKENS / avgTokensPerItem); const filterParam = hasFilter ? `,\n filter: "${input.filter!}"` : ''; const errorMessage = `Semantic search matched ${formattedResults.length} results but response would be ${tokenCount.toLocaleString()} tokens (exceeds ${MAX_RESPONSE_TOKENS.toLocaleString()} token limit). To retrieve results, use the limit parameter. **Suggested query:** \`\`\` search_tiddlers({ semantic: "${semantic}", includeText: ${includeText}, limit: ${suggestedLimit}${filterParam} }) \`\`\` Note: Semantic search returns results ordered by similarity, so using a lower limit will return the most relevant matches.`; return { content: [{ type: 'text', text: errorMessage }], isError: true, }; } return { content: [ { type: 'text', text: JSON.stringify( { query: semantic, total_results: formattedResults.length, indexed_tiddlers: indexedCount, results: formattedResults, }, null, 2 ), }, ], }; } // Should never reach here due to Zod validation throw new Error('Either semantic or filter must be provided'); } - src/tools/types.ts:32-68 (schema)Zod schema defining the input shape for search_tiddlers. Validates parameters: semantic (string), filter (string), includeText (boolean), offset (non-negative int), limit (1-100 int). Requires at least one of semantic or filter.
export const SearchTiddlersInput = z .object({ semantic: z .string() .optional() .describe( 'Natural language semantic search query (e.g., "times I felt anxious about parenting")' ), filter: z .string() .optional() .describe( 'TiddlyWiki filter expression (e.g., "[tag[Journal]prefix[2025-11]]"). Can be used alone for filter-based search, or combined with semantic for hybrid search.' ), includeText: z .boolean() .optional() .describe('Include text content in results (default: false)'), offset: z .number() .int() .min(0) .optional() .describe('Number of results to skip (default: 0). Only applies to filter-based search.'), limit: z .number() .int() .min(1) .max(100) .optional() .describe( 'Maximum number of results to return (default: 10 for semantic, unlimited for filter, max: 100)' ), }) .refine((data) => data.semantic !== undefined || data.filter !== undefined, { message: 'At least one of semantic or filter must be provided', }); - src/index.ts:116-152 (registration)Registration of search_tiddlers as an MCP tool in the ListToolsRequestSchema handler. Includes tool name, description, and JSON Schema input schema.
{ name: 'search_tiddlers', description: 'Search tiddlers using filter syntax, semantic similarity, or both. Supports filter-based queries (e.g., by tag, date, title), semantic/conceptual search, and hybrid combinations. Returns matching tiddlers with metadata and optionally text content.', inputSchema: { type: 'object', properties: { semantic: { type: 'string', description: 'Natural language semantic search query (e.g., "times I felt anxious about parenting", "entries about work stress"). Finds conceptually related entries even without exact keyword matches.', }, filter: { type: 'string', description: 'TiddlyWiki filter expression (e.g., "[tag[Journal]prefix[2025-11]]" for November 2025 journal entries, "[title[2025-11-12]]" for specific entry). Can be used alone for filter-based search, or combined with semantic for hybrid search.', }, includeText: { type: 'boolean', description: 'Include text content in results (default: false). Set to true to get full tiddler content.', default: false, }, offset: { type: 'number', description: 'Number of results to skip for pagination (default: 0). Only applies to filter-based search.', default: 0, }, limit: { type: 'number', description: 'Maximum number of results to return (default: 10 for semantic search, unlimited for filter-only, max: 100). Use for pagination to avoid response size limits.', }, }, }, }, - src/index.ts:244-245 (registration)Routing of 'search_tiddlers' tool calls in the CallToolRequestSchema handler. Delegates to handleSearchTiddlers with dependencies injected.
case 'search_tiddlers': return await handleSearchTiddlers(args, getToolDependencies()); - src/tools/search-tiddlers.ts:23-56 (helper)Helper function that validates response size against a 23k token limit. Returns null if OK, or an error message with pagination suggestion if too large.
function validateResponseSize( results: unknown[], filter: string, includeText: boolean ): string | null { const responseJson = JSON.stringify(results, null, 2); const tokenCount = countTokens(responseJson); if (tokenCount <= MAX_RESPONSE_TOKENS) { return null; // Response is fine } // Calculate how many items would fit const avgTokensPerItem = tokenCount / results.length; const suggestedLimit = Math.floor(MAX_RESPONSE_TOKENS / avgTokensPerItem); const errorMessage = `Query matched ${results.length} tiddlers but response would be ${tokenCount.toLocaleString()} tokens (exceeds ${MAX_RESPONSE_TOKENS.toLocaleString()} token limit). To retrieve results, use the limit parameter with offset for pagination. **Suggested query:** \`\`\` query_tiddlers({ filter: "${filter}", includeText: ${includeText}, limit: ${suggestedLimit}, offset: 0 }) \`\`\` Then increment offset by ${suggestedLimit} for subsequent batches (offset: ${suggestedLimit}, offset: ${suggestedLimit * 2}, etc.) until you've retrieved all ${results.length} tiddlers.`; return errorMessage; }