server.js•16.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
};
}
}