@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.
Files changed (42) hide show
  1. package/dist/config/index.d.ts +12 -22
  2. package/dist/config/index.js +29 -51
  3. package/dist/index.js +77 -29
  4. package/dist/mcp-server/server.d.ts +3 -2
  5. package/dist/mcp-server/server.js +16 -14
  6. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +2 -1
  7. package/dist/mcp-server/tools/generatePubMedChart/logic.js +2 -1
  8. package/dist/mcp-server/tools/generatePubMedChart/registration.js +2 -2
  9. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -1
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +2 -1
  11. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +1 -1
  12. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +19 -10
  13. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -1
  14. package/dist/mcp-server/transports/authentication/authContext.d.ts +33 -0
  15. package/dist/mcp-server/transports/authentication/authContext.js +24 -0
  16. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +21 -15
  17. package/dist/mcp-server/transports/authentication/authMiddleware.js +51 -69
  18. package/dist/mcp-server/transports/authentication/authUtils.d.ts +18 -0
  19. package/dist/mcp-server/transports/authentication/authUtils.js +45 -0
  20. package/dist/mcp-server/transports/authentication/oauthMiddleware.d.ts +24 -0
  21. package/dist/mcp-server/transports/authentication/oauthMiddleware.js +109 -0
  22. package/dist/mcp-server/transports/authentication/types.d.ts +17 -0
  23. package/dist/mcp-server/transports/authentication/types.js +5 -0
  24. package/dist/mcp-server/transports/httpTransport.d.ts +5 -4
  25. package/dist/mcp-server/transports/httpTransport.js +177 -143
  26. package/dist/services/NCBI/ncbiCoreApiClient.js +0 -5
  27. package/dist/services/NCBI/ncbiRequestQueueManager.js +2 -4
  28. package/dist/services/NCBI/ncbiResponseHandler.js +0 -3
  29. package/dist/services/NCBI/ncbiService.d.ts +1 -1
  30. package/dist/services/NCBI/ncbiService.js +11 -4
  31. package/dist/utils/internal/logger.js +52 -74
  32. package/package.json +21 -9
  33. package/dist/services/index.d.ts +0 -7
  34. package/dist/services/index.js +0 -7
  35. package/dist/services/llm-providers/index.d.ts +0 -7
  36. package/dist/services/llm-providers/index.js +0 -7
  37. package/dist/services/llm-providers/llmFactory.d.ts +0 -69
  38. package/dist/services/llm-providers/llmFactory.js +0 -132
  39. package/dist/services/llm-providers/openRouter/index.d.ts +0 -6
  40. package/dist/services/llm-providers/openRouter/index.js +0 -7
  41. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +0 -99
  42. 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 an Express server, configuring middleware (CORS, Authentication),
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 express from "express";
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 { logger, requestContextService, } from "../../utils/index.js";
20
- import { mcpAuthMiddleware } from "./authentication/authMiddleware.js";
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
- * Checks if an incoming HTTP request's `Origin` header is permissible based on configuration.
59
- * MCP Spec Security: Servers MUST validate the `Origin` header for cross-origin requests.
60
- * This function checks the request's origin against the `config.mcpAllowedOrigins` list.
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
- function isOriginAllowed(req, res) {
70
- const origin = req.headers.origin;
71
- const host = req.hostname;
72
- const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(host);
73
- const allowedOrigins = config.mcpAllowedOrigins || [];
74
- const context = requestContextService.createRequestContext({
75
- operation: "isOriginAllowed",
76
- origin,
77
- host,
78
- isLocalhostBinding,
79
- allowedOrigins,
80
- });
81
- logger.debug("Checking origin allowance", context);
82
- const allowed = (origin && allowedOrigins.includes(origin)) ||
83
- (isLocalhostBinding && (!origin || origin === "null"));
84
- if (allowed && origin) {
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 serverInstance - The Node.js HTTP server instance.
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 port number the server successfully bound to.
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(serverInstance, initialPort, host, maxRetries, parentContext) {
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
- await new Promise((listenResolve, listenReject) => {
172
- serverInstance
173
- .listen(currentPort, host, () => {
174
- const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
175
- logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
176
- listenResolve();
177
- })
178
- .on("error", (err) => {
179
- listenReject(err);
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(currentPort);
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
- const app = express();
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 Express app for HTTP transport...", transportContext);
220
- app.use(express.json());
221
- app.options(MCP_ENDPOINT_PATH, (req, res) => {
222
- const optionsContext = requestContextService.createRequestContext({
223
- ...transportContext,
224
- operation: "handleOptions",
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(`Received OPTIONS request for ${MCP_ENDPOINT_PATH}`, optionsContext);
230
- if (isOriginAllowed(req, res)) {
231
- logger.debug("OPTIONS request origin allowed, sending 204.", optionsContext);
232
- res.sendStatus(204);
233
- }
234
- else {
235
- logger.debug("OPTIONS request origin denied, sending 403.", optionsContext);
236
- res.status(403).send("Forbidden: Invalid Origin");
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((req, res, next) => {
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.headers.origin,
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
- if (!isOriginAllowed(req, res)) {
249
- logger.debug("Origin check failed, sending 403.", securityContext);
250
- res.status(403).send("Forbidden: Invalid Origin");
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(mcpAuthMiddleware);
260
- app.post(MCP_ENDPOINT_PATH, async (req, res) => {
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.headers.origin,
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.headers,
271
- bodyPreview: JSON.stringify(req.body).substring(0, 100),
311
+ headers: c.req.header(),
312
+ bodyPreview: JSON.stringify(body).substring(0, 100),
272
313
  });
273
- const sessionId = req.headers["mcp-session-id"];
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(req.body);
327
+ const isInitReq = isInitializeRequest(body);
284
328
  logger.debug(`Is InitializeRequest: ${isInitReq}`, {
285
329
  ...basePostContext,
286
330
  sessionId,
287
331
  });
288
- const requestId = req.body?.id || null;
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[sessionId];
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
- res.status(404).json({
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(req, res, req.body);
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 (req, res) => {
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.headers.origin,
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.headers,
436
+ headers: c.req.header(),
393
437
  });
394
- const sessionId = req.headers["mcp-session-id"];
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
- res.status(404).json({
456
+ return c.json({
410
457
  jsonrpc: "2.0",
411
458
  error: { code: -32004, message: "Session not found or expired" },
412
- id: null, // Or a relevant request identifier if available from context
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(req, res);
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
- if (!res.headersSent) {
429
- res.status(500).json({
430
- jsonrpc: "2.0",
431
- error: { code: -32603, message: "Internal Server Error" },
432
- id: null, // Or a relevant request identifier
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 actualPort = await startHttpServerWithRetry(serverInstance, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
444
- let serverAddressLog = `http://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
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
- logger.debug("NcbiRequestQueueManager initialized", requestContextService.createRequestContext({
13
- service: "NcbiRequestQueueManager",
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 const ncbiService: NcbiService;
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
- export const ncbiService = new NcbiService();
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
+ }