@cyanheads/pubmed-mcp-server 1.0.16 ā 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/index.d.ts +12 -22
- package/dist/config/index.js +29 -51
- package/dist/index.js +77 -29
- package/dist/mcp-server/server.d.ts +3 -2
- package/dist/mcp-server/server.js +16 -14
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +2 -1
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +2 -1
- package/dist/mcp-server/tools/generatePubMedChart/registration.js +2 -2
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +2 -1
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +1 -1
- package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +19 -10
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -1
- package/dist/mcp-server/transports/authentication/authContext.d.ts +33 -0
- package/dist/mcp-server/transports/authentication/authContext.js +24 -0
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +21 -15
- package/dist/mcp-server/transports/authentication/authMiddleware.js +51 -69
- package/dist/mcp-server/transports/authentication/authUtils.d.ts +18 -0
- package/dist/mcp-server/transports/authentication/authUtils.js +45 -0
- package/dist/mcp-server/transports/authentication/oauthMiddleware.d.ts +24 -0
- package/dist/mcp-server/transports/authentication/oauthMiddleware.js +109 -0
- package/dist/mcp-server/transports/authentication/types.d.ts +17 -0
- package/dist/mcp-server/transports/authentication/types.js +5 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +5 -4
- package/dist/mcp-server/transports/httpTransport.js +177 -143
- package/dist/services/NCBI/ncbiCoreApiClient.js +0 -5
- package/dist/services/NCBI/ncbiRequestQueueManager.js +2 -4
- package/dist/services/NCBI/ncbiResponseHandler.js +0 -3
- package/dist/services/NCBI/ncbiService.d.ts +1 -1
- package/dist/services/NCBI/ncbiService.js +11 -4
- package/dist/utils/internal/logger.js +52 -74
- package/package.json +21 -9
- package/dist/services/index.d.ts +0 -7
- package/dist/services/index.js +0 -7
- package/dist/services/llm-providers/index.d.ts +0 -7
- package/dist/services/llm-providers/index.js +0 -7
- package/dist/services/llm-providers/llmFactory.d.ts +0 -69
- package/dist/services/llm-providers/llmFactory.js +0 -132
- package/dist/services/llm-providers/openRouter/index.d.ts +0 -6
- package/dist/services/llm-providers/openRouter/index.js +0 -7
- package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +0 -99
- package/dist/services/llm-providers/openRouter/openRouterProvider.js +0 -329
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
|
|
2
|
+
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
|
|
3
3
|
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
-
* This includes creating
|
|
4
|
+
* This includes creating a Hono server, configuring middleware (CORS, Authentication),
|
|
5
5
|
* defining request routing for the single MCP endpoint (POST/GET/DELETE),
|
|
6
6
|
* managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
|
|
7
7
|
* and binding to a network port with retry logic for port conflicts.
|
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
11
11
|
* @module src/mcp-server/transports/httpTransport
|
|
12
12
|
*/
|
|
13
|
+
import { serve } from "@hono/node-server";
|
|
13
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
14
15
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
-
import
|
|
16
|
+
import { Hono } from "hono";
|
|
17
|
+
import { cors } from "hono/cors";
|
|
16
18
|
import http from "http";
|
|
17
19
|
import { randomUUID } from "node:crypto";
|
|
18
20
|
import { config } from "../../config/index.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
22
|
+
import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
|
|
23
|
+
import { initializeAuthMiddleware, mcpAuthMiddleware, } from "./authentication/authMiddleware.js";
|
|
24
|
+
import { oauthMiddleware } from "./authentication/oauthMiddleware.js";
|
|
21
25
|
/**
|
|
22
26
|
* The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
|
|
23
27
|
* Defaults to 3010 if not specified (default is managed by the config module).
|
|
@@ -55,44 +59,27 @@ const MAX_PORT_RETRIES = 15;
|
|
|
55
59
|
*/
|
|
56
60
|
const httpTransports = {};
|
|
57
61
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* If the server is bound to localhost, requests from localhost or with no/null origin are also permitted.
|
|
62
|
-
* Sets appropriate CORS headers (`Access-Control-Allow-Origin`, etc.) if the origin is allowed.
|
|
63
|
-
*
|
|
64
|
-
* @param req - The Express request object.
|
|
65
|
-
* @param res - The Express response object.
|
|
66
|
-
* @returns True if the origin is allowed, false otherwise.
|
|
62
|
+
* Stores the last activity timestamp for each session, keyed by session ID.
|
|
63
|
+
* Used for garbage collecting stale/abandoned sessions.
|
|
64
|
+
* @type {Record<string, number>}
|
|
67
65
|
* @private
|
|
68
66
|
*/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
86
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
87
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
|
|
88
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
89
|
-
}
|
|
90
|
-
else if (!allowed && origin) {
|
|
91
|
-
logger.warning(`Origin denied: ${origin}`, context);
|
|
92
|
-
}
|
|
93
|
-
logger.debug(`Origin check result: ${allowed}`, { ...context, allowed });
|
|
94
|
-
return allowed;
|
|
95
|
-
}
|
|
67
|
+
const sessionActivity = {};
|
|
68
|
+
/**
|
|
69
|
+
* The timeout period in milliseconds for inactive sessions. If a session has no
|
|
70
|
+
* activity for this duration, it will be considered stale and garbage collected.
|
|
71
|
+
* Defaults to 30 minutes.
|
|
72
|
+
* @constant {number} SESSION_TIMEOUT_MS
|
|
73
|
+
* @private
|
|
74
|
+
*/
|
|
75
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
76
|
+
/**
|
|
77
|
+
* The interval in milliseconds at which the session garbage collector runs to
|
|
78
|
+
* clean up stale sessions. Defaults to 1 minute.
|
|
79
|
+
* @constant {number} SESSION_GC_INTERVAL_MS
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
const SESSION_GC_INTERVAL_MS = 60 * 1000; // 1 minute
|
|
96
83
|
/**
|
|
97
84
|
* Proactively checks if a specific network port is already in use.
|
|
98
85
|
* @param port - The port number to check.
|
|
@@ -132,16 +119,16 @@ async function isPortInUse(port, host, parentContext) {
|
|
|
132
119
|
/**
|
|
133
120
|
* Attempts to start the HTTP server, retrying on incrementing ports if `EADDRINUSE` occurs.
|
|
134
121
|
*
|
|
135
|
-
* @param
|
|
122
|
+
* @param app - The Hono application instance.
|
|
136
123
|
* @param initialPort - The initial port number to try.
|
|
137
124
|
* @param host - The host address to bind to.
|
|
138
125
|
* @param maxRetries - Maximum number of additional ports to attempt.
|
|
139
126
|
* @param parentContext - Logging context from the caller.
|
|
140
|
-
* @returns A promise that resolves with the
|
|
127
|
+
* @returns A promise that resolves with the Node.js `http.Server` instance the server successfully bound to.
|
|
141
128
|
* @throws {Error} If binding fails after all retries or for a non-EADDRINUSE error.
|
|
142
129
|
* @private
|
|
143
130
|
*/
|
|
144
|
-
function startHttpServerWithRetry(
|
|
131
|
+
function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
|
|
145
132
|
const startContext = requestContextService.createRequestContext({
|
|
146
133
|
...parentContext,
|
|
147
134
|
operation: "startHttpServerWithRetry",
|
|
@@ -168,18 +155,21 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
|
|
|
168
155
|
continue;
|
|
169
156
|
}
|
|
170
157
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
158
|
+
const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
|
|
159
|
+
const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
160
|
+
logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
|
|
161
|
+
// Display user-friendly startup message only after server is confirmed listening
|
|
162
|
+
let serverAddressLog = serverAddress;
|
|
163
|
+
let productionNote = "";
|
|
164
|
+
if (config.environment === "production") {
|
|
165
|
+
serverAddressLog = `https://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
166
|
+
productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
|
|
167
|
+
}
|
|
168
|
+
if (process.stdout.isTTY) {
|
|
169
|
+
console.log(`\nš MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
|
|
170
|
+
}
|
|
181
171
|
});
|
|
182
|
-
resolve(
|
|
172
|
+
resolve(serverInstance);
|
|
183
173
|
return;
|
|
184
174
|
}
|
|
185
175
|
catch (err) {
|
|
@@ -206,92 +196,146 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
|
|
|
206
196
|
*
|
|
207
197
|
* @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
|
|
208
198
|
* @param parentContext - Logging context from the main server startup process.
|
|
209
|
-
* @returns A promise that resolves when the HTTP server is successfully listening.
|
|
199
|
+
* @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
|
|
210
200
|
* @throws {Error} If the server fails to start after all port retries.
|
|
211
201
|
*/
|
|
212
202
|
export async function startHttpTransport(createServerInstanceFn, parentContext) {
|
|
213
|
-
|
|
203
|
+
initializeAuthMiddleware(); // Explicitly initialize auth middleware config
|
|
204
|
+
const app = new Hono();
|
|
214
205
|
const transportContext = requestContextService.createRequestContext({
|
|
215
206
|
...parentContext,
|
|
216
207
|
transportType: "HTTP",
|
|
217
208
|
component: "HttpTransportSetup",
|
|
218
209
|
});
|
|
219
|
-
logger.debug("Setting up
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
operation: "
|
|
225
|
-
origin: req.headers.origin,
|
|
226
|
-
method: req.method,
|
|
227
|
-
path: req.path,
|
|
210
|
+
logger.debug("Setting up Hono app for HTTP transport...", transportContext);
|
|
211
|
+
// Start the session garbage collector
|
|
212
|
+
setInterval(() => {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const gcContext = requestContextService.createRequestContext({
|
|
215
|
+
operation: "SessionGarbageCollector",
|
|
228
216
|
});
|
|
229
|
-
logger.debug(
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
217
|
+
logger.debug("Running session garbage collector...", gcContext);
|
|
218
|
+
for (const sessionId in sessionActivity) {
|
|
219
|
+
if (now - sessionActivity[sessionId] > SESSION_TIMEOUT_MS) {
|
|
220
|
+
logger.info(`Session ${sessionId} timed out due to inactivity. Cleaning up.`, { ...gcContext, sessionId });
|
|
221
|
+
const transport = httpTransports[sessionId];
|
|
222
|
+
if (transport) {
|
|
223
|
+
transport.close(); // This will trigger the onclose handler to delete it from httpTransports
|
|
224
|
+
}
|
|
225
|
+
delete sessionActivity[sessionId];
|
|
226
|
+
}
|
|
237
227
|
}
|
|
238
|
-
});
|
|
239
|
-
app.use(
|
|
228
|
+
}, SESSION_GC_INTERVAL_MS);
|
|
229
|
+
app.use("*", cors({
|
|
230
|
+
origin: config.mcpAllowedOrigins || [],
|
|
231
|
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
232
|
+
allowHeaders: [
|
|
233
|
+
"Content-Type",
|
|
234
|
+
"Mcp-Session-Id",
|
|
235
|
+
"Last-Event-ID",
|
|
236
|
+
"Authorization",
|
|
237
|
+
],
|
|
238
|
+
credentials: true,
|
|
239
|
+
}));
|
|
240
|
+
app.use("*", async (c, next) => {
|
|
240
241
|
const securityContext = requestContextService.createRequestContext({
|
|
241
242
|
...transportContext,
|
|
242
243
|
operation: "securityMiddleware",
|
|
243
|
-
path: req.path,
|
|
244
|
-
method: req.method,
|
|
245
|
-
origin: req.
|
|
244
|
+
path: c.req.path,
|
|
245
|
+
method: c.req.method,
|
|
246
|
+
origin: c.req.header("origin"),
|
|
246
247
|
});
|
|
247
248
|
logger.debug(`Applying security middleware...`, securityContext);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
254
|
-
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
255
|
-
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
|
|
249
|
+
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
250
|
+
c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
251
|
+
c.res.headers.set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
|
|
256
252
|
logger.debug("Security middleware passed.", securityContext);
|
|
257
|
-
next();
|
|
253
|
+
await next();
|
|
258
254
|
});
|
|
259
|
-
app.use(
|
|
260
|
-
|
|
255
|
+
app.use(MCP_ENDPOINT_PATH, async (c, next) => {
|
|
256
|
+
const xff = c.req.header("x-forwarded-for");
|
|
257
|
+
const clientIp = xff ? xff.split(",")[0].trim() : "unknown_ip";
|
|
258
|
+
const rateLimitKey = clientIp || c.req.header("host") || "unknown_ip_for_rate_limit";
|
|
259
|
+
const context = requestContextService.createRequestContext({
|
|
260
|
+
operation: "httpRateLimitCheck",
|
|
261
|
+
ipAddress: rateLimitKey,
|
|
262
|
+
method: c.req.method,
|
|
263
|
+
path: c.req.path,
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
rateLimiter.check(rateLimitKey, context);
|
|
267
|
+
logger.debug("Rate limit check passed.", context);
|
|
268
|
+
await next();
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (error instanceof McpError &&
|
|
272
|
+
error.code === BaseErrorCode.RATE_LIMITED) {
|
|
273
|
+
logger.warning(`Rate limit exceeded for IP: ${rateLimitKey}`, {
|
|
274
|
+
...context,
|
|
275
|
+
errorMessage: error.message,
|
|
276
|
+
details: error.details,
|
|
277
|
+
});
|
|
278
|
+
return c.json({
|
|
279
|
+
jsonrpc: "2.0",
|
|
280
|
+
error: { code: -32000, message: "Too Many Requests" },
|
|
281
|
+
id: (await c.req.json().catch(() => ({})))?.id || null,
|
|
282
|
+
}, 429);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
logger.error("Unexpected error in rate limit middleware", {
|
|
286
|
+
...context,
|
|
287
|
+
error: error instanceof Error ? error.message : String(error),
|
|
288
|
+
});
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// Use the appropriate authentication middleware based on config
|
|
294
|
+
if (config.mcpAuthMode === "oauth") {
|
|
295
|
+
app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
app.use(MCP_ENDPOINT_PATH, mcpAuthMiddleware);
|
|
299
|
+
}
|
|
300
|
+
app.post(MCP_ENDPOINT_PATH, async (c) => {
|
|
261
301
|
const basePostContext = requestContextService.createRequestContext({
|
|
262
302
|
...transportContext,
|
|
263
303
|
operation: "handlePost",
|
|
264
304
|
method: "POST",
|
|
265
|
-
path: req.path,
|
|
266
|
-
origin: req.
|
|
305
|
+
path: c.req.path,
|
|
306
|
+
origin: c.req.header("origin"),
|
|
267
307
|
});
|
|
308
|
+
const body = await c.req.json();
|
|
268
309
|
logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
|
|
269
310
|
...basePostContext,
|
|
270
|
-
headers: req.
|
|
271
|
-
bodyPreview: JSON.stringify(
|
|
311
|
+
headers: c.req.header(),
|
|
312
|
+
bodyPreview: JSON.stringify(body).substring(0, 100),
|
|
272
313
|
});
|
|
273
|
-
const sessionId = req.
|
|
314
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
274
315
|
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
275
316
|
...basePostContext,
|
|
276
317
|
sessionId,
|
|
277
318
|
});
|
|
278
319
|
let transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
320
|
+
if (transport && sessionId) {
|
|
321
|
+
sessionActivity[sessionId] = Date.now(); // Update activity timestamp
|
|
322
|
+
}
|
|
279
323
|
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
280
324
|
...basePostContext,
|
|
281
325
|
sessionId,
|
|
282
326
|
});
|
|
283
|
-
const isInitReq = isInitializeRequest(
|
|
327
|
+
const isInitReq = isInitializeRequest(body);
|
|
284
328
|
logger.debug(`Is InitializeRequest: ${isInitReq}`, {
|
|
285
329
|
...basePostContext,
|
|
286
330
|
sessionId,
|
|
287
331
|
});
|
|
288
|
-
const requestId =
|
|
332
|
+
const requestId = body?.id || null;
|
|
289
333
|
try {
|
|
290
334
|
if (isInitReq) {
|
|
291
335
|
if (transport) {
|
|
292
336
|
logger.warning("Received InitializeRequest on an existing session ID. Closing old session and creating new.", { ...basePostContext, sessionId });
|
|
293
337
|
await transport.close();
|
|
294
|
-
delete httpTransports
|
|
338
|
+
// onclose handler will delete from httpTransports and sessionActivity
|
|
295
339
|
}
|
|
296
340
|
logger.info("Handling Initialize Request: Creating new session...", {
|
|
297
341
|
...basePostContext,
|
|
@@ -306,6 +350,7 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
306
350
|
onsessioninitialized: (newId) => {
|
|
307
351
|
logger.debug(`Session initialized callback triggered for ID: ${newId}`, { ...basePostContext, newSessionId: newId });
|
|
308
352
|
httpTransports[newId] = transport;
|
|
353
|
+
sessionActivity[newId] = Date.now(); // Initialize activity timestamp
|
|
309
354
|
logger.info(`HTTP Session created: ${newId}`, {
|
|
310
355
|
...basePostContext,
|
|
311
356
|
newSessionId: newId,
|
|
@@ -317,6 +362,7 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
317
362
|
if (closedSessionId) {
|
|
318
363
|
logger.debug(`onclose handler triggered for session ID: ${closedSessionId}`, { ...basePostContext, closedSessionId });
|
|
319
364
|
delete httpTransports[closedSessionId];
|
|
365
|
+
delete sessionActivity[closedSessionId]; // Clean up activity tracker
|
|
320
366
|
logger.info(`HTTP Session closed: ${closedSessionId}`, {
|
|
321
367
|
...basePostContext,
|
|
322
368
|
closedSessionId,
|
|
@@ -334,17 +380,17 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
334
380
|
}
|
|
335
381
|
else if (!transport) {
|
|
336
382
|
logger.warning("Invalid or missing session ID for non-initialize POST request.", { ...basePostContext, sessionId });
|
|
337
|
-
|
|
383
|
+
return c.json({
|
|
338
384
|
jsonrpc: "2.0",
|
|
339
385
|
error: { code: -32004, message: "Invalid or expired session ID" },
|
|
340
386
|
id: requestId,
|
|
341
|
-
});
|
|
342
|
-
return;
|
|
387
|
+
}, 404);
|
|
343
388
|
}
|
|
344
389
|
const currentSessionId = transport.sessionId;
|
|
345
390
|
logger.debug(`Processing POST request content for session ${currentSessionId}...`, { ...basePostContext, sessionId: currentSessionId, isInitReq });
|
|
346
|
-
await transport.handleRequest(
|
|
391
|
+
const response = await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
|
|
347
392
|
logger.debug(`Finished processing POST request content for session ${currentSessionId}.`, { ...basePostContext, sessionId: currentSessionId });
|
|
393
|
+
return response;
|
|
348
394
|
}
|
|
349
395
|
catch (err) {
|
|
350
396
|
const errorSessionId = transport?.sessionId || sessionId;
|
|
@@ -355,16 +401,6 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
355
401
|
error: err instanceof Error ? err.message : String(err),
|
|
356
402
|
stack: err instanceof Error ? err.stack : undefined,
|
|
357
403
|
});
|
|
358
|
-
if (!res.headersSent) {
|
|
359
|
-
res.status(500).json({
|
|
360
|
-
jsonrpc: "2.0",
|
|
361
|
-
error: {
|
|
362
|
-
code: -32603,
|
|
363
|
-
message: "Internal server error during POST handling",
|
|
364
|
-
},
|
|
365
|
-
id: requestId,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
404
|
if (isInitReq && transport && !transport.sessionId) {
|
|
369
405
|
logger.debug("Cleaning up transport after initialization failure.", {
|
|
370
406
|
...basePostContext,
|
|
@@ -376,27 +412,38 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
376
412
|
closeError: closeErr,
|
|
377
413
|
}));
|
|
378
414
|
}
|
|
415
|
+
return c.json({
|
|
416
|
+
jsonrpc: "2.0",
|
|
417
|
+
error: {
|
|
418
|
+
code: -32603,
|
|
419
|
+
message: "Internal server error during POST handling",
|
|
420
|
+
},
|
|
421
|
+
id: requestId,
|
|
422
|
+
}, 500);
|
|
379
423
|
}
|
|
380
424
|
});
|
|
381
|
-
const handleSessionReq = async (
|
|
382
|
-
const method = req.method;
|
|
425
|
+
const handleSessionReq = async (c) => {
|
|
426
|
+
const method = c.req.method;
|
|
383
427
|
const baseSessionReqContext = requestContextService.createRequestContext({
|
|
384
428
|
...transportContext,
|
|
385
429
|
operation: `handle${method}`,
|
|
386
430
|
method,
|
|
387
|
-
path: req.path,
|
|
388
|
-
origin: req.
|
|
431
|
+
path: c.req.path,
|
|
432
|
+
origin: c.req.header("origin"),
|
|
389
433
|
});
|
|
390
434
|
logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
|
|
391
435
|
...baseSessionReqContext,
|
|
392
|
-
headers: req.
|
|
436
|
+
headers: c.req.header(),
|
|
393
437
|
});
|
|
394
|
-
const sessionId = req.
|
|
438
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
395
439
|
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
396
440
|
...baseSessionReqContext,
|
|
397
441
|
sessionId,
|
|
398
442
|
});
|
|
399
443
|
const transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
444
|
+
if (transport && sessionId) {
|
|
445
|
+
sessionActivity[sessionId] = Date.now(); // Update activity timestamp
|
|
446
|
+
}
|
|
400
447
|
logger.debug(`Found existing transport for session ID: ${!!transport}`, {
|
|
401
448
|
...baseSessionReqContext,
|
|
402
449
|
sessionId,
|
|
@@ -406,17 +453,17 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
406
453
|
...baseSessionReqContext,
|
|
407
454
|
sessionId,
|
|
408
455
|
});
|
|
409
|
-
|
|
456
|
+
return c.json({
|
|
410
457
|
jsonrpc: "2.0",
|
|
411
458
|
error: { code: -32004, message: "Session not found or expired" },
|
|
412
|
-
id: null,
|
|
413
|
-
});
|
|
414
|
-
return;
|
|
459
|
+
id: null,
|
|
460
|
+
}, 404);
|
|
415
461
|
}
|
|
416
462
|
try {
|
|
417
463
|
logger.debug(`Delegating ${method} request to transport for session ${sessionId}...`, { ...baseSessionReqContext, sessionId });
|
|
418
|
-
await transport.handleRequest(
|
|
464
|
+
const response = await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
419
465
|
logger.info(`Successfully handled ${method} request for session ${sessionId}`, { ...baseSessionReqContext, sessionId });
|
|
466
|
+
return response;
|
|
420
467
|
}
|
|
421
468
|
catch (err) {
|
|
422
469
|
logger.error(`Error handling ${method} request for session ${sessionId}`, {
|
|
@@ -425,33 +472,20 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
425
472
|
error: err instanceof Error ? err.message : String(err),
|
|
426
473
|
stack: err instanceof Error ? err.stack : undefined,
|
|
427
474
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
});
|
|
434
|
-
}
|
|
475
|
+
return c.json({
|
|
476
|
+
jsonrpc: "2.0",
|
|
477
|
+
error: { code: -32603, message: "Internal Server Error" },
|
|
478
|
+
id: null,
|
|
479
|
+
}, 500);
|
|
435
480
|
}
|
|
436
481
|
};
|
|
437
482
|
app.get(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
438
483
|
app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
439
484
|
logger.debug("Creating HTTP server instance...", transportContext);
|
|
440
|
-
const serverInstance = http.createServer(app);
|
|
441
485
|
try {
|
|
442
486
|
logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
let productionNote = "";
|
|
446
|
-
if (config.environment === "production") {
|
|
447
|
-
// The server itself runs HTTP, but it's expected to be behind an HTTPS proxy in production.
|
|
448
|
-
// The log reflects the effective public-facing URL.
|
|
449
|
-
serverAddressLog = `https://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
|
|
450
|
-
productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
|
|
451
|
-
}
|
|
452
|
-
if (process.stdout.isTTY) {
|
|
453
|
-
console.log(`\nš MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
|
|
454
|
-
}
|
|
487
|
+
const serverInstance = await startHttpServerWithRetry(app, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
|
|
488
|
+
return serverInstance;
|
|
455
489
|
}
|
|
456
490
|
catch (err) {
|
|
457
491
|
logger.fatal("HTTP server failed to start after multiple port retries.", {
|
|
@@ -13,11 +13,6 @@ export class NcbiCoreApiClient {
|
|
|
13
13
|
this.axiosInstance = axios.create({
|
|
14
14
|
timeout: 30000, // 30 seconds timeout for NCBI requests
|
|
15
15
|
});
|
|
16
|
-
logger.debug("NcbiCoreApiClient initialized", requestContextService.createRequestContext({
|
|
17
|
-
service: "NcbiCoreApiClient",
|
|
18
|
-
ncbiBaseUrl: NCBI_EUTILS_BASE_URL,
|
|
19
|
-
maxRetries: config.ncbiMaxRetries,
|
|
20
|
-
}));
|
|
21
16
|
}
|
|
22
17
|
/**
|
|
23
18
|
* Makes an HTTP request to the specified NCBI E-utility endpoint.
|
|
@@ -9,10 +9,8 @@ export class NcbiRequestQueueManager {
|
|
|
9
9
|
this.requestQueue = [];
|
|
10
10
|
this.isProcessingQueue = false;
|
|
11
11
|
this.lastRequestTime = 0;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
requestDelay: config.ncbiRequestDelayMs,
|
|
15
|
-
}));
|
|
12
|
+
// Constructor should not have side-effects like logging.
|
|
13
|
+
// The service that uses this manager can log its creation if needed.
|
|
16
14
|
}
|
|
17
15
|
/**
|
|
18
16
|
* Processes the request queue, ensuring delays between requests to respect NCBI rate limits.
|
|
@@ -32,9 +32,6 @@ export class NcbiResponseHandler {
|
|
|
32
32
|
return arrayTags.includes(jpath);
|
|
33
33
|
},
|
|
34
34
|
});
|
|
35
|
-
logger.debug("NcbiResponseHandler initialized", requestContextService.createRequestContext({
|
|
36
|
-
service: "NcbiResponseHandler",
|
|
37
|
-
}));
|
|
38
35
|
}
|
|
39
36
|
extractNcbiErrorMessages(parsedXml) {
|
|
40
37
|
const messages = [];
|
|
@@ -21,4 +21,4 @@ export declare class NcbiService {
|
|
|
21
21
|
eLink(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
22
22
|
eInfo(params: NcbiRequestParams, context: RequestContext): Promise<any>;
|
|
23
23
|
}
|
|
24
|
-
export declare
|
|
24
|
+
export declare function getNcbiService(): NcbiService;
|
|
@@ -16,9 +16,6 @@ export class NcbiService {
|
|
|
16
16
|
this.queueManager = new NcbiRequestQueueManager();
|
|
17
17
|
this.apiClient = new NcbiCoreApiClient();
|
|
18
18
|
this.responseHandler = new NcbiResponseHandler();
|
|
19
|
-
logger.debug("NcbiService initialized with new modular architecture", requestContextService.createRequestContext({
|
|
20
|
-
service: "NcbiService",
|
|
21
|
-
}));
|
|
22
19
|
}
|
|
23
20
|
async performNcbiRequest(endpoint, params, context, options = {}) {
|
|
24
21
|
const task = async () => {
|
|
@@ -54,4 +51,14 @@ export class NcbiService {
|
|
|
54
51
|
});
|
|
55
52
|
}
|
|
56
53
|
}
|
|
57
|
-
|
|
54
|
+
let ncbiServiceInstance;
|
|
55
|
+
export function getNcbiService() {
|
|
56
|
+
if (!ncbiServiceInstance) {
|
|
57
|
+
ncbiServiceInstance = new NcbiService();
|
|
58
|
+
logger.debug("NcbiService lazily initialized.", requestContextService.createRequestContext({
|
|
59
|
+
service: "NcbiService",
|
|
60
|
+
operation: "getNcbiServiceInstance",
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
return ncbiServiceInstance;
|
|
64
|
+
}
|