@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
JavaScript
#!/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);