@nic0xflamel/defillama-mcp-server
Version:
MCP server for the DefiLlama API
487 lines • 21.2 kB
JavaScript
export class OpenAPIToMCPConverter {
constructor(openApiSpec) {
this.openApiSpec = openApiSpec;
this.schemaCache = {};
this.nameCounter = 0;
}
/**
* Resolve a $ref reference to its schema in the openApiSpec.
* Returns the raw OpenAPI SchemaObject or null if not found.
*/
internalResolveRef(ref, resolvedRefs) {
if (!ref.startsWith('#/')) {
return null;
}
if (resolvedRefs.has(ref)) {
return null;
}
const parts = ref.replace(/^#\//, '').split('/');
let current = this.openApiSpec;
for (const part of parts) {
current = current[part];
if (!current)
return null;
}
resolvedRefs.add(ref);
return current;
}
/**
* Convert an OpenAPI schema (or reference) into a JSON Schema object.
* Uses caching and handles cycles by returning $ref nodes.
*/
convertOpenApiSchemaToJsonSchema(schema, resolvedRefs, resolveRefs = false) {
if ('$ref' in schema) {
const ref = schema.$ref;
if (!resolveRefs) {
if (ref.startsWith('#/components/schemas/')) {
return {
$ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
...('description' in schema ? { description: schema.description } : {}),
};
}
console.error(`Attempting to resolve ref ${ref} not found in components collection.`);
// deliberate fall through
}
// Create base schema with $ref and description if present
const refSchema = { $ref: ref };
if ('description' in schema && schema.description) {
refSchema.description = schema.description;
}
// If already cached, return immediately with description
if (this.schemaCache[ref]) {
return this.schemaCache[ref];
}
const resolved = this.internalResolveRef(ref, resolvedRefs);
if (!resolved) {
// TODO: need extensive tests for this and we definitely need to handle the case of self references
console.error(`Failed to resolve ref ${ref}`);
return {
$ref: ref.replace(/^#\/components\/schemas\//, '#/$defs/'),
description: 'description' in schema ? (schema.description ?? '') : '',
};
}
else {
const converted = this.convertOpenApiSchemaToJsonSchema(resolved, resolvedRefs, resolveRefs);
this.schemaCache[ref] = converted;
return converted;
}
}
// Handle inline schema
const result = {};
if (schema.type) {
result.type = schema.type;
}
// Convert binary format to uri-reference and enhance description
if (schema.format === 'binary') {
result.format = 'uri-reference';
const binaryDesc = 'absolute paths to local files';
result.description = schema.description ? `${schema.description} (${binaryDesc})` : binaryDesc;
}
else {
if (schema.format) {
result.format = schema.format;
}
if (schema.description) {
result.description = schema.description;
}
}
if (schema.enum) {
result.enum = schema.enum;
}
if (schema.default !== undefined) {
result.default = schema.default;
}
// Handle object properties
if (schema.type === 'object') {
result.type = 'object';
if (schema.properties) {
result.properties = {};
for (const [name, propSchema] of Object.entries(schema.properties)) {
result.properties[name] = this.convertOpenApiSchemaToJsonSchema(propSchema, resolvedRefs, resolveRefs);
}
}
if (schema.required) {
result.required = schema.required;
}
if (schema.additionalProperties === true || schema.additionalProperties === undefined) {
result.additionalProperties = true;
}
else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
result.additionalProperties = this.convertOpenApiSchemaToJsonSchema(schema.additionalProperties, resolvedRefs, resolveRefs);
}
else {
result.additionalProperties = false;
}
}
// Handle arrays - ensure binary format conversion happens for array items too
if (schema.type === 'array' && schema.items) {
result.type = 'array';
result.items = this.convertOpenApiSchemaToJsonSchema(schema.items, resolvedRefs, resolveRefs);
}
// oneOf, anyOf, allOf
if (schema.oneOf) {
result.oneOf = schema.oneOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs));
}
if (schema.anyOf) {
result.anyOf = schema.anyOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs));
}
if (schema.allOf) {
result.allOf = schema.allOf.map((s) => this.convertOpenApiSchemaToJsonSchema(s, resolvedRefs, resolveRefs));
}
return result;
}
convertToMCPTools() {
const openApiLookup = {};
const mcpToolsList = [];
const zip = {};
// Define the set of allowed tool names based on user input
const allowedToolNames = new Set([
'get_protocols',
'get_v2_chains',
'get_v2_historicalChainTvl',
'get_v2_historicalChainTvl__by_chain',
'get_tvl__by_protocol',
'get_stablecoincharts_all',
'get_pools',
'get_chart__by_pool',
'get_overview_dexs',
'get_overview_dexs__by_chain',
'get_summary_dexs__by_protocol',
'get_overview_fees',
'get_overview_fees__by_chain',
'get_summary_fees__by_protocol'
]);
for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
if (!pathItem)
continue;
for (const [method, operation] of Object.entries(pathItem)) {
if (!this.isOperation(method, operation))
continue;
// --- Filtering Logic ---
// First, determine the potential tool name
let potentialMethodName = operation.operationId;
if (!potentialMethodName) {
const sanitizedPath = path
.replace(/^\/|\/$/g, '')
.replace(/\//g, '_')
.replace(/\{/g, '_by_')
.replace(/\}/g, '');
potentialMethodName = `${method.toLowerCase()}_${sanitizedPath}`;
console.error(`Operation at ${method} ${path} missing operationId. Generated name: ${potentialMethodName}`);
}
// Skip if the potential name is not in the allowed list
if (!potentialMethodName || !allowedToolNames.has(potentialMethodName)) {
continue;
}
// --- End Filtering Logic ---
const mcpMethod = this.convertOperationToMCPMethod(operation, method, path);
if (mcpMethod) {
const uniqueName = this.ensureUniqueName(mcpMethod.name);
mcpMethod.name = uniqueName;
mcpToolsList.push(mcpMethod);
openApiLookup[uniqueName] = { ...operation, method, path };
zip[uniqueName] = { openApi: { ...operation, method, path }, mcp: mcpMethod };
}
}
}
return { tools: mcpToolsList, openApiLookup, zip };
}
/**
* Convert the OpenAPI spec to OpenAI's ChatCompletionTool format
*/
convertToOpenAITools() {
const tools = [];
for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
if (!pathItem)
continue;
for (const [method, operation] of Object.entries(pathItem)) {
if (!this.isOperation(method, operation))
continue;
const parameters = this.convertOperationToJsonSchema(operation, method, path);
const tool = {
type: 'function',
function: {
name: operation.operationId,
description: operation.summary || operation.description || '',
parameters: parameters,
},
};
tools.push(tool);
}
}
return tools;
}
/**
* Convert the OpenAPI spec to Anthropic's Tool format
*/
convertToAnthropicTools() {
const tools = [];
for (const [path, pathItem] of Object.entries(this.openApiSpec.paths || {})) {
if (!pathItem)
continue;
for (const [method, operation] of Object.entries(pathItem)) {
if (!this.isOperation(method, operation))
continue;
const parameters = this.convertOperationToJsonSchema(operation, method, path);
const tool = {
name: operation.operationId,
description: operation.summary || operation.description || '',
input_schema: parameters,
};
tools.push(tool);
}
}
return tools;
}
convertComponentsToJsonSchema() {
const components = this.openApiSpec.components || {};
const schema = {};
for (const [key, value] of Object.entries(components.schemas || {})) {
schema[key] = this.convertOpenApiSchemaToJsonSchema(value, new Set());
}
return schema;
}
/**
* Helper method to convert an operation to a JSON Schema for parameters
*/
convertOperationToJsonSchema(operation, method, path) {
const schema = {
type: 'object',
properties: {},
required: [],
$defs: this.convertComponentsToJsonSchema(),
};
// Handle parameters (path, query, header, cookie)
if (operation.parameters) {
for (const param of operation.parameters) {
const paramObj = this.resolveParameter(param);
if (paramObj && paramObj.schema) {
const paramSchema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set());
// Merge parameter-level description if available
if (paramObj.description) {
paramSchema.description = paramObj.description;
}
schema.properties[paramObj.name] = paramSchema;
if (paramObj.required) {
schema.required.push(paramObj.name);
}
}
}
}
// Handle requestBody
if (operation.requestBody) {
const bodyObj = this.resolveRequestBody(operation.requestBody);
if (bodyObj?.content) {
if (bodyObj.content['application/json']?.schema) {
const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set());
if (bodySchema.type === 'object' && bodySchema.properties) {
for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
schema.properties[name] = propSchema;
}
if (bodySchema.required) {
schema.required.push(...bodySchema.required);
}
}
}
}
}
return schema;
}
isOperation(method, operation) {
return ['get', 'post', 'put', 'delete', 'patch'].includes(method.toLowerCase());
}
isParameterObject(param) {
return !('$ref' in param);
}
isRequestBodyObject(body) {
return !('$ref' in body);
}
resolveParameter(param) {
if (this.isParameterObject(param)) {
return param;
}
else {
const resolved = this.internalResolveRef(param.$ref, new Set());
if (resolved && resolved.name) {
return resolved;
}
}
return null;
}
resolveRequestBody(body) {
if (this.isRequestBodyObject(body)) {
return body;
}
else {
const resolved = this.internalResolveRef(body.$ref, new Set());
if (resolved) {
return resolved;
}
}
return null;
}
resolveResponse(response) {
if ('$ref' in response) {
const resolved = this.internalResolveRef(response.$ref, new Set());
if (resolved) {
return resolved;
}
else {
return null;
}
}
return response;
}
convertOperationToMCPMethod(operation, method, path) {
let methodName = operation.operationId;
if (!methodName) {
// Generate a name based on method and path if operationId is missing
// Sanitize path: remove leading/trailing slashes, replace other slashes and curly braces with underscores
const sanitizedPath = path
.replace(/^\/|\/$/g, '') // Remove leading/trailing slashes
.replace(/\//g, '_') // Replace slashes with underscores
.replace(/\{/g, '_by_') // Replace opening curly brace with _by_
.replace(/\}/g, ''); // Remove closing curly brace
methodName = `${method.toLowerCase()}_${sanitizedPath}`;
console.error(`Operation at ${method} ${path} missing operationId. Generated name: ${methodName}`);
}
const inputSchema = {
$defs: this.convertComponentsToJsonSchema(),
type: 'object',
properties: {},
required: [],
};
// Handle parameters (path, query, header, cookie)
if (operation.parameters) {
for (const param of operation.parameters) {
const paramObj = this.resolveParameter(param);
if (paramObj && paramObj.schema) {
const schema = this.convertOpenApiSchemaToJsonSchema(paramObj.schema, new Set(), false);
// Merge parameter-level description if available
if (paramObj.description) {
schema.description = paramObj.description;
}
inputSchema.properties[paramObj.name] = schema;
if (paramObj.required) {
inputSchema.required.push(paramObj.name);
}
}
}
}
// Handle requestBody
if (operation.requestBody) {
const bodyObj = this.resolveRequestBody(operation.requestBody);
if (bodyObj?.content) {
// Handle multipart/form-data for file uploads
// We convert the multipart/form-data schema to a JSON schema and we require
// that the user passes in a string for each file that points to the local file
if (bodyObj.content['multipart/form-data']?.schema) {
const formSchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['multipart/form-data'].schema, new Set(), false);
if (formSchema.type === 'object' && formSchema.properties) {
for (const [name, propSchema] of Object.entries(formSchema.properties)) {
inputSchema.properties[name] = propSchema;
}
if (formSchema.required) {
inputSchema.required.push(...formSchema.required);
}
}
}
// Handle application/json
else if (bodyObj.content['application/json']?.schema) {
const bodySchema = this.convertOpenApiSchemaToJsonSchema(bodyObj.content['application/json'].schema, new Set(), false);
// Merge body schema into the inputSchema's properties
if (bodySchema.type === 'object' && bodySchema.properties) {
for (const [name, propSchema] of Object.entries(bodySchema.properties)) {
inputSchema.properties[name] = propSchema;
}
if (bodySchema.required) {
inputSchema.required.push(...bodySchema.required);
}
}
else {
// If the request body is not an object, just put it under "body"
inputSchema.properties['body'] = bodySchema;
inputSchema.required.push('body');
}
}
}
}
// Build description including error responses
let description = operation.summary || operation.description || '';
if (operation.responses) {
const errorResponses = Object.entries(operation.responses)
.filter(([code]) => code.startsWith('4') || code.startsWith('5'))
.map(([code, response]) => {
const responseObj = this.resolveResponse(response);
let errorDesc = responseObj?.description || '';
return `${code}: ${errorDesc}`;
});
if (errorResponses.length > 0) {
description += '\nError Responses:\n' + errorResponses.join('\n');
}
}
// Extract return type (response schema)
const returnSchema = this.extractResponseType(operation.responses);
// Generate Zod schema from input schema
try {
// const zodSchemaStr = jsonSchemaToZod(inputSchema, { module: "cjs" })
// console.log(zodSchemaStr)
// // Execute the function with the zod instance
// const zodSchema = eval(zodSchemaStr) as z.ZodType
return {
name: methodName,
description,
inputSchema,
...(returnSchema ? { returnSchema } : {}),
};
}
catch (error) {
console.error(`Failed to generate Zod schema for ${methodName}:`, error);
// Fallback to a basic object schema
return {
name: methodName,
description,
inputSchema,
...(returnSchema ? { returnSchema } : {}),
};
}
}
extractResponseType(responses) {
// Look for a success response
const successResponse = responses?.['200'] || responses?.['201'] || responses?.['202'] || responses?.['204'];
if (!successResponse)
return null;
const responseObj = this.resolveResponse(successResponse);
if (!responseObj || !responseObj.content)
return null;
if (responseObj.content['application/json']?.schema) {
const returnSchema = this.convertOpenApiSchemaToJsonSchema(responseObj.content['application/json'].schema, new Set(), false);
returnSchema['$defs'] = this.convertComponentsToJsonSchema();
// Preserve the response description if available and not already set
if (responseObj.description && !returnSchema.description) {
returnSchema.description = responseObj.description;
}
return returnSchema;
}
// If no JSON response, fallback to a generic string or known formats
if (responseObj.content['image/png'] || responseObj.content['image/jpeg']) {
return { type: 'string', format: 'binary', description: responseObj.description || '' };
}
// Fallback
return { type: 'string', description: responseObj.description || '' };
}
ensureUniqueName(name) {
if (name.length <= 64) {
return name;
}
const truncatedName = name.slice(0, 64 - 5); // Reserve space for suffix
const uniqueSuffix = this.generateUniqueSuffix();
return `${truncatedName}-${uniqueSuffix}`;
}
generateUniqueSuffix() {
this.nameCounter += 1;
return this.nameCounter.toString().padStart(4, '0');
}
}
//# sourceMappingURL=parser.js.map