Skip to main content
Glama

OpenAPI MCP Server

by aaker
server.js16.4 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import http from 'http'; import os from 'os'; import { OpenAPIProcessor } from './openapi-processor.js'; import { HttpClient } from './http-client.js'; export class MCPServer { constructor(options = {}) { this.server = new Server( { name: 'openapi-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } } ); this.processor = new OpenAPIProcessor(); this.httpClient = null; this.options = { specPath: options.specPath, baseURL: options.baseURL || `https://${os.hostname()}/ns-api/v2`, bearerToken: options.bearerToken, timeout: options.timeout || 30000, transport: options.transport || 'http', httpPort: options.httpPort || 8020, httpHost: options.httpHost || 'localhost', ...options }; this.setupHandlers(); } setupHandlers() { // Handle list tools request this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools = this.processor.getTools(); return { tools: tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })) }; }); // Handle call tool request this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { return await this.callTool(name, args || {}); } catch (error) { throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error.message}` ); } }); // Handle errors this.server.onerror = (error) => { console.error('[MCP Error]', error); }; } async initialize() { try { // Load and process OpenAPI specification if (!this.options.specPath) { throw new Error('OpenAPI specification path is required'); } await this.processor.loadSpec(this.options.specPath); console.error(`[MCP] Loaded OpenAPI spec with ${this.processor.getTools().length} tools`); // Determine base URL let baseURL = this.options.baseURL; if (!baseURL) { baseURL = this.processor.getBaseUrl(); } if (!baseURL) { console.error('[MCP] Warning: No base URL specified and none found in OpenAPI spec'); } // Initialize HTTP client console.error(`[MCP] === INITIALIZING DEFAULT HTTP CLIENT ===`); this.httpClient = new HttpClient( baseURL, this.options.bearerToken, this.options.timeout ); console.error(`[MCP] Initialized HTTP client with base URL: ${baseURL || 'none'}`); if (this.options.bearerToken) { console.error('[MCP] Bearer token authentication configured'); } // Log available tools const tools = this.processor.getTools(); console.error('[MCP] Available tools:'); tools.forEach(tool => { console.error(` - ${tool.name}: ${tool.description}`); }); } catch (error) { console.error('[MCP] Initialization failed:', error.message); throw error; } } async callTool(toolName, args, config = {}) { console.error(`[MCP DEBUG] Starting tool execution: ${toolName}`); console.error(`[MCP DEBUG] Arguments:`, JSON.stringify(args, null, 2)); console.error(`[MCP DEBUG] Config:`, JSON.stringify(config, null, 2)); const tool = this.processor.getTool(toolName); if (!tool) { console.error(`[MCP DEBUG] Tool not found: ${toolName}`); throw new Error(`Tool not found: ${toolName}`); } console.error(`[MCP DEBUG] Tool found:`, { name: tool.name, method: tool.method, path: tool.path, description: tool.description }); // Validate arguments against schema console.error(`[MCP DEBUG] Validating arguments against schema...`); this.validateArguments(args, tool.inputSchema); console.error(`[MCP DEBUG] Arguments validation passed`); // Create HTTP client with dynamic configuration if provided let httpClient = this.httpClient; const effectiveConfig = { baseURL: config.baseURL || this.options.baseURL || `https://localhost/ns-api/v2`, bearerToken: config.bearerToken || this.options.bearerToken, timeout: this.options.timeout }; console.error(`[MCP DEBUG] Effective configuration:`, effectiveConfig); if (config.baseURL || config.bearerToken) { console.error(`[MCP DEBUG] Creating temporary HTTP client with custom config`); // Create a temporary HTTP client with the request-specific configuration const { HttpClient } = await import('./http-client.js'); httpClient = new HttpClient( effectiveConfig.baseURL, effectiveConfig.bearerToken, effectiveConfig.timeout ); } else { console.error(`[MCP DEBUG] Using default HTTP client`); } // Special debug for ListUsers if (toolName === 'ListUsers') { console.error(`[MCP DEBUG] === LISTUSERS EXECUTION ===`); console.error(`[MCP DEBUG] HTTP Method: ${tool.method.toUpperCase()}`); console.error(`[MCP DEBUG] API Path: ${tool.path}`); console.error(`[MCP DEBUG] Base URL: ${effectiveConfig.baseURL || 'NONE'}`); console.error(`[MCP DEBUG] Bearer Token: ${effectiveConfig.bearerToken ? 'PROVIDED' : 'NONE'}`); console.error(`[MCP DEBUG] Full URL will be: ${effectiveConfig.baseURL}${tool.path}`); } // Execute HTTP request console.error(`[MCP DEBUG] Executing HTTP request...`); const result = await httpClient.executeRequest( tool.method, tool.path, args ); console.error(`[MCP DEBUG] HTTP request completed:`, { success: result.success, status: result.status, statusText: result.statusText, hasData: !!result.data, dataType: result.data ? typeof result.data : 'none' }); if (!result.success) { console.error(`[MCP DEBUG] HTTP request failed:`, result.error); console.error(`[MCP DEBUG] Error data:`, result.data); } else if (toolName === 'ListUsers') { console.error(`[MCP DEBUG] ListUsers response data:`, JSON.stringify(result.data, null, 2)); } if (result.success) { return { content: [ { type: 'text', text: JSON.stringify({ status: result.status, statusText: result.statusText, data: result.data }, null, 2) } ] }; } else { // Return error information return { content: [ { type: 'text', text: JSON.stringify({ error: true, status: result.status, statusText: result.statusText, message: result.error, data: result.data }, null, 2) } ], isError: true }; } } validateArguments(args, schema) { // Basic validation - check required fields const required = schema.required || []; const missing = required.filter(field => !(field in args)); if (missing.length > 0) { throw new Error(`Missing required parameters: ${missing.join(', ')}`); } // Type validation for known properties Object.entries(args).forEach(([key, value]) => { const propSchema = schema.properties?.[key]; if (propSchema && !this.validateValue(value, propSchema)) { throw new Error(`Invalid value for parameter '${key}': expected ${propSchema.type}`); } }); } validateValue(value, schema) { if (value === null || value === undefined) { return !schema.required; } switch (schema.type) { case 'string': return typeof value === 'string'; case 'number': case 'integer': return typeof value === 'number'; case 'boolean': return typeof value === 'boolean'; case 'array': return Array.isArray(value); case 'object': return typeof value === 'object' && !Array.isArray(value); default: return true; // Allow unknown types } } async run() { if (this.options.transport === 'http') { await this.startHttpServer(); } else { const transport = new StdioServerTransport(); console.error('[MCP] Using stdio transport'); await this.server.connect(transport); console.error('[MCP] Server connected and ready'); } } async startHttpServer() { const { httpHost, httpPort } = this.options; const httpServer = http.createServer(); httpServer.on('request', async (req, res) => { console.error(`[MCP] Incoming HTTP request: ${req.method} ${req.url}`); const url = new URL(req.url, `http://${req.headers.host}`); // Enable CORS res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, Mcp-Session-Id, X-OpenAPI-Base-URL, X-OpenAPI-Bearer-Token'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } if (url.pathname === '/message') { if (req.method === 'POST') { // Handle Streamable HTTP POST requests await this.handleStreamableHttpPost(req, res); } else if (req.method === 'GET') { // Handle Streamable HTTP GET requests (for streaming responses) await this.handleStreamableHttpGet(req, res); } else { res.writeHead(405, { 'Content-Type': 'text/plain' }); res.end('Method Not Allowed'); } } else if (url.pathname === '/health') { // Health check endpoint res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ status: 'ok', server: 'openapi-mcp' })); } else { // 404 for other paths res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); await new Promise((resolve, reject) => { httpServer.listen(httpPort, httpHost, (error) => { if (error) { reject(error); } else { console.error(`[MCP] HTTP server started on http://${httpHost}:${httpPort}`); console.error(`[MCP] MCP endpoint: http://${httpHost}:${httpPort}/message`); console.error(`[MCP] Health check: http://${httpHost}:${httpPort}/health`); resolve(); } }); }); // Store server reference for cleanup this.httpServer = httpServer; } async handleStreamableHttpPost(req, res) { try { // Read request body let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', async () => { try { const jsonRpcRequest = JSON.parse(body); // Log all incoming headers for debugging console.error(`[MCP DEBUG] === INCOMING HTTP REQUEST ===`); console.error(`[MCP DEBUG] Request Headers:`, JSON.stringify(req.headers, null, 2)); console.error(`[MCP DEBUG] Request Body:`, body); // Check for dynamic configuration headers const baseURLHeader = req.headers['x-openapi-base-url'] || `https://${os.hostname()}/ns-api/v2`; const bearerTokenHeader = req.headers['x-openapi-bearer-token'] || req.headers['authorization']?.replace(/^Bearer\s+/i, '').replace(/^Bearer\s+/i, ''); console.error(`[MCP DEBUG] Header Base URL: ${baseURLHeader || 'NONE'}`); console.error(`[MCP DEBUG] Header Bearer Token: ${bearerTokenHeader ? 'PROVIDED' : 'NONE'}`); // Create request-specific configuration const requestConfig = { baseURL: baseURLHeader || this.options.baseURL, bearerToken: bearerTokenHeader || this.options.bearerToken }; console.error(`[MCP DEBUG] Final request config for this call:`, requestConfig); // Process the JSON-RPC request with dynamic config const response = await this.processJsonRpcRequest(jsonRpcRequest, requestConfig); // Send JSON response console.error(`[MCP DEBUG] Sending JSON-RPC response:`, JSON.stringify(response, null, 2)); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(response)); } catch (error) { console.error('[MCP] Error processing request:', error); res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null })); } }); } catch (error) { console.error('[MCP] Error in handleStreamableHttpPost:', error); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); } } async handleStreamableHttpGet(_req, res) { // For now, return a simple response indicating no streaming messages res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ messages: [] })); } async processJsonRpcRequest(request, config = {}) { console.error(`[MCP DEBUG] Processing JSON-RPC request:`, request.method); console.error(`[MCP DEBUG] Request ID:`, request.id); console.error(`[MCP DEBUG] Request params:`, JSON.stringify(request.params, null, 2)); try { if (request.method === 'initialize') { return { jsonrpc: '2.0', result: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'openapi-mcp-server', version: '1.0.0' } }, id: request.id }; } if (request.method === 'tools/list') { const tools = this.processor.getTools(); return { jsonrpc: '2.0', result: { tools: tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })) }, id: request.id }; } if (request.method === 'tools/call') { const { name, arguments: args } = request.params; const result = await this.callTool(name, args || {}, config); return { jsonrpc: '2.0', result, id: request.id }; } // Unknown method return { jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' }, id: request.id }; } catch (error) { console.error('[MCP] Error processing JSON-RPC request:', error); return { jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' }, id: request.id }; } } async close() { if (this.httpServer) { this.httpServer.close(); } await this.server.close(); } // Update configuration methods updateBearerToken(token) { this.options.bearerToken = token; if (this.httpClient) { this.httpClient.updateBearerToken(token); } } updateBaseURL(baseURL) { this.options.baseURL = baseURL; if (this.httpClient) { this.httpClient.updateBaseURL(baseURL); } } getServerInfo() { return { name: 'openapi-mcp-server', version: '1.0.0', specPath: this.options.specPath, baseURL: this.options.baseURL, transport: this.options.transport, httpHost: this.options.httpHost, httpPort: this.options.httpPort, toolCount: this.processor.getTools().length, hasAuthentication: !!this.options.bearerToken }; } }

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/aaker/mini-openapi-mcp'

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