Skip to main content
Glama
rest-api.ts13.6 kB
/** * REST API Router for MCP Tools * * Provides HTTP REST endpoints for all registered MCP tools. * Handles routing, validation, execution, and response formatting. */ import { IncomingMessage, ServerResponse } from 'node:http'; import { URL } from 'node:url'; import { RestToolRegistry, ToolInfo } from './rest-registry'; import { OpenApiGenerator } from './openapi-generator'; import { Logger } from '../core/error-handling'; import { DotAI } from '../core/index'; /** * HTTP status codes for REST responses */ export enum HttpStatus { OK = 200, BAD_REQUEST = 400, NOT_FOUND = 404, METHOD_NOT_ALLOWED = 405, INTERNAL_SERVER_ERROR = 500 } /** * Standard REST API response format */ export interface RestApiResponse { success: boolean; data?: any; error?: { code: string; message: string; details?: any; }; meta?: { timestamp: string; requestId?: string; version: string; }; } /** * Tool execution response format */ export interface ToolExecutionResponse extends RestApiResponse { data?: { result: any; tool: string; executionTime?: number; }; } /** * Tool discovery response format */ export interface ToolDiscoveryResponse extends RestApiResponse { data?: { tools: ToolInfo[]; total: number; categories?: string[]; tags?: string[]; }; } /** * REST API router configuration */ export interface RestApiConfig { basePath: string; version: string; enableCors: boolean; requestTimeout: number; } /** * REST API Router for MCP tools */ export class RestApiRouter { private registry: RestToolRegistry; private logger: Logger; private dotAI: DotAI; private config: RestApiConfig; private openApiGenerator: OpenApiGenerator; private requestCounter: number = 0; constructor( registry: RestToolRegistry, dotAI: DotAI, logger: Logger, config: Partial<RestApiConfig> = {} ) { this.registry = registry; this.dotAI = dotAI; this.logger = logger; this.config = { basePath: '/api', version: 'v1', enableCors: true, requestTimeout: 1800000, // 30 minutes for long-running operations (capability scan with slower AI providers) ...config }; // Initialize OpenAPI generator this.openApiGenerator = new OpenApiGenerator(registry, logger, { basePath: this.config.basePath, apiVersion: this.config.version }); } /** * Handle incoming HTTP requests for REST API */ async handleRequest(req: IncomingMessage, res: ServerResponse, body?: any): Promise<void> { const requestId = this.generateRequestId(); const startTime = Date.now(); try { this.logger.debug('REST API request received', { requestId, method: req.method, url: req.url, hasBody: !!body }); // Handle CORS preflight if (this.config.enableCors) { this.setCorsHeaders(res); if (req.method === 'OPTIONS') { res.writeHead(HttpStatus.OK); res.end(); return; } } // Parse URL and route const url = new URL(req.url || '/', 'http://localhost'); const pathMatch = this.parseApiPath(url.pathname); if (!pathMatch) { await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'API endpoint not found'); return; } // Route to appropriate handler switch (pathMatch.endpoint) { case 'tools': if (req.method === 'GET') { await this.handleToolDiscovery(req, res, requestId, url.searchParams); } else { await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for tool discovery'); } break; case 'tool': if (req.method === 'POST' && pathMatch.toolName) { await this.handleToolExecution(req, res, requestId, pathMatch.toolName, body, startTime); } else if (req.method !== 'POST') { await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only POST method allowed for tool execution'); } else { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'BAD_REQUEST', 'Tool name is required'); } break; case 'openapi': if (req.method === 'GET') { await this.handleOpenApiSpec(req, res, requestId); } else { await this.sendErrorResponse(res, requestId, HttpStatus.METHOD_NOT_ALLOWED, 'METHOD_NOT_ALLOWED', 'Only GET method allowed for OpenAPI specification'); } break; default: await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'NOT_FOUND', 'Unknown API endpoint'); } } catch (error) { this.logger.error('REST API request failed', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage: error instanceof Error ? error.message : String(error) }); await this.sendErrorResponse( res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'INTERNAL_ERROR', 'An internal server error occurred' ); } } /** * Parse API path and extract route information */ private parseApiPath(pathname: string): { endpoint: string; toolName?: string } | null { // Expected patterns: // /api/v1/tools -> tools discovery // /api/v1/tools/{toolName} -> tool execution // /api/v1/openapi -> OpenAPI spec const basePath = `${this.config.basePath}/${this.config.version}`; if (!pathname.startsWith(basePath)) { return null; } const pathSuffix = pathname.substring(basePath.length); // Remove leading slash const cleanPath = pathSuffix.startsWith('/') ? pathSuffix.substring(1) : pathSuffix; if (cleanPath === 'tools') { return { endpoint: 'tools' }; } if (cleanPath === 'openapi') { return { endpoint: 'openapi' }; } if (cleanPath.startsWith('tools/')) { const toolName = cleanPath.substring(6); // Remove 'tools/' if (toolName) { return { endpoint: 'tool', toolName }; } } return null; } /** * Handle tool discovery requests */ private async handleToolDiscovery( req: IncomingMessage, res: ServerResponse, requestId: string, searchParams: URLSearchParams ): Promise<void> { try { const category = searchParams.get('category') || undefined; const tag = searchParams.get('tag') || undefined; const search = searchParams.get('search') || undefined; const tools = this.registry.getToolsFiltered({ category, tag, search }); const categories = this.registry.getCategories(); const tags = this.registry.getTags(); const response: ToolDiscoveryResponse = { success: true, data: { tools, total: tools.length, categories, tags }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version } }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Tool discovery request completed', { requestId, totalTools: tools.length, filters: { category, tag, search } }); } catch (error) { await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'DISCOVERY_ERROR', 'Failed to retrieve tool information'); } } /** * Handle tool execution requests */ private async handleToolExecution( req: IncomingMessage, res: ServerResponse, requestId: string, toolName: string, body: any, startTime: number ): Promise<void> { try { // Check if tool exists const toolMetadata = this.registry.getTool(toolName); if (!toolMetadata) { await this.sendErrorResponse(res, requestId, HttpStatus.NOT_FOUND, 'TOOL_NOT_FOUND', `Tool '${toolName}' not found`); return; } // Validate request body if (!body || typeof body !== 'object') { await this.sendErrorResponse(res, requestId, HttpStatus.BAD_REQUEST, 'INVALID_REQUEST', 'Request body must be a JSON object'); return; } this.logger.info('Executing tool via REST API', { requestId, toolName, parameters: Object.keys(body) }); // Execute the tool handler with timeout // Note: Tool handlers expect the same format as MCP calls const timeoutMs = this.config.requestTimeout; const toolPromise = toolMetadata.handler(body, this.dotAI, this.logger, requestId); const timeoutPromise = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Request timeout exceeded')), timeoutMs) ); // Prevent unhandled rejection if toolPromise resolves after timeout toolPromise.catch(() => {}); const mcpResult = await Promise.race([toolPromise, timeoutPromise]); // Transform MCP format to proper REST JSON // All MCP tools return JSON.stringify() in content[0].text, so parse it back to proper JSON let transformedResult; if (mcpResult?.content?.[0]?.type === 'text') { try { transformedResult = JSON.parse(mcpResult.content[0].text); } catch (parseError) { this.logger.warn('Failed to parse MCP tool result as JSON, returning as text', { requestId, toolName, error: parseError instanceof Error ? parseError.message : String(parseError) }); transformedResult = mcpResult.content[0].text; } } else { // Fallback for unexpected format transformedResult = mcpResult; } const executionTime = Date.now() - startTime; const response: ToolExecutionResponse = { success: true, data: { result: transformedResult, tool: toolName, executionTime }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version } }; await this.sendJsonResponse(res, HttpStatus.OK, response); this.logger.info('Tool execution completed', { requestId, toolName, executionTime, success: true }); } catch (error) { this.logger.error('Tool execution failed', error instanceof Error ? error : new Error(String(error)), { requestId, toolName, errorMessage: error instanceof Error ? error.message : String(error) }); await this.sendErrorResponse(res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'EXECUTION_ERROR', 'Tool execution failed'); } } /** * Handle OpenAPI specification requests */ private async handleOpenApiSpec(req: IncomingMessage, res: ServerResponse, requestId: string): Promise<void> { try { this.logger.debug('Generating OpenAPI specification', { requestId }); const spec = this.openApiGenerator.generateSpec(); await this.sendJsonResponse(res, HttpStatus.OK, spec); this.logger.info('OpenAPI specification served successfully', { requestId, pathCount: Object.keys(spec.paths).length, componentCount: Object.keys(spec.components?.schemas || {}).length }); } catch (error) { this.logger.error('Failed to generate OpenAPI specification', error instanceof Error ? error : new Error(String(error)), { requestId, errorMessage: error instanceof Error ? error.message : String(error) }); await this.sendErrorResponse( res, requestId, HttpStatus.INTERNAL_SERVER_ERROR, 'OPENAPI_ERROR', 'Failed to generate OpenAPI specification' ); } } /** * Set CORS headers */ private setCorsHeaders(res: ServerResponse): void { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Max-Age', '86400'); } /** * Send JSON response */ private async sendJsonResponse(res: ServerResponse, status: HttpStatus, data: any): Promise<void> { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data, null, 2)); } /** * Send error response */ private async sendErrorResponse( res: ServerResponse, requestId: string, status: HttpStatus, code: string, message: string, details?: any ): Promise<void> { const response: RestApiResponse = { success: false, error: { code, message, details }, meta: { timestamp: new Date().toISOString(), requestId, version: this.config.version } }; await this.sendJsonResponse(res, status, response); } /** * Generate unique request ID */ private generateRequestId(): string { return `rest_${Date.now()}_${++this.requestCounter}`; } /** * Check if the given path matches the REST API pattern */ isApiRequest(pathname: string): boolean { return pathname.startsWith(`${this.config.basePath}/${this.config.version}`); } /** * Get API configuration */ getConfig(): RestApiConfig { return { ...this.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/vfarcic/dot-ai'

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