UNPKG

@cortexguardai/mcp

Version:

A Node.js-based MCP adapter for seamless integration with AI development environments.

484 lines 19.4 kB
import { JsonRpcErrorCode } from './types.js'; // UUID validation regex (reuse server-side pattern) const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; export class MethodDispatcher { projectId; logger; httpClient; serverInfo = { name: 'cortex-mcp-adapter', version: '1.0.0' }; isInitialized = false; initializationTimeout = null; initializationStartTime; constructor(httpClient, projectId, logger = console) { this.projectId = projectId; this.logger = logger; this.httpClient = httpClient; // Log debug status to backend this.logProtocolMessage('outgoing', 'debug', { event: 'adapter_constructor', projectId: this.projectId, timestamp: new Date().toISOString() }); // Record initialization start time for better logging this.initializationStartTime = Date.now(); // Set up fallback initialization timeout // Some IDEs may not send 'initialized' notification properly this.initializationTimeout = setTimeout(() => { if (!this.isInitialized) { const elapsed = Date.now() - (this.initializationStartTime || 0); this.logger.warn(`[MCP-ADAPTER] No initialized notification received within ${elapsed}ms, assuming ready`); this.logProtocolMessage('internal', 'timeout', { event: 'initialization_timeout', forced_initialization: true, elapsed_ms: elapsed, reason: 'IDE did not send initialized notification within timeout period', timestamp: new Date().toISOString() }); this.isInitialized = true; } }, 15000); } cleanup() { if (this.initializationTimeout) { clearTimeout(this.initializationTimeout); this.initializationTimeout = null; } } isReady() { return this.isInitialized; } /** * Log protocol messages to backend for debugging */ async logProtocolMessage(direction, method, message) { try { await this.httpClient.post('/api/mcp/debug/log-message', { direction, method, message, timestamp: new Date().toISOString() }); } catch (error) { // Don't let debug logging failures affect the main functionality this.logger.debug('Failed to log protocol message:', error); } } async handleRequest(request) { const { method, params, id } = request; const timestamp = new Date().toISOString(); this.logger.info(`[MCP-ADAPTER] ${timestamp} - Handling request:`, { method, id, params, initialized: this.isInitialized }); // Allow initialization methods and be resilient for other methods const initializationMethods = ['initialize', 'initialized']; const isInitMethod = initializationMethods.includes(method); if (!isInitMethod && !this.isInitialized) { this.logger.warn(`[MCP-ADAPTER] Received ${method} before initialization complete, proceeding anyway`); } try { let result; switch (method) { case 'initialize': return await this.handleInitialize(params, id); case 'initialized': return await this.handleInitialized(id); case 'resources/list': result = await this.handleResourcesList(id); break; case 'resources/read': result = await this.handleResourcesRead(params, id); break; case 'tools/list': result = await this.handleToolsList(id); break; case 'tools/call': result = await this.handleToolsCall(params, id); break; default: throw new Error(`Unknown method: ${method}`); } this.logger.info(`[MCP-ADAPTER] ${timestamp} - Request completed:`, { method, id, success: true }); return result; } catch (error) { this.logger.error(`[MCP-ADAPTER] ${timestamp} - Request failed:`, { method, id, error: error instanceof Error ? error.message : String(error) }); throw error; } } async handleInitialize(params, id) { this.logger.info(`[MCP-ADAPTER] Initialize request received`, { id, params }); // Log initialization request await this.logProtocolMessage('incoming', 'initialize', { id, params, event: 'initialize_request' }); // Support multiple protocol versions with fallback (prefer latest stable) const supportedVersions = ['2024-11-05', '2024-10-07', '1.0.0']; const clientVersion = params?.protocolVersion; const protocolVersion = supportedVersions.includes(clientVersion) ? clientVersion : supportedVersions[0]; if (clientVersion && clientVersion !== protocolVersion) { this.logger.info(`[MCP-ADAPTER] Client requested version ${clientVersion}, using ${protocolVersion}`); } const result = { protocolVersion, capabilities: { resources: { subscribe: false, listChanged: false }, tools: { listChanged: false }, logging: {}, prompts: { listChanged: false } }, serverInfo: { name: 'cortex-context-mcp', version: '1.0.0' }, instructions: 'MCP server for Cortex Context - provides access to project context files and tools for AI coding assistance.' }; this.logger.info(`[MCP-ADAPTER] Initialize response prepared`, { id, protocolVersion, capabilities: Object.keys(result.capabilities) }); // Log initialization response await this.logProtocolMessage('outgoing', 'initialize', { id, response: result, event: 'initialize_response' }); // Return only the result payload; RpcHandler will wrap in JSON-RPC envelope return result; } async handleInitialized(id) { const elapsed = Date.now() - (this.initializationStartTime || 0); this.logger.info(`[MCP-ADAPTER] Client initialization complete after ${elapsed}ms`, { id }); // Log initialized notification await this.logProtocolMessage('incoming', 'initialized', { id, event: 'initialized_notification', elapsed_ms: elapsed, method: 'initialized_notification', timestamp: new Date().toISOString() }); // Mark as initialized and clear timeout this.isInitialized = true; if (this.initializationTimeout) { clearTimeout(this.initializationTimeout); this.initializationTimeout = null; } // No response needed for notifications return null; } handleResourcesList(id) { this.logger.info('[MCP-ADAPTER] Handling resources/list request', { id }); const result = { resources: [ { uri: `cortex://project/${this.projectId}`, name: 'Project Context', description: 'Access to project context files and information', mimeType: 'application/json' } ] }; this.logger.info('[MCP-ADAPTER] Resources list response:', result); return result; } async handleResourcesRead(params, id) { this.logger.info('[MCP-ADAPTER] Handling resources/read request', { id, params }); try { // Expecting a URI like cortex://project/{projectId} const match = typeof params?.uri === 'string' && params.uri.match(/^cortex:\/\/project\/([0-9a-f\-]{36})$/i); const projectId = match ? match[1] : this.projectId; const response = await this.httpClient.get(`/api/mcp/contexts/${projectId}`); const contexts = response.data?.result?.contexts || response.data?.contexts || response.data; const result = { contents: [{ type: 'text', text: JSON.stringify(contexts, null, 2) }] }; this.logger.info('[MCP-ADAPTER] Resources read response:', result); return result; } catch (error) { // Throw to let RpcHandler produce a proper JSON-RPC error envelope this.logger.error('[MCP-ADAPTER] Resources read failed:', { id, error: error?.message }); throw { code: JsonRpcErrorCode.INTERNAL_ERROR, message: 'Failed to read resource', data: { uri: params?.uri, error: error?.message } }; } } async handleToolsList(id) { this.logger.info(`[MCP-ADAPTER] Tools list request`, { id, timestamp: new Date().toISOString() }); // Log tools list request await this.logProtocolMessage('incoming', 'tools/list', { id, event: 'tools_list_request' }); const tools = [ { name: 'get_contexts', title: 'Get Contexts', description: 'Get all contexts for a specific project', inputSchema: { type: 'object', properties: { project_id: { type: 'string', description: 'Project UUID' } }, required: ['project_id'], additionalProperties: false } }, { name: 'get_file', title: 'Get File', description: 'Get a specific file from a project context', inputSchema: { type: 'object', properties: { project_id: { type: 'string', description: 'Project UUID' }, file_id: { type: 'string', description: 'File UUID' } }, required: ['project_id', 'file_id'], additionalProperties: false } }, { name: 'add_file', title: 'Add File', description: 'Add a new file to the project context', inputSchema: { type: 'object', properties: { project_id: { type: 'string', description: 'Project UUID' }, filename: { type: 'string', description: 'Name of the file to add' }, content: { type: 'string', description: 'Content of the file' }, file_type: { type: 'string', description: 'Logical file type (e.g., javascript, text, json)' } }, required: ['project_id', 'filename', 'content'], additionalProperties: false } } ]; const response = { tools }; // Log tools list response await this.logProtocolMessage('outgoing', 'tools/list', { id, response, event: 'tools_list_response', toolCount: tools.length }); this.logger.info(`[MCP-ADAPTER] Tools list response`, { id, toolCount: tools.length, timestamp: new Date().toISOString() }); return response; } async handleToolsCall(params, id) { this.logger.info('[MCP-ADAPTER] Handling tools/call request', { id, params }); // Log tool call request await this.logProtocolMessage('incoming', 'tools/call', { id, tool: params.name, arguments: params.arguments, event: 'tool_call_request' }); try { if (!params.name || typeof params.name !== 'string') { throw { code: JsonRpcErrorCode.INVALID_PARAMS, message: 'Missing or invalid tool name', data: { received: params } }; } const toolName = params.name; const args = params.arguments || {}; let result; switch (toolName) { case 'get_contexts': result = await this.callGetContexts(args); break; case 'get_file': result = await this.callGetFile(args); break; case 'add_file': result = await this.callAddFile(args); break; default: throw { code: JsonRpcErrorCode.METHOD_NOT_FOUND, message: `Unknown tool: ${toolName}`, data: { tool_name: toolName } }; } // Log tool call response await this.logProtocolMessage('outgoing', 'tools/call', { id, tool: params.name, response: result, event: 'tool_call_response' }); this.logger.info('[MCP-ADAPTER] Tools call response (result payload):', result); // Return only the ToolResult payload; RpcHandler will wrap it return result; } catch (error) { const mappedError = { code: error?.code ?? JsonRpcErrorCode.INTERNAL_ERROR, message: error?.message ?? 'Internal tool error', data: error?.data }; // Log tool call error await this.logProtocolMessage('outgoing', 'tools/call', { id, tool: params?.name, error: mappedError, event: 'tool_call_error' }); this.logger.error('[MCP-ADAPTER] Tools call error:', { id, error: mappedError.message }); // Rethrow to let RpcHandler produce JSON-RPC error envelope throw mappedError; } } async callGetContexts(args) { if (!args.project_id || !UUID_REGEX.test(args.project_id)) { throw { code: JsonRpcErrorCode.INVALID_PARAMS, message: 'Invalid or missing project_id', data: { project_id: args.project_id } }; } try { const response = await this.httpClient.get(`/api/mcp/contexts/${args.project_id}`); const contexts = response.data?.result?.contexts || []; return { content: [{ type: 'text', text: `Found ${contexts.length} context file(s):\n\n${contexts.map((c) => `• ${c.name} (${c.id}) - ${c.mimeType || c.metadata?.file_type || 'unknown'}`).join('\n')}` }] }; } catch (error) { return { content: [{ type: 'text', text: `Error getting contexts: ${error.message}` }], isError: true }; } } async callGetFile(args) { if (!args.project_id || !UUID_REGEX.test(args.project_id)) { throw { code: JsonRpcErrorCode.INVALID_PARAMS, message: 'Invalid or missing project_id', data: { project_id: args.project_id } }; } if (!args.file_id || !UUID_REGEX.test(args.file_id)) { throw { code: JsonRpcErrorCode.INVALID_PARAMS, message: 'Invalid or missing file_id', data: { file_id: args.file_id } }; } try { const response = await this.httpClient.get(`/api/mcp/contexts/${args.project_id}/files/${args.file_id}`); const file = response.data?.result?.context || response.data; return { content: [{ type: 'text', text: `File: ${file.name}\nSize: ${file.size} bytes\nType: ${file.mimeType || file.metadata?.file_type || 'unknown'}\n\nContent:\n${file.content}` }] }; } catch (error) { return { content: [{ type: 'text', text: `Error getting file: ${error.message}` }], isError: true }; } } async callAddFile(args) { if (!args.project_id || !UUID_REGEX.test(args.project_id)) { throw { code: JsonRpcErrorCode.INVALID_PARAMS, message: 'Invalid or missing project_id', data: { project_id: args.project_id } }; } if (!args.filename || typeof args.filename !== 'string') { throw { code: JsonRpcErrorCode.INVALID_PARAMS, message: 'Invalid or missing filename', data: { filename: args.filename } }; } if (!args.content || typeof args.content !== 'string') { throw { code: JsonRpcErrorCode.INVALID_PARAMS, message: 'Invalid or missing file content', data: { content: typeof args.content } }; } try { const response = await this.httpClient.post(`/api/mcp/contexts/${args.project_id}/files`, { filename: args.filename, content: args.content, file_type: args.file_type || 'text' }); const file = response.data?.result?.context || response.data; return { content: [{ type: 'text', text: `File added successfully:\n• Name: ${file.name}\n• ID: ${file.id}\n• Size: ${file.size} bytes\n• Type: ${file.mimeType || file.metadata?.file_type || 'unknown'}` }] }; } catch (error) { return { content: [{ type: 'text', text: `Error adding file: ${error.message}` }], isError: true }; } } } //# sourceMappingURL=method-dispatcher.js.map