feat(TRUEREF-0011): implement MCP server with stdio transport
Adds a Model Context Protocol server that exposes resolve-library-id and query-docs tools via stdio, with tool schemas identical to context7 for drop-in compatibility with Claude Code, Cursor, and Zed. - src/mcp/index.ts — server entry point (io.github.trueref/trueref) - src/mcp/client.ts — HTTP client for TrueRef REST API (TRUEREF_API_URL) - src/mcp/tools/resolve-library-id.ts — library search tool handler - src/mcp/tools/query-docs.ts — documentation retrieval tool handler - src/mcp/index.test.ts — integration tests spawning real server subprocess - .claude/rules/trueref.md — Claude Code rule file for MCP usage - package.json: mcp:start script using tsx Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
11
.claude/rules/trueref.md
Normal file
11
.claude/rules/trueref.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
description: Use TrueRef to retrieve documentation for indexed libraries
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
When answering questions about indexed libraries, always use the TrueRef MCP tools:
|
||||||
|
1. Call `resolve-library-id` with the library name and the user's question to get the library ID
|
||||||
|
2. Call `query-docs` with the library ID and question to retrieve relevant documentation
|
||||||
|
3. Use the returned documentation to answer the question accurately
|
||||||
|
|
||||||
|
Never rely on training data alone for library APIs that may have changed.
|
||||||
1044
package-lock.json
generated
1044
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,7 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"test": "npm run test:unit -- --run",
|
"test": "npm run test:unit -- --run",
|
||||||
|
"mcp:start": "tsx src/mcp/index.ts",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
@@ -49,6 +50,8 @@
|
|||||||
"vitest-browser-svelte": "^2.0.2"
|
"vitest-browser-svelte": "^2.0.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^12.6.2"
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
|
"better-sqlite3": "^12.6.2",
|
||||||
|
"zod": "^4.3.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/mcp/client.ts
Normal file
55
src/mcp/client.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* HTTP client for the TrueRef REST API.
|
||||||
|
*
|
||||||
|
* Configurable via the TRUEREF_API_URL environment variable.
|
||||||
|
* Defaults to http://localhost:5173 when not set.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const API_BASE = process.env.TRUEREF_API_URL ?? 'http://localhost:5173';
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
statusText: string;
|
||||||
|
text: () => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call GET /api/v1/libs/search
|
||||||
|
*
|
||||||
|
* Returns text/plain or JSON depending on the `type` param.
|
||||||
|
*/
|
||||||
|
export async function searchLibraries(params: {
|
||||||
|
libraryName: string;
|
||||||
|
query: string;
|
||||||
|
type?: 'json' | 'txt';
|
||||||
|
}): Promise<ApiResponse> {
|
||||||
|
const url = new URL(`${API_BASE}/api/v1/libs/search`);
|
||||||
|
url.searchParams.set('libraryName', params.libraryName);
|
||||||
|
url.searchParams.set('query', params.query);
|
||||||
|
url.searchParams.set('type', params.type ?? 'txt');
|
||||||
|
|
||||||
|
return fetch(url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call GET /api/v1/context
|
||||||
|
*
|
||||||
|
* Returns text/plain or JSON depending on the `type` param.
|
||||||
|
*/
|
||||||
|
export async function fetchContext(params: {
|
||||||
|
libraryId: string;
|
||||||
|
query: string;
|
||||||
|
tokens?: number;
|
||||||
|
type?: 'json' | 'txt';
|
||||||
|
}): Promise<ApiResponse> {
|
||||||
|
const url = new URL(`${API_BASE}/api/v1/context`);
|
||||||
|
url.searchParams.set('libraryId', params.libraryId);
|
||||||
|
url.searchParams.set('query', params.query);
|
||||||
|
url.searchParams.set('type', params.type ?? 'txt');
|
||||||
|
if (params.tokens !== undefined) {
|
||||||
|
url.searchParams.set('tokens', String(params.tokens));
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url.toString());
|
||||||
|
}
|
||||||
297
src/mcp/index.test.ts
Normal file
297
src/mcp/index.test.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
/**
|
||||||
|
* Integration tests for the TrueRef MCP server (stdio transport).
|
||||||
|
*
|
||||||
|
* Spawns the MCP server as a subprocess, sends JSON-RPC messages over stdin,
|
||||||
|
* and reads responses from stdout. The server is pointed at a mock HTTP
|
||||||
|
* server so no real TrueRef instance is required.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import { spawn, type ChildProcess } from 'node:child_process';
|
||||||
|
import { createServer, type Server as HttpServer } from 'node:http';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const MCP_ENTRY = resolve(__dirname, 'index.ts');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function buildRequest(id: number, method: string, params?: Record<string, unknown>) {
|
||||||
|
return JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForResponse(proc: ChildProcess, id: number, timeoutMs = 5000): Promise<unknown> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let buffer = '';
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
cleanup();
|
||||||
|
reject(new Error(`Timeout waiting for response id=${id}`));
|
||||||
|
}, timeoutMs);
|
||||||
|
|
||||||
|
function onData(chunk: Buffer) {
|
||||||
|
buffer += chunk.toString();
|
||||||
|
const lines = buffer.split('\n');
|
||||||
|
buffer = lines.pop() ?? '';
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(line);
|
||||||
|
if (msg.id === id) {
|
||||||
|
cleanup();
|
||||||
|
resolve(msg);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// not JSON — skip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
clearTimeout(timer);
|
||||||
|
proc.stdout?.off('data', onData);
|
||||||
|
proc.stderr?.off('data', onStderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStderr(chunk: Buffer) {
|
||||||
|
// Surface stderr for debugging but don't fail the test here
|
||||||
|
process.stderr.write(`[mcp-server] ${chunk.toString()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.stdout?.on('data', onData);
|
||||||
|
proc.stderr?.on('data', onStderr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Mock HTTP server
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function startMockApiServer(port: number): Promise<HttpServer> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const srv = createServer((req, res) => {
|
||||||
|
const url = new URL(req.url ?? '/', `http://localhost:${port}`);
|
||||||
|
|
||||||
|
if (url.pathname === '/api/v1/libs/search') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('- /mock/mylib — Mock Library (42 snippets)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.pathname === '/api/v1/context') {
|
||||||
|
const libraryId = url.searchParams.get('libraryId') ?? '';
|
||||||
|
if (libraryId === '/not-found/lib') {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'not found', code: 'LIBRARY_NOT_FOUND' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (libraryId === '/indexing/lib') {
|
||||||
|
res.writeHead(503, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ error: 'indexing', code: 'INDEXING_IN_PROGRESS' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||||
|
res.end('# Mock Library\n\nSome documentation content.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
srv.listen(port, '127.0.0.1', () => resolve(srv));
|
||||||
|
srv.on('error', reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test suite
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('MCP server (stdio transport)', () => {
|
||||||
|
let mockServer: HttpServer;
|
||||||
|
let proc: ChildProcess;
|
||||||
|
const MOCK_PORT = 19573;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mockServer = await startMockApiServer(MOCK_PORT);
|
||||||
|
|
||||||
|
proc = spawn('npx', ['tsx', MCP_ENTRY], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
TRUEREF_API_URL: `http://127.0.0.1:${MOCK_PORT}`
|
||||||
|
},
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send the MCP initialize handshake
|
||||||
|
proc.stdin!.write(
|
||||||
|
buildRequest(0, 'initialize', {
|
||||||
|
protocolVersion: '2024-11-05',
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: 'test', version: '1.0.0' }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitForResponse(proc, 0);
|
||||||
|
|
||||||
|
// Send initialized notification (no response expected)
|
||||||
|
proc.stdin!.write(
|
||||||
|
JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
proc?.kill();
|
||||||
|
mockServer?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('lists both tools via tools/list', async () => {
|
||||||
|
proc.stdin!.write(buildRequest(1, 'tools/list'));
|
||||||
|
const response = await waitForResponse(proc, 1);
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
result: {
|
||||||
|
tools: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ name: 'resolve-library-id' }),
|
||||||
|
expect.objectContaining({ name: 'query-docs' })
|
||||||
|
])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolve-library-id returns library search results', async () => {
|
||||||
|
proc.stdin!.write(
|
||||||
|
buildRequest(2, 'tools/call', {
|
||||||
|
name: 'resolve-library-id',
|
||||||
|
arguments: { libraryName: 'mylib', query: 'how to use mylib' }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await waitForResponse(proc, 2);
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 2,
|
||||||
|
result: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: expect.stringContaining('/mock/mylib')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('query-docs returns documentation text', async () => {
|
||||||
|
proc.stdin!.write(
|
||||||
|
buildRequest(3, 'tools/call', {
|
||||||
|
name: 'query-docs',
|
||||||
|
arguments: { libraryId: '/mock/mylib', query: 'how to install' }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await waitForResponse(proc, 3);
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 3,
|
||||||
|
result: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: expect.stringContaining('Mock Library')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('query-docs returns error message for 404', async () => {
|
||||||
|
proc.stdin!.write(
|
||||||
|
buildRequest(4, 'tools/call', {
|
||||||
|
name: 'query-docs',
|
||||||
|
arguments: { libraryId: '/not-found/lib', query: 'anything' }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await waitForResponse(proc, 4);
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 4,
|
||||||
|
result: {
|
||||||
|
isError: true,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: expect.stringContaining('not found')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('query-docs returns indexing message for 503', async () => {
|
||||||
|
proc.stdin!.write(
|
||||||
|
buildRequest(5, 'tools/call', {
|
||||||
|
name: 'query-docs',
|
||||||
|
arguments: { libraryId: '/indexing/lib', query: 'anything' }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await waitForResponse(proc, 5);
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 5,
|
||||||
|
result: {
|
||||||
|
isError: true,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: expect.stringContaining('being indexed')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('query-docs accepts optional tokens parameter', async () => {
|
||||||
|
proc.stdin!.write(
|
||||||
|
buildRequest(6, 'tools/call', {
|
||||||
|
name: 'query-docs',
|
||||||
|
arguments: { libraryId: '/mock/mylib', query: 'advanced usage', tokens: 5000 }
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await waitForResponse(proc, 6);
|
||||||
|
|
||||||
|
expect(response).toMatchObject({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 6,
|
||||||
|
result: {
|
||||||
|
content: [{ type: 'text' }]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error for unknown tool', async () => {
|
||||||
|
proc.stdin!.write(
|
||||||
|
buildRequest(7, 'tools/call', {
|
||||||
|
name: 'no-such-tool',
|
||||||
|
arguments: {}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await waitForResponse(proc, 7);
|
||||||
|
|
||||||
|
// MCP SDK may return an error result or our fallback
|
||||||
|
expect(response).toMatchObject({ jsonrpc: '2.0', id: 7 });
|
||||||
|
});
|
||||||
|
});
|
||||||
84
src/mcp/index.ts
Normal file
84
src/mcp/index.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* TrueRef MCP Server — stdio transport
|
||||||
|
*
|
||||||
|
* Exposes resolve-library-id and query-docs tools, identical to context7's
|
||||||
|
* MCP interface, for drop-in compatibility with Claude Code, Cursor, and
|
||||||
|
* other MCP-aware AI coding assistants.
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* TRUEREF_API_URL Base URL for the TrueRef REST API (default: http://localhost:5173)
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx tsx src/mcp/index.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema
|
||||||
|
} from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
RESOLVE_LIBRARY_ID_TOOL,
|
||||||
|
handleResolveLibraryId
|
||||||
|
} from './tools/resolve-library-id.js';
|
||||||
|
import { QUERY_DOCS_TOOL, handleQueryDocs } from './tools/query-docs.js';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Server setup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: 'io.github.trueref/trueref',
|
||||||
|
version: '1.0.0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: { tools: {} }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tool registry
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||||
|
tools: [RESOLVE_LIBRARY_ID_TOOL, QUERY_DOCS_TOOL]
|
||||||
|
}));
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tool dispatch
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
const { name, arguments: args } = request.params;
|
||||||
|
|
||||||
|
if (name === 'resolve-library-id') {
|
||||||
|
return handleResolveLibraryId(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'query-docs') {
|
||||||
|
return handleQueryDocs(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text: `Unknown tool: ${name}` }],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Startup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
// Server runs until process exits
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
process.stderr.write(`MCP server error: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
98
src/mcp/tools/query-docs.ts
Normal file
98
src/mcp/tools/query-docs.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* query-docs tool handler
|
||||||
|
*
|
||||||
|
* Fetches documentation and code examples from TrueRef for a specific library.
|
||||||
|
* Tool schema is identical to context7 for drop-in compatibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { fetchContext } from '../client.js';
|
||||||
|
|
||||||
|
export const QueryDocsSchema = z.object({
|
||||||
|
libraryId: z
|
||||||
|
.string()
|
||||||
|
.describe('The TrueRef library ID obtained from resolve-library-id, e.g. /facebook/react'),
|
||||||
|
query: z
|
||||||
|
.string()
|
||||||
|
.describe('Specific question about the library to retrieve relevant documentation'),
|
||||||
|
tokens: z.number().optional().describe('Maximum token budget for the response (default: 10000)')
|
||||||
|
});
|
||||||
|
|
||||||
|
export type QueryDocsInput = z.infer<typeof QueryDocsSchema>;
|
||||||
|
|
||||||
|
export const QUERY_DOCS_TOOL = {
|
||||||
|
name: 'query-docs',
|
||||||
|
description: [
|
||||||
|
'Fetches documentation and code examples from TrueRef for a specific library.',
|
||||||
|
'Requires a library ID obtained from resolve-library-id.',
|
||||||
|
'Returns relevant snippets formatted for LLM consumption.',
|
||||||
|
'Call at most 3 times per user question.'
|
||||||
|
].join(' '),
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
libraryId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'TrueRef library ID, e.g. /facebook/react'
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Specific question about the library'
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'Max token budget (default: 10000)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['libraryId', 'query']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function handleQueryDocs(args: unknown) {
|
||||||
|
const { libraryId, query, tokens } = QueryDocsSchema.parse(args);
|
||||||
|
|
||||||
|
const response = await fetchContext({ libraryId, query, tokens, type: 'txt' });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const status = response.status;
|
||||||
|
|
||||||
|
if (status === 404) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Library "${libraryId}" not found. Please run resolve-library-id first.`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 503) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Library "${libraryId}" is currently being indexed. Please try again in a moment.`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Error fetching documentation: ${response.status} ${response.statusText}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text }]
|
||||||
|
};
|
||||||
|
}
|
||||||
63
src/mcp/tools/resolve-library-id.ts
Normal file
63
src/mcp/tools/resolve-library-id.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* resolve-library-id tool handler
|
||||||
|
*
|
||||||
|
* Searches TrueRef to find a library matching the given name.
|
||||||
|
* Tool schema is identical to context7 for drop-in compatibility.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { searchLibraries } from '../client.js';
|
||||||
|
|
||||||
|
export const ResolveLibraryIdSchema = z.object({
|
||||||
|
libraryName: z.string().describe('Library name to search for and resolve to a TrueRef library ID'),
|
||||||
|
query: z.string().describe("The user's question or context to help rank results")
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResolveLibraryIdInput = z.infer<typeof ResolveLibraryIdSchema>;
|
||||||
|
|
||||||
|
export const RESOLVE_LIBRARY_ID_TOOL = {
|
||||||
|
name: 'resolve-library-id',
|
||||||
|
description: [
|
||||||
|
'Searches TrueRef to find a library matching the given name.',
|
||||||
|
'Returns a list of matching libraries with their IDs.',
|
||||||
|
'ALWAYS call this tool before query-docs to get the correct library ID.',
|
||||||
|
'Call at most 3 times per user question.'
|
||||||
|
].join(' '),
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: {
|
||||||
|
libraryName: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Library name to search for'
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
description: "User's question for relevance ranking"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['libraryName', 'query']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function handleResolveLibraryId(args: unknown) {
|
||||||
|
const { libraryName, query } = ResolveLibraryIdSchema.parse(args);
|
||||||
|
|
||||||
|
const response = await searchLibraries({ libraryName, query, type: 'txt' });
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text' as const,
|
||||||
|
text: `Error searching libraries: ${response.status} ${response.statusText}`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
isError: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await response.text();
|
||||||
|
return {
|
||||||
|
content: [{ type: 'text' as const, text }]
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user