UNPKG

linea-mcp

Version:

A Model Context Protocol server for interacting with the Linea blockchain

200 lines (199 loc) 9.86 kB
#!/usr/bin/env node import 'dotenv/config'; // Load .env file at the very beginning import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, // McpTool, // Removed import } from '@modelcontextprotocol/sdk/types.js'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { z, ZodSchema } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; // Enhanced error handling process.on('uncaughtException', (error) => { console.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled Rejection at:', promise, 'reason:', reason); }); // Define __dirname equivalent for ESM const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const discoveredTools = new Map(); // Discover tools dynamically (adapted from previous mcp.ts) async function discoverTools() { discoveredTools.clear(); const toolsDir = path.join(__dirname, 'tools'); console.error(`Discovering tools in: ${toolsDir}`); // Log to stderr for debugging try { const categories = await fs.readdir(toolsDir, { withFileTypes: true }); console.error(`Found ${categories.length} tool categories`); for (const category of categories) { if (category.isDirectory()) { const categoryName = category.name; // Use fileURLToPath for Windows compatibility with ESM const indexPath = path.join(toolsDir, categoryName, 'index.js'); const indexUrl = `file://${indexPath.replace(/\\/g, '/')}`; console.error(`Attempting to import: ${indexUrl}`); try { const toolModule = await import(indexUrl); console.error(`Successfully imported module for ${categoryName}`); console.error(`Module exports: ${Object.keys(toolModule).join(', ')}`); const metadata = toolModule.toolMetadata || {}; console.error(`Found metadata: ${Object.keys(metadata).join(', ')}`); for (const exportName in toolModule) { if (exportName.endsWith('Schema') && toolModule[exportName] instanceof ZodSchema) { const schema = toolModule[exportName]; // Get the handler name, removing the 'Schema' suffix const handlerNameBase = exportName.replace(/Schema$/, ''); // Look for handler both with same case and with first letter lowercase const handlerNameExact = handlerNameBase; const handlerNameLowerFirst = handlerNameBase.charAt(0).toLowerCase() + handlerNameBase.slice(1); // Try to find the handler with either name let handler = toolModule[handlerNameExact]; if (!handler) { handler = toolModule[handlerNameLowerFirst]; } if (typeof handler === 'function') { // Find the correct metadata key (matching the actual handler name) const metadataKey = Object.keys(metadata).find(key => key === handlerNameExact || key === handlerNameLowerFirst) || handlerNameLowerFirst; const toolMeta = metadata[metadataKey] || {}; const mcpToolName = `${categoryName}_${handlerNameLowerFirst}`; const description = toolMeta.description || `Tool for ${handlerNameLowerFirst} in ${categoryName}`; const jsonSchema = zodToJsonSchema(schema, { $refStrategy: 'none' }); delete jsonSchema.$schema; delete jsonSchema.description; const mcpDefinition = { name: mcpToolName, description: description, inputSchema: jsonSchema, }; discoveredTools.set(mcpToolName, { mcpDefinition, handler, schema, }); console.error(`Discovered tool: ${mcpToolName}`); // Log to stderr } else { console.error(`Found schema ${exportName} but no matching handler ${handlerNameExact} or ${handlerNameLowerFirst}`); } } } } catch (importError) { console.error(`Error importing tool module ${indexUrl}:`, importError); } } } console.error(`Tool discovery finished. Found ${discoveredTools.size} tools.`); } catch (error) { console.error('Error discovering tools:', error); } } class LineaMcpServer { server; constructor() { this.server = new Server({ name: 'linea-mcp-server', version: '0.1.0', // Consider reading from package.json }, { capabilities: { resources: {}, // No resources defined for now tools: { list: true, // Explicitly enable tools.list capability call: true // Explicitly enable tools.call capability }, }, }); this.setupRequestHandlers(); // Enhanced error handling for MCP server this.server.onerror = (error) => { console.error('[MCP Error]', error); console.error('Error details:', JSON.stringify(error, null, 2)); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupRequestHandlers() { // List Tools Handler this.server.setRequestHandler(ListToolsRequestSchema, async () => { // Ensure tools are discovered before listing if (discoveredTools.size === 0) { console.error('No tools discovered yet, attempting discovery again...'); await discoverTools(); // Attempt discovery again if needed } if (discoveredTools.size === 0) { console.error('Tool discovery failed or yielded no tools.'); // Optionally throw an error or return an empty list with a warning } const toolsList = Array.from(discoveredTools.values()).map(t => t.mcpDefinition); console.error(`Listing ${toolsList.length} tools.`); // Log to stderr return { tools: toolsList }; }); // Call Tool Handler this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; console.error(`Received call for tool: ${name} with args:`, args); // Log to stderr const tool = discoveredTools.get(name); if (!tool) { console.error(`Tool '${name}' not found.`); throw new McpError(ErrorCode.MethodNotFound, `Tool '${name}' not found`); } try { const validatedArgs = tool.schema.parse(args || {}); console.error(`Executing handler for ${name} with validated args:`, validatedArgs); const result = await tool.handler(validatedArgs); console.error(`Handler for ${name} returned:`, result); return { content: [ { type: 'text', // Changed from 'application/json' to 'text' for MCP compatibility text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { console.error(`Error executing tool '${name}':`, error); let errorMessage = 'Internal Server Error'; let errorCode = ErrorCode.InternalError; let errorDetails = {}; if (error instanceof z.ZodError) { errorCode = ErrorCode.InvalidParams; errorMessage = 'Invalid input parameters'; errorDetails = error.format(); } else if (error instanceof Error) { errorMessage = error.message; } // Throwing an McpError is preferred for standard MCP error handling throw new McpError(errorCode, errorMessage, errorDetails); } }); } async run() { try { // Discover tools before starting the server await discoverTools(); const transport = new StdioServerTransport(); console.error('Connecting MCP server with stdio transport...'); await this.server.connect(transport); console.error('Linea MCP server running on stdio'); } catch (error) { console.error('Error in MCP server run:', error); throw error; } } } // Start the server const server = new LineaMcpServer(); server.run().catch(error => { console.error('Failed to start Linea MCP server:', error); process.exit(1); });