Skip to main content
Glama
index.ts27.6 kB
#!/usr/bin/env node /** * Trace MCP Server * * MCP server for detecting producer/consumer schema mismatches * between MCP tool definitions and client code that uses them. */ 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 { z } from 'zod'; import { extractProducerSchemas, extractFromFile } from './extract/index.js'; import { traceConsumerUsage, traceFromFile } from './trace/index.js'; import { compareSchemas, compareDirectories } from './compare/index.js'; import { formatResult, type OutputFormat } from './report/index.js'; import { previewContractComments, addContractComments, scaffoldConsumerFromProducer, scaffoldProducerFromConsumer, } from './tools/index.js'; import { TraceProject, loadProject, findProject, getWatcher, stopWatcher, listActiveWatchers, type WatchEvent, } from './watch/index.js'; import { bootstrapLanguageParsers } from './languages/bootstrap.js'; // Bootstrap language parsers at startup bootstrapLanguageParsers(); // Helper: log to stderr (MCP protocol requires stdout for JSON-RPC only) const log = (...args: unknown[]) => console.error('[trace-mcp]', ...args); // Tool input schemas const ExtractSchemasInput = z.object({ rootDir: z.string().describe('Root directory of MCP server source code'), include: z.array(z.string()).optional().describe('Glob patterns to include (default: **/*.ts)'), exclude: z.array(z.string()).optional().describe('Glob patterns to exclude (default: node_modules, dist)'), }); const TraceUsageInput = z.object({ rootDir: z.string().describe('Root directory of consumer/client source code'), include: z.array(z.string()).optional().describe('Glob patterns to include'), exclude: z.array(z.string()).optional().describe('Glob patterns to exclude'), }); const CompareInput = z.object({ producerDir: z.string().describe('Path to MCP server source directory'), consumerDir: z.string().describe('Path to consumer/client source directory'), format: z.enum(['json', 'markdown', 'summary']).optional().describe('Output format (default: json)'), strict: z.boolean().optional().describe('Strict mode: treat missing optional properties as warnings'), direction: z.enum(['producer_to_consumer', 'consumer_to_producer', 'bidirectional']).optional().describe('Data flow direction for compatibility checking (default: producer_to_consumer)'), }); const ExtractFileInput = z.object({ filePath: z.string().describe('Path to a single TypeScript file to extract schemas from'), }); const TraceFileInput = z.object({ filePath: z.string().describe('Path to a single TypeScript file to trace tool usage in'), }); const ScaffoldConsumerInput = z.object({ producerDir: z.string().describe('Path to MCP server source directory'), toolName: z.string().describe('Name of the tool to scaffold consumer for'), target: z.enum(['typescript', 'javascript', 'react-hook', 'zustand-action']).optional().describe('Output target format (default: typescript)'), includeErrorHandling: z.boolean().optional().describe('Include try/catch error handling (default: true)'), includeTypes: z.boolean().optional().describe('Include TypeScript type definitions (default: true)'), }); const ScaffoldProducerInput = z.object({ consumerDir: z.string().describe('Path to consumer source directory'), toolName: z.string().describe('Name of the tool to scaffold producer for'), includeHandler: z.boolean().optional().describe('Include handler stub (default: true)'), }); const CommentContractInput = z.object({ producerDir: z.string().describe('Path to MCP server source directory'), consumerDir: z.string().describe('Path to consumer source directory'), toolName: z.string().describe('Name of the validated tool'), dryRun: z.boolean().optional().describe('Preview comments without writing to files (default: true)'), style: z.enum(['jsdoc', 'inline', 'block']).optional().describe('Comment style (default: block)'), }); // Watch mode input schemas const InitProjectInput = z.object({ projectDir: z.string().describe('Root directory for the trace project'), producerPath: z.string().describe('Relative path to producer/server code'), consumerPath: z.string().describe('Relative path to consumer/client code'), producerLanguage: z.enum(['typescript', 'python', 'go', 'rust', 'json_schema']).optional().default('typescript'), consumerLanguage: z.enum(['typescript', 'python', 'go', 'rust', 'json_schema']).optional().default('typescript'), }); const WatchInput = z.object({ projectDir: z.string().describe('Root directory with .trace-mcp config'), action: z.enum(['start', 'stop', 'status', 'poll']).default('start').describe('Watch action to perform'), }); const GetProjectStatusInput = z.object({ projectDir: z.string().describe('Root directory with .trace-mcp config'), }); // Create server const server = new Server( { name: 'trace-mcp', version: '0.5.0', }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'extract_schemas', description: 'Extract MCP tool definitions (ProducerSchemas) from server source code. Scans for server.tool() calls and parses their Zod schemas.', inputSchema: { type: 'object', properties: { rootDir: { type: 'string', description: 'Root directory of MCP server source code' }, include: { type: 'array', items: { type: 'string' }, description: 'Glob patterns to include' }, exclude: { type: 'array', items: { type: 'string' }, description: 'Glob patterns to exclude' }, }, required: ['rootDir'], }, }, { name: 'extract_file', description: 'Extract MCP tool definitions from a single TypeScript file.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to a TypeScript file' }, }, required: ['filePath'], }, }, { name: 'trace_usage', description: 'Trace how client code uses MCP tools. Finds callTool() invocations and tracks which properties are accessed on results.', inputSchema: { type: 'object', properties: { rootDir: { type: 'string', description: 'Root directory of consumer source code' }, include: { type: 'array', items: { type: 'string' }, description: 'Glob patterns to include' }, exclude: { type: 'array', items: { type: 'string' }, description: 'Glob patterns to exclude' }, }, required: ['rootDir'], }, }, { name: 'trace_file', description: 'Trace MCP tool usage in a single TypeScript file.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to a TypeScript file' }, }, required: ['filePath'], }, }, { name: 'compare', description: 'Full analysis pipeline: extract producer schemas, trace consumer usage, and compare them to find mismatches. Returns a detailed report.', inputSchema: { type: 'object', properties: { producerDir: { type: 'string', description: 'Path to MCP server source directory' }, consumerDir: { type: 'string', description: 'Path to consumer/client source directory' }, format: { type: 'string', enum: ['json', 'markdown', 'summary'], description: 'Output format' }, strict: { type: 'boolean', description: 'Strict mode for comparison' }, direction: { type: 'string', enum: ['producer_to_consumer', 'consumer_to_producer', 'bidirectional'], description: 'Data flow direction (default: producer_to_consumer)' }, }, required: ['producerDir', 'consumerDir'], }, }, { name: 'scaffold_consumer', description: 'Generate consumer code from a producer schema. Creates TypeScript functions, React hooks, or Zustand actions that correctly call MCP tools.', inputSchema: { type: 'object', properties: { producerDir: { type: 'string', description: 'Path to MCP server source directory' }, toolName: { type: 'string', description: 'Name of the tool to scaffold consumer for' }, target: { type: 'string', enum: ['typescript', 'javascript', 'react-hook', 'zustand-action'], description: 'Output target format' }, includeErrorHandling: { type: 'boolean', description: 'Include try/catch error handling' }, includeTypes: { type: 'boolean', description: 'Include TypeScript type definitions' }, }, required: ['producerDir', 'toolName'], }, }, { name: 'scaffold_producer', description: 'Generate producer schema stub from consumer usage. Creates MCP tool definition based on how client code calls it.', inputSchema: { type: 'object', properties: { consumerDir: { type: 'string', description: 'Path to consumer source directory' }, toolName: { type: 'string', description: 'Name of the tool to scaffold producer for' }, includeHandler: { type: 'boolean', description: 'Include handler stub' }, }, required: ['consumerDir', 'toolName'], }, }, { name: 'comment_contract', description: 'Add cross-reference comments to validated producer/consumer pairs. Documents the contract relationship in both files.', inputSchema: { type: 'object', properties: { producerDir: { type: 'string', description: 'Path to MCP server source directory' }, consumerDir: { type: 'string', description: 'Path to consumer source directory' }, toolName: { type: 'string', description: 'Name of the validated tool' }, dryRun: { type: 'boolean', description: 'Preview comments without writing to files (default: true)' }, style: { type: 'string', enum: ['jsdoc', 'inline', 'block'], description: 'Comment style' }, }, required: ['producerDir', 'consumerDir', 'toolName'], }, }, // Watch mode tools { name: 'init_project', description: 'Initialize a trace project with .trace-mcp config directory. Creates project structure for watch mode and caching.', inputSchema: { type: 'object', properties: { projectDir: { type: 'string', description: 'Root directory for the trace project' }, producerPath: { type: 'string', description: 'Relative path to producer/server code' }, consumerPath: { type: 'string', description: 'Relative path to consumer/client code' }, producerLanguage: { type: 'string', enum: ['typescript', 'python', 'go', 'rust', 'json_schema'], description: 'Producer language (default: typescript)' }, consumerLanguage: { type: 'string', enum: ['typescript', 'python', 'go', 'rust', 'json_schema'], description: 'Consumer language (default: typescript)' }, }, required: ['projectDir', 'producerPath', 'consumerPath'], }, }, { name: 'watch', description: 'Watch project files for changes and auto-revalidate contracts. Actions: start (begin watching), stop (end watching), status (check state), poll (get pending events).', inputSchema: { type: 'object', properties: { projectDir: { type: 'string', description: 'Root directory with .trace-mcp config' }, action: { type: 'string', enum: ['start', 'stop', 'status', 'poll'], description: 'Watch action (default: start)' }, }, required: ['projectDir'], }, }, { name: 'get_project_status', description: 'Get the status of a trace project including config, cache state, and last validation result.', inputSchema: { type: 'object', properties: { projectDir: { type: 'string', description: 'Root directory with .trace-mcp config' }, }, required: ['projectDir'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'extract_schemas': { const input = ExtractSchemasInput.parse(args); log(`Extracting schemas from: ${input.rootDir}`); const schemas = await extractProducerSchemas({ rootDir: input.rootDir, include: input.include, exclude: input.exclude, }); log(`Found ${schemas.length} tool definitions`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, count: schemas.length, schemas, }, null, 2), }, ], }; } case 'extract_file': { const input = ExtractFileInput.parse(args); log(`Extracting from file: ${input.filePath}`); const schemas = await extractFromFile(input.filePath); return { content: [ { type: 'text', text: JSON.stringify({ success: true, count: schemas.length, schemas, }, null, 2), }, ], }; } case 'trace_usage': { const input = TraceUsageInput.parse(args); log(`Tracing usage in: ${input.rootDir}`); const usage = await traceConsumerUsage({ rootDir: input.rootDir, include: input.include, exclude: input.exclude, }); log(`Found ${usage.length} tool calls`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, count: usage.length, usage, }, null, 2), }, ], }; } case 'trace_file': { const input = TraceFileInput.parse(args); log(`Tracing file: ${input.filePath}`); const usage = await traceFromFile(input.filePath); return { content: [ { type: 'text', text: JSON.stringify({ success: true, count: usage.length, usage, }, null, 2), }, ], }; } case 'compare': { const input = CompareInput.parse(args); log(`Comparing: ${input.producerDir} vs ${input.consumerDir}`); const result = await compareDirectories( input.producerDir, input.consumerDir, { strict: input.strict, direction: input.direction } ); const format = (input.format || 'json') as OutputFormat; const output = formatResult(result, format); log(`Analysis complete: ${result.summary.matchCount} matches, ${result.summary.mismatchCount} mismatches`); return { content: [ { type: 'text', text: output, }, ], }; } case 'scaffold_consumer': { const input = ScaffoldConsumerInput.parse(args); log(`Scaffolding consumer for tool: ${input.toolName}`); // Extract producer schemas to find the requested tool const producers = await extractProducerSchemas({ rootDir: input.producerDir }); const producer = producers.find(p => p.toolName === input.toolName); if (!producer) { throw new Error(`Tool "${input.toolName}" not found in ${input.producerDir}`); } const result = scaffoldConsumerFromProducer(producer, { target: input.target || 'typescript', includeErrorHandling: input.includeErrorHandling ?? true, includeTypes: input.includeTypes ?? true, includeJSDoc: true, }); log(`Generated ${input.target || 'typescript'} consumer code`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, toolName: input.toolName, target: input.target || 'typescript', suggestedFilename: result.suggestedFilename, code: result.code, types: result.types, example: result.example, }, null, 2), }, ], }; } case 'scaffold_producer': { const input = ScaffoldProducerInput.parse(args); log(`Scaffolding producer for tool: ${input.toolName}`); // Trace consumer usage to find the requested tool const consumers = await traceConsumerUsage({ rootDir: input.consumerDir }); const consumer = consumers.find(c => c.toolName === input.toolName); if (!consumer) { throw new Error(`Tool "${input.toolName}" not found in consumer code at ${input.consumerDir}`); } const result = scaffoldProducerFromConsumer(consumer, { includeHandler: input.includeHandler ?? true, }); log(`Generated producer schema stub`); return { content: [ { type: 'text', text: JSON.stringify({ success: true, toolName: input.toolName, suggestedFilename: result.suggestedFilename, code: result.code, example: result.example, }, null, 2), }, ], }; } case 'comment_contract': { const input = CommentContractInput.parse(args); log(`Commenting contract for tool: ${input.toolName}`); // Get both producer and consumer const producers = await extractProducerSchemas({ rootDir: input.producerDir }); const consumers = await traceConsumerUsage({ rootDir: input.consumerDir }); const producer = producers.find(p => p.toolName === input.toolName); const consumer = consumers.find(c => c.toolName === input.toolName); if (!producer) { throw new Error(`Tool "${input.toolName}" not found in producer at ${input.producerDir}`); } if (!consumer) { throw new Error(`Tool "${input.toolName}" not found in consumer at ${input.consumerDir}`); } const match = { toolName: input.toolName, producerLocation: producer.location, consumerLocation: consumer.callSite, }; const commentOptions = { match, producer, consumer, style: input.style || 'block' as const, includeTimestamp: true, }; if (input.dryRun !== false) { // Preview mode (default) const preview = previewContractComments(commentOptions); return { content: [ { type: 'text', text: JSON.stringify({ success: true, mode: 'preview', toolName: input.toolName, producerPreview: preview.producerPreview, consumerPreview: preview.consumerPreview, note: 'Set dryRun: false to actually add these comments to files', }, null, 2), }, ], }; } else { // Actually add comments const result = await addContractComments(commentOptions); return { content: [ { type: 'text', text: JSON.stringify({ success: result.success, mode: 'applied', toolName: input.toolName, producerFile: result.producerFile, consumerFile: result.consumerFile, producerComment: result.producerComment, consumerComment: result.consumerComment, error: result.error, }, null, 2), }, ], }; } } // Watch mode tools case 'init_project': { const input = InitProjectInput.parse(args); log(`Initializing trace project at: ${input.projectDir}`); const project = loadProject(input.projectDir); if (project.exists()) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: `Project already exists at ${input.projectDir}. Use get_project_status to view it.`, }, null, 2), }], }; } const config = project.init({ producer: { path: input.producerPath, language: input.producerLanguage || 'typescript', include: ['**/*.ts'], exclude: ['**/*.test.ts', '**/node_modules/**', '**/dist/**'], }, consumer: { path: input.consumerPath, language: input.consumerLanguage || 'typescript', include: ['**/*.ts', '**/*.tsx'], exclude: ['**/*.test.ts', '**/node_modules/**', '**/dist/**'], }, }); log(`Project initialized with config:`, config); return { content: [{ type: 'text', text: JSON.stringify({ success: true, projectDir: input.projectDir, traceDir: project.traceDir, config, }, null, 2), }], }; } case 'watch': { const input = WatchInput.parse(args); const action = input.action || 'start'; log(`Watch action: ${action} for ${input.projectDir}`); const project = loadProject(input.projectDir); if (!project.exists()) { throw new Error(`No trace project found at ${input.projectDir}. Run init_project first.`); } const watcher = getWatcher(project); switch (action) { case 'start': { // Collect events during startup const events: WatchEvent[] = []; const eventHandler = (event: WatchEvent) => events.push(event); watcher.on('watch-event', eventHandler); await watcher.start(); // Wait a moment for initial validation await new Promise(resolve => setTimeout(resolve, 100)); watcher.off('watch-event', eventHandler); return { content: [{ type: 'text', text: JSON.stringify({ success: true, action: 'started', projectDir: input.projectDir, status: watcher.getStatus(), events, }, null, 2), }], }; } case 'stop': { await stopWatcher(project); return { content: [{ type: 'text', text: JSON.stringify({ success: true, action: 'stopped', projectDir: input.projectDir, }, null, 2), }], }; } case 'status': { return { content: [{ type: 'text', text: JSON.stringify({ success: true, action: 'status', projectDir: input.projectDir, status: watcher.getStatus(), activeWatchers: listActiveWatchers(), }, null, 2), }], }; } case 'poll': { // For polling, collect recent events const status = watcher.getStatus(); return { content: [{ type: 'text', text: JSON.stringify({ success: true, action: 'poll', projectDir: input.projectDir, status, lastResult: status.lastResult, }, null, 2), }], }; } default: throw new Error(`Unknown watch action: ${action}`); } } case 'get_project_status': { const input = GetProjectStatusInput.parse(args); log(`Getting project status for: ${input.projectDir}`); const project = loadProject(input.projectDir); if (!project.exists()) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, exists: false, error: `No trace project found at ${input.projectDir}`, }, null, 2), }], }; } const config = project.config; const activeWatchers = listActiveWatchers(); const isWatching = activeWatchers.includes(project.rootDir); let watcherStatus = null; if (isWatching) { const watcher = getWatcher(project); watcherStatus = watcher.getStatus(); } return { content: [{ type: 'text', text: JSON.stringify({ success: true, exists: true, projectDir: project.rootDir, traceDir: project.traceDir, config, isWatching, watcherStatus, paths: { producer: project.producerPath, consumer: project.consumerPath, }, }, null, 2), }], }; } default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { const message = error instanceof Error ? error.message : String(error); log(`Error in ${name}:`, message); return { content: [ { type: 'text', text: JSON.stringify({ success: false, error: message, }), }, ], isError: true, }; } }); // Also fix console.log in submodules - redirect to stderr const originalLog = console.log; console.log = (...args: unknown[]) => { console.error(...args); }; // Start server async function main() { log('Starting trace-mcp server v0.5.0'); const transport = new StdioServerTransport(); await server.connect(transport); log('Server connected and ready'); } main().catch((error) => { console.error('Fatal error:', error); process.exit(1); });

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Mnehmos/mnehmos.trace.mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server