Search ProsodyAI docs
search_docsSearch across ProsodyAI documentation, SDKs, recipes, and API reference. Get ranked results with snippets and stable IDs to retrieve full content via read_doc.
Instructions
Search ProsodyAI docs, SDK READMEs, recipes, and OpenAPI metadata. Returns a ranked list of matches with snippets and stable ids. Follow up with read_doc to fetch full content.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Free-text search query. | |
| section | No | Restrict to a single section: docs | sdks | recipes | api. | |
| limit | No | Max results (default 10). |
Implementation Reference
- src/server.ts:56-84 (registration)Registration of the 'search_docs' tool on the MCP server, including its metadata (title, description, inputSchema) and handler invocation.
server.registerTool( "search_docs", { title: "Search ProsodyAI docs", description: "Search ProsodyAI docs, SDK READMEs, recipes, and OpenAPI metadata. Returns a ranked list of matches with snippets and stable `id`s. Follow up with `read_doc` to fetch full content.", inputSchema: { query: z.string().min(1).describe("Free-text search query."), section: sectionEnum .optional() .describe("Restrict to a single section: docs | sdks | recipes | api."), limit: z.number().int().min(1).max(50).optional().describe("Max results (default 10)."), }, }, async ({ query, section, limit }) => { const results = await searchContent(query, { section, limit }); if (!results.length) { return textResponse(`No matches for "${query}".`); } const lines = results.map((r, i) => [ `${i + 1}. [${r.section}] ${r.title} (id: ${r.id})`, ` ${r.description}`, ` snippet: ${r.snippet}`, ].join("\n"), ); return textResponse(lines.join("\n\n")); }, ); - src/server.ts:70-83 (handler)Handler function for 'search_docs': calls searchContent with query, optional section filter, and optional limit, then formats results into a text response.
async ({ query, section, limit }) => { const results = await searchContent(query, { section, limit }); if (!results.length) { return textResponse(`No matches for "${query}".`); } const lines = results.map((r, i) => [ `${i + 1}. [${r.section}] ${r.title} (id: ${r.id})`, ` ${r.description}`, ` snippet: ${r.snippet}`, ].join("\n"), ); return textResponse(lines.join("\n\n")); }, - src/server.ts:62-68 (schema)Input schema for 'search_docs': defines 'query' (required string), 'section' (optional enum: docs/sdks/recipes/api), and 'limit' (optional int 1-50).
inputSchema: { query: z.string().min(1).describe("Free-text search query."), section: sectionEnum .optional() .describe("Restrict to a single section: docs | sdks | recipes | api."), limit: z.number().int().min(1).max(50).optional().describe("Max results (default 10)."), }, - src/lib/content.ts:150-166 (helper)The searchContent function that performs fuzzy search using Fuse.js over the loaded content entries, supporting section filtering and result limiting. Returns results with score and snippet.
export async function searchContent( query: string, options: { section?: ContentSection; limit?: number } = {}, ): Promise<Array<ContentEntry & { score: number; snippet: string }>> { await loadContent(); if (!fuse) return []; const limit = options.limit ?? 10; const results = fuse.search(query, { limit: limit * 3 }); const filtered = options.section ? results.filter((r) => r.item.section === options.section) : results; return filtered.slice(0, limit).map((r) => ({ ...r.item, score: r.score ?? 0, snippet: makeSnippet(r.item.body, query), })); } - src/lib/content.ts:168-179 (helper)Helper function makeSnippet that extracts a relevant text snippet around the search query match from the document body.
function makeSnippet(body: string, query: string): string { const lower = body.toLowerCase(); const idx = lower.indexOf(query.toLowerCase().split(/\s+/)[0] ?? ""); if (idx === -1) { return body.slice(0, 240).trim(); } const start = Math.max(0, idx - 80); const end = Math.min(body.length, idx + 200); const prefix = start > 0 ? "..." : ""; const suffix = end < body.length ? "..." : ""; return prefix + body.slice(start, end).replace(/\s+/g, " ").trim() + suffix; }