@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
JavaScript
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);
}