UNPKG

@polymerdao/mcp-polymer

Version:

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

179 lines (178 loc) 6.33 kB
import axios from 'axios'; import { z } from 'zod'; export class OpenRpcSpecLoader { constructor(specUrl) { this.spec = null; this.specUrl = specUrl; } async loadSpec() { if (this.spec) { return this.spec; } try { const response = await axios.get(this.specUrl, { timeout: 10000, headers: { 'Accept': 'application/json' } }); this.spec = response.data; this.validateSpec(this.spec); return this.spec; } catch (error) { throw new Error(`Failed to load OpenRPC spec from ${this.specUrl}: ${error}`); } } getSpec() { return this.spec; } validateSpec(spec) { if (!spec.openrpc) { throw new Error('Invalid OpenRPC spec: missing openrpc field'); } if (!spec.info || !spec.info.title || !spec.info.version) { throw new Error('Invalid OpenRPC spec: missing or invalid info field'); } if (!Array.isArray(spec.methods)) { throw new Error('Invalid OpenRPC spec: methods must be an array'); } } getMethodByName(methodName) { return this.spec?.methods.find(method => method.name === methodName); } getAllMethods() { return this.spec?.methods || []; } getMethodNames() { return this.getAllMethods().map(method => method.name); } getMethodsByCategory(prefix) { return this.getAllMethods().filter(method => method.name.startsWith(prefix)); } getServers() { return this.spec?.servers || []; } getServerForEnvironment(environment) { const servers = this.getServers(); // Environment-specific defaults for URLs not in OpenRPC spec const environmentDefaults = { 'devnet': 'https://api.devnet.polymer.zone/v1', 'shadownet': 'https://api.shadownet.polymer.zone/v1' }; const envLower = environment.toLowerCase(); // First, try to find server by environment name in summary or name let server = servers.find(s => s.summary?.toLowerCase().includes(envLower) || s.name?.toLowerCase().includes(envLower)); // If mainnet requested but not found, use server without 'testnet' in name if (!server && envLower === 'mainnet') { server = servers.find(s => !s.summary?.toLowerCase().includes('testnet') && !s.name?.toLowerCase().includes('testnet')); } // If no server found in spec but we have a default, create a virtual server if (!server && environmentDefaults[envLower]) { return { url: environmentDefaults[envLower], name: environment, summary: `${environment} (default override)` }; } // Default to first server if no match return server || servers[0]; } getServerUrl(environment) { const server = this.getServerForEnvironment(environment); return server?.url; } } export function convertOpenRpcParamToZod(param) { return convertOpenRpcSchemaToZod(param.schema, param.required !== false); } export function convertOpenRpcSchemaToZod(schema, required = true) { let zodSchema; // Handle $ref (schema references) if (schema.$ref) { // For now, treat as unknown - could be enhanced to resolve refs zodSchema = z.unknown(); } // Handle union types else if (schema.anyOf || schema.oneOf) { const unionSchemas = (schema.anyOf || schema.oneOf || []).map(s => convertOpenRpcSchemaToZod(s, true)); if (unionSchemas.length === 0) { zodSchema = z.unknown(); } else if (unionSchemas.length === 1) { zodSchema = unionSchemas[0]; } else { zodSchema = z.union([unionSchemas[0], unionSchemas[1], ...unionSchemas.slice(2)]); } } // Handle intersection types else if (schema.allOf) { // For now, treat as unknown - intersection types are complex zodSchema = z.unknown(); } // Handle primitive types else { switch (schema.type) { case 'string': if (schema.enum) { zodSchema = z.enum(schema.enum); } else { zodSchema = z.string(); } break; case 'number': case 'integer': zodSchema = z.number(); break; case 'boolean': zodSchema = z.boolean(); break; case 'array': { const itemSchema = schema.items ? convertOpenRpcSchemaToZod(schema.items, true) : z.unknown(); zodSchema = z.array(itemSchema); break; } case 'object': if (schema.properties) { const shape = {}; const requiredFields = schema.required || []; for (const [propName, propSchema] of Object.entries(schema.properties)) { const isRequired = requiredFields.includes(propName); shape[propName] = convertOpenRpcSchemaToZod(propSchema, isRequired); if (!isRequired) { shape[propName] = shape[propName].optional(); } } zodSchema = z.object(shape); } else { zodSchema = z.record(z.unknown()); } break; default: zodSchema = z.unknown(); } } // Add description if available if (schema.description) { zodSchema = zodSchema.describe(schema.description); } // Make optional if not required if (!required) { zodSchema = zodSchema.optional(); } return zodSchema; } export function convertMethodParamsToZodObject(method) { const shape = {}; for (const param of method.params) { shape[param.name] = convertOpenRpcParamToZod(param); } return z.object(shape); }