@gebrai/gebrai
Version:
Model Context Protocol server for GeoGebra mathematical visualization
330 lines • 12 kB
JavaScript
"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