UNPKG

jezweb-mcp-core

Version:

Jezweb Model Context Protocol (MCP) Core - A universal server for providing AI tools and resources, designed for seamless integration with various AI models and clients. Features adaptable multi-provider support, comprehensive tool and resource management

378 lines 13.1 kB
/** * Transport Adapters for Thin Deployment Adapters * * This module provides shared transport-specific logic that can be used * by thin deployment adapters to handle protocol-specific concerns while * delegating all business logic to the BaseMCPHandler. */ import { ErrorCodes, LegacyErrorCodes, createStandardErrorResponse, createEnhancedError, } from '../types/index.js'; /** * Cloudflare Workers Transport Adapter * Handles HTTP-specific optimizations for Cloudflare Workers */ export class CloudflareWorkerTransportAdapter { async preprocessRequest(request) { // HTTP transport doesn't need special preprocessing return request; } async postprocessResponse(response) { // HTTP transport doesn't need special postprocessing // The Worker handles JSON -> HTTP conversion return response; } formatError(error, requestId) { return createStandardErrorResponse(requestId, error.code, error.message, error.data); } } /** * HTTP Transport Adapter for Cloudflare Workers * Handles HTTP-specific request/response processing */ export class HTTPTransportAdapter { /** * Extract API key from URL path */ extractApiKeyFromPath(pathname) { const pathParts = pathname.split('/'); // Expected format: /mcp/{api-key} if (pathParts.length !== 3 || pathParts[1] !== 'mcp') { const errorResponse = createStandardErrorResponse(null, ErrorCodes.INVALID_REQUEST, 'Invalid URL format. Expected: /mcp/{api-key}', { receivedPath: pathname, expectedFormat: '/mcp/{api-key}', documentation: 'https://docs.openai.com/api-reference' }); return { apiKey: null, error: errorResponse }; } const apiKey = pathParts[2]; if (!apiKey || apiKey.length < 10) { const authError = createEnhancedError(LegacyErrorCodes.UNAUTHORIZED, 'Invalid or missing API key', { keyLength: apiKey?.length || 0, minLength: 10, documentation: 'https://docs.openai.com/api-reference/authentication' }); const errorResponse = createStandardErrorResponse(null, authError.code, authError.message, authError.data); return { apiKey: null, error: errorResponse }; } return { apiKey, error: null }; } /** * Parse and validate JSON-RPC request from HTTP body */ async parseRequest(request) { // Parse request body let mcpRequest; try { mcpRequest = await request.json(); } catch (error) { const errorResponse = createStandardErrorResponse(null, ErrorCodes.PARSE_ERROR, 'Invalid JSON in request body', { parseError: error instanceof Error ? error.message : 'Unknown parse error', documentation: 'https://www.jsonrpc.org/specification' }); return { mcpRequest: null, error: errorResponse }; } // Validate JSON-RPC format if (!mcpRequest.jsonrpc || mcpRequest.jsonrpc !== '2.0' || !mcpRequest.method) { const errorResponse = createStandardErrorResponse(mcpRequest.id || null, ErrorCodes.INVALID_REQUEST, 'Invalid JSON-RPC 2.0 request format', { missingFields: { jsonrpc: !mcpRequest.jsonrpc || mcpRequest.jsonrpc !== '2.0', method: !mcpRequest.method }, received: { jsonrpc: mcpRequest.jsonrpc, method: mcpRequest.method }, documentation: 'https://www.jsonrpc.org/specification' }); return { mcpRequest: null, error: errorResponse }; } return { mcpRequest, error: null }; } /** * Create HTTP Response with CORS headers */ createResponse(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json', }, }); } /** * Create CORS preflight response */ createCORSResponse() { return new Response(null, { status: 200, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', }, }); } /** * Create method not allowed response */ createMethodNotAllowedResponse() { return new Response('Method not allowed', { status: 405, headers: { 'Access-Control-Allow-Origin': '*', 'Content-Type': 'text/plain', }, }); } /** * Create error response with enhanced error details */ createErrorResponse(error, requestId = null) { const errorResponse = createStandardErrorResponse(requestId, ErrorCodes.INTERNAL_ERROR, 'Internal server error', { timestamp: new Date().toISOString(), error: error instanceof Error ? { name: error.name, message: error.message } : 'Unknown error' }); return this.createResponse(errorResponse, 500); } } /** * Stdio Transport Helper Class * Provides stdio-specific utilities for NPM package deployment */ class StdioTransportHelper { debug; constructor(debug = false) { this.debug = debug; } /** * Setup error handling for the process */ setupErrorHandling() { // Prevent crashes that cause connection issues with MCP clients process.on('uncaughtException', (error) => { this.logError('Uncaught exception:', error); this.sendErrorResponse(null, -32603, 'Internal server error', error.message); }); process.on('unhandledRejection', (reason, promise) => { this.logError('Unhandled rejection at:', promise, 'reason:', reason); this.sendErrorResponse(null, -32603, 'Internal server error', String(reason)); }); // Handle graceful shutdown process.on('SIGTERM', () => { this.logDebug('Received SIGTERM, shutting down gracefully'); process.exit(0); }); process.on('SIGINT', () => { this.logDebug('Received SIGINT, shutting down gracefully'); process.exit(0); }); } /** * Setup stdio interface with readline */ setupStdioInterface(handleInput) { const readline = require('readline'); // Ensure stdout is line-buffered for MCP client compatibility process.stdout.setEncoding('utf8'); if (process.stdout.isTTY) { // Type assertion for Node.js internal property process.stdout._flush = process.stdout._flush || (() => { }); } const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); rl.on('line', (line) => { handleInput(line.trim()); }); rl.on('close', () => { this.logDebug('Stdin closed, exiting'); process.exit(0); }); } /** * Parse and validate JSON-RPC request from stdio line */ parseRequest(line) { // Handle empty line handshake that MCP clients send if (line === '') { this.logDebug('Received empty line handshake from MCP client'); return { mcpRequest: null, error: null }; } this.logDebug('Received input:', line); // Parse JSON-RPC message let request; try { request = JSON.parse(line); } catch (parseError) { this.logError('JSON parse error:', parseError); const errorResponse = this.createErrorResponse(null, -32700, 'Parse error', parseError instanceof Error ? parseError.message : 'Unknown parse error'); return { mcpRequest: null, error: errorResponse }; } // Validate JSON-RPC 2.0 format if (request.jsonrpc !== '2.0') { const errorResponse = this.createErrorResponse(request.id, -32600, 'Invalid Request', 'Invalid JSON-RPC version'); return { mcpRequest: null, error: errorResponse }; } return { mcpRequest: request, error: null }; } /** * Send response via stdout */ sendResponse(response) { // Ensure messages are UTF-8 encoded and delimited by newlines // Messages MUST NOT contain embedded newlines const message = JSON.stringify(response); // Validate no embedded newlines if (message.includes('\n') || message.includes('\r')) { this.logError('Response contains embedded newlines, this will break MCP client compatibility'); // Remove embedded newlines to prevent protocol violation const cleanMessage = message.replace(/[\n\r]/g, ' '); process.stdout.write(cleanMessage + '\n'); } else { process.stdout.write(message + '\n'); } this.logDebug('Sent response:', message); } /** * Send error response */ sendErrorResponse(id, code, message, data = null) { const response = this.createErrorResponse(id, code, message, data); this.sendResponse(response); } /** * Create error response */ createErrorResponse(id, code, message, data = null) { return { jsonrpc: '2.0', id: id, error: { code: code, message: message, ...(data && { data: data }) } }; } /** * Create initialize response */ createInitializeResponse(id, serverName, serverVersion) { return { jsonrpc: '2.0', id: id, result: { protocolVersion: '2024-11-05', capabilities: { tools: { listChanged: false }, resources: { subscribe: false, listChanged: false } }, serverInfo: { name: serverName, version: serverVersion } } }; } /** * Create initialized notification */ createInitializedNotification() { return { jsonrpc: '2.0', method: 'notifications/initialized', params: {} }; } /** * Validate API key from environment */ validateApiKey() { const apiKey = process.env.OPENAI_API_KEY; if (!apiKey) { const errorResponse = this.createErrorResponse(null, -32602, 'Invalid params', 'OPENAI_API_KEY environment variable is required. Please configure it in your MCP client.'); return { apiKey: null, error: errorResponse }; } return { apiKey, error: null }; } /** * Debug logging */ logDebug(...args) { if (this.debug) { console.error('[DEBUG]', ...args); } } /** * Error logging */ logError(...args) { console.error('[ERROR]', ...args); } } /** * Stdio Transport Adapter that implements the TransportAdapter interface */ export class StdioTransportAdapter extends StdioTransportHelper { async preprocessRequest(request) { // Stdio transport doesn't need special preprocessing return request; } async postprocessResponse(response) { // Stdio transport doesn't need special postprocessing return response; } formatError(error, requestId) { return this.createErrorResponse(requestId, error.code, error.message, error.data); } } /** * Alias for backward compatibility */ export const ProxyTransportAdapter = CloudflareWorkerTransportAdapter; export const LocalDevTransportAdapter = StdioTransportAdapter; /** * Shared request routing logic */ export class RequestRouter { /** * Route request to appropriate method */ static getMethodType(method) { switch (method) { case 'initialize': return 'initialize'; case 'tools/list': case 'tools/call': case 'resources/list': case 'resources/read': case 'prompts/list': case 'prompts/get': case 'completion/complete': return 'mcp'; default: return 'unknown'; } } /** * Check if server is initialized for MCP requests */ static requiresInitialization(method) { return method !== 'initialize'; } } //# sourceMappingURL=transport-adapters.js.map