@cyanheads/pubmed-mcp-server 1.1.4 → 1.2.1

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 (33) hide show
  1. package/README.md +8 -10
  2. package/dist/mcp-server/server.d.ts +0 -7
  3. package/dist/mcp-server/server.js +17 -53
  4. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +6 -2
  5. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +94 -202
  6. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +43 -7
  7. package/dist/mcp-server/tools/generatePubMedChart/logic.d.ts +9 -28
  8. package/dist/mcp-server/tools/generatePubMedChart/logic.js +133 -192
  9. package/dist/mcp-server/tools/generatePubMedChart/registration.js +57 -13
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +1 -1
  11. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +1 -1
  12. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +27 -4
  13. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +50 -45
  14. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +1 -1
  15. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +0 -24
  16. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +51 -45
  17. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +2 -4
  18. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -41
  19. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +49 -8
  20. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +12 -10
  21. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +68 -126
  22. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +46 -6
  23. package/dist/mcp-server/transports/httpTransport.d.ts +0 -8
  24. package/dist/mcp-server/transports/httpTransport.js +57 -354
  25. package/package.json +7 -15
  26. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
  27. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
  28. package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
  29. package/dist/mcp-server/resources/echoResource/index.js +0 -13
  30. package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
  31. package/dist/mcp-server/resources/echoResource/registration.js +0 -168
  32. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
  33. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +0 -6
@@ -19,84 +19,17 @@ import http from "http";
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { config } from "../../config/index.js";
21
21
  import { BaseErrorCode, McpError } from "../../types-global/errors.js";
22
- import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
22
+ import { ErrorHandler, logger, rateLimiter, requestContextService, } from "../../utils/index.js";
23
23
  import { initializeAuthMiddleware, mcpAuthMiddleware, } from "./authentication/authMiddleware.js";
24
24
  import { oauthMiddleware } from "./authentication/oauthMiddleware.js";
25
- /**
26
- * The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
27
- * Defaults to 3010 if not specified (default is managed by the config module).
28
- * @constant {number} HTTP_PORT
29
- * @private
30
- */
31
25
  const HTTP_PORT = config.mcpHttpPort;
32
- /**
33
- * The host address for the HTTP transport, configured via `MCP_HTTP_HOST` environment variable.
34
- * Defaults to '127.0.0.1' if not specified (default is managed by the config module).
35
- * MCP Spec Security Note: Recommends binding to localhost for local servers to minimize exposure.
36
- * @private
37
- */
38
26
  const HTTP_HOST = config.mcpHttpHost;
39
- /**
40
- * The single HTTP endpoint path for all MCP communication, as required by the MCP specification.
41
- * This endpoint supports POST, GET, DELETE, and OPTIONS methods.
42
- * @constant {string} MCP_ENDPOINT_PATH
43
- * @private
44
- */
45
27
  const MCP_ENDPOINT_PATH = "/mcp";
46
- /**
47
- * Maximum number of attempts to find an available port if the initial `HTTP_PORT` is in use.
48
- * The server will try ports sequentially: `HTTP_PORT`, `HTTP_PORT + 1`, ..., up to `MAX_PORT_RETRIES`.
49
- * @constant {number} MAX_PORT_RETRIES
50
- * @private
51
- */
52
28
  const MAX_PORT_RETRIES = 15;
53
- /**
54
- * Stores active `StreamableHTTPServerTransport` instances from the SDK, keyed by their session ID.
55
- * This is essential for routing subsequent HTTP requests (GET, DELETE, non-initialize POST)
56
- * to the correct stateful session transport instance.
57
- *
58
- * @scalability Note: This is an in-memory store. For horizontal scaling across
59
- * multiple processes or machines, this state would need to be moved to a shared,
60
- * distributed store like Redis or a database.
61
- *
62
- * @type {Record<string, StreamableHTTPServerTransport>}
63
- * @private
64
- */
29
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
30
+ const SESSION_GC_INTERVAL_MS = 60 * 1000;
65
31
  const httpTransports = {};
66
- /**
67
- * Stores the last activity timestamp for each session, keyed by session ID.
68
- * Used for garbage collecting stale/abandoned sessions.
69
- *
70
- * @scalability Note: This is an in-memory store. Like `httpTransports`, this
71
- * would need to be moved to a distributed store for multi-instance deployments.
72
- *
73
- * @type {Record<string, number>}
74
- * @private
75
- */
76
32
  const sessionActivity = {};
77
- /**
78
- * The timeout period in milliseconds for inactive sessions. If a session has no
79
- * activity for this duration, it will be considered stale and garbage collected.
80
- * Defaults to 30 minutes.
81
- * @constant {number} SESSION_TIMEOUT_MS
82
- * @private
83
- */
84
- const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
85
- /**
86
- * The interval in milliseconds at which the session garbage collector runs to
87
- * clean up stale sessions. Defaults to 1 minute.
88
- * @constant {number} SESSION_GC_INTERVAL_MS
89
- * @private
90
- */
91
- const SESSION_GC_INTERVAL_MS = 60 * 1000; // 1 minute
92
- /**
93
- * Proactively checks if a specific network port is already in use.
94
- * @param port - The port number to check.
95
- * @param host - The host address to check the port on.
96
- * @param parentContext - Logging context from the caller.
97
- * @returns A promise that resolves to `true` if the port is in use, or `false` otherwise.
98
- * @private
99
- */
100
33
  async function isPortInUse(port, host, parentContext) {
101
34
  const checkContext = requestContextService.createRequestContext({
102
35
  ...parentContext,
@@ -104,133 +37,66 @@ async function isPortInUse(port, host, parentContext) {
104
37
  port,
105
38
  host,
106
39
  });
107
- logger.debug(`Proactively checking port usability...`, checkContext);
108
40
  return new Promise((resolve) => {
109
41
  const tempServer = http.createServer();
110
42
  tempServer
111
43
  .once("error", (err) => {
112
- if (err.code === "EADDRINUSE") {
113
- logger.debug(`Proactive check: Port confirmed in use (EADDRINUSE).`, checkContext);
114
- resolve(true);
115
- }
116
- else {
117
- logger.debug(`Proactive check: Non-EADDRINUSE error encountered: ${err.message}`, { ...checkContext, errorCode: err.code });
118
- resolve(false);
119
- }
44
+ resolve(err.code === "EADDRINUSE");
120
45
  })
121
46
  .once("listening", () => {
122
- logger.debug(`Proactive check: Port is available.`, checkContext);
123
47
  tempServer.close(() => resolve(false));
124
48
  })
125
49
  .listen(port, host);
126
50
  });
127
51
  }
128
- /**
129
- * Attempts to start the HTTP server, retrying on incrementing ports if `EADDRINUSE` occurs.
130
- *
131
- * @param app - The Hono application instance.
132
- * @param initialPort - The initial port number to try.
133
- * @param host - The host address to bind to.
134
- * @param maxRetries - Maximum number of additional ports to attempt.
135
- * @param parentContext - Logging context from the caller.
136
- * @returns A promise that resolves with the Node.js `http.Server` instance the server successfully bound to.
137
- * @throws {Error} If binding fails after all retries or for a non-EADDRINUSE error.
138
- * @private
139
- */
140
52
  function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
141
53
  const startContext = requestContextService.createRequestContext({
142
54
  ...parentContext,
143
55
  operation: "startHttpServerWithRetry",
144
- initialPort,
145
- host,
146
- maxRetries,
147
56
  });
148
- logger.debug(`Attempting to start HTTP server...`, startContext);
149
57
  return new Promise(async (resolve, reject) => {
150
- let lastError = null;
151
58
  for (let i = 0; i <= maxRetries; i++) {
152
59
  const currentPort = initialPort + i;
153
- const attemptContext = requestContextService.createRequestContext({
154
- ...startContext,
155
- port: currentPort,
156
- attempt: i + 1,
157
- maxAttempts: maxRetries + 1,
158
- });
159
- logger.debug(`Attempting port ${currentPort} (${attemptContext.attempt}/${attemptContext.maxAttempts})`, attemptContext);
60
+ const attemptContext = { ...startContext, port: currentPort, attempt: i + 1 };
160
61
  if (await isPortInUse(currentPort, host, attemptContext)) {
161
- logger.warning(`Proactive check detected port ${currentPort} is in use, retrying...`, attemptContext);
162
- lastError = new Error(`EADDRINUSE: Port ${currentPort} detected as in use by proactive check.`);
163
- await new Promise((res) => setTimeout(res, 100));
62
+ logger.warning(`Port ${currentPort} is in use, retrying...`, attemptContext);
164
63
  continue;
165
64
  }
166
65
  try {
167
66
  const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
168
67
  const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
169
- logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
170
- // Display user-friendly startup message only after server is confirmed listening
171
- let serverAddressLog = serverAddress;
172
- let productionNote = "";
173
- if (config.environment === "production") {
174
- serverAddressLog = `https://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
175
- productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
176
- }
68
+ logger.info(`HTTP transport listening at ${serverAddress}`, { ...attemptContext, address: serverAddress });
177
69
  if (process.stdout.isTTY) {
178
- console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
70
+ console.log(`\n🚀 MCP Server running at: ${serverAddress}\n`);
179
71
  }
180
72
  });
181
73
  resolve(serverInstance);
182
74
  return;
183
75
  }
184
76
  catch (err) {
185
- lastError = err;
186
- logger.debug(`Listen error on port ${currentPort}: Code=${err.code}, Message=${err.message}`, { ...attemptContext, errorCode: err.code, errorMessage: err.message });
187
- if (err.code === "EADDRINUSE") {
188
- logger.warning(`Port ${currentPort} already in use (EADDRINUSE), retrying...`, attemptContext);
189
- await new Promise((res) => setTimeout(res, 100));
190
- }
191
- else {
192
- logger.error(`Failed to bind to port ${currentPort} due to non-EADDRINUSE error: ${err.message}`, { ...attemptContext, error: err.message });
77
+ if (err.code !== "EADDRINUSE") {
193
78
  reject(err);
194
79
  return;
195
80
  }
196
81
  }
197
82
  }
198
- logger.error(`Failed to bind to any port after ${maxRetries + 1} attempts. Last error: ${lastError?.message}`, { ...startContext, error: lastError?.message });
199
- reject(lastError ||
200
- new Error("Failed to bind to any port after multiple retries."));
83
+ reject(new Error("Failed to bind to any port after multiple retries."));
201
84
  });
202
85
  }
203
- /**
204
- * Sets up and starts the Streamable HTTP transport layer for the MCP server.
205
- *
206
- * @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
207
- * @param parentContext - Logging context from the main server startup process.
208
- * @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
209
- * @throws {Error} If the server fails to start after all port retries.
210
- */
211
86
  export async function startHttpTransport(createServerInstanceFn, parentContext) {
212
- initializeAuthMiddleware(); // Explicitly initialize auth middleware config
87
+ initializeAuthMiddleware();
213
88
  const app = new Hono();
214
89
  const transportContext = requestContextService.createRequestContext({
215
90
  ...parentContext,
216
- transportType: "HTTP",
217
91
  component: "HttpTransportSetup",
218
92
  });
219
- logger.debug("Setting up Hono app for HTTP transport...", transportContext);
220
- // Start the session garbage collector
221
93
  setInterval(() => {
222
94
  const now = Date.now();
223
- const gcContext = requestContextService.createRequestContext({
224
- operation: "SessionGarbageCollector",
225
- });
226
- logger.debug("Running session garbage collector...", gcContext);
95
+ const gcContext = requestContextService.createRequestContext({ operation: "SessionGarbageCollector" });
227
96
  for (const sessionId in sessionActivity) {
228
97
  if (now - sessionActivity[sessionId] > SESSION_TIMEOUT_MS) {
229
- logger.info(`Session ${sessionId} timed out due to inactivity. Cleaning up.`, { ...gcContext, sessionId });
230
- const transport = httpTransports[sessionId];
231
- if (transport) {
232
- transport.close(); // This will trigger the onclose handler to delete it from httpTransports
233
- }
98
+ logger.info(`Session ${sessionId} timed out. Cleaning up.`, { ...gcContext, sessionId });
99
+ httpTransports[sessionId]?.close();
234
100
  delete sessionActivity[sessionId];
235
101
  }
236
102
  }
@@ -238,68 +104,29 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
238
104
  app.use("*", cors({
239
105
  origin: config.mcpAllowedOrigins || [],
240
106
  allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
241
- allowHeaders: [
242
- "Content-Type",
243
- "Mcp-Session-Id",
244
- "Last-Event-ID",
245
- "Authorization",
246
- ],
107
+ allowHeaders: ["Content-Type", "Mcp-Session-Id", "Last-Event-ID", "Authorization"],
247
108
  credentials: true,
248
109
  }));
249
110
  app.use("*", async (c, next) => {
250
- const securityContext = requestContextService.createRequestContext({
251
- ...transportContext,
252
- operation: "securityMiddleware",
253
- path: c.req.path,
254
- method: c.req.method,
255
- origin: c.req.header("origin"),
256
- });
257
- logger.debug(`Applying security middleware...`, securityContext);
258
111
  c.res.headers.set("X-Content-Type-Options", "nosniff");
259
- c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
260
- 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'");
261
- logger.debug("Security middleware passed.", securityContext);
262
112
  await next();
263
113
  });
264
114
  app.use(MCP_ENDPOINT_PATH, async (c, next) => {
265
- const xff = c.req.header("x-forwarded-for");
266
- const clientIp = xff ? xff.split(",")[0].trim() : "unknown_ip";
267
- const rateLimitKey = clientIp || c.req.header("host") || "unknown_ip_for_rate_limit";
268
- const context = requestContextService.createRequestContext({
269
- operation: "httpRateLimitCheck",
270
- ipAddress: rateLimitKey,
271
- method: c.req.method,
272
- path: c.req.path,
273
- });
115
+ const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
116
+ const context = requestContextService.createRequestContext({ operation: "httpRateLimitCheck", ipAddress: clientIp });
274
117
  try {
275
- rateLimiter.check(rateLimitKey, context);
276
- logger.debug("Rate limit check passed.", context);
118
+ rateLimiter.check(clientIp, context);
277
119
  await next();
278
120
  }
279
121
  catch (error) {
280
- if (error instanceof McpError &&
281
- error.code === BaseErrorCode.RATE_LIMITED) {
282
- logger.warning(`Rate limit exceeded for IP: ${rateLimitKey}`, {
283
- ...context,
284
- errorMessage: error.message,
285
- details: error.details,
286
- });
287
- return c.json({
288
- jsonrpc: "2.0",
289
- error: { code: -32000, message: "Too Many Requests" },
290
- id: (await c.req.json().catch(() => ({})))?.id || null,
291
- }, 429);
292
- }
293
- else {
294
- logger.error("Unexpected error in rate limit middleware", {
295
- ...context,
296
- error: error instanceof Error ? error.message : String(error),
297
- });
298
- throw error;
299
- }
122
+ const handledError = ErrorHandler.handleError(error, { operation: "rateLimitMiddleware", context });
123
+ return c.json({
124
+ jsonrpc: "2.0",
125
+ error: { code: -32000, message: handledError.message },
126
+ id: (await c.req.json().catch(() => ({})))?.id || null,
127
+ }, 429);
300
128
  }
301
129
  });
302
- // Use the appropriate authentication middleware based on config
303
130
  if (config.mcpAuthMode === "oauth") {
304
131
  app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
305
132
  }
@@ -307,200 +134,76 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
307
134
  app.use(MCP_ENDPOINT_PATH, mcpAuthMiddleware);
308
135
  }
309
136
  app.post(MCP_ENDPOINT_PATH, async (c) => {
310
- const basePostContext = requestContextService.createRequestContext({
311
- ...transportContext,
312
- operation: "handlePost",
313
- method: "POST",
314
- path: c.req.path,
315
- origin: c.req.header("origin"),
316
- });
317
- const body = await c.req.json();
318
- logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
319
- ...basePostContext,
320
- headers: c.req.header(),
321
- bodyPreview: JSON.stringify(body).substring(0, 100),
322
- });
323
- const sessionId = c.req.header("mcp-session-id");
324
- logger.debug(`Extracted session ID: ${sessionId}`, {
325
- ...basePostContext,
326
- sessionId,
327
- });
328
- let transport = sessionId ? httpTransports[sessionId] : undefined;
329
- if (transport && sessionId) {
330
- sessionActivity[sessionId] = Date.now(); // Update activity timestamp
331
- }
332
- logger.debug(`Found existing transport for session ID: ${!!transport}`, {
333
- ...basePostContext,
334
- sessionId,
335
- });
336
- const isInitReq = isInitializeRequest(body);
337
- logger.debug(`Is InitializeRequest: ${isInitReq}`, {
338
- ...basePostContext,
339
- sessionId,
340
- });
341
- const requestId = body?.id || null;
137
+ const postContext = requestContextService.createRequestContext({ ...transportContext, operation: "handlePost" });
138
+ let transport;
342
139
  try {
343
- if (isInitReq) {
140
+ const body = await c.req.json();
141
+ const sessionId = c.req.header("mcp-session-id");
142
+ transport = sessionId ? httpTransports[sessionId] : undefined;
143
+ if (transport && sessionId)
144
+ sessionActivity[sessionId] = Date.now();
145
+ if (isInitializeRequest(body)) {
344
146
  if (transport) {
345
- logger.warning("Received InitializeRequest on an existing session ID. Closing old session and creating new.", { ...basePostContext, sessionId });
147
+ logger.warning("Re-initializing existing session.", { ...postContext, sessionId });
346
148
  await transport.close();
347
- // onclose handler will delete from httpTransports and sessionActivity
348
149
  }
349
- logger.info("Handling Initialize Request: Creating new session...", {
350
- ...basePostContext,
351
- sessionId,
352
- });
353
150
  transport = new StreamableHTTPServerTransport({
354
- sessionIdGenerator: () => {
355
- const newId = randomUUID();
356
- logger.debug(`Generated new session ID: ${newId}`, basePostContext);
357
- return newId;
358
- },
151
+ sessionIdGenerator: () => randomUUID(),
359
152
  onsessioninitialized: (newId) => {
360
- logger.debug(`Session initialized callback triggered for ID: ${newId}`, { ...basePostContext, newSessionId: newId });
361
153
  httpTransports[newId] = transport;
362
- sessionActivity[newId] = Date.now(); // Initialize activity timestamp
363
- logger.info(`HTTP Session created: ${newId}`, {
364
- ...basePostContext,
365
- newSessionId: newId,
366
- });
154
+ sessionActivity[newId] = Date.now();
155
+ logger.info(`HTTP Session created: ${newId}`, { ...postContext, newSessionId: newId });
367
156
  },
368
157
  });
369
158
  transport.onclose = () => {
370
159
  const closedSessionId = transport.sessionId;
371
160
  if (closedSessionId) {
372
- logger.debug(`onclose handler triggered for session ID: ${closedSessionId}`, { ...basePostContext, closedSessionId });
373
161
  delete httpTransports[closedSessionId];
374
- delete sessionActivity[closedSessionId]; // Clean up activity tracker
375
- logger.info(`HTTP Session closed: ${closedSessionId}`, {
376
- ...basePostContext,
377
- closedSessionId,
378
- });
379
- }
380
- else {
381
- logger.debug("onclose handler triggered for transport without session ID (likely init failure).", basePostContext);
162
+ delete sessionActivity[closedSessionId];
163
+ logger.info(`HTTP Session closed: ${closedSessionId}`, { ...postContext, closedSessionId });
382
164
  }
383
165
  };
384
- logger.debug("Creating McpServer instance for new session...", basePostContext);
385
166
  const server = await createServerInstanceFn();
386
- logger.debug("Connecting McpServer to new transport...", basePostContext);
387
167
  await server.connect(transport);
388
- logger.debug("McpServer connected to transport.", basePostContext);
389
168
  }
390
169
  else if (!transport) {
391
- logger.warning("Invalid or missing session ID for non-initialize POST request.", { ...basePostContext, sessionId });
392
- return c.json({
393
- jsonrpc: "2.0",
394
- error: { code: -32004, message: "Invalid or expired session ID" },
395
- id: requestId,
396
- }, 404);
170
+ throw new McpError(BaseErrorCode.NOT_FOUND, "Invalid or expired session ID.");
397
171
  }
398
- const currentSessionId = transport.sessionId;
399
- logger.debug(`Processing POST request content for session ${currentSessionId}...`, { ...basePostContext, sessionId: currentSessionId, isInitReq });
400
- const response = await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
401
- logger.debug(`Finished processing POST request content for session ${currentSessionId}.`, { ...basePostContext, sessionId: currentSessionId });
402
- return response;
172
+ return await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
403
173
  }
404
174
  catch (err) {
405
- const errorSessionId = transport?.sessionId || sessionId;
406
- logger.error("Error handling POST request", {
407
- ...basePostContext,
408
- sessionId: errorSessionId,
409
- isInitReq,
410
- error: err instanceof Error ? err.message : String(err),
411
- stack: err instanceof Error ? err.stack : undefined,
412
- });
413
- if (isInitReq && transport && !transport.sessionId) {
414
- logger.debug("Cleaning up transport after initialization failure.", {
415
- ...basePostContext,
416
- sessionId: errorSessionId,
417
- });
418
- await transport.close().catch((closeErr) => logger.error("Error closing transport after init failure", {
419
- ...basePostContext,
420
- sessionId: errorSessionId,
421
- closeError: closeErr,
422
- }));
423
- }
175
+ const handledError = ErrorHandler.handleError(err, { operation: "handlePost", context: postContext });
176
+ const requestId = (await c.req.json().catch(() => ({})))?.id || null;
424
177
  return c.json({
425
178
  jsonrpc: "2.0",
426
- error: {
427
- code: -32603,
428
- message: "Internal server error during POST handling",
429
- },
179
+ error: { code: -32603, message: handledError.message },
430
180
  id: requestId,
431
- }, 500);
181
+ }, handledError instanceof McpError && handledError.code === BaseErrorCode.NOT_FOUND ? 404 : 500);
432
182
  }
433
183
  });
434
184
  const handleSessionReq = async (c) => {
435
185
  const method = c.req.method;
436
- const baseSessionReqContext = requestContextService.createRequestContext({
437
- ...transportContext,
438
- operation: `handle${method}`,
439
- method,
440
- path: c.req.path,
441
- origin: c.req.header("origin"),
442
- });
443
- logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
444
- ...baseSessionReqContext,
445
- headers: c.req.header(),
446
- });
447
- const sessionId = c.req.header("mcp-session-id");
448
- logger.debug(`Extracted session ID: ${sessionId}`, {
449
- ...baseSessionReqContext,
450
- sessionId,
451
- });
452
- const transport = sessionId ? httpTransports[sessionId] : undefined;
453
- if (transport && sessionId) {
454
- sessionActivity[sessionId] = Date.now(); // Update activity timestamp
455
- }
456
- logger.debug(`Found existing transport for session ID: ${!!transport}`, {
457
- ...baseSessionReqContext,
458
- sessionId,
459
- });
460
- if (!transport) {
461
- logger.warning(`Session not found for ${method} request`, {
462
- ...baseSessionReqContext,
463
- sessionId,
464
- });
465
- return c.json({
466
- jsonrpc: "2.0",
467
- error: { code: -32004, message: "Session not found or expired" },
468
- id: null,
469
- }, 404);
470
- }
186
+ const sessionReqContext = requestContextService.createRequestContext({ ...transportContext, operation: `handle${method}` });
471
187
  try {
472
- logger.debug(`Delegating ${method} request to transport for session ${sessionId}...`, { ...baseSessionReqContext, sessionId });
473
- const response = await transport.handleRequest(c.env.incoming, c.env.outgoing);
474
- logger.info(`Successfully handled ${method} request for session ${sessionId}`, { ...baseSessionReqContext, sessionId });
475
- return response;
188
+ const sessionId = c.req.header("mcp-session-id");
189
+ const transport = sessionId ? httpTransports[sessionId] : undefined;
190
+ if (!transport) {
191
+ throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.");
192
+ }
193
+ if (sessionId)
194
+ sessionActivity[sessionId] = Date.now();
195
+ return await transport.handleRequest(c.env.incoming, c.env.outgoing);
476
196
  }
477
197
  catch (err) {
478
- logger.error(`Error handling ${method} request for session ${sessionId}`, {
479
- ...baseSessionReqContext,
480
- sessionId,
481
- error: err instanceof Error ? err.message : String(err),
482
- stack: err instanceof Error ? err.stack : undefined,
483
- });
198
+ const handledError = ErrorHandler.handleError(err, { operation: `handle${method}`, context: sessionReqContext });
484
199
  return c.json({
485
200
  jsonrpc: "2.0",
486
- error: { code: -32603, message: "Internal Server Error" },
201
+ error: { code: -32603, message: handledError.message },
487
202
  id: null,
488
- }, 500);
203
+ }, handledError instanceof McpError && handledError.code === BaseErrorCode.NOT_FOUND ? 404 : 500);
489
204
  }
490
205
  };
491
206
  app.get(MCP_ENDPOINT_PATH, handleSessionReq);
492
207
  app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
493
- logger.debug("Creating HTTP server instance...", transportContext);
494
- try {
495
- logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
496
- const serverInstance = await startHttpServerWithRetry(app, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
497
- return serverInstance;
498
- }
499
- catch (err) {
500
- logger.fatal("HTTP server failed to start after multiple port retries.", {
501
- ...transportContext,
502
- error: err instanceof Error ? err.message : String(err),
503
- });
504
- throw err;
505
- }
208
+ return startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, MAX_PORT_RETRIES, transportContext);
506
209
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/pubmed-mcp-server",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
4
4
  "description": "A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -33,22 +33,16 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@hono/node-server": "^1.14.4",
36
- "@modelcontextprotocol/sdk": "^1.12.1",
37
- "@node-oauth/oauth2-server": "^5.2.0",
36
+ "@modelcontextprotocol/sdk": "^1.12.3",
38
37
  "@types/jsonwebtoken": "^9.0.9",
39
38
  "@types/node": "^24.0.1",
40
39
  "@types/sanitize-html": "^2.16.0",
41
40
  "@types/validator": "13.15.1",
42
- "axios": "^1.9.0",
43
- "canvas": "^3.1.0",
44
- "chalk": "^5.4.1",
45
- "chrono-node": "^2.8.2",
46
- "cli-table3": "^0.6.5",
41
+ "axios": "^1.10.0",
42
+ "chrono-node": "^2.8.3",
47
43
  "dotenv": "^16.5.0",
48
- "express": "^5.1.0",
49
44
  "fast-xml-parser": "^5.2.5",
50
45
  "hono": "^4.7.11",
51
- "ignore": "^7.0.5",
52
46
  "jose": "^6.0.11",
53
47
  "jsonwebtoken": "^9.0.2",
54
48
  "openai": "^5.3.0",
@@ -58,11 +52,10 @@
58
52
  "ts-node": "^10.9.2",
59
53
  "typescript": "^5.8.3",
60
54
  "validator": "13.15.15",
61
- "vega": "^6.1.2",
62
- "vega-lite": "^6.1.0",
55
+ "chart.js": "^4.5.0",
56
+ "chartjs-node-canvas": "^5.0.0",
63
57
  "winston": "^3.17.0",
64
- "winston-daily-rotate-file": "^5.0.0",
65
- "yargs": "^18.0.0",
58
+ "winston-transport": "^4.9.0",
66
59
  "zod": "^3.25.64"
67
60
  },
68
61
  "keywords": [
@@ -103,7 +96,6 @@
103
96
  "node": ">=16.0.0"
104
97
  },
105
98
  "devDependencies": {
106
- "@types/express": "^5.0.3",
107
99
  "@types/js-yaml": "^4.0.9",
108
100
  "js-yaml": "^4.1.0",
109
101
  "prettier": "^3.5.3",