UNPKG

@polymerdao/mcp-polymer

Version:

A Model Context Protocol (MCP) server for blockchain event verification using Polymer's Prove API.

201 lines (200 loc) 8.24 kB
import axios from 'axios'; import { z } from 'zod'; export class EthereumToolsManager { constructor(chainlistClient) { this.ethereumSpec = null; this.specFetchTime = 0; this.CACHE_DURATION = 60 * 60 * 1000; // 1 hour this.chainlistClient = chainlistClient; } async fetchEthereumSpec() { if (this.ethereumSpec && (Date.now() - this.specFetchTime < this.CACHE_DURATION)) { return this.ethereumSpec; } const response = await axios.get('https://raw.githubusercontent.com/etclabscore/ethereum-json-rpc-specification/refs/heads/master/openrpc.json'); this.ethereumSpec = response.data; this.specFetchTime = Date.now(); return this.ethereumSpec; } convertSchemaToZod(schema) { if (!schema) { return z.unknown(); } switch (schema.type) { case 'string': if (schema.format === 'hex') { return this.createHexValidation(); } if (schema.enum) { return z.enum(schema.enum); } return z.string(); case 'boolean': return z.boolean(); case 'integer': return z.number().int(); case 'number': return z.number(); case 'array': if (schema.items) { return z.array(this.convertSchemaToZod(schema.items)); } return z.array(z.unknown()); case 'object': return z.object({}).passthrough(); default: return z.unknown(); } } createHexValidation() { return z.string().refine((value) => { // Basic hex format validation if (!/^0x[0-9a-fA-F]*$/.test(value)) { return false; } const hexPart = value.slice(2); // Remove '0x' prefix // Allow empty hex (0x) if (hexPart.length === 0) { return true; } // Validate specific hex value lengths switch (hexPart.length) { case 8: // 32-bit integers (4 bytes) case 16: // 64-bit integers (8 bytes) case 40: // Ethereum addresses (20 bytes) case 64: // Block hashes, transaction hashes, storage values (32 bytes) case 130: // Public keys (65 bytes) return true; // Allow variable length for: // - Transaction data (any length) // - ABI encoded data (multiples of 32 bytes) // - Block numbers (1-8 bytes) default: // Allow 1-8 bytes for block numbers and similar values if (hexPart.length >= 2 && hexPart.length <= 16 && hexPart.length % 2 === 0) { return true; } // Allow multiples of 64 chars (32 bytes) for ABI data if (hexPart.length % 64 === 0) { return true; } // Allow any even length for transaction data and other variable-length hex return hexPart.length % 2 === 0; } }, { message: 'Must be a valid hex string with appropriate length (0x + even number of hex chars). Expected lengths: addresses (42 chars), hashes (66 chars), or other valid hex formats' }); } createEthRpcCall(method) { return async (chainName, ...params) => { const result = await this.chainlistClient.executeWithRetry(chainName, method, params, async (rpcUrl) => { const response = await axios.post(rpcUrl, { jsonrpc: '2.0', method, params, id: Date.now() }, { timeout: 30000, headers: { 'Content-Type': 'application/json' } }); if (response.data.error) { const errorMsg = response.data.error.message || JSON.stringify(response.data.error); throw new Error(`RPC Error from ${rpcUrl}: ${errorMsg}`); } return response.data.result; }); if (!result.success) { throw new Error(result.error); } return { result: result.data, rpc_used: result.usedRpc, attempted_rpcs: result.attemptedRpcs, failed_rpcs: result.failedRpcs }; }; } createToolFromMethod(method) { const paramSchemas = { chain_name: z.string().describe('Name of the blockchain (e.g., ethereum, polygon)') }; // All eth_* methods take a params array following JSON-RPC convention // Some methods take no parameters (empty array) paramSchemas['params'] = z.array(z.unknown()).optional().default([]).describe('RPC method parameters array'); // Create Zod object schema const zodSchema = z.object(paramSchemas); // Get required parameter names // chain_name is always required, params is optional with default empty array const requiredParams = ['chain_name']; return { name: method.name, description: method.summary || method.description || `Execute ${method.name} on the specified blockchain`, inputSchema: { type: 'object', properties: Object.fromEntries(Object.entries(paramSchemas).map(([key, zodSchema]) => [ key, { type: this.getJsonSchemaType(zodSchema), description: zodSchema.description } ])), required: requiredParams }, zodSchema }; } getJsonSchemaType(zodSchema) { // Handle ZodOptional wrapping if (zodSchema instanceof z.ZodOptional) { return this.getJsonSchemaType(zodSchema._def.innerType); } // Handle ZodDefault wrapping if (zodSchema instanceof z.ZodDefault) { return this.getJsonSchemaType(zodSchema._def.innerType); } if (zodSchema instanceof z.ZodString) return 'string'; if (zodSchema instanceof z.ZodNumber) return 'number'; if (zodSchema instanceof z.ZodBoolean) return 'boolean'; if (zodSchema instanceof z.ZodArray) return 'array'; if (zodSchema instanceof z.ZodObject) return 'object'; return 'string'; } async generateEthereumTools() { const spec = await this.fetchEthereumSpec(); const tools = []; const handlers = new Map(); // Filter for eth_* methods only const ethMethods = spec.methods.filter(method => method.name.startsWith('eth_')); for (const method of ethMethods) { const tool = this.createToolFromMethod(method); const handler = this.createEthRpcCall(method.name); tools.push(tool); handlers.set(tool.name, handler); } return { tools, handlers }; } async handleEthereumToolCall(toolName, args, handlers) { const handler = handlers.get(toolName); if (!handler) { throw new Error(`No handler found for tool: ${toolName}`); } const typedArgs = args; const { chain_name, params = [] } = typedArgs; if (!chain_name || typeof chain_name !== 'string') { throw new Error(`Missing or invalid chain_name parameter. Got: ${typeof chain_name}`); } if (!Array.isArray(params)) { throw new Error(`params must be an array. Got: ${typeof params}`); } // Pass the params array directly to the RPC call // chain_name is used internally to select the RPC endpoint return await handler(chain_name, ...params); } }