Skip to main content
Glama
MCPSchemaExtractor.js17.7 kB
import { spawn } from 'child_process'; import { EventEmitter } from 'events'; /** * Real MCP protocol implementation for extracting tool schemas */ export class MCPSchemaExtractor extends EventEmitter { constructor() { super(); this.timeout = parseInt(process.env.MCP_TIMEOUT) || 10000; this.requestId = 0; } /** * Extract schemas from a single MCP server using real protocol * @param {Object} serverConfig Server configuration * @returns {Promise<Object>} Server schema data */ async extractServerSchema(serverConfig) { const result = { serverName: serverConfig.name, success: false, tools: [], error: null, metadata: { connectionTime: 0, toolCount: 0, totalSchemaSize: 0, protocolVersion: null }, // New investigation fields investigation: { rawResponseSize: 0, rawResponse: null, initializeResponseSize: 0, toolsWithOutputSchemas: 0, additionalFieldsFound: [], jsonFormattingTests: {} } }; const startTime = Date.now(); try { // Start the MCP server process const serverProcess = await this.startServer(serverConfig); result.metadata.connectionTime = Date.now() - startTime; // Perform MCP handshake const handshakeResult = await this.performHandshake(serverProcess, serverConfig); result.investigation.initializeResponseSize = JSON.stringify(handshakeResult).length; // Get ALL MCP protocol data that Claude might include in context const toolsResult = await this.getToolsFromServer(serverProcess, serverConfig); const resourcesResult = await this.getResourcesFromServer(serverProcess, serverConfig); const promptsResult = await this.getPromptsFromServer(serverProcess, serverConfig); result.tools = toolsResult.tools; result.investigation.rawResponse = toolsResult.rawResponse; result.investigation.rawResponseSize = toolsResult.rawResponseSize; result.investigation.toolsWithOutputSchemas = toolsResult.toolsWithOutputSchemas; result.investigation.additionalFieldsFound = toolsResult.additionalFieldsFound; result.investigation.jsonFormattingTests = toolsResult.jsonFormattingTests; // NEW: Additional protocol data result.investigation.resourcesData = resourcesResult; result.investigation.promptsData = promptsResult; result.metadata.toolCount = toolsResult.tools.length; result.metadata.totalSchemaSize = this.calculateSchemaSize(toolsResult.tools); result.success = true; // Clean up await this.stopServer(serverProcess); } catch (error) { result.error = error.message; this.emit('error', `Failed to extract schema from ${serverConfig.name}: ${error.message}`); } return result; } /** * Start an MCP server process * @param {Object} serverConfig Server configuration * @returns {Promise<Object>} Server process information */ async startServer(serverConfig) { return new Promise((resolve, reject) => { const command = serverConfig.command; const args = serverConfig.args || []; const env = { ...process.env, ...serverConfig.env }; const cwd = serverConfig.cwd || process.cwd(); this.emit('info', `Starting server: ${command} ${args.join(' ')}`); const child = spawn(command, args, { env, cwd, stdio: ['pipe', 'pipe', 'pipe'] }); const timeout = setTimeout(() => { child.kill(); reject(new Error(`Server startup timeout after ${this.timeout}ms`)); }, this.timeout); child.on('error', (error) => { clearTimeout(timeout); reject(new Error(`Failed to start server: ${error.message}`)); }); // Give server time to start and set up stdio setTimeout(() => { clearTimeout(timeout); resolve({ process: child, pid: child.pid, command: command, args: args, stdin: child.stdin, stdout: child.stdout, stderr: child.stderr }); }, 2000); }); } /** * Perform MCP protocol handshake * @param {Object} serverProcess Server process information * @param {Object} serverConfig Server configuration * @returns {Promise<Object>} Handshake result */ async performHandshake(serverProcess, serverConfig) { // Step 1: Send initialize request const initializeRequest = { jsonrpc: "2.0", id: ++this.requestId, method: "initialize", params: { protocolVersion: "2025-06-18", capabilities: { roots: { listChanged: true }, sampling: {} }, clientInfo: { name: "MCP-Token-Analyzer", version: "1.0.0" } } }; const initResponse = await this.sendRequest(serverProcess, initializeRequest); if (initResponse.error) { throw new Error(`Initialize failed: ${initResponse.error.message}`); } // Step 2: Send initialized notification const initializedNotification = { jsonrpc: "2.0", method: "notifications/initialized" }; await this.sendNotification(serverProcess, initializedNotification); return initResponse.result; } /** * Get tools from MCP server using tools/list request with investigation data * @param {Object} serverProcess Server process information * @param {Object} serverConfig Server configuration * @returns {Promise<Object>} Detailed tool analysis result */ async getToolsFromServer(serverProcess, serverConfig) { const toolsListRequest = { jsonrpc: "2.0", id: ++this.requestId, method: "tools/list", params: {} }; const response = await this.sendRequest(serverProcess, toolsListRequest); if (response.error) { throw new Error(`Tools list failed: ${response.error.message}`); } const tools = response.result.tools || []; // Investigation analysis const rawResponseSize = JSON.stringify(response).length; const rawResponsePretty = JSON.stringify(response, null, 2).length; const rawResponseMinified = JSON.stringify(response).length; let toolsWithOutputSchemas = 0; const additionalFieldsFound = new Set(); // Analyze each tool for missing content tools.forEach(tool => { // Check for output schema if (tool.outputSchema) { toolsWithOutputSchemas++; } // Catalog all fields beyond the basic ones we currently count const basicFields = new Set(['name', 'title', 'description', 'inputSchema', 'annotations']); Object.keys(tool).forEach(field => { if (!basicFields.has(field)) { additionalFieldsFound.add(field); } }); }); return { tools, rawResponse: response, rawResponseSize: rawResponsePretty, // Use pretty-printed size as baseline toolsWithOutputSchemas, additionalFieldsFound: Array.from(additionalFieldsFound), jsonFormattingTests: { prettyPrinted: rawResponsePretty, minified: rawResponseMinified, difference: rawResponsePretty - rawResponseMinified } }; } /** * Get resources from MCP server using resources/list request * @param {Object} serverProcess Server process information * @param {Object} serverConfig Server configuration * @returns {Promise<Object>} Resources analysis result */ async getResourcesFromServer(serverProcess, serverConfig) { const resourcesListRequest = { jsonrpc: "2.0", id: ++this.requestId, method: "resources/list", params: {} }; try { const response = await this.sendRequest(serverProcess, resourcesListRequest); if (response.error) { return { success: false, error: response.error.message, resources: [], tokenCount: 0 }; } const resources = response.result.resources || []; const responseSize = JSON.stringify(response, null, 2).length; return { success: true, resources: resources, responseSize: responseSize, tokenCount: Math.ceil(responseSize / 3.5), resourceCount: resources.length }; } catch (error) { return { success: false, error: error.message, resources: [], tokenCount: 0 }; } } /** * Get prompts from MCP server using prompts/list request * @param {Object} serverProcess Server process information * @param {Object} serverConfig Server configuration * @returns {Promise<Object>} Prompts analysis result */ async getPromptsFromServer(serverProcess, serverConfig) { const promptsListRequest = { jsonrpc: "2.0", id: ++this.requestId, method: "prompts/list", params: {} }; try { const response = await this.sendRequest(serverProcess, promptsListRequest); if (response.error) { return { success: false, error: response.error.message, prompts: [], tokenCount: 0 }; } const prompts = response.result.prompts || []; const responseSize = JSON.stringify(response, null, 2).length; return { success: true, prompts: prompts, responseSize: responseSize, tokenCount: Math.ceil(responseSize / 3.5), promptCount: prompts.length }; } catch (error) { return { success: false, error: error.message, prompts: [], tokenCount: 0 }; } } /** * Send JSON-RPC request and wait for response * @param {Object} serverProcess Server process information * @param {Object} request JSON-RPC request * @returns {Promise<Object>} Response object */ async sendRequest(serverProcess, request) { return new Promise((resolve, reject) => { const requestStr = JSON.stringify(request) + '\n'; let responseBuffer = ''; let responseReceived = false; const responseHandler = (data) => { responseBuffer += data.toString(); // Look for complete JSON-RPC response const lines = responseBuffer.split('\n'); for (let i = 0; i < lines.length - 1; i++) { const line = lines[i].trim(); if (line) { try { const response = JSON.parse(line); if (response.id === request.id && !responseReceived) { responseReceived = true; serverProcess.stdout.removeListener('data', responseHandler); resolve(response); return; } } catch (parseError) { // Continue looking for valid JSON } } } }; const errorHandler = (data) => { const errorStr = data.toString(); // Don't treat informational/startup messages as errors if (errorStr.includes('running on stdio') || errorStr.includes('Server starting') || errorStr.includes('Listening') || errorStr.includes('Initializing') || errorStr.includes('Started') || errorStr.includes('Ready') || errorStr.toLowerCase().includes('info:') || errorStr.toLowerCase().includes('debug:') || errorStr.toLowerCase().includes('warning:')) { // Log but don't fail on informational messages this.emit('info', `Server info: ${errorStr.trim()}`); return; } // Only fail on actual errors like "Error:", "Failed:", "Exception:" if (errorStr.toLowerCase().includes('error:') || errorStr.toLowerCase().includes('failed:') || errorStr.toLowerCase().includes('exception:') || errorStr.toLowerCase().includes('fatal:')) { if (!responseReceived) { responseReceived = true; serverProcess.stdout.removeListener('data', responseHandler); serverProcess.stderr.removeListener('data', errorHandler); reject(new Error(`Server error: ${errorStr}`)); } } else { // Log other stderr output but don't fail this.emit('info', `Server message: ${errorStr.trim()}`); } }; // Set up response handlers serverProcess.stdout.on('data', responseHandler); serverProcess.stderr.on('data', errorHandler); // Send request try { serverProcess.stdin.write(requestStr); } catch (error) { responseReceived = true; serverProcess.stdout.removeListener('data', responseHandler); serverProcess.stderr.removeListener('data', errorHandler); reject(new Error(`Failed to send request: ${error.message}`)); } // Set timeout setTimeout(() => { if (!responseReceived) { responseReceived = true; serverProcess.stdout.removeListener('data', responseHandler); serverProcess.stderr.removeListener('data', errorHandler); reject(new Error(`Request timeout after ${this.timeout}ms`)); } }, this.timeout); }); } /** * Send JSON-RPC notification (no response expected) * @param {Object} serverProcess Server process information * @param {Object} notification JSON-RPC notification */ async sendNotification(serverProcess, notification) { const notificationStr = JSON.stringify(notification) + '\n'; try { serverProcess.stdin.write(notificationStr); // Small delay to ensure notification is processed await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { throw new Error(`Failed to send notification: ${error.message}`); } } /** * Calculate the approximate size of tool schemas in characters * @param {Array} tools Array of tool definitions * @returns {number} Total schema size in characters */ calculateSchemaSize(tools) { return tools.reduce((total, tool) => { const toolJson = JSON.stringify(tool, null, 2); return total + toolJson.length; }, 0); } /** * Stop an MCP server process * @param {Object} serverProcess Server process information * @returns {Promise<void>} */ async stopServer(serverProcess) { return new Promise((resolve) => { if (serverProcess.process && !serverProcess.process.killed) { // Close stdin first to signal shutdown try { serverProcess.stdin.end(); } catch (error) { // Ignore errors when closing stdin } // Give server time to shutdown gracefully setTimeout(() => { if (!serverProcess.process.killed) { serverProcess.process.kill('SIGTERM'); // Force kill if it doesn't respond setTimeout(() => { if (!serverProcess.process.killed) { serverProcess.process.kill('SIGKILL'); } resolve(); }, 2000); } else { resolve(); } }, 1000); } else { resolve(); } }); } /** * Extract schemas from multiple servers * @param {Array} serverConfigs Array of server configurations * @returns {Promise<Array>} Array of server schema results */ async extractMultipleServerSchemas(serverConfigs) { const results = []; for (const serverConfig of serverConfigs) { this.emit('progress', `Extracting schema from ${serverConfig.name}...`); try { const result = await this.extractServerSchema(serverConfig); results.push(result); if (result.success) { this.emit('info', `✅ ${serverConfig.name}: ${result.metadata.toolCount} tools, ${result.metadata.totalSchemaSize} chars`); } else { this.emit('info', `⚠️ ${serverConfig.name}: ${result.error}`); } } catch (error) { // If extraction completely fails, create a failed result const failedResult = { serverName: serverConfig.name, success: false, tools: [], error: error.message, metadata: { connectionTime: 0, toolCount: 0, totalSchemaSize: 0, protocolVersion: null } }; results.push(failedResult); this.emit('info', `❌ ${serverConfig.name}: ${error.message}`); } } return results; } /** * Get summary statistics from extraction results * @param {Array} extractionResults Array of extraction results * @returns {Object} Summary statistics */ getSummaryStatistics(extractionResults) { const successful = extractionResults.filter(r => r.success); const failed = extractionResults.filter(r => !r.success); const totalTools = successful.reduce((sum, r) => sum + r.metadata.toolCount, 0); const totalSchemaSize = successful.reduce((sum, r) => sum + r.metadata.totalSchemaSize, 0); const avgConnectionTime = successful.length > 0 ? successful.reduce((sum, r) => sum + r.metadata.connectionTime, 0) / successful.length : 0; return { totalServers: extractionResults.length, successfulServers: successful.length, failedServers: failed.length, totalTools, totalSchemaSize, averageConnectionTime: Math.round(avgConnectionTime), averageToolsPerServer: successful.length > 0 ? Math.round(totalTools / successful.length) : 0, failedServersList: failed.map(f => ({ name: f.serverName, error: f.error })) }; } }

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/cordlesssteve/token-analyzer-mcp'

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