import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { MCPToolDefinition } from '../types/mcp-tool.js';
import type { Config } from '../config/schema.js';
import { getToolList, createMcpServer } from './mcp.js';
import { log } from '../utils/logger.js';
import { randomUUID } from 'crypto';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
// Store transports by session ID for stateful mode
const transports: Map<string, WebStandardStreamableHTTPServerTransport> = new Map();
/**
* Filter tools by URL query params (?tools=tool1,tool2)
* Returns filtered tools if ?tools param is present, otherwise returns all tools
*/
function filterToolsByQueryParams(
tools: MCPToolDefinition[],
url: URL
): MCPToolDefinition[] {
const toolsParam = url.searchParams.get('tools');
if (!toolsParam) {
return tools;
}
const requestedTools = new Set(
toolsParam.split(',').map((t) => t.trim()).filter((t) => t.length > 0)
);
if (requestedTools.size === 0) {
return tools;
}
const filtered = tools.filter((t) => requestedTools.has(t.name));
log.info('Filtered tools by URL params', {
requested: Array.from(requestedTools),
matched: filtered.map((t) => t.name),
});
return filtered;
}
/**
* Create Hono HTTP server with MCP and health endpoints
*/
export function createHttpServer(
_mcpServer: McpServer, // Initial server instance (kept for API compatibility)
tools: MCPToolDefinition[],
config: Config
) {
// Factory function to create fresh MCP server per session with optional URL filtering
const createServer = (requestUrl?: URL) => {
const filteredTools = requestUrl ? filterToolsByQueryParams(tools, requestUrl) : tools;
return createMcpServer(filteredTools, config);
};
const app = new Hono();
// Health check endpoint
app.get('/health', (c) => {
return c.json({
status: 'healthy',
server: 'openapi-mcp-ts',
version: '1.0.0',
tools: {
total: tools.length,
enabled: tools.filter((t) => t._ui.enabled).length,
},
sessions: transports.size,
});
});
// List tools endpoint (for debugging)
app.get('/tools', (c) => {
return c.json({
tools: getToolList(tools),
});
});
// MCP endpoint - handles all HTTP methods for MCP protocol
app.all(config.server.basePath, async (c) => {
const sessionId = c.req.header('mcp-session-id');
try {
// For POST requests, we need to handle session management
if (c.req.method === 'POST') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const body = await c.req.json();
let transport: WebStandardStreamableHTTPServerTransport;
if (sessionId && transports.has(sessionId)) {
// Reuse existing transport for this session
transport = transports.get(sessionId)!;
log.debug('Reusing transport for session', { sessionId });
} else if (!sessionId && isInitializeRequest(body)) {
// New initialization request - create new transport and server
transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true,
onsessioninitialized: (newSessionId) => {
log.info('Session initialized', { sessionId: newSessionId });
transports.set(newSessionId, transport);
},
});
// Set up cleanup on transport close
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports.has(sid)) {
log.info('Transport closed, removing session', { sessionId: sid });
transports.delete(sid);
}
};
// Connect a fresh MCP server to the transport BEFORE handling request
// Filter tools by ?tools= query param if present
const requestUrl = new URL(c.req.url);
const server = createServer(requestUrl);
await server.connect(transport);
log.debug('Created new transport for initialization');
} else if (!sessionId) {
// Non-initialization request without session ID
return c.json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
}, 400);
} else {
// Session ID provided but not found
return c.json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Session not found',
},
id: null,
}, 404);
}
// Handle the request using Web Standard API
const response = await transport.handleRequest(c.req.raw, { parsedBody: body });
return response;
}
// For GET requests (SSE streams)
if (c.req.method === 'GET') {
if (!sessionId || !transports.has(sessionId)) {
return c.text('Invalid or missing session ID', 400);
}
const transport = transports.get(sessionId)!;
return transport.handleRequest(c.req.raw);
}
// For DELETE requests (session termination)
if (c.req.method === 'DELETE') {
if (!sessionId || !transports.has(sessionId)) {
return c.text('Invalid or missing session ID', 400);
}
const transport = transports.get(sessionId)!;
return transport.handleRequest(c.req.raw);
}
// Unsupported method
return c.text('Method not allowed', 405);
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error';
log.error('MCP endpoint error', { error: message, method: c.req.method });
return c.json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
},
id: null,
}, 500);
}
});
// 404 for other routes
app.notFound((c) => {
return c.json({
error: 'Not Found',
message: `Route ${c.req.path} not found`,
availableRoutes: ['/health', '/tools', config.server.basePath],
}, 404);
});
return app;
}
/**
* Start the HTTP server
*/
export function startServer(
app: Hono,
config: Config
): void {
const { port, host } = config.server;
serve({
fetch: app.fetch,
port,
hostname: host,
}, (info) => {
log.info('Server started', {
host: info.address,
port: info.port,
health: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/health`,
mcp: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}${config.server.basePath}`,
});
});
}