UNPKG

mcp-ai-gateway

Version:

Universal MCP server enabling AI clients (Claude Desktop, etc.) to access third-party models (GPT-5, Claude Opus 4, Gemini 2.5 Pro) with intelligent model selection and custom endpoints

364 lines 16.5 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const axios_1 = __importDefault(require("axios")); const https_proxy_agent_1 = require("https-proxy-agent"); /** * MCP AI Gateway Server * Acts as a unified gateway for multiple AI API providers */ class MCPAIGatewayServer { constructor() { this.config = this.loadConfiguration(); this.httpClient = this.createHttpClient(); this.server = new index_js_1.Server({ name: 'mcp-ai-gateway', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); this.setupErrorHandling(); } /** * Load configuration from environment variables */ loadConfiguration() { const apiFormat = process.env.API_FORMAT?.toLowerCase() || 'openai'; const apiKey = process.env.API_KEY; if (!apiKey) { throw new Error('API_KEY environment variable is required'); } // Default endpoints based on API format let defaultEndpoint; if (apiFormat === 'anthropic') { defaultEndpoint = 'https://api.anthropic.com/v1/messages'; } else { defaultEndpoint = 'https://api.openai.com/v1/chat/completions'; } const customDescription = process.env.DESCRIPTION; return { apiFormat, apiKey, apiEndpoint: process.env.API_ENDPOINT || defaultEndpoint, anthropicVersion: process.env.ANTHROPIC_VERSION || '2023-06-01', openaiOrganization: process.env.OPENAI_ORGANIZATION, defaultModel: process.env.DEFAULT_MODEL, defaultTemperature: process.env.DEFAULT_TEMPERATURE ? parseFloat(process.env.DEFAULT_TEMPERATURE) : undefined, defaultMaxTokens: process.env.DEFAULT_MAX_TOKENS ? parseInt(process.env.DEFAULT_MAX_TOKENS) : undefined, description: customDescription, }; } /** * Create HTTP client with proxy support */ createHttpClient() { const timeoutSeconds = process.env.REQUEST_TIMEOUT ? parseInt(process.env.REQUEST_TIMEOUT) : 60; // Default 60 seconds const timeoutMs = timeoutSeconds * 1000; const axiosConfig = { timeout: timeoutMs, headers: { 'Content-Type': 'application/json', }, }; // Proxy support for enterprise environments const proxyUrl = process.env.PROXY_URL || process.env.HTTP_PROXY; if (proxyUrl) { axiosConfig.httpsAgent = new https_proxy_agent_1.HttpsProxyAgent(proxyUrl); axiosConfig.proxy = false; } return axios_1.default.create(axiosConfig); } /** * Setup tool handlers */ setupToolHandlers() { this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => { const customDescriptionInfo = this.config.description ? `\n\n${this.config.description}` : ''; return { tools: [ { name: 'chat_completion', description: `Send a chat completion request to the configured AI API provider (${this.config.apiFormat.toUpperCase()}). ` + `Supports parameters like model, messages, temperature, max_tokens, stream, etc. ` + `Returns the raw response from the API without format conversion.${customDescriptionInfo}`, inputSchema: { type: 'object', properties: { model: { type: 'string', description: `Model to use for completion${this.config.defaultModel ? ` (default: ${this.config.defaultModel})` : ''}`, }, messages: { type: 'array', description: 'Array of message objects with role and content', items: { type: 'object', properties: { role: { type: 'string', enum: ['system', 'user', 'assistant'], }, content: { type: 'string', }, }, required: ['role', 'content'], }, }, temperature: { type: 'number', description: `Controls randomness in the response${this.config.defaultTemperature ? ` (default: ${this.config.defaultTemperature})` : ''}`, minimum: 0, maximum: 2, }, max_tokens: { type: 'number', description: `Maximum number of tokens to generate${this.config.defaultMaxTokens ? ` (default: ${this.config.defaultMaxTokens})` : ''}`, minimum: 1, }, stream: { type: 'boolean', description: 'Whether to stream the response', default: false, }, top_p: { type: 'number', description: 'Controls diversity via nucleus sampling', minimum: 0, maximum: 1, }, frequency_penalty: { type: 'number', description: 'Penalizes new tokens based on their frequency', minimum: -2, maximum: 2, }, presence_penalty: { type: 'number', description: 'Penalizes new tokens based on whether they appear in the text', minimum: -2, maximum: 2, }, stop: { oneOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' }, }, ], description: 'Up to 4 sequences where the API will stop generating further tokens', }, response_format: { type: 'object', description: 'Format of the response (OpenAI only). Supports json_object and json_schema types.', properties: { type: { type: 'string', enum: ['text', 'json_object', 'json_schema'], description: 'The type of response format', }, json_schema: { type: 'object', description: 'JSON schema definition (required when type is json_schema)', properties: { name: { type: 'string', description: 'Name of the schema', }, schema: { type: 'object', description: 'JSON schema object', }, strict: { type: 'boolean', description: 'Whether to use strict validation', }, }, required: ['name', 'schema'], }, }, required: ['type'], }, }, required: ['messages'], }, }, ], }; }); this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { if (request.params.name !== 'chat_completion') { throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } return await this.handleChatCompletion(request.params.arguments); }); } /** * Handle chat completion requests */ async handleChatCompletion(params) { try { // Apply defaults from configuration const completionParams = { ...params, model: params.model || this.config.defaultModel, temperature: params.temperature ?? this.config.defaultTemperature, max_tokens: params.max_tokens ?? this.config.defaultMaxTokens, }; // Validate required parameters if (!completionParams.messages || completionParams.messages.length === 0) { throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidParams, 'messages parameter is required and cannot be empty'); } // Prepare request based on API format const requestData = this.prepareRequestData(completionParams); const headers = this.prepareHeaders(); // Construct URL based on API format let url = this.config.apiEndpoint; if (this.config.apiFormat === 'openai' && !url.endsWith('/chat/completions')) { url = url.endsWith('/') ? url + 'chat/completions' : url + '/chat/completions'; } // Make the API request const response = await this.httpClient.post(url, requestData, { headers }); return { content: [ { type: 'text', text: JSON.stringify(response.data, null, 2), }, ], }; } catch (error) { if (axios_1.default.isAxiosError(error)) { const errorMessage = error.response?.data?.error?.message || error.message; const statusCode = error.response?.status || 500; throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `API request failed (${statusCode}): ${errorMessage}`); } throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Prepare request data based on API format */ prepareRequestData(params) { if (this.config.apiFormat === 'anthropic') { // Convert to Anthropic format const systemMessage = params.messages.find(m => m.role === 'system'); const nonSystemMessages = params.messages.filter(m => m.role !== 'system'); const anthropicData = { model: params.model, messages: nonSystemMessages, max_tokens: params.max_tokens || 1024, }; if (systemMessage) { anthropicData.system = systemMessage.content; } if (params.temperature !== undefined) { anthropicData.temperature = params.temperature; } if (params.top_p !== undefined) { anthropicData.top_p = params.top_p; } if (params.stream !== undefined) { anthropicData.stream = params.stream; } if (params.stop) { anthropicData.stop_sequences = Array.isArray(params.stop) ? params.stop : [params.stop]; } return anthropicData; } else { // OpenAI format (default) const openaiData = { model: params.model, messages: params.messages, }; if (params.temperature !== undefined) { openaiData.temperature = params.temperature; } if (params.max_tokens !== undefined) { openaiData.max_tokens = params.max_tokens; } if (params.stream !== undefined) { openaiData.stream = params.stream; } if (params.top_p !== undefined) { openaiData.top_p = params.top_p; } if (params.frequency_penalty !== undefined) { openaiData.frequency_penalty = params.frequency_penalty; } if (params.presence_penalty !== undefined) { openaiData.presence_penalty = params.presence_penalty; } if (params.stop !== undefined) { openaiData.stop = params.stop; } if (params.response_format !== undefined) { openaiData.response_format = params.response_format; } return openaiData; } } /** * Prepare headers based on API format */ prepareHeaders() { const headers = { 'Content-Type': 'application/json', }; if (this.config.apiFormat === 'anthropic') { headers['x-api-key'] = this.config.apiKey; if (this.config.anthropicVersion) { headers['anthropic-version'] = this.config.anthropicVersion; } } else { // OpenAI format headers['Authorization'] = `Bearer ${this.config.apiKey}`; if (this.config.openaiOrganization) { headers['OpenAI-Organization'] = this.config.openaiOrganization; } } return headers; } /** * Setup error handling */ setupErrorHandling() { this.server.onerror = (error) => { console.error('[MCP AI Gateway Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } /** * Start the server */ async run() { const transport = new stdio_js_1.StdioServerTransport(); await this.server.connect(transport); console.error('MCP AI Gateway server started'); } } // Start the server const server = new MCPAIGatewayServer(); server.run().catch(console.error); //# sourceMappingURL=index.js.map