-
Notifications
You must be signed in to change notification settings - Fork 659
feat(genkit-tools/mcp): Add docs search in MCP #4452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
61d2bc3
c24c0d5
e6efae9
fd06a7f
eb79446
3550c17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -72,58 +72,168 @@ export async function defineDocsTool(server: McpServer) { | |||||
| ) as Record<string, Doc>; | ||||||
|
|
||||||
| server.registerTool( | ||||||
| 'lookup_genkit_docs', | ||||||
| 'list_genkit_docs', | ||||||
| { | ||||||
| title: 'Genkit Docs', | ||||||
| title: 'List Genkit Docs', | ||||||
| description: | ||||||
| 'Use this to look up documentation for the Genkit AI framework.', | ||||||
| 'Use this to see a list of available Genkit documentation files. Returns `filePaths` that can be passed to `read_genkit_docs`.', | ||||||
| inputSchema: { | ||||||
| language: z | ||||||
| .enum(['js', 'go', 'python']) | ||||||
| .describe('which language these docs are for (default js).') | ||||||
| .describe( | ||||||
| 'Which language to list docs for (default js); type: string.' | ||||||
| ) | ||||||
| .default('js'), | ||||||
| files: z | ||||||
| .array(z.string()) | ||||||
| }, | ||||||
| }, | ||||||
| async ({ language }) => { | ||||||
| await record(new McpRunToolEvent('list_genkit_docs')); | ||||||
| const lang = language || 'js'; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
|
|
||||||
| const fileList = Object.keys(documents) | ||||||
| .filter((file) => file.startsWith(lang)) | ||||||
| .sort(); | ||||||
|
|
||||||
| const output = | ||||||
| `Genkit Documentation Index (${lang}):\n\n` + | ||||||
| fileList | ||||||
| .map((file) => { | ||||||
| const doc = documents[file]; | ||||||
| let summary = ` - FilePath: ${file}\n Title: ${doc.title}\n`; | ||||||
| if (doc.description) { | ||||||
| summary += ` Description: ${doc.description}\n`; | ||||||
| } | ||||||
| if (doc.headers) { | ||||||
| summary += ` Headers:\n ${doc.headers.split('\n').join('\n ')}\n`; | ||||||
| } | ||||||
| return summary; | ||||||
| }) | ||||||
| .join('\n') + | ||||||
| `\n\nUse 'search_genkit_docs' to find specific topics. Copy the 'FilePath' values to 'read_genkit_docs' to read content.`; | ||||||
|
|
||||||
| return { | ||||||
| content: [{ type: 'text', text: output }], | ||||||
| }; | ||||||
| } | ||||||
| ); | ||||||
|
|
||||||
| server.registerTool( | ||||||
| 'search_genkit_docs', | ||||||
| { | ||||||
| title: 'Search Genkit Docs', | ||||||
| description: | ||||||
| 'Use this to search the Genkit documentation using keywords. Returns ranked results with `filePaths` for `read_genkit_docs`. Warning: Generic terms (e.g. "the", "and") may return false positives; use specific technical terms (e.g. "rag", "firebase", "context").', | ||||||
| inputSchema: { | ||||||
| query: z | ||||||
| .string() | ||||||
| .describe('Keywords to search for in documentation; type: string.'), | ||||||
| language: z | ||||||
| .enum(['js', 'go', 'python']) | ||||||
| .describe( | ||||||
| 'Specific docs files to look up. If empty or not specified an index will be returned. Always lookup index first for exact file names.' | ||||||
| 'Which language to search docs for (default js); type: string.' | ||||||
| ) | ||||||
| .optional(), | ||||||
| .default('js'), | ||||||
| }, | ||||||
| }, | ||||||
| async ({ language, files }) => { | ||||||
| await record(new McpRunToolEvent('lookup_genkit_docs')); | ||||||
| async ({ query, language }) => { | ||||||
| await record(new McpRunToolEvent('search_genkit_docs')); | ||||||
| const lang = language || 'js'; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||
| const terms = query | ||||||
| .toLowerCase() | ||||||
| .split(/\s+/) | ||||||
| .filter((t) => t.length > 2); // Filter out short words to reduce noise | ||||||
|
|
||||||
| const results = Object.keys(documents) | ||||||
| .filter((file) => file.startsWith(lang)) | ||||||
| .map((file) => { | ||||||
| const doc = documents[file]; | ||||||
| let score = 0; | ||||||
| const title = doc.title.toLowerCase(); | ||||||
| const desc = (doc.description || '').toLowerCase(); | ||||||
| const headers = (doc.headers || '').toLowerCase(); | ||||||
|
|
||||||
| terms.forEach((term) => { | ||||||
| if (title.includes(term)) score += 10; | ||||||
| if (desc.includes(term)) score += 5; | ||||||
| if (headers.includes(term)) score += 3; | ||||||
| if (file.includes(term)) score += 5; | ||||||
| }); | ||||||
|
|
||||||
| return { file, doc, score }; | ||||||
| }) | ||||||
| .filter((r) => r.score > 0) | ||||||
| .sort((a, b) => b.score - a.score) | ||||||
| .slice(0, 10); // Top 10 | ||||||
|
|
||||||
| if (results.length === 0) { | ||||||
| return { | ||||||
| content: [ | ||||||
| { | ||||||
| type: 'text', | ||||||
| text: `No results found for "${query}" in ${lang} docs. Try broader keywords or use 'list_genkit_docs' to see all files.`, | ||||||
| }, | ||||||
| ], | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| const output = | ||||||
| `Found ${results.length} matching documents for "${query}":\n\n` + | ||||||
| results | ||||||
| .map((r) => { | ||||||
| let summary = `### ${r.doc.title}\n**FilePath**: ${r.file}\n`; | ||||||
| if (r.doc.description) | ||||||
| summary += `**Description**: ${r.doc.description}\n`; | ||||||
| return summary; | ||||||
| }) | ||||||
| .join('\n') + | ||||||
| `\n\nCopy the 'FilePath' values to 'read_genkit_docs' to read content.`; | ||||||
|
|
||||||
| return { | ||||||
| content: [{ type: 'text', text: output }], | ||||||
| }; | ||||||
| } | ||||||
| ); | ||||||
|
|
||||||
| server.registerTool( | ||||||
| 'read_genkit_docs', | ||||||
| { | ||||||
| title: 'Read Genkit Docs', | ||||||
| description: | ||||||
| 'Use this to read the full content of specific Genkit documentation files. You must provide `filePaths` from the list/search tools.', | ||||||
| inputSchema: { | ||||||
| filePaths: z | ||||||
| .array(z.string()) | ||||||
| .describe( | ||||||
| 'The `filePaths` of the docs to read. Obtain these exactly from `list_genkit_docs` or `search_genkit_docs` (e.g. "js/overview.md"); type: string[].' | ||||||
| ), | ||||||
| }, | ||||||
| }, | ||||||
| async ({ filePaths }) => { | ||||||
| await record(new McpRunToolEvent('read_genkit_docs')); | ||||||
|
|
||||||
| const content = [] as ContentBlock[]; | ||||||
| if (!language) { | ||||||
| language = 'js'; | ||||||
|
|
||||||
| // filePaths is required by Zod schema, but check length just in case | ||||||
| if (!filePaths || !filePaths.length) { | ||||||
| return { | ||||||
| content: [ | ||||||
| { | ||||||
| type: 'text', | ||||||
| text: 'No filePaths provided. Please provide an array of file paths.', | ||||||
| }, | ||||||
| ], | ||||||
| isError: true, | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| if (!files || !files.length) { | ||||||
| content.push({ | ||||||
| type: 'text', | ||||||
| text: | ||||||
| Object.keys(documents) | ||||||
| .filter((file) => file.startsWith(language)) | ||||||
| .map((file) => { | ||||||
| let fileSummary = ` - File: ${file}\n Title: ${documents[file].title}\n`; | ||||||
| if (documents[file].description) { | ||||||
| fileSummary += ` Description: ${documents[file].description}\n`; | ||||||
| } | ||||||
| if (documents[file].headers) { | ||||||
| fileSummary += ` Headers:\n ${documents[file].headers.split('\n').join('\n ')}\n`; | ||||||
| } | ||||||
| return fileSummary; | ||||||
| }) | ||||||
| .join('\n') + | ||||||
| `\n\nIMPORTANT: if doing anything more than basic model calling, look up "${language}/models.md" file, it contains important details about how to work with models.\n\n`, | ||||||
| }); | ||||||
| } else { | ||||||
| for (const file of files) { | ||||||
| if (documents[file]) { | ||||||
| content.push({ type: 'text', text: documents[file]?.text }); | ||||||
| } else { | ||||||
| content.push({ type: 'text', text: `${file} not found` }); | ||||||
| } | ||||||
| for (const file of filePaths) { | ||||||
| if (documents[file]) { | ||||||
| content.push({ type: 'text', text: documents[file].text }); | ||||||
| } else { | ||||||
| content.push({ | ||||||
| type: 'text', | ||||||
| text: `Document not found: ${file}. Please check the path using list_genkit_docs.`, | ||||||
| }); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Node.js projects, it's more idiomatic to install the Genkit CLI via npm. This is also consistent with the instructions in
genkit-tools/cli/src/mcp/prompts/init.tsfor Node.js setup. Using a package manager for CLI tools is generally preferred overcurl | bashfor better version management and security.