@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
JavaScript
// 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