@nic0xflamel/defillama-mcp-server
Version:
MCP server for the DefiLlama API
132 lines • 5.45 kB
JavaScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { OpenAPIToMCPConverter } from '../openapi/parser.js';
import { HttpClient, HttpClientError } from '../client/http-client.js';
// import this class, extend and return server
export class MCPProxy {
constructor(name, openApiSpec) {
this.server = new Server({ name, version: '1.0.0' }, { capabilities: { tools: {} } });
const baseUrl = openApiSpec.servers?.[0].url;
if (!baseUrl) {
throw new Error('No base URL found in OpenAPI spec');
}
this.httpClient = new HttpClient({
baseUrl,
headers: this.parseHeadersFromEnv(),
}, openApiSpec);
// Convert OpenAPI spec to MCP tools
const converter = new OpenAPIToMCPConverter(openApiSpec);
const { tools, openApiLookup } = converter.convertToMCPTools();
this.tools = tools;
this.openApiLookup = openApiLookup;
this.setupHandlers();
}
setupHandlers() {
// Handle tool listing
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const mcpTools = [];
// Iterate over the flat list of tools
this.tools.forEach(method => {
// method.name is already the unique, potentially truncated tool name like 'get_protocols'
// The truncateToolName call here is a final safety for the 64 char MCP limit,
// though ensureUniqueName in parser should have already handled it.
const toolNameForClient = this.truncateToolName(method.name);
mcpTools.push({
name: toolNameForClient,
description: method.description,
inputSchema: method.inputSchema,
});
});
return { tools: mcpTools };
});
// Handle tool calling
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name: toolName, arguments: params } = request.params;
// Find the operation in OpenAPI spec
const operation = this.findOperation(toolName);
if (!operation) {
// Use a more specific JSON-RPC error code if possible, e.g., -32601 Method not found
throw new Error(`Method ${toolName} not found`);
}
try {
// Execute the operation
const response = await this.httpClient.executeOperation(operation, params);
// Convert response to MCP format
return {
content: [
{
type: 'text', // currently this is the only type that seems to be used by mcp server
text: JSON.stringify(response.data), // TODO: pass through the http status code text?
},
],
};
}
catch (error) {
console.error('Error in tool call', error);
if (error instanceof HttpClientError) {
console.error('HttpClientError encountered, returning structured error', error);
const data = error.data?.response?.data ?? error.data ?? {};
return {
content: [
{
type: 'text',
text: JSON.stringify({
status: 'error', // TODO: get this from http status code?
...(typeof data === 'object' ? data : { data: data }),
}),
},
],
};
}
throw error;
}
});
}
findOperation(operationId) {
return this.openApiLookup[operationId] ?? null;
}
parseHeadersFromEnv() {
const headersJson = process.env.OPENAPI_MCP_HEADERS;
if (!headersJson) {
return {};
}
try {
const headers = JSON.parse(headersJson);
if (typeof headers !== 'object' || headers === null) {
console.error('OPENAPI_MCP_HEADERS environment variable must be a JSON object, got:', typeof headers);
return {};
}
return headers;
}
catch (error) {
console.error('Failed to parse OPENAPI_MCP_HEADERS environment variable:', error);
return {};
}
}
getContentType(headers) {
const contentType = headers.get('content-type');
if (!contentType)
return 'binary';
if (contentType.includes('text') || contentType.includes('json')) {
return 'text';
}
else if (contentType.includes('image')) {
return 'image';
}
return 'binary';
}
truncateToolName(name) {
if (name.length <= 64) {
return name;
}
return name.slice(0, 64);
}
async connect(transport) {
// The SDK will handle stdio communication
await this.server.connect(transport);
}
getServer() {
return this.server;
}
}
//# sourceMappingURL=proxy.js.map