Skip to main content
Glama
execute-api-tool.ts10.1 kB
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import axios, { type AxiosRequestConfig, type AxiosError } from "axios"; import { ZodError, z } from "zod"; import { jsonSchemaToZod } from 'json-schema-to-zod'; import { Config } from "../config.js"; import type { JsonObject } from "../types/index.js"; /** * Interface for MCP Tool Definition */ export interface McpToolDefinition { name: string; description: string; inputSchema: any; method: string; pathTemplate: string; executionParameters: { name: string; in: string }[]; requestBodyContentType?: string; securityRequirements: any[]; prompt?: string; parameters: any[]; baseUrl: string; } /** * Executes an API tool with the provided arguments * * @param toolName Name of the tool to execute * @param definition Tool definition * @param toolArgs Arguments provided by the user * @param allSecuritySchemes Security schemes from the OpenAPI spec * @returns Call tool result */ export async function executeApiTool( toolName: string, definition: McpToolDefinition, toolArgs: JsonObject, allSecuritySchemes: Record<string, any>, token?: string, ): Promise<CallToolResult> { try { // Validate arguments against the input schema let validatedArgs: JsonObject; try { const zodSchema = getZodSchemaFromJsonSchema( definition.inputSchema, toolName, ); const argsToParse = typeof toolArgs === 'object' && toolArgs !== null ? toolArgs : {}; validatedArgs = zodSchema.parse(argsToParse); } catch (error: unknown) { if (error instanceof ZodError) { const validationErrorMessage = `Invalid arguments for tool '${toolName}': ${error.errors.map((e) => `${e.path.join('.')} (${e.code}): ${e.message}`).join(', ')}`; return { content: [{ type: 'text', text: validationErrorMessage }] }; } else { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: 'text', text: `Internal error during validation setup: ${errorMessage}`, }, ], }; } } // Prepare URL, query parameters, headers, and request body let urlPath = definition.pathTemplate; const queryParams: Record<string, any> = {}; const headers: Record<string, string> = { Accept: 'application/json', 'X-Moralis-Platform': 'MCP', }; let requestBodyData: any = undefined; // Apply parameters to the URL path, query, or headers definition.executionParameters.forEach((param) => { const value = validatedArgs[param.name]; if (typeof value !== 'undefined' && value !== null) { if (param.in === 'path') { urlPath = urlPath.replace( `{${param.name}}`, encodeURIComponent(String(value)), ); } else if (param.in === 'query') { queryParams[param.name] = value; } else if (param.in === 'header') { headers[param.name.toLowerCase()] = String(value); } } }); // Ensure all path parameters are resolved if (urlPath.includes('{')) { throw new Error(`Failed to resolve path parameters: ${urlPath}`); } // Construct the full URL const requestUrl = `${definition.baseUrl}${urlPath}`; // Handle request body if needed if ( definition.requestBodyContentType && typeof validatedArgs['requestBody'] !== 'undefined' ) { requestBodyData = validatedArgs['requestBody']; headers['content-type'] = definition.requestBodyContentType; } // Apply security requirements if available // Security requirements use OR between array items and AND within each object const appliedSecurity = definition.securityRequirements?.find((req) => { // Try each security requirement (combined with OR) return Object.entries(req).every(([schemeName, scopesArray]) => { const scheme = allSecuritySchemes[schemeName]; if (!scheme) return false; // API Key security (header, query, cookie) if (scheme.type === 'apiKey') { return !!token || !!Config.MORALIS_API_KEY; } return false; }); }); // If we found matching security scheme(s), apply them if (appliedSecurity) { // Apply each security scheme from this requirement (combined with AND) for (const [schemeName, scopesArray] of Object.entries(appliedSecurity)) { const scheme = allSecuritySchemes[schemeName]; // API Key security if (scheme?.type === 'apiKey') { const apiKey = token || Config.MORALIS_API_KEY; if (apiKey) { if (scheme.in === 'header') { headers[scheme.name.toLowerCase()] = apiKey; console.error( `Applied API key '${schemeName}' in header '${scheme.name}'`, ); } } } } } // Log warning if security is required but not available else if (definition.securityRequirements?.length > 0) { // First generate a more readable representation of the security requirements const securityRequirementsString = definition.securityRequirements .map((req) => { const parts = Object.entries(req) .map(([name, scopesArray]) => { const scopes = scopesArray as string[]; if (scopes.length === 0) return name; return `${name} (scopes: ${scopes.join(', ')})`; }) .join(' AND '); return `[${parts}]`; }) .join(' OR '); console.warn( `Tool '${toolName}' requires security: ${securityRequirementsString}, but no suitable credentials found.`, ); } // Prepare the axios request configuration const config: AxiosRequestConfig = { method: definition.method.toUpperCase(), url: requestUrl, params: queryParams, headers: headers, ...(requestBodyData !== undefined && { data: requestBodyData }), }; // Log request info to stderr (doesn't affect MCP output) console.error( `Executing tool "${toolName}": ${config.method} ${config.url}`, ); // Execute the request const response = await axios(config); // Process and format the response let responseText = ''; const contentType = response.headers['content-type']?.toLowerCase() || ''; // Handle JSON responses if ( contentType.includes('application/json') && typeof response.data === 'object' && response.data !== null ) { try { responseText = JSON.stringify(response.data, null, 2); } catch (e) { responseText = '[Stringify Error]'; } } // Handle string responses else if (typeof response.data === 'string') { responseText = response.data; } // Handle other response types else if (response.data !== undefined && response.data !== null) { responseText = String(response.data); } // Handle empty responses else { responseText = `(Status: ${response.status} - No body content)`; } // Return formatted response return { content: [ { type: 'text', text: `API Response (Status: ${response.status}):\n${responseText}`, }, ], }; } catch (error: unknown) { // Handle errors during execution let errorMessage: string; // Format Axios errors specially if (axios.isAxiosError(error)) { errorMessage = formatApiError(error); } // Handle standard errors else if (error instanceof Error) { errorMessage = error.message; } // Handle unexpected error types else { errorMessage = `Unexpected error: ${String(error)}`; } // Log error to stderr console.error( `Error during execution of tool '${toolName}':`, errorMessage, ); // Return error message to client return { content: [{ type: 'text', text: errorMessage }] }; } } /** * Formats API errors for better readability * * @param error Axios error * @returns Formatted error message */ function formatApiError(error: AxiosError): string { let message = 'API request failed.'; if (error.response) { message = `API Error: Status ${error.response.status} (${error.response.statusText || 'Status text not available'}). `; const responseData = error.response.data; const MAX_LEN = 200; if (typeof responseData === 'string') { message += `Response: ${responseData.substring(0, MAX_LEN)}${responseData.length > MAX_LEN ? '...' : ''}`; } else if (responseData) { try { const jsonString = JSON.stringify(responseData); message += `Response: ${jsonString.substring(0, MAX_LEN)}${jsonString.length > MAX_LEN ? '...' : ''}`; } catch { message += 'Response: [Could not serialize data]'; } } else { message += 'No response body received.'; } } else if (error.request) { message = 'API Network Error: No response received from server.'; if (error.code) message += ` (Code: ${error.code})`; } else { message += `API Request Setup Error: ${error.message}`; } return message; } /** * Converts a JSON Schema to a Zod schema for runtime validation * * @param jsonSchema JSON Schema * @param toolName Tool name for error reporting * @returns Zod schema */ function getZodSchemaFromJsonSchema( jsonSchema: any, toolName: string, ): z.ZodTypeAny { if (typeof jsonSchema !== 'object' || jsonSchema === null) { return z.object({}).passthrough(); } try { const zodSchemaString = jsonSchemaToZod(jsonSchema); const zodSchema = eval(zodSchemaString); if (typeof zodSchema?.parse !== 'function') { throw new Error('Eval did not produce a valid Zod schema.'); } return zodSchema as z.ZodTypeAny; } catch (err: any) { console.error( `Failed to generate/evaluate Zod schema for '${toolName}':`, err, ); return z.object({}).passthrough(); } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/MoralisWeb3/moralis-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server