Skip to main content
Glama
executor.ts9.88 kB
import type { MCPToolDefinition, MCPToolResult, ProxyMetadata } from '../types/mcp-tool.js'; import type { Config } from '../config/schema.js'; import { applyAuth } from './auth.js'; import { log } from '../utils/logger.js'; import { randomUUID } from 'crypto'; /** * Request context for system variable substitution */ export interface RequestContext { /** Client IP from X-Forwarded-For or direct connection */ clientIp?: string; } /** * Substitute system variables in a string value * Supported variables: * - {{TIMESTAMP}} - Unix timestamp in milliseconds * - {{REQUEST_ID}} - Unique UUID for this request * - {{ISO_DATE}} - ISO 8601 formatted date * - {{USER_IP}} - Client IP address (from context) */ function substituteSystemVariables(value: string, context: RequestContext): string { const requestId = randomUUID(); const now = new Date(); return value .replace(/\{\{TIMESTAMP\}\}/g, String(now.getTime())) .replace(/\{\{REQUEST_ID\}\}/g, requestId) .replace(/\{\{ISO_DATE\}\}/g, now.toISOString()) .replace(/\{\{USER_IP\}\}/g, context.clientIp ?? ''); } /** * Apply system variable substitution to all header values */ function substituteHeaderVariables( headers: Record<string, string>, context: RequestContext ): Record<string, string> { const result: Record<string, string> = {}; for (const [name, value] of Object.entries(headers)) { result[name] = substituteSystemVariables(value, context); } return result; } /** * Tool call arguments (input from MCP client) */ export type ToolArgs = Record<string, unknown>; /** * Build the full URL for the request */ function buildUrl( baseUrl: string, path: string, pathParams: Record<string, string>, queryParams: Record<string, string | string[]> ): string { // Replace path parameters let resolvedPath = path; for (const [name, value] of Object.entries(pathParams)) { resolvedPath = resolvedPath.replace(`{${name}}`, encodeURIComponent(value)); } // Warn if any path params were not replaced (defensive check) if (resolvedPath.includes('{') && resolvedPath.includes('}')) { const unresolvedParams = resolvedPath.match(/\{[^}]+\}/g); log.warn('Unresolved path parameters in URL', { path: resolvedPath, params: unresolvedParams }); } // Ensure baseUrl ends with / for proper path joining const normalizedBase = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'; // Remove leading / from path since we'll append to base const normalizedPath = resolvedPath.startsWith('/') ? resolvedPath.slice(1) : resolvedPath; // Build URL by joining base and path const url = new URL(normalizedPath, normalizedBase); // Add query parameters (handling arrays with explode=true style) for (const [name, value] of Object.entries(queryParams)) { if (value === undefined || value === null) continue; if (Array.isArray(value)) { // Arrays: add each value separately (OpenAPI explode=true default) for (const item of value) { if (item !== undefined && item !== null && item !== '') { url.searchParams.append(name, item); } } } else if (value !== '') { url.searchParams.set(name, value); } } return url.toString(); } /** * Safely convert a value to string */ function stringifyValue(value: unknown): string { if (value === null || value === undefined) { return ''; } if (typeof value === 'object') { return JSON.stringify(value); } if (typeof value === 'string') { return value; } if (typeof value === 'number' || typeof value === 'boolean') { return value.toString(); } // At this point, value can only be symbol, bigint, or function - all have toString // eslint-disable-next-line @typescript-eslint/no-base-to-string return String(value); } /** * Extract path parameters from tool args */ function extractPathParams(args: ToolArgs, paramNames: string[]): Record<string, string> { const params: Record<string, string> = {}; for (const name of paramNames) { const value = args[name]; if (value !== undefined && value !== null) { params[name] = stringifyValue(value); } } return params; } /** * Extract query parameters from tool args * Arrays are preserved for proper OpenAPI serialization */ function extractQueryParams(args: ToolArgs, paramNames: string[]): Record<string, string | string[]> { const params: Record<string, string | string[]> = {}; for (const name of paramNames) { const value = args[name]; if (value !== undefined && value !== null) { if (Array.isArray(value)) { // Preserve arrays for proper serialization in buildUrl params[name] = value.map((v) => stringifyValue(v)); } else { params[name] = stringifyValue(value); } } } return params; } /** * Extract headers from tool args */ function extractHeaders(args: ToolArgs, paramNames: string[]): Record<string, string> { const headers: Record<string, string> = {}; for (const name of paramNames) { const value = args[name]; if (value !== undefined && value !== null) { headers[name] = stringifyValue(value); } } return headers; } /** * Build request body from tool args */ function buildRequestBody(args: ToolArgs, proxy: ProxyMetadata): string | undefined { if (proxy.bodyParams.length === 0) { return undefined; } // If there's a single "body" param, use it directly if (proxy.bodyParams.length === 1 && proxy.bodyParams[0] === 'body') { const body = args['body']; return body !== undefined ? JSON.stringify(body) : undefined; } // Otherwise, extract body params and create object const body: Record<string, unknown> = {}; const nonBodyParams = new Set([ ...proxy.pathParams, ...proxy.queryParams, ...proxy.headerParams, ]); for (const name of proxy.bodyParams) { if (!nonBodyParams.has(name) && name in args) { body[name] = args[name]; } } return Object.keys(body).length > 0 ? JSON.stringify(body) : undefined; } /** * Format response as MCP tool result */ function formatResponse( _status: number, headers: Headers, body: string ): MCPToolResult { const contentType = headers.get('content-type') ?? 'text/plain'; // Try to parse as JSON and pretty print if (contentType.includes('application/json')) { try { const parsed: unknown = JSON.parse(body); return { content: [ { type: 'text', text: JSON.stringify(parsed, null, 2), }, ], }; } catch { // Fall through to raw text } } return { content: [ { type: 'text', text: body, }, ], }; } /** * Format error as MCP tool result */ function formatError(message: string, details?: string): MCPToolResult { const text = details ? `Error: ${message}\n\nDetails: ${details}` : `Error: ${message}`; return { content: [{ type: 'text', text }], isError: true, }; } /** * Execute a tool call by making an HTTP request to the upstream API */ export async function executeTool( tool: MCPToolDefinition, args: ToolArgs, config: Config, context: RequestContext = {} ): Promise<MCPToolResult> { const { _proxy: proxy } = tool; try { // Extract parameters const pathParams = extractPathParams(args, proxy.pathParams); const queryParams = extractQueryParams(args, proxy.queryParams); const headers = extractHeaders(args, proxy.headerParams); // Apply auth const authResult = applyAuth(headers, queryParams, config.auth); // Build URL const url = buildUrl( config.upstream.baseUrl, proxy.path, pathParams, authResult.queryParams ); // Build request options const requestHeaders: Record<string, string> = { ...authResult.headers, }; // Add content-type for requests with body const body = buildRequestBody(args, proxy); if (body && proxy.contentType) { requestHeaders['Content-Type'] = proxy.contentType; } // Add any static headers from config if (config.upstream.headers) { Object.assign(requestHeaders, config.upstream.headers); } // Substitute system variables in headers ({{TIMESTAMP}}, {{REQUEST_ID}}, etc.) const finalHeaders = substituteHeaderVariables(requestHeaders, context); log.debug('Executing tool request', { tool: tool.name, method: proxy.method, url, }); // Make the request const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), config.upstream.timeout); try { const response = await fetch(url, { method: proxy.method, headers: finalHeaders, body, signal: controller.signal, }); const responseBody = await response.text(); log.debug('Tool request completed', { tool: tool.name, status: response.status, size: responseBody.length, }); if (!response.ok) { return formatError( `HTTP ${response.status} ${response.statusText}`, responseBody ); } return formatResponse(response.status, response.headers, responseBody); } finally { clearTimeout(timeout); } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; if (error instanceof Error && error.name === 'AbortError') { return formatError('Request timed out', `Timeout: ${config.upstream.timeout}ms`); } log.error('Tool execution failed', { tool: tool.name, error: message }); return formatError('Request failed', message); } } /** * Create an executor function for a specific tool */ export function createToolExecutor(tool: MCPToolDefinition, config: Config) { return (args: ToolArgs) => executeTool(tool, args, config); }

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/procoders/openapi-mcp-ts'

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