Skip to main content
Glama

OpenAPI to MCP Server

apiClient.ts19.3 kB
import axios, { AxiosRequestConfig, AxiosError } from 'axios'; import type { ApiCallDetails, ApiClientResponse } from './types'; import { config } from './config'; import type { OpenAPIV3 } from 'openapi-types'; /** * Applies security requirements to an API request based on OpenAPI security definitions * @param requestConfig Axios request configuration to modify * @param securityRequirements Security requirements from OpenAPI operation * @param securitySchemes Security schemes definitions from OpenAPI components */ async function applySecurity( requestConfig: AxiosRequestConfig, securityRequirements: OpenAPIV3.SecurityRequirementObject[] | null, securitySchemes: Record<string, OpenAPIV3.SecuritySchemeObject> | undefined ): Promise<void> { if (!securityRequirements || securityRequirements.length === 0) { console.error("No security requirements for this operation."); return; // No security needed } if (!securitySchemes) { console.error("Security requirements defined but no security schemes available"); return; } // Loop through the security requirements to find one we can satisfy for (const requirement of securityRequirements) { // Each requirement is an AND of multiple schemes (all must be satisfied) const schemeNames = Object.keys(requirement); let allSchemesSatisfied = true; for (const schemeName of schemeNames) { console.error(`Attempting to apply security scheme: ${schemeName}`); // Get the security scheme definition const scheme = securitySchemes[schemeName]; if (!scheme) { console.error(`Security scheme '${schemeName}' not found in OpenAPI definitions`); allSchemesSatisfied = false; break; } if (!requestConfig.headers) { requestConfig.headers = {}; } // Apply security based on the scheme type switch (scheme.type) { case 'apiKey': // Handle API Key (in header, query, or cookie) const apiKey = config.securityCredentials?.[schemeName] || config.apiKey; if (!apiKey) { console.error(`No API key found for scheme '${schemeName}'`); allSchemesSatisfied = false; break; } if (scheme.in === 'header') { requestConfig.headers[scheme.name] = apiKey; console.error(`Applied API Key header '${scheme.name}' for scheme: ${schemeName}`); } else if (scheme.in === 'query') { requestConfig.params = { ...requestConfig.params || {}, [scheme.name]: apiKey }; console.error(`Applied API Key query param '${scheme.name}' for scheme: ${schemeName}`); } else if (scheme.in === 'cookie') { // Simple cookie handling - in production, consider using a cookie jar const cookieValue = `${scheme.name}=${apiKey}`; requestConfig.headers['Cookie'] = requestConfig.headers['Cookie'] ? `${requestConfig.headers['Cookie']}; ${cookieValue}` : cookieValue; console.error(`Applied API Key cookie '${scheme.name}' for scheme: ${schemeName}`); } break; case 'http': // Handle HTTP authentication (Basic, Bearer) const authCred = config.securityCredentials?.[schemeName] || config.apiKey; if (!authCred) { console.error(`No credentials found for scheme '${schemeName}'`); allSchemesSatisfied = false; break; } if (scheme.scheme?.toLowerCase() === 'basic') { // For Basic auth, credential should be username:password or we use a default // In production, would need proper username/password configuration const credentials = authCred.includes(':') ? Buffer.from(authCred).toString('base64') : Buffer.from(`${authCred}:password`).toString('base64'); requestConfig.headers['Authorization'] = `Basic ${credentials}`; console.error(`Applied Basic auth for scheme: ${schemeName}`); } else if (scheme.scheme?.toLowerCase() === 'bearer') { requestConfig.headers['Authorization'] = `Bearer ${authCred}`; console.error(`Applied Bearer token for scheme: ${schemeName}`); } else { // Unknown HTTP auth type console.error(`Unsupported HTTP auth scheme: ${scheme.scheme}`); allSchemesSatisfied = false; } break; case 'oauth2': // For OAuth2, we'd typically have a token already acquired const oauthToken = config.securityCredentials?.[schemeName] || config.apiKey; if (!oauthToken) { console.error(`No OAuth token found for scheme '${schemeName}'`); allSchemesSatisfied = false; break; } requestConfig.headers['Authorization'] = `Bearer ${oauthToken}`; console.error(`Applied OAuth2 token for scheme: ${schemeName}`); break; case 'openIdConnect': // Similar to OAuth2 const oidcToken = config.securityCredentials?.[schemeName] || config.apiKey; if (!oidcToken) { console.error(`No OpenID Connect token found for scheme '${schemeName}'`); allSchemesSatisfied = false; break; } requestConfig.headers['Authorization'] = `Bearer ${oidcToken}`; console.error(`Applied OpenID Connect token for scheme: ${schemeName}`); break; default: console.error(`Unsupported security scheme type: ${(scheme as any).type}`); allSchemesSatisfied = false; } if (!allSchemesSatisfied) { break; } } if (allSchemesSatisfied) { // We found and applied a security requirement that we could satisfy return; } } // If we get here, we tried all requirements but couldn't satisfy any console.error(`Could not satisfy any security requirements. API call may fail.`); // You might want to throw an error if security is mandatory for your API // throw new Error(`Required security schemes could not be applied.`); } export async function executeApiCall( details: ApiCallDetails, mcpInput: Record<string, any> // The raw input object from MCP ): Promise<ApiClientResponse> { const { method, pathTemplate, serverUrl, parameters, requestBody, securityRequirements, securitySchemes } = details; // Input validation if (!method) { return { success: false, statusCode: 400, error: 'API call details missing HTTP method' }; } if (!pathTemplate) { return { success: false, statusCode: 400, error: 'API call details missing path template' }; } if (!serverUrl) { return { success: false, statusCode: 400, error: 'API call details missing server URL' }; } // Ensure mcpInput is a valid object if (!mcpInput || typeof mcpInput !== 'object' || Array.isArray(mcpInput)) { console.error(`Invalid input type: ${typeof mcpInput}. Expected an object.`); return { success: false, statusCode: 400, error: 'Invalid input: expected an object' }; } // Normalize the input object to handle different formats // Some MCP clients might send parameters differently const normalizedInput = { ...mcpInput }; let url = `${serverUrl}${pathTemplate}`; const queryParams: Record<string, any> = {}; const headers: Record<string, any> = {}; let body: any = undefined; console.error(`Executing API call for tool: ${details.method} ${details.pathTemplate}`); console.error(`MCP Input received:`, normalizedInput); // Map MCP input back to HTTP request components for (const paramDef of parameters) { const paramName = paramDef.name; const paramValue = normalizedInput[paramName]; if (paramValue !== undefined) { // Only map if present in MCP input // Validate parameter value according to schema if possible if (paramDef.schema) { const validationError = validateParameterValue(paramValue, paramDef); if (validationError) { return { success: false, statusCode: 400, error: `Parameter '${paramName}': ${validationError}` }; } } switch (paramDef.in) { case 'path': url = url.replace(`{${paramName}}`, encodeURIComponent(String(paramValue))); break; case 'query': queryParams[paramName] = paramValue; break; case 'header': headers[paramName] = String(paramValue); break; case 'cookie': // Cookie handling is more complex, often managed by agents or specific header logic console.error(`Cookie parameter '${paramName}' handling not implemented.`); break; } } else if (paramDef.required) { console.error(`Error: Required parameter '${paramName}' missing in MCP input.`); return { success: false, statusCode: 400, error: `Missing required parameter: ${paramName}` }; } } // Map request body if defined and present in input if (requestBody && normalizedInput.requestBody !== undefined) { // Validate request body against schema if available const bodyValidationError = validateRequestBody(normalizedInput.requestBody, requestBody); if (bodyValidationError) { return { success: false, statusCode: 400, error: `Request body validation failed: ${bodyValidationError}` }; } // Assuming the nested 'requestBody' property in mcpInput holds the body body = normalizedInput.requestBody; // Assume application/json for now, get content type from requestBody definition if needed headers['Content-Type'] = 'application/json'; } else if (requestBody?.required) { console.error(`Error: Required requestBody missing in MCP input.`); return { success: false, statusCode: 400, error: `Missing required request body` }; } const requestConfig: AxiosRequestConfig = { method: method as any, // Cast needed for Axios types url: url, params: queryParams, headers: headers, data: body, // Validate status to handle non-2xx as resolved promises validateStatus: (status: number) => status >= 200 && status < 500, // Handle 4xx as well }; // Apply custom headers from configuration if (config.customHeaders && Object.keys(config.customHeaders).length > 0) { requestConfig.headers = { ...requestConfig.headers, ...config.customHeaders }; } // Add X-MCP header unless disabled if (!config.disableXMcp) { requestConfig.headers = { ...requestConfig.headers, 'X-MCP': '1' }; } // Apply security before making the call try { await applySecurity(requestConfig, securityRequirements, securitySchemes); } catch (secErr: any) { console.error("Security application failed:", secErr); return { success: false, statusCode: 401, error: `Security setup failed: ${secErr.message}` }; } console.error(`Making HTTP request:`, { method: requestConfig.method, url: requestConfig.url, params: requestConfig.params, headers: requestConfig.headers, data: requestConfig.data ? '[Request Body Present]' : undefined // Avoid logging sensitive data }); try { const response = await axios(requestConfig); console.error(`API response received: Status ${response.status}`); if (response.status >= 200 && response.status < 300) { return { success: true, statusCode: response.status, data: response.data, }; } else { // Handle 4xx client errors reported by the API console.error(`API returned client error ${response.status}:`, response.data); return { success: false, statusCode: response.status, error: `API Error ${response.status}: ${JSON.stringify(response.data)}`, data: response.data // Optionally include error data }; } } catch (error) { const axiosError = error as AxiosError; console.error(`API call failed: ${axiosError.message}`, axiosError.response?.data || axiosError.code); if (axiosError.response) { // Errors during the request setup or >= 500 if validateStatus wasn't broad enough return { success: false, statusCode: axiosError.response.status || 500, error: `API Error ${axiosError.response.status}: ${JSON.stringify(axiosError.response.data) || axiosError.message}`, data: axiosError.response.data }; } else { // Network error, DNS error, etc. return { success: false, statusCode: 503, // Service Unavailable or similar error: `Network or request setup error: ${axiosError.message}`, }; } } } /** * Validates a parameter value against its schema definition * @param value The parameter value to validate * @param paramDef The parameter definition from OpenAPI * @returns Error message or null if valid */ function validateParameterValue(value: any, paramDef: OpenAPIV3.ParameterObject): string | null { const schema = paramDef.schema as OpenAPIV3.SchemaObject; if (!schema) return null; // No schema to validate against // Type validation if (schema.type === 'integer' || schema.type === 'number') { if (typeof value !== 'number') { return `expected ${schema.type}, got ${typeof value}`; } if (schema.type === 'integer' && !Number.isInteger(value)) { return 'must be an integer'; } // Range check if (schema.minimum !== undefined && value < schema.minimum) { return `must be >= ${schema.minimum}`; } if (schema.maximum !== undefined && value > schema.maximum) { return `must be <= ${schema.maximum}`; } } else if (schema.type === 'string') { if (typeof value !== 'string') { return `expected string, got ${typeof value}`; } // Length check if (schema.minLength !== undefined && value.length < schema.minLength) { return `length must be >= ${schema.minLength}`; } if (schema.maxLength !== undefined && value.length > schema.maxLength) { return `length must be <= ${schema.maxLength}`; } // Pattern check if (schema.pattern && !new RegExp(schema.pattern).test(value)) { return `must match pattern ${schema.pattern}`; } // Format check if (schema.format === 'date-time') { const dateValue = new Date(value); if (isNaN(dateValue.getTime())) { return 'must be a valid date-time string'; } } else if (schema.format === 'email') { // Simple email validation if (!value.includes('@')) { return 'must be a valid email address'; } } } else if (schema.type === 'boolean') { if (typeof value !== 'boolean') { return `expected boolean, got ${typeof value}`; } } else if (schema.type === 'array') { if (!Array.isArray(value)) { return `expected array, got ${typeof value}`; } // Check array length if (schema.minItems !== undefined && value.length < schema.minItems) { return `array length must be >= ${schema.minItems}`; } if (schema.maxItems !== undefined && value.length > schema.maxItems) { return `array length must be <= ${schema.maxItems}`; } } // Enum check for any type if (schema.enum && !schema.enum.includes(value)) { return `must be one of: ${schema.enum.join(', ')}`; } return null; // No validation errors } /** * Validates a request body against its schema definition * @param body The request body to validate * @param requestBodyDef The request body definition from OpenAPI * @returns Error message or null if valid */ function validateRequestBody(body: any, requestBodyDef: OpenAPIV3.RequestBodyObject): string | null { if (!requestBodyDef.content || !requestBodyDef.content['application/json']) { // No JSON schema to validate against return null; } const schema = requestBodyDef.content['application/json'].schema as OpenAPIV3.SchemaObject; if (!schema) return null; // Basic type check if (schema.type === 'object') { if (!body || typeof body !== 'object' || Array.isArray(body)) { return 'Request body must be an object'; } // Check required properties if (schema.required && Array.isArray(schema.required)) { for (const requiredProp of schema.required) { if (body[requiredProp] === undefined) { return `Missing required property: ${requiredProp}`; } } } // Property type validation could be added here // This would involve recursively checking properties against schema.properties } else if (schema.type === 'array') { if (!Array.isArray(body)) { return 'Request body must be an array'; } // Array validation logic would go here } return null; // No validation errors }

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/TykTechnologies/api-to-mcp'

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