UNPKG

@apideck/docs-mcp

Version:

MCP server that lets agents search and retrieve your documentation at query time instead of relying on stale training data.

91 lines • 4 kB
// SPDX-License-Identifier: MIT import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; import { z } from 'zod'; import { createDocTools } from './tools.js'; const instructionsFor = (about, extraTools, extra) => [ `This server exposes ${about ?? 'a documentation set'} as read-only, always-current content.`, 'Prefer it over training data when answering questions about this product.', 'Workflow: call search_docs with a few specific terms, then get_doc on the best path (optionally with a section anchor) to read the actual text before answering.', 'Use list_docs to browse structure when a search returns nothing useful.', extraTools.length > 0 ? `Additional tools for this documentation set: ${extraTools.map((t) => t.name).join(', ')}.` : '', extra?.trim() ?? '', ] .filter(Boolean) .join(' '); const resultCount = (result) => { const s = result.structuredContent; const list = s?.['results'] ?? s?.['pages']; return Array.isArray(list) ? list.length : undefined; }; const report = (hook, event) => { try { void Promise.resolve(hook(event)).catch(() => undefined); } catch { // A failing hook must never affect the tool call. } }; const withReporting = (server, tool, hook, caller) => async (args) => { const started = performance.now(); const client = caller?.client ?? server.server.getClientVersion()?.name; const base = { tool: tool.name, args, ...(client !== undefined ? { client } : {}), ...(caller?.userAgent !== undefined ? { userAgent: caller.userAgent } : {}), }; try { const result = await tool.handler(args); const count = resultCount(result); report(hook, { ...base, isError: result.isError === true, durationMs: performance.now() - started, ...(count !== undefined ? { resultCount: count } : {}), }); return result; } catch (err) { report(hook, { ...base, isError: true, durationMs: performance.now() - started, error: err instanceof Error ? err.message : String(err), }); throw err; } }; export const createServer = (opts) => { const extraTools = opts.extraTools ?? []; const server = new McpServer({ name: opts.name ?? 'docs-mcp', version: opts.version ?? process.env['npm_package_version'] ?? '0.0.0' }, { capabilities: { tools: {}, resources: {} }, instructions: instructionsFor(opts.about, extraTools, opts.instructions) }); for (const tool of [...createDocTools(opts.store), ...extraTools]) { const handler = opts.onToolCall ? withReporting(server, tool, opts.onToolCall, opts.caller) : tool.handler; server.registerTool(tool.name, { title: tool.title, description: tool.description, inputSchema: z.object(tool.inputSchema), annotations: tool.annotations, }, async (args) => ({ ...(await handler(args)) })); } server.registerResource('doc', new ResourceTemplate('docs://{+path}', { list: async () => ({ resources: opts.store.listPages().map((p) => ({ uri: `docs://${p.path}`, name: p.title, mimeType: 'text/markdown', ...(p.description !== undefined ? { description: p.description } : {}), })), }), }), { title: 'Documentation page', description: 'Markdown source of one documentation page.', mimeType: 'text/markdown' }, async (uri, variables) => { const raw = variables['path']; const ref = Array.isArray(raw) ? raw.join('/') : (raw ?? ''); const page = opts.store.get(ref); if (!page) throw new Error(`No page at ${uri.href}`); return { contents: [{ uri: uri.href, mimeType: 'text/markdown', text: page.body.trim() }] }; }); return server; }; //# sourceMappingURL=server.js.map