UNPKG

@medikode/mcp-server

Version:

Model Context Protocol (MCP) server for Medikode healthcare SaaS platform

489 lines (439 loc) 15.1 kB
const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const http = require('http'); const WebSocket = require('ws'); require('dotenv').config(); const { validateApiKey } = require('./src/middleware/auth'); const { initializeDatabase } = require('./src/database/usageLogger'); const { getVersion } = require('./src/utils/version'); const mcpRoutes = require('./src/routes/mcp'); const capabilitiesRoutes = require('./src/routes/capabilities'); const healthRoutes = require('./src/routes/health'); const { createWebSocketHandler } = require('./src/websocket/mcpWebSocket'); const app = express(); const server = http.createServer(app); const PORT = process.env.PORT || 3000; // Security middleware app.use(helmet()); app.use(cors({ origin: [ 'http://localhost:6274', // MCP Inspector true // Allow all origins for development ], credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'x-api-key'] })); // Body parsing middleware app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Request logging middleware app.use((req, res, next) => { const timestamp = new Date().toISOString(); console.log(`[${timestamp}] ${req.method} ${req.path} - IP: ${req.ip}`); next(); }); // Health check endpoint (no auth required) app.use('/health', healthRoutes); // Capabilities endpoint (no auth required - needed for MCP discovery) app.use('/capabilities', capabilitiesRoutes); // Root endpoint (no auth required - basic service info) app.get('/', (req, res) => { res.json({ service: 'medikode-mcp-server', version: getVersion(), description: 'Model Context Protocol server for Medikode healthcare SaaS platform', endpoints: { health: '/health', capabilities: '/capabilities', mcp: '/mcp', mcpJson: '/mcp.json', mcpInspector: '/mcp/inspect' }, timestamp: new Date().toISOString() }); }); // MCP JSON endpoint (no auth required - for MCP discovery) app.get('/mcp.json', (req, res) => { res.json({ "name": "Medikode MCP Server", "version": getVersion(), "description": "AI-powered medical coding via Assistant, Validator, and Parser APIs.", "tools": [ { "name": "assistant", "description": "Interactive coding assistant for CPT/ICD queries.", "inputSchema": { "type": "object", "properties": { "chart": { "type": "string", "description": "Patient chart text" } }, "required": ["chart"] } }, { "name": "validator", "description": "Validates CPT/ICD codes against chart notes.", "inputSchema": { "type": "object", "properties": { "chart": { "type": "string", "description": "Patient chart text" }, "codes": { "type": "array", "items": { "type": "string" }, "description": "List of CPT/ICD codes" } }, "required": ["chart", "codes"] } }, { "name": "parser", "description": "Parses EOB (Explanation of Benefits) documents into structured JSON.", "inputSchema": { "type": "object", "properties": { "document": { "type": "string", "description": "EOB PDF or text" } }, "required": ["document"] } } ] }); }); // MCP Inspector HTTP endpoint (no auth required for initial connection) app.get('/mcp/inspect', (req, res) => { res.json({ message: 'MCP Inspector endpoint. Send POST JSON-RPC 2.0 requests to this URL.', usage: { method: 'POST', headers: { 'Content-Type': 'application/json' }, exampleBody: { jsonrpc: '2.0', id: 1, method: 'initialize', params: { clientInfo: { name: 'inspector', version: '1.0.0' } } } } }); }); app.post('/mcp/inspect', async (req, res) => { try { const { jsonrpc, id, method, params } = req.body; // Validate JSON-RPC format if (jsonrpc !== '2.0') { return res.status(400).json({ jsonrpc: '2.0', id, error: { code: -32600, message: 'Invalid Request' } }); } // Handle MCP protocol messages const response = await handleMCPHTTPMessage(req, { jsonrpc, id, method, params }); res.json(response); } catch (error) { console.error('Error handling MCP HTTP message:', error); res.status(500).json({ jsonrpc: '2.0', id: req.body.id || null, error: { code: -32603, message: 'Internal error', data: error.message } }); } }); // API key validation for all other routes app.use(validateApiKey); // MCP routes app.use('/mcp', mcpRoutes); // Error handling middleware app.use((err, req, res, next) => { const timestamp = new Date().toISOString(); console.error(`[${timestamp}] Error:`, err); res.status(err.status || 500).json({ error: err.message || 'Internal server error', timestamp: timestamp, service: 'medikode-mcp-server' }); }); // 404 handler app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found', timestamp: new Date().toISOString(), service: 'medikode-mcp-server' }); }); /** * Handle MCP protocol messages over HTTP */ async function handleMCPHTTPMessage(req, message) { const { jsonrpc, id, method, params } = message; try { switch (method) { case 'initialize': return handleInitializeHTTP(id, params); case 'tools/list': return handleToolsListHTTP(id); case 'tools/call': return await handleToolsCallHTTP(req, id, params); case 'ping': return { jsonrpc: '2.0', id, result: {} }; default: return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }; } } catch (error) { console.error('Error handling MCP HTTP message:', error); return { jsonrpc: '2.0', id, error: { code: -32603, message: 'Internal error', data: error.message } }; } } /** * Handle initialize request over HTTP */ function handleInitializeHTTP(id, params) { return { jsonrpc: '2.0', id, result: { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: true } }, serverInfo: { name: 'medikode-mcp-server', version: getVersion() } } }; } /** * Handle tools/list request over HTTP */ function handleToolsListHTTP(id) { const tools = [ { name: 'assistant', description: 'Interactive coding assistant for CPT/ICD queries.', inputSchema: { type: 'object', properties: { chart: { type: 'string', description: 'Patient chart text' } }, required: ['chart'] } }, { name: 'validator', description: 'Validates CPT/ICD codes against chart notes.', inputSchema: { type: 'object', properties: { chart: { type: 'string', description: 'Patient chart text' }, codes: { type: 'array', items: { type: 'string' }, description: 'List of CPT/ICD codes' } }, required: ['chart', 'codes'] } }, { name: 'parser', description: 'Parses EOB (Explanation of Benefits) documents into structured JSON.', inputSchema: { type: 'object', properties: { document: { type: 'string', description: 'EOB PDF or text' } }, required: ['document'] } } ]; return { jsonrpc: '2.0', id, result: { tools } }; } /** * Handle tools/call request over HTTP */ async function handleToolsCallHTTP(req, id, params) { const { name, arguments: args } = params; if (!name || !args) { return { jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params' } }; } // Extract API key from headers const apiKey = req.headers['x-api-key'] || req.headers['authorization']?.replace('Bearer ', ''); // For MCP Inspector, allow requests without authentication for testing if (!apiKey) { console.log('No API key provided for MCP Inspector, using default'); req.apiKeyData = { id: 1, user_id: 1, environment: 'prod' }; } else { // Validate API key try { const { validateApiKey } = require('./src/middleware/auth'); // Create a mock request/response for validation const mockReq = { headers: { 'x-api-key': apiKey } }; const mockRes = { status: () => ({ json: () => {} }) }; const mockNext = () => {}; // For now, we'll use a simplified validation req.apiKeyData = { id: 1, user_id: 1, environment: 'prod' }; } catch (error) { console.log('API key validation failed, using default'); req.apiKeyData = { id: 1, user_id: 1, environment: 'prod' }; } } const startTime = Date.now(); try { let result; // Map MCP Inspector tool names to our internal tool names switch (name) { case 'assistant': result = await callApiService('/assistant', { text: args.chart }, apiKey, 'prod'); break; case 'validator': result = await callApiService('/validator', { patient_chart: args.chart, human_coded_output: args.codes.join(', ') }, apiKey, 'prod'); break; case 'parser': result = await callApiService('/eobparser', { content: args.document }, apiKey, 'prod'); break; default: return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Unknown tool' } }; } const processingTime = Date.now() - startTime; return { jsonrpc: '2.0', id, result: { content: [ { type: 'text', text: JSON.stringify(result.data, null, 2) } ] } }; } catch (error) { const processingTime = Date.now() - startTime; console.error(`Error calling tool ${name}:`, error); return { jsonrpc: '2.0', id, error: { code: -32603, message: 'Internal error', data: error.message } }; } } /** * Helper function to make API calls to the api-service */ async function callApiService(endpoint, data, apiKey, environment) { try { const apiServiceRouter = require('./src/services/apiServiceRouter'); let responseData; switch (endpoint) { case '/assistant': responseData = await apiServiceRouter.processChart(environment, data, apiKey); break; case '/validator': responseData = await apiServiceRouter.validateCodes(environment, data, apiKey); break; case '/eobparser': responseData = await apiServiceRouter.parseEob(environment, data, apiKey); break; default: throw new Error(`Unknown endpoint: ${endpoint}`); } return { status: 200, data: responseData }; } catch (error) { console.error(`Error calling API service for ${endpoint}:`, error); throw error; } } // Initialize database and start server async function startServer() { try { await initializeDatabase(); console.log('Database initialized successfully'); // Create WebSocket server const wss = new WebSocket.Server({ server, path: '/mcp', verifyClient: (info) => { // Allow all connections for now - authentication will be handled in the WebSocket handler return true; } }); // Set up WebSocket handler createWebSocketHandler(wss); server.listen(PORT, '0.0.0.0', () => { console.log(`Medikode MCP Server running on port ${PORT}`); console.log(`Environment: ${process.env.NODE_ENV || 'development'}`); console.log(`API Service URL: ${process.env.API_SERVICE_URL || 'http://localhost:3001'}`); console.log(`WebSocket MCP endpoint: ws://localhost:${PORT}/mcp`); console.log(`MCP Inspector HTTP endpoint: http://localhost:${PORT}/mcp/inspect`); console.log(`MCP JSON endpoint: http://localhost:${PORT}/mcp.json`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } } // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully'); process.exit(0); }); startServer();