UNPKG

@polymerdao/mcp-polymer

Version:

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

219 lines (218 loc) 10 kB
#!/usr/bin/env node import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { z } from 'zod'; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { PolymerClient } from './polymer.js'; import { ProofVerifier } from './verification.js'; import { getEnvironmentConfig, validateEnvironment, listEnvironments, getApiKey } from './config.js'; import { DynamicToolRegistry } from './dynamic-tools.js'; import { EthereumToolsManager } from './ethereum-tools.js'; import { ChainlistClient } from './chainlist.js'; import { readFileSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; let cachedVersion = null; function getPackageVersion() { if (cachedVersion !== null) return cachedVersion; try { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Try multiple possible locations for package.json const possiblePaths = [ join(__dirname, '..', 'package.json'), join(__dirname, 'package.json'), join(__dirname, '..', '..', 'package.json') ]; for (const packageJsonPath of possiblePaths) { try { const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); const version = packageJson.version || '0.2.0'; cachedVersion = version; return version; } catch { // Continue to next path } } // If no package.json found, use fallback throw new Error('package.json not found in any expected location'); } catch (error) { const fallbackVersion = process.env.npm_package_version || '0.2.0'; cachedVersion = fallbackVersion; return fallbackVersion; } } async function registerFallbackTools(server, polymerClient, verifier, config) { // Fallback manual tool registrations if dynamic loading fails server.registerTool("request_proof", { title: "Request Proof", description: "Request proof of a blockchain event from the Polymer API.", inputSchema: { chain_name: z.string(), global_log_index: z.number(), block_number: z.number() } }, async (input) => { const response = await polymerClient.requestProof(input.chain_name, input.global_log_index, input.block_number, config.apiKey, config.rpcEnvironment); if (response.proof && verifier) { response.verified = verifier.verifyPolymerProof(response.proof).valid; } const result = { content: [{ type: "text", text: JSON.stringify(response) }] }; return result; }); server.registerTool("query_proof_status", { title: "Query Proof Status", description: "Query the status of a proof job.", inputSchema: { job_id: z.string() } }, async (input) => { const response = await polymerClient.getProofStatus(input.job_id, config.apiKey); if (response.proof && verifier) { response.verified = verifier.verifyPolymerProof(response.proof).valid; } const result = { content: [{ type: "text", text: JSON.stringify(response) }] }; return result; }); server.registerTool("get_connected_chains", { title: "Get Connected Chains", description: "Get information about connected chains.", inputSchema: {} }, async () => { const result = await polymerClient.getConnectedChains(config.apiKey); return { content: [{ type: "text", text: JSON.stringify(result) }] }; }); server.registerTool("get_client_updates", { title: "Get Client Updates", description: "Get client update statuses for all chains.", inputSchema: {} }, async () => { const result = await polymerClient.getClientUpdates(config.apiKey, config.rpcEnvironment); return { content: [{ type: "text", text: JSON.stringify(result) }] }; }); } async function main() { const argv = await yargs(hideBin(process.argv)) .option('environment', { type: 'string', choices: listEnvironments(), description: 'Polymer environment (devnet, testnet, shadownet, mainnet)', default: 'mainnet' }) .option('version', { type: 'boolean', description: 'Show version number', alias: 'v' }) .env('POLYMER') .help() .argv; if (argv.version) { console.log(getPackageVersion()); process.exit(0); } const environment = validateEnvironment(argv.environment); const envConfig = getEnvironmentConfig(environment); const apiKey = getApiKey(); const config = { apiKey, environment, validationKey: envConfig.validationKey, polymerApiBase: envConfig.polymerApiBase, chainId: envConfig.chainId, rpcEnvironment: envConfig.rpcEnvironment, }; const polymerClient = new PolymerClient(config.polymerApiBase); const verifier = config.validationKey ? new ProofVerifier(config.validationKey, config.chainId) : undefined; const server = new McpServer({ name: "mcp-polymer", version: getPackageVersion(), }, { capabilities: { prompts: {} } }); // Initialize chainlist client and dynamic tool registries const chainlistClient = new ChainlistClient(); const openRpcSpecUrl = 'https://cdn.polymerlabs.org/static/openrpc.json'; const toolRegistry = new DynamicToolRegistry(openRpcSpecUrl, polymerClient, verifier, config); const ethereumToolsManager = new EthereumToolsManager(chainlistClient); try { // Register Polymer API tools await toolRegistry.initialize(); await toolRegistry.registerAllTools(server); console.error(`Dynamically registered ${toolRegistry.getAvailableMethods().length} tools from Polymer OpenRPC spec`); // Register dynamic Ethereum RPC tools const { tools: ethTools, handlers: ethHandlers } = await ethereumToolsManager.generateEthereumTools(); for (const tool of ethTools) { server.registerTool(tool.name, { title: tool.name, description: tool.description || `Execute ${tool.name} on the specified blockchain`, inputSchema: tool.zodSchema.shape }, async (input) => { try { const result = await ethereumToolsManager.handleEthereumToolCall(tool.name, input, ethHandlers); return { content: [{ type: "text", text: JSON.stringify(result) }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: JSON.stringify({ error: errorMessage }) }] }; } }); } console.error(`Dynamically registered ${ethTools.length} Ethereum RPC tools`); // Register unified introspection tools that know about both Polymer and Ethereum tools server.registerTool("list_all_tools", { title: "List All Available Tools", description: "Get a comprehensive list of all available tools including Polymer API and Ethereum RPC methods", inputSchema: {} }, async () => { const polymerMethods = toolRegistry.getMethodsByCategory('').map(method => ({ name: method.name, summary: method.summary, description: method.description, paramCount: method.params.length, category: 'Polymer API' })); const ethereumMethods = ethTools.map(tool => ({ name: tool.name, summary: tool.description, description: tool.description, paramCount: Object.keys(tool.inputSchema.properties || {}).length - 1, // -1 for chain_name category: 'Ethereum RPC' })); const allMethods = [...polymerMethods, ...ethereumMethods]; return { content: [{ type: "text", text: JSON.stringify(allMethods, null, 2) }] }; }); server.registerTool("get_tool_details", { title: "Get Tool Details", description: "Get detailed information about any available tool (Polymer API or Ethereum RPC)", inputSchema: { tool_name: z.string() } }, async (input) => { // First check Polymer methods const polymerMethods = toolRegistry.getMethodsByCategory(''); const polymerMethod = polymerMethods.find(m => m.name === input.tool_name); if (polymerMethod) { return { content: [{ type: "text", text: JSON.stringify(polymerMethod, null, 2) }] }; } // Then check Ethereum tools const ethTool = ethTools.find(t => t.name === input.tool_name); if (ethTool) { return { content: [{ type: "text", text: JSON.stringify({ name: ethTool.name, description: ethTool.description, inputSchema: ethTool.inputSchema, category: 'Ethereum RPC' }, null, 2) }] }; } throw new Error(`Tool ${input.tool_name} not found`); }); } catch (error) { console.error('Failed to load OpenRPC specs, falling back to manual tool registration:', error); // Fallback to manual registration if dynamic fails await registerFallbackTools(server, polymerClient, verifier, config); } console.error("MCP server started via stdio"); const transport = new StdioServerTransport(); await server.connect(transport); } main().catch(console.error);