UNPKG

@gebrai/gebrai

Version:

Model Context Protocol server for GeoGebra mathematical visualization

330 lines 12 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.McpServer = void 0; const events_1 = require("events"); const readline_1 = __importDefault(require("readline")); const tools_1 = require("./tools"); const errors_1 = require("./utils/errors"); const gemini_compatibility_1 = require("./utils/gemini-compatibility"); const logger_1 = __importDefault(require("./utils/logger")); // Check if we're in MCP mode (stdio communication) // When piping input, process.stdin.isTTY is undefined, not false const isMcpMode = !process.stdin.isTTY; /** * MCP Server implementation following JSON-RPC 2.0 protocol over stdio */ class McpServer extends events_1.EventEmitter { config; isRunning = false; isInitialized = false; readline; constructor(config) { super(); this.config = config; if (!isMcpMode) { logger_1.default.info('MCP Server initialized', { config }); } } /** * Start the MCP server with stdio communication */ async start() { if (this.isRunning) { if (!isMcpMode) { logger_1.default.warn('Server is already running'); } return; } try { this.isRunning = true; // Create readline interface for stdin/stdout communication this.readline = readline_1.default.createInterface({ input: process.stdin, output: process.stdout, terminal: false }); // Handle incoming JSON-RPC requests from stdin this.readline.on('line', async (line) => { try { const trimmedLine = line.trim(); if (!trimmedLine) return; // Parse JSON-RPC request const request = JSON.parse(trimmedLine); // Process the request and get response const response = await this.processRequest(request); // Send JSON-RPC response to stdout (only if not null - notifications don't get responses) if (response !== null) { process.stdout.write(JSON.stringify(response) + '\n'); } } catch (error) { logger_1.default.error('Error processing stdin line', { line, error }); // Send error response const errorResponse = { jsonrpc: '2.0', id: null, error: (0, errors_1.handleError)(error) }; process.stdout.write(JSON.stringify(errorResponse) + '\n'); } }); this.readline.on('close', () => { if (!isMcpMode) { logger_1.default.info('Stdin closed, shutting down server'); } this.stop(); }); if (!isMcpMode) { logger_1.default.info(`MCP Server started: ${this.config.name} v${this.config.version}`); logger_1.default.info(`Available tools: ${tools_1.toolRegistry.getToolCount()}`); logger_1.default.info('Listening for JSON-RPC requests on stdin...'); } this.emit('started'); } catch (error) { this.isRunning = false; logger_1.default.error('Failed to start MCP server', error); throw error; } } /** * Stop the MCP server */ async stop() { if (!this.isRunning) { if (!isMcpMode) { logger_1.default.warn('Server is not running'); } return; } try { this.isRunning = false; if (this.readline) { this.readline.close(); this.readline = undefined; } if (!isMcpMode) { logger_1.default.info('MCP Server stopped'); } this.emit('stopped'); } catch (error) { logger_1.default.error('Error stopping MCP server', error); throw error; } } /** * Process a JSON-RPC request */ async processRequest(requestData) { try { // Validate basic JSON-RPC structure if (!(0, errors_1.validateJsonRpcRequest)(requestData)) { return this.createErrorResponse(null, (0, errors_1.handleError)(errors_1.errors.invalidRequest())); } const request = requestData; logger_1.default.debug('Processing request', { method: request.method, id: request.id }); // Route to appropriate handler switch (request.method) { case 'initialize': return await this.handleInitialize(request); case 'notifications/initialized': return await this.handleInitialized(request); case 'tools/list': return await this.handleToolsList(request); case 'tools/call': return await this.handleToolsCall(request); default: return this.createErrorResponse(request.id ?? null, (0, errors_1.handleError)(errors_1.errors.methodNotFound(request.method))); } } catch (error) { logger_1.default.error('Error processing request', error); return this.createErrorResponse(null, (0, errors_1.handleError)(error)); } } /** * Handle initialize requests */ async handleInitialize(request) { try { logger_1.default.debug('Handling initialize request', { clientInfo: request.params?.clientInfo }); // Validate protocol version compatibility const requestedVersion = request.params?.protocolVersion; const supportedVersion = '2024-11-05'; if (requestedVersion !== supportedVersion) { logger_1.default.warn(`Client requested protocol version ${requestedVersion}, server supports ${supportedVersion}`); } this.isInitialized = true; return { jsonrpc: '2.0', id: request.id ?? null, result: { protocolVersion: supportedVersion, capabilities: { tools: { listChanged: false } }, serverInfo: { name: this.config.name, version: this.config.version } } }; } catch (error) { logger_1.default.error('Error handling initialize', error); return this.createInitializeErrorResponse(request.id ?? null, (0, errors_1.handleError)(error)); } } /** * Handle notifications/initialized requests (notification, no response expected) */ async handleInitialized(_request) { try { logger_1.default.debug('Client initialization complete'); // For notifications, we don't send a response return null; } catch (error) { logger_1.default.error('Error handling initialized notification', error); // Even for errors in notifications, we typically don't send responses return null; } } /** * Handle tools/list requests */ async handleToolsList(request) { try { // Check if server is initialized if (!this.isInitialized) { return this.createToolsListErrorResponse(request.id ?? null, (0, errors_1.handleError)(errors_1.errors.invalidRequest('Server not initialized'))); } const rawTools = tools_1.toolRegistry.getTools(); // Apply Gemini compatibility transformations to tool schemas const tools = rawTools.map(tool => { // Check if the tool schema needs Gemini compatibility fixes if ((0, gemini_compatibility_1.needsGeminiCompatibility)(tool.inputSchema)) { logger_1.default.debug(`Applying Gemini compatibility fixes to tool: ${tool.name}`); return { ...tool, inputSchema: (0, gemini_compatibility_1.makeGeminiCompatible)(tool.inputSchema) }; } return tool; }); logger_1.default.debug(`Returning ${tools.length} tools (with Gemini compatibility applied)`); return { jsonrpc: '2.0', id: request.id ?? null, result: { tools } }; } catch (error) { logger_1.default.error('Error handling tools/list', error); return this.createToolsListErrorResponse(request.id ?? null, (0, errors_1.handleError)(error)); } } /** * Handle tools/call requests */ async handleToolsCall(request) { try { // Check if server is initialized if (!this.isInitialized) { return this.createToolsCallErrorResponse(request.id ?? null, (0, errors_1.handleError)(errors_1.errors.invalidRequest('Server not initialized'))); } // Validate request parameters if (!request.params || typeof request.params.name !== 'string') { return this.createToolsCallErrorResponse(request.id ?? null, (0, errors_1.handleError)(errors_1.errors.invalidParams('Missing or invalid tool name'))); } const { name, arguments: toolArgs = {} } = request.params; // Execute the tool const result = await tools_1.toolRegistry.executeTool(name, toolArgs); return { jsonrpc: '2.0', id: request.id ?? null, result }; } catch (error) { logger_1.default.error('Error handling tools/call', error); return this.createToolsCallErrorResponse(request.id ?? null, (0, errors_1.handleError)(error)); } } /** * Create an error response */ createErrorResponse(id, error) { return { jsonrpc: '2.0', id, error }; } /** * Create a tools/list error response */ createToolsListErrorResponse(id, error) { return { jsonrpc: '2.0', id, error }; } /** * Create a tools/call error response */ createToolsCallErrorResponse(id, error) { return { jsonrpc: '2.0', id, error }; } /** * Create an initialize error response */ createInitializeErrorResponse(id, error) { return { jsonrpc: '2.0', id, error }; } /** * Get server status */ getStatus() { return { name: this.config.name, version: this.config.version, description: this.config.description, isRunning: this.isRunning, toolCount: tools_1.toolRegistry.getToolCount(), uptime: this.isRunning ? process.uptime() : 0 }; } /** * Get server configuration */ getConfig() { return { ...this.config }; } /** * Check if server is running */ isServerRunning() { return this.isRunning; } } exports.McpServer = McpServer; //# sourceMappingURL=server.js.map