@nic0xflamel/defillama-mcp-server
Version:
MCP server for the DefiLlama API
195 lines • 8.22 kB
JavaScript
import OpenAPIClientAxios from 'openapi-client-axios';
import FormData from 'form-data';
import fs from 'fs';
import { isFileUploadParameter } from '../openapi/file-upload.js';
export class HttpClientError extends Error {
constructor(message, status, data, headers) {
super(`${status} ${message}`);
this.status = status;
this.data = data;
this.headers = headers;
this.name = 'HttpClientError';
}
}
export class HttpClient {
constructor(config, openApiSpec) {
// @ts-expect-error
this.client = new (OpenAPIClientAxios.default ?? OpenAPIClientAxios)({
definition: openApiSpec,
axiosConfigDefaults: {
baseURL: config.baseUrl,
headers: {
Accept: 'application/json',
'User-Agent': 'defillama-mcp-server',
...config.headers,
},
},
});
this.api = this.client.init();
}
async prepareFileUpload(operation, params) {
const fileParams = isFileUploadParameter(operation);
if (fileParams.length === 0)
return null;
const formData = new FormData();
// Handle file uploads
for (const param of fileParams) {
const filePath = params[param];
if (!filePath) {
throw new Error(`File path must be provided for parameter: ${param}`);
}
switch (typeof filePath) {
case 'string':
addFile(param, filePath);
break;
case 'object':
if (Array.isArray(filePath)) {
let fileCount = 0;
for (const file of filePath) {
addFile(param, file);
fileCount++;
}
break;
}
//deliberate fallthrough
default:
throw new Error(`Unsupported file type: ${typeof filePath}`);
}
function addFile(name, filePath) {
try {
const fileStream = fs.createReadStream(filePath);
formData.append(name, fileStream);
}
catch (error) {
throw new Error(`Failed to read file at ${filePath}: ${error}`);
}
}
}
// Add non-file parameters to form data
for (const [key, value] of Object.entries(params)) {
if (!fileParams.includes(key)) {
formData.append(key, value);
}
}
return formData;
}
/**
* Execute an OpenAPI operation
*/
async executeOperation(operation, params = {}) {
const axios = await this.client.getAxiosInstance(); // Get the underlying Axios instance
// Handle file uploads if present
const formData = await this.prepareFileUpload(operation, params);
// Separate parameters based on their location
const pathParams = {};
const queryParams = {};
let requestBody = formData || undefined; // Start with formData or undefined
const requestHeaders = formData ? formData.getHeaders() : {};
const processedParams = new Set();
// Extract path and query parameters based on operation definition
if (operation.parameters) {
for (const param of operation.parameters) {
if ('name' in param && param.name && params[param.name] !== undefined) {
if (param.in === 'path') {
pathParams[param.name] = params[param.name];
processedParams.add(param.name);
}
else if (param.in === 'query') {
queryParams[param.name] = params[param.name];
processedParams.add(param.name);
}
else if (param.in === 'header') {
requestHeaders[param.name] = params[param.name];
processedParams.add(param.name);
}
// We ignore cookie parameters for typical server-side API calls
}
}
}
// Determine request body if not using formData
if (!formData) {
if (operation.requestBody) {
// If the schema expects an object, collect unprocessed params
// This assumes simple cases where top-level args match body properties
// or a single 'body' arg contains the object.
// More complex requestBody schemas might need more sophisticated mapping.
if (params['body']) { // Check if a specific 'body' argument exists
requestBody = params['body'];
processedParams.add('body');
}
else {
// Collect remaining params as body properties
const bodyData = {};
for (const key in params) {
if (!processedParams.has(key)) {
bodyData[key] = params[key];
}
}
if (Object.keys(bodyData).length > 0) {
requestBody = bodyData;
}
}
// Set Content-Type for JSON if we have a body
if (requestBody) {
requestHeaders['Content-Type'] = 'application/json';
}
}
else {
// If no requestBody is defined in OpenAPI, treat remaining params as query params (common for GET)
for (const key in params) {
if (!processedParams.has(key)) {
queryParams[key] = params[key];
}
}
}
}
// Construct the full URL
let url = operation.path;
for (const [key, value] of Object.entries(pathParams)) {
url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
}
try {
const requestConfig = {
method: operation.method.toUpperCase(),
url: url,
params: queryParams,
data: requestBody,
headers: requestHeaders,
// Ensure axios does not automatically transform binary data
...(formData && { responseType: 'arraybuffer' }),
};
const response = await axios.request(requestConfig);
// Convert axios headers to Headers object
const responseHeaders = new Headers();
Object.entries(response.headers).forEach(([key, value]) => {
if (value)
responseHeaders.append(key, value.toString());
});
// Handle potential binary data response if formData was used
let responseData = response.data;
if (formData && response.data instanceof ArrayBuffer) {
// Assuming binary response should be returned as base64 string or similar
// Adjust based on expected MCP client handling
responseData = Buffer.from(response.data).toString('base64');
}
return {
data: responseData,
status: response.status,
headers: responseHeaders,
};
}
catch (error) {
if (error.response) {
console.error('Error in http client', error);
const headers = new Headers();
Object.entries(error.response.headers).forEach(([key, value]) => {
if (value)
headers.append(key, value.toString());
});
throw new HttpClientError(error.response.statusText || 'Request failed', error.response.status, error.response.data, headers);
}
throw error;
}
}
}
//# sourceMappingURL=http-client.js.map