Skip to main content
Glama

OpenAPI MCP Server

api-client.ts19 kB
import axios, { AxiosInstance, AxiosError } from "axios" import { Tool } from "@modelcontextprotocol/sdk/types.js" import { AuthProvider, StaticAuthProvider, isAuthError } from "./auth-provider.js" import { parseToolId as parseToolIdUtil, generateToolId } from "./utils/tool-id.js" import { isValidHttpMethod, isGetLikeMethod, VALID_HTTP_METHODS } from "./utils/http-methods.js" import { OpenAPISpecLoader } from "./openapi-loader.js" import { OpenAPIV3 } from "openapi-types" /** * Client for making API calls to the backend service */ export class ApiClient { private axiosInstance: AxiosInstance private toolsMap: Map<string, Tool> = new Map() private authProvider: AuthProvider private specLoader?: OpenAPISpecLoader private openApiSpec?: OpenAPIV3.Document /** * Create a new API client * * @param baseUrl - Base URL for the API * @param authProviderOrHeaders - AuthProvider instance or static headers for backward compatibility * @param specLoader - Optional OpenAPI spec loader for dynamic meta-tools */ constructor( baseUrl: string, authProviderOrHeaders?: AuthProvider | Record<string, string>, specLoader?: OpenAPISpecLoader, ) { this.axiosInstance = axios.create({ baseURL: baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`, }) // Handle backward compatibility if (!authProviderOrHeaders) { this.authProvider = new StaticAuthProvider() } else if ( typeof authProviderOrHeaders === "object" && !("getAuthHeaders" in authProviderOrHeaders) ) { // It's a headers object (backward compatibility) this.authProvider = new StaticAuthProvider(authProviderOrHeaders) } else { // It's an AuthProvider this.authProvider = authProviderOrHeaders as AuthProvider } this.specLoader = specLoader } /** * Set the available tools for the client * * @param tools - Map of tool ID to tool definition */ setTools(tools: Map<string, Tool>): void { this.toolsMap = tools } /** * Set the OpenAPI specification for dynamic meta-tools * * @param spec - The OpenAPI specification document */ setOpenApiSpec(spec: OpenAPIV3.Document): void { this.openApiSpec = spec } /** * Get a tool definition by ID * * @param toolId - The tool ID * @returns The tool definition if found */ private getToolDefinition(toolId: string): Tool | undefined { return this.toolsMap.get(toolId) } /** * Execute an API call based on the tool ID and parameters * * @param toolId - The tool ID in format METHOD-path-parts * @param params - Parameters for the API call * @returns The API response data */ async executeApiCall(toolId: string, params: Record<string, any>): Promise<any> { return this.executeApiCallWithRetry(toolId, params, false) } /** * Execute an API call with optional retry on auth error * * @param toolId - The tool ID in format METHOD-path-parts * @param params - Parameters for the API call * @param isRetry - Whether this is a retry attempt * @returns The API response data */ private async executeApiCallWithRetry( toolId: string, params: Record<string, any>, isRetry: boolean, ): Promise<any> { try { // Handle dynamic meta-tools that don't follow the standard HTTP method::path format if (toolId === "LIST-API-ENDPOINTS") { return await this.handleListApiEndpoints() } if (toolId === "GET-API-ENDPOINT-SCHEMA") { return this.handleGetApiEndpointSchema(toolId, params) } if (toolId === "INVOKE-API-ENDPOINT") { return this.handleInvokeApiEndpoint(toolId, params) } // Parse method and path from the tool ID const { method, path } = this.parseToolId(toolId) // Get the tool definition, if available const toolDef = this.getToolDefinition(toolId) // Interpolate path parameters into the URL and remove them from params const paramsCopy: Record<string, any> = { ...params } let resolvedPath = path // Helper function to escape regex special characters const escapeRegExp = (str: string): string => { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // $& means the whole matched string } // Handle path, query, and header parameters const headerParams: Record<string, string> = {} const queryParams: Record<string, any> = {} if (toolDef?.inputSchema?.properties) { // Check each parameter to see if it's a path, query, or header parameter for (const [key, value] of Object.entries(paramsCopy)) { const paramDef = toolDef.inputSchema.properties[key] // Get the parameter location from the extended schema const paramDef_any = paramDef as any const paramLocation = paramDef_any?.["x-parameter-location"] // If it's a path parameter, interpolate it into the URL and remove from params if (paramLocation === "path") { // Escape key before using it in regex patterns const escapedKey = escapeRegExp(key) // Try standard OpenAPI, Express-style parameters, and unique markers const paramRegex = new RegExp( `\\{${escapedKey}\\}|:${escapedKey}(?:\\/|$)|---${escapedKey}(?=__|/|$)`, "g", ) // If specific parameter style was found, use it if (paramRegex.test(resolvedPath)) { resolvedPath = resolvedPath.replace( paramRegex, (match) => encodeURIComponent(value) + (match.endsWith("/") ? "/" : ""), ) } else { // Fall back to the original simple replacement for backward compatibility resolvedPath = resolvedPath.replace(`/${key}`, `/${encodeURIComponent(value)}`) } delete paramsCopy[key] } // If it's a query parameter, add to query params and remove from params else if (paramLocation === "query") { queryParams[key] = value delete paramsCopy[key] } // If it's a header parameter, add to headers and remove from params else if (paramLocation === "header") { headerParams[key] = String(value) delete paramsCopy[key] } } } else { // Fallback behavior if tool definition is not available for (const key of Object.keys(paramsCopy)) { const value = paramsCopy[key] // Escape key before using it in regex patterns const escapedKey = escapeRegExp(key) // First try standard OpenAPI, Express-style parameters, and unique markers const paramRegex = new RegExp( `\\{${escapedKey}\\}|:${escapedKey}(?:\\/|$)|---${escapedKey}(?=__|/|$)`, "g", ) // If found, replace using regex if (paramRegex.test(resolvedPath)) { resolvedPath = resolvedPath.replace( paramRegex, (match) => encodeURIComponent(value) + (match.endsWith("/") ? "/" : ""), ) delete paramsCopy[key] } // Fall back to original simple replacement for backward compatibility else if (resolvedPath.includes(`/${key}`)) { resolvedPath = resolvedPath.replace(`/${key}`, `/${encodeURIComponent(value)}`) delete paramsCopy[key] } } } // Get fresh authentication headers const authHeaders = await this.authProvider.getAuthHeaders() // Prepare request configuration const config: any = { method: method.toLowerCase(), url: resolvedPath, headers: { ...authHeaders, ...headerParams }, } // Add query parameters if any exist if (Object.keys(queryParams).length > 0) { config.params = this.processQueryParams(queryParams) } // Handle remaining parameters (body parameters) based on HTTP method if (isGetLikeMethod(method)) { // For GET-like methods, remaining parameters also go in the query string if (Object.keys(paramsCopy).length > 0) { config.params = { ...config.params, ...this.processQueryParams(paramsCopy), } } } else { // For POST-like methods, remaining parameters go in the request body config.data = Object.keys(paramsCopy).length > 0 ? paramsCopy : {} } // Execute the request const response = await this.axiosInstance(config) return response.data } catch (error) { // Handle errors if (axios.isAxiosError(error)) { const axiosError = error as AxiosError // Check if it's an authentication error and we haven't already retried if (!isRetry && isAuthError(axiosError)) { const shouldRetry = await this.authProvider.handleAuthError(axiosError) if (shouldRetry) { // Retry the request once return this.executeApiCallWithRetry(toolId, params, true) } // If auth handler throws, use that error instead } throw new Error( `API request failed: ${axiosError.message}${ axiosError.response ? ` (${axiosError.response.status}: ${ typeof axiosError.response.data === "object" ? JSON.stringify(axiosError.response.data) : axiosError.response.data })` : "" }`, ) } throw error } } /** * Parse a tool ID into HTTP method and path * * @param toolId - Tool ID in format METHOD::pathPart * @returns Object containing method and path */ private parseToolId(toolId: string): { method: string; path: string } { return parseToolIdUtil(toolId) } /** * Process query parameters for GET requests * Converts arrays to comma-separated strings * * @param params - The original parameters * @returns Processed parameters */ private processQueryParams( params: Record<string, any>, ): Record<string, string | number | boolean> { const result: Record<string, string | number | boolean> = {} for (const [key, value] of Object.entries(params)) { if (Array.isArray(value)) { result[key] = value.join(",") } else { result[key] = value } } return result } /** * Handle the LIST-API-ENDPOINTS meta-tool * Returns a list of all available API endpoints from the loaded tools */ private async handleListApiEndpoints(): Promise<any> { const endpoints: any[] = [] // If we have the OpenAPI spec, use it to get all available endpoints if (this.openApiSpec) { for (const [path, pathItem] of Object.entries(this.openApiSpec.paths)) { if (!pathItem) continue for (const [method, operation] of Object.entries(pathItem)) { if (method === "parameters" || !operation) continue // Skip invalid HTTP methods if (!isValidHttpMethod(method)) { continue } const op = operation as any endpoints.push({ method: method.toUpperCase(), path, summary: op.summary || "", description: op.description || "", operationId: op.operationId || "", tags: op.tags || [], }) } } } else { // Fallback: use the current toolsMap for (const [toolId, tool] of this.toolsMap.entries()) { // Skip other meta-tools in the listing if ( toolId.startsWith("LIST-API-ENDPOINTS") || toolId.startsWith("GET-API-ENDPOINT-SCHEMA") || toolId.startsWith("INVOKE-API-ENDPOINT") ) { continue } try { const { method, path } = this.parseToolId(toolId) endpoints.push({ toolId, name: tool.name, description: tool.description, method: method.toUpperCase(), path, }) } catch (error) { // Skip tools that don't follow the standard format continue } } } return { endpoints, total: endpoints.length, note: this.openApiSpec ? "Use INVOKE-API-ENDPOINT to call specific endpoints with the path parameter" : "Limited endpoint information - OpenAPI spec not available", } } /** * Handle the GET-API-ENDPOINT-SCHEMA meta-tool * Returns the JSON schema for a specified API endpoint */ private handleGetApiEndpointSchema(toolId: string, params: Record<string, any>): any { const { endpoint } = params if (!endpoint) { throw new Error(`Missing required parameter 'endpoint' for tool '${toolId}'`) } // If we have the OpenAPI spec, use it to get detailed schema information if (this.openApiSpec) { const pathItem = this.openApiSpec.paths[endpoint] if (!pathItem) { throw new Error(`No endpoint found for path '${endpoint}' in tool '${toolId}'`) } const operations: any[] = [] for (const [method, operation] of Object.entries(pathItem)) { if (method === "parameters" || !operation) continue // Skip invalid HTTP methods if (!isValidHttpMethod(method)) { continue } const op = operation as any operations.push({ method: method.toUpperCase(), operationId: op.operationId || "", summary: op.summary || "", description: op.description || "", parameters: op.parameters || [], requestBody: op.requestBody || null, responses: op.responses || {}, tags: op.tags || [], }) } if (operations.length === 0) { throw new Error(`No valid HTTP operations found for path '${endpoint}' in tool '${toolId}'`) } return { path: endpoint, operations, pathParameters: pathItem.parameters || [], } } else { // Fallback: find the tool that matches the requested endpoint path let matchingTool: Tool | undefined let matchingToolId: string | undefined for (const [toolId, tool] of this.toolsMap.entries()) { try { const { path } = this.parseToolId(toolId) if (path === endpoint) { matchingTool = tool matchingToolId = toolId break } } catch (error) { // Skip tools that don't follow the standard format continue } } if (!matchingTool || !matchingToolId) { throw new Error(`No endpoint found for path: ${endpoint}`) } return { toolId: matchingToolId, name: matchingTool.name, description: matchingTool.description, inputSchema: matchingTool.inputSchema, note: "Limited schema information - using tool definition instead of OpenAPI spec", } } } /** * Handle the INVOKE-API-ENDPOINT meta-tool * Dynamically invokes an API endpoint with the provided parameters */ private async handleInvokeApiEndpoint(toolId: string, params: Record<string, any>): Promise<any> { const { endpoint, method, params: endpointParams = {} } = params if (!endpoint) { throw new Error(`Missing required parameter 'endpoint' for tool '${toolId}'`) } // If method is specified, construct the tool ID directly if (method) { const toolId = generateToolId(method, endpoint) // Check if this tool exists in our toolsMap or if we can derive it from the OpenAPI spec if (this.toolsMap.has(toolId)) { return this.executeApiCall(toolId, endpointParams) } else if (this.openApiSpec) { // Check if the endpoint and method exist in the OpenAPI spec const pathItem = this.openApiSpec.paths[endpoint] if (pathItem && (pathItem as any)[method.toLowerCase()]) { // Make the HTTP request directly since we have the spec but not the tool const { method: httpMethod, path } = { method: method.toUpperCase(), path: endpoint } return this.makeDirectHttpRequest(httpMethod, path, endpointParams) } else { throw new Error( `No endpoint found for path '${endpoint}' with method '${method}' in tool '${toolId}'`, ) } } else { throw new Error(`Tool not found: ${toolId}`) } } // If no method is specified, try to find the first available method for this endpoint if (this.openApiSpec) { const pathItem = this.openApiSpec.paths[endpoint] if (pathItem) { // Find the first available HTTP method for this path for (const method of VALID_HTTP_METHODS) { if ((pathItem as any)[method]) { return this.makeDirectHttpRequest(method.toUpperCase(), endpoint, endpointParams) } } throw new Error(`No HTTP operations found for endpoint '${endpoint}' in tool '${toolId}'`) } else { throw new Error(`No endpoint found for path '${endpoint}' in tool '${toolId}'`) } } // Fallback: try to find a tool that matches this endpoint path throw new Error(`No endpoint found for path '${endpoint}' in tool '${toolId}'`) } /** * Make a direct HTTP request without going through the tool system * Used by dynamic meta-tools when we have OpenAPI spec but no corresponding tool */ private async makeDirectHttpRequest( method: string, path: string, params: Record<string, any>, ): Promise<any> { // Get fresh authentication headers const authHeaders = await this.authProvider.getAuthHeaders() // Prepare request configuration const config: any = { method: method.toLowerCase(), url: path, headers: authHeaders, } // Handle parameters based on HTTP method if (isGetLikeMethod(method)) { // For GET-like methods, parameters go in the query string config.params = this.processQueryParams(params) } else { // For POST-like methods, parameters go in the request body config.data = params } try { // Execute the request const response = await this.axiosInstance.request(config) return response.data } catch (error) { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError throw new Error( `API request failed: ${axiosError.message}${ axiosError.response ? ` (${axiosError.response.status}: ${ typeof axiosError.response.data === "object" ? JSON.stringify(axiosError.response.data) : axiosError.response.data })` : "" }`, ) } throw error } } }

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/ivo-toby/mcp-openapi-server'

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