Skip to content

Commit cb86bbd

Browse files
committed
refactor http setup to separate class
1 parent 970682b commit cb86bbd

2 files changed

Lines changed: 218 additions & 145 deletions

File tree

mcp-openapi/src/httpSetup.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import express from 'express';
2+
import { MCPTool, MCPResource, PromptSpec, OpenAPISpec } from './types.js';
3+
import { Telemetry } from './telemetry.js';
4+
import { PACKAGE_VERSION } from './package-info.js';
5+
6+
export interface HttpSetupContext {
7+
app: express.Application;
8+
specs: Map<string, OpenAPISpec>;
9+
tools: MCPTool[];
10+
resources: MCPResource[];
11+
prompts: Map<string, PromptSpec>;
12+
telemetry: Telemetry;
13+
executeTool: (toolName: string, args: any, userContext?: { token?: string }) => Promise<any>;
14+
readResource: (uri: string, userContext?: { token?: string }, resourceParams?: Record<string, any>) => Promise<any>;
15+
getPrompt: (promptName: string, args: any) => Promise<any>;
16+
extractUserContext: (request?: any) => { token?: string };
17+
}
18+
19+
export class HttpSetup {
20+
private context: HttpSetupContext;
21+
22+
constructor(context: HttpSetupContext) {
23+
this.context = context;
24+
}
25+
26+
setupRoutes(): void {
27+
this.setupMcpRoute();
28+
this.setupHealthRoute();
29+
this.setupInfoRoute();
30+
}
31+
32+
private setupMcpRoute(): void {
33+
this.context.app.post('/mcp', async (req, res) => {
34+
try {
35+
// Extract user context from request
36+
const userContext = this.context.extractUserContext(req);
37+
38+
// Handle JSON-RPC 2.0 method calls
39+
const { method, params, id } = req.body;
40+
41+
this.context.telemetry.debug(`MCP method call: ${method}`);
42+
43+
let result;
44+
45+
switch (method) {
46+
case 'initialize':
47+
result = {
48+
message: "MCP server running",
49+
authMode: userContext.token ? "user-token" : "service-token",
50+
capabilities: {
51+
tools: { listChanged: true },
52+
resources: { listChanged: true, subscribe: false },
53+
prompts: { listChanged: true }
54+
}
55+
};
56+
break;
57+
58+
case 'tools/list':
59+
result = {
60+
tools: this.context.tools.map(tool => ({
61+
name: tool.name,
62+
description: tool.description,
63+
inputSchema: tool.inputSchema
64+
}))
65+
};
66+
break;
67+
68+
case 'resources/list':
69+
result = {
70+
resources: this.context.resources.map(resource => ({
71+
uri: resource.uri,
72+
name: resource.name,
73+
description: resource.description,
74+
mimeType: resource.mimeType,
75+
parameters: resource.parameters
76+
}))
77+
};
78+
break;
79+
80+
case 'prompts/list':
81+
result = {
82+
prompts: Array.from(this.context.prompts.entries()).map(([name, spec]) => ({
83+
name: name,
84+
description: spec.description || `${name} prompt template`,
85+
arguments: spec.arguments || []
86+
}))
87+
};
88+
break;
89+
90+
case 'tools/call':
91+
const toolName = params?.name;
92+
const toolArgs = params?.arguments || {};
93+
if (!toolName) {
94+
throw new Error('Tool name is required');
95+
}
96+
result = await this.context.executeTool(toolName, toolArgs, userContext);
97+
break;
98+
99+
case 'resources/read':
100+
const resourceUri = params?.uri;
101+
if (!resourceUri) {
102+
throw new Error('Resource URI is required');
103+
}
104+
const resourceParams = params?.parameters || {};
105+
106+
result = await this.context.readResource(resourceUri, userContext, resourceParams);
107+
break;
108+
109+
case 'prompts/get':
110+
const promptName = params?.name;
111+
const promptArgs = params?.arguments || {};
112+
if (!promptName) {
113+
throw new Error('Prompt name is required');
114+
}
115+
result = await this.context.getPrompt(promptName, promptArgs);
116+
break;
117+
118+
default:
119+
throw new Error(`Unknown method: ${method}`);
120+
}
121+
122+
res.json({
123+
jsonrpc: "2.0",
124+
result,
125+
id
126+
});
127+
} catch (error) {
128+
res.status(500).json({
129+
jsonrpc: "2.0",
130+
error: { code: -32603, message: (error as Error).message },
131+
id: req.body.id
132+
});
133+
}
134+
});
135+
}
136+
137+
private setupHealthRoute(): void {
138+
this.context.app.get('/health', (req, res) => {
139+
res.json({
140+
status: 'ok',
141+
specs: Array.from(this.context.specs.keys()),
142+
tools: this.context.tools.length,
143+
resources: this.context.resources.length,
144+
prompts: this.context.prompts.size,
145+
version: PACKAGE_VERSION
146+
});
147+
});
148+
}
149+
150+
private setupInfoRoute(): void {
151+
this.context.app.get('/info', (req, res) => {
152+
res.json({
153+
specs: Array.from(this.context.specs.entries()).map(([id, spec]) => ({
154+
id,
155+
title: spec.info.title,
156+
version: spec.info.version
157+
})),
158+
tools: this.context.tools.map(t => ({ name: t.name, description: t.description })),
159+
resources: this.context.resources.map(r => ({ uri: r.uri, name: r.name })),
160+
prompts: Array.from(this.context.prompts.keys())
161+
});
162+
});
163+
}
164+
165+
startServer(port: number): Promise<void> {
166+
return new Promise((resolve) => {
167+
const server = this.context.app.listen(port, () => {
168+
// HTTP server startup - use info level
169+
const address = server.address();
170+
const host = typeof address === 'object' && address ?
171+
(address.family === 'IPv6' ? `[${address.address}]` : address.address) :
172+
'localhost';
173+
174+
this.context.telemetry.info(`🚀 MCP OpenAPI Server running on port ${port}`);
175+
this.context.telemetry.info(`📊 Health check: http://${host}:${port}/health`);
176+
this.context.telemetry.info(`ℹ️ Server info: http://${host}:${port}/info`);
177+
178+
this.context.telemetry.debug(`📋 Loaded ${this.context.specs.size} specs, ${this.context.tools.length} tools, ${this.context.resources.length} resources, ${this.context.prompts.size} prompts`);
179+
180+
resolve();
181+
});
182+
});
183+
}
184+
}

mcp-openapi/src/server.ts

Lines changed: 34 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ServerOptions
2424
} from './types.js';
2525
import { Telemetry, TelemetryContext } from './telemetry.js';
26+
import { HttpSetup } from './httpSetup.js';
2627
import { PACKAGE_NAME, PACKAGE_VERSION } from './package-info.js';
2728

2829
export class MCPOpenAPIServer {
@@ -329,6 +330,14 @@ export class MCPOpenAPIServer {
329330
return this.generateShortToolName(specId, pathPattern, method);
330331
}
331332

333+
/**
334+
* Generates concise, human-readable MCP tool names from OpenAPI specs by combining
335+
* specId, HTTP method, and path components while staying under length limits for
336+
* MCP client compatibility (e.g., Cursor IDE's 60-character tool name restriction).
337+
*
338+
* MCP tool names are required as per the specs to register all the various MCP
339+
* capabilities (tools, resources, prompts). It also has to be unique.
340+
*/
332341
private generateShortToolName(specId: string, pathPattern: string, method: string): string {
333342
// Server name is "mcp-openapi" (11 chars), leaving ~49 chars for tool name to stay under 60
334343
// Reason for short tool name is because some MCP clients like Cursor IDE, as of this writing,
@@ -518,10 +527,11 @@ export class MCPOpenAPIServer {
518527
};
519528
}
520529

521-
private sanitizePath(pathPattern: string): string {
522-
return pathPattern.replace(/[^a-zA-Z0-9]/g, '_');
523-
}
524-
530+
/**
531+
* Builds JSON Schema for MCP tool input parameters by extracting path parameters,
532+
* query parameters, and request body schema from OpenAPI operation definitions.
533+
* Used during tool generation to define the input validation schema for each MCP tool.
534+
*/
525535
private buildInputSchema(operation: any, pathPattern: string) {
526536
const properties: any = {};
527537
const required: string[] = [];
@@ -570,6 +580,12 @@ export class MCPOpenAPIServer {
570580
};
571581
}
572582

583+
/**
584+
* Configures MCP protocol request handlers for stdio transport, mapping standard
585+
* MCP methods (tools/list, resources/read, etc.) to server implementation functions.
586+
* Sets up the bidirectional communication layer between MCP clients and this server.
587+
* Note: HTTP mode uses manual request routing instead of these handlers.
588+
*/
573589
private setupRequestHandlers(): void {
574590
// Set up MCP protocol handlers for stdio transport
575591
this.telemetry.debug('🔧 Setting up MCP request handlers...');
@@ -1136,147 +1152,20 @@ export class MCPOpenAPIServer {
11361152

11371153
const serverPort = port || this.options.port!;
11381154

1139-
this.app.post('/mcp', async (req, res) => {
1140-
try {
1141-
// Extract user context from request
1142-
const userContext = this.extractUserContext(req);
1143-
1144-
// Handle JSON-RPC 2.0 method calls
1145-
const { method, params, id } = req.body;
1146-
1147-
this.telemetry.debug(`MCP method call: ${method}`);
1148-
1149-
let result;
1150-
1151-
switch (method) {
1152-
case 'initialize':
1153-
result = {
1154-
message: "MCP server running",
1155-
authMode: userContext.token ? "user-token" : "service-token",
1156-
capabilities: {
1157-
tools: { listChanged: true },
1158-
resources: { listChanged: true, subscribe: false },
1159-
prompts: { listChanged: true }
1160-
}
1161-
};
1162-
break;
1163-
1164-
case 'tools/list':
1165-
result = {
1166-
tools: this.tools.map(tool => ({
1167-
name: tool.name,
1168-
description: tool.description,
1169-
inputSchema: tool.inputSchema
1170-
}))
1171-
};
1172-
break;
1173-
1174-
case 'resources/list':
1175-
result = {
1176-
resources: this.resources.map(resource => ({
1177-
uri: resource.uri,
1178-
name: resource.name,
1179-
description: resource.description,
1180-
mimeType: resource.mimeType,
1181-
parameters: resource.parameters
1182-
}))
1183-
};
1184-
break;
1185-
1186-
case 'prompts/list':
1187-
result = {
1188-
prompts: Array.from(this.prompts.entries()).map(([name, spec]) => ({
1189-
name: name,
1190-
description: spec.description || `${name} prompt template`,
1191-
arguments: spec.arguments || []
1192-
}))
1193-
};
1194-
break;
1195-
1196-
case 'tools/call':
1197-
const toolName = params?.name;
1198-
const toolArgs = params?.arguments || {};
1199-
if (!toolName) {
1200-
throw new Error('Tool name is required');
1201-
}
1202-
result = await this.executeTool(toolName, toolArgs, userContext);
1203-
break;
1204-
1205-
case 'resources/read':
1206-
const resourceUri = params?.uri;
1207-
if (!resourceUri) {
1208-
throw new Error('Resource URI is required');
1209-
}
1210-
const resourceParams = params?.parameters || {};
1211-
1212-
1213-
1214-
result = await this.readResource(resourceUri, userContext, resourceParams);
1215-
break;
1216-
1217-
case 'prompts/get':
1218-
const promptName = params?.name;
1219-
const promptArgs = params?.arguments || {};
1220-
if (!promptName) {
1221-
throw new Error('Prompt name is required');
1222-
}
1223-
result = await this.getPrompt(promptName, promptArgs);
1224-
break;
1225-
1226-
default:
1227-
throw new Error(`Unknown method: ${method}`);
1228-
}
1229-
1230-
res.json({
1231-
jsonrpc: "2.0",
1232-
result,
1233-
id
1234-
});
1235-
} catch (error) {
1236-
res.status(500).json({
1237-
jsonrpc: "2.0",
1238-
error: { code: -32603, message: (error as Error).message },
1239-
id: req.body.id
1240-
});
1241-
}
1242-
});
1243-
1244-
this.app.get('/health', (req, res) => {
1245-
res.json({
1246-
status: 'ok',
1247-
specs: Array.from(this.specs.keys()),
1248-
tools: this.tools.length,
1249-
resources: this.resources.length,
1250-
prompts: this.prompts.size,
1251-
version: PACKAGE_VERSION
1252-
});
1253-
});
1254-
1255-
this.app.get('/info', (req, res) => {
1256-
res.json({
1257-
specs: Array.from(this.specs.entries()).map(([id, spec]) => ({
1258-
id,
1259-
title: spec.info.title,
1260-
version: spec.info.version
1261-
})),
1262-
tools: this.tools.map(t => ({ name: t.name, description: t.description })),
1263-
resources: this.resources.map(r => ({ uri: r.uri, name: r.name })),
1264-
prompts: Array.from(this.prompts.keys())
1265-
});
1266-
});
1267-
1268-
const server = this.app.listen(serverPort, () => {
1269-
// HTTP server startup - use info level
1270-
const address = server.address();
1271-
const host = typeof address === 'object' && address ?
1272-
(address.family === 'IPv6' ? `[${address.address}]` : address.address) :
1273-
'localhost';
1274-
1275-
this.telemetry.info(`🚀 MCP OpenAPI Server running on port ${serverPort}`);
1276-
this.telemetry.info(`📊 Health check: http://${host}:${serverPort}/health`);
1277-
this.telemetry.info(`ℹ️ Server info: http://${host}:${serverPort}/info`);
1278-
1279-
this.telemetry.debug(`📋 Loaded ${this.specs.size} specs, ${this.tools.length} tools, ${this.resources.length} resources, ${this.prompts.size} prompts`);
1155+
const httpSetup = new HttpSetup({
1156+
app: this.app,
1157+
specs: this.specs,
1158+
tools: this.tools,
1159+
resources: this.resources,
1160+
prompts: this.prompts,
1161+
telemetry: this.telemetry,
1162+
executeTool: this.executeTool.bind(this),
1163+
readResource: this.readResource.bind(this),
1164+
getPrompt: this.getPrompt.bind(this),
1165+
extractUserContext: this.extractUserContext.bind(this)
12801166
});
1167+
1168+
httpSetup.setupRoutes();
1169+
await httpSetup.startServer(serverPort);
12811170
}
12821171
}

0 commit comments

Comments
 (0)