@cyanheads/pubmed-mcp-server 1.0.16 → 1.1.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 (41) hide show
  1. package/dist/config/index.d.ts +8 -20
  2. package/dist/config/index.js +16 -45
  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/registration.js +2 -2
  8. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -1
  9. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +2 -1
  10. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +1 -1
  11. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +21 -10
  12. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -1
  13. package/dist/mcp-server/transports/authentication/authContext.d.ts +33 -0
  14. package/dist/mcp-server/transports/authentication/authContext.js +24 -0
  15. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +21 -15
  16. package/dist/mcp-server/transports/authentication/authMiddleware.js +51 -69
  17. package/dist/mcp-server/transports/authentication/authUtils.d.ts +18 -0
  18. package/dist/mcp-server/transports/authentication/authUtils.js +45 -0
  19. package/dist/mcp-server/transports/authentication/oauthMiddleware.d.ts +24 -0
  20. package/dist/mcp-server/transports/authentication/oauthMiddleware.js +109 -0
  21. package/dist/mcp-server/transports/authentication/types.d.ts +17 -0
  22. package/dist/mcp-server/transports/authentication/types.js +5 -0
  23. package/dist/mcp-server/transports/httpTransport.d.ts +5 -4
  24. package/dist/mcp-server/transports/httpTransport.js +177 -143
  25. package/dist/services/NCBI/ncbiCoreApiClient.js +0 -5
  26. package/dist/services/NCBI/ncbiRequestQueueManager.js +2 -4
  27. package/dist/services/NCBI/ncbiResponseHandler.js +0 -3
  28. package/dist/services/NCBI/ncbiService.d.ts +1 -1
  29. package/dist/services/NCBI/ncbiService.js +11 -4
  30. package/dist/utils/internal/logger.js +10 -30
  31. package/package.json +20 -7
  32. package/dist/services/index.d.ts +0 -7
  33. package/dist/services/index.js +0 -7
  34. package/dist/services/llm-providers/index.d.ts +0 -7
  35. package/dist/services/llm-providers/index.js +0 -7
  36. package/dist/services/llm-providers/llmFactory.d.ts +0 -69
  37. package/dist/services/llm-providers/llmFactory.js +0 -132
  38. package/dist/services/llm-providers/openRouter/index.d.ts +0 -6
  39. package/dist/services/llm-providers/openRouter/index.js +0 -7
  40. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +0 -99
  41. package/dist/services/llm-providers/openRouter/openRouterProvider.js +0 -329
@@ -39,26 +39,14 @@ export declare const config: {
39
39
  mcpAllowedOrigins: string[] | undefined;
40
40
  /** Auth secret key (JWTs, http transport). From `MCP_AUTH_SECRET_KEY`. CRITICAL. */
41
41
  mcpAuthSecretKey: string | undefined;
42
- /** OpenRouter App URL. From `OPENROUTER_APP_URL`. Default: "http://localhost:3000". */
43
- openrouterAppUrl: string;
44
- /** OpenRouter App Name. From `OPENROUTER_APP_NAME`. Defaults to `mcpServerName`. */
45
- openrouterAppName: string;
46
- /** OpenRouter API Key. From `OPENROUTER_API_KEY`. */
47
- openrouterApiKey: string | undefined;
48
- /** Default LLM model. From `LLM_DEFAULT_MODEL`. */
49
- llmDefaultModel: string;
50
- /** Default LLM temperature. From `LLM_DEFAULT_TEMPERATURE`. */
51
- llmDefaultTemperature: number | undefined;
52
- /** Default LLM top_p. From `LLM_DEFAULT_TOP_P`. */
53
- llmDefaultTopP: number | undefined;
54
- /** Default LLM max tokens. From `LLM_DEFAULT_MAX_TOKENS`. */
55
- llmDefaultMaxTokens: number | undefined;
56
- /** Default LLM top_k. From `LLM_DEFAULT_TOP_K`. */
57
- llmDefaultTopK: number | undefined;
58
- /** Default LLM min_p. From `LLM_DEFAULT_MIN_P`. */
59
- llmDefaultMinP: number | undefined;
60
- /** Gemini API Key. From `GEMINI_API_KEY`. */
61
- geminiApiKey: string | undefined;
42
+ /** Auth mode ('jwt' or 'oauth'). From `MCP_AUTH_MODE`. */
43
+ mcpAuthMode: "jwt" | "oauth";
44
+ /** OAuth Issuer URL. From `OAUTH_ISSUER_URL`. */
45
+ oauthIssuerUrl: string | undefined;
46
+ /** OAuth Audience. From `OAUTH_AUDIENCE`. */
47
+ oauthAudience: string | undefined;
48
+ /** OAuth JWKS URI. From `OAUTH_JWKS_URI`. */
49
+ oauthJwksUri: string | undefined;
62
50
  /** NCBI API Key. From `NCBI_API_KEY`. */
63
51
  ncbiApiKey: string | undefined;
64
52
  /** NCBI Tool Identifier. From `NCBI_TOOL_IDENTIFIER`. Defaults to server name/version. */
@@ -95,31 +95,14 @@ const EnvSchema = z.object({
95
95
  .string()
96
96
  .min(32, "MCP_AUTH_SECRET_KEY must be at least 32 characters long for security reasons.")
97
97
  .optional(),
98
- /** Optional. Application URL for OpenRouter integration. */
99
- OPENROUTER_APP_URL: z
100
- .string()
101
- .url("OPENROUTER_APP_URL must be a valid URL (e.g., http://localhost:3000)")
102
- .optional(),
103
- /** Optional. Application name for OpenRouter. Defaults to MCP_SERVER_NAME or package name. */
104
- OPENROUTER_APP_NAME: z.string().optional(),
105
- /** Optional. API key for OpenRouter services. */
106
- OPENROUTER_API_KEY: z.string().optional(),
107
- /** Default LLM model. Default: "google/gemini-2.5-flash-preview:thinking". */
108
- LLM_DEFAULT_MODEL: z
109
- .string()
110
- .default("google/gemini-2.5-flash-preview-05-20"),
111
- /** Optional. Default LLM temperature (0.0-2.0). */
112
- LLM_DEFAULT_TEMPERATURE: z.coerce.number().min(0).max(2).optional(),
113
- /** Optional. Default LLM top_p (0.0-1.0). */
114
- LLM_DEFAULT_TOP_P: z.coerce.number().min(0).max(1).optional(),
115
- /** Optional. Default LLM max tokens (positive integer). */
116
- LLM_DEFAULT_MAX_TOKENS: z.coerce.number().int().positive().optional(),
117
- /** Optional. Default LLM top_k (non-negative integer). */
118
- LLM_DEFAULT_TOP_K: z.coerce.number().int().nonnegative().optional(),
119
- /** Optional. Default LLM min_p (0.0-1.0). */
120
- LLM_DEFAULT_MIN_P: z.coerce.number().min(0).max(1).optional(),
121
- /** Optional. API key for Google Gemini services. */
122
- GEMINI_API_KEY: z.string().optional(),
98
+ /** Authentication mode ('jwt' or 'oauth'). Default: 'jwt'. */
99
+ MCP_AUTH_MODE: z.enum(["jwt", "oauth"]).default("jwt"),
100
+ /** OAuth: The expected issuer of the JWT. */
101
+ OAUTH_ISSUER_URL: z.string().url().optional(),
102
+ /** OAuth: The expected audience of the JWT. */
103
+ OAUTH_AUDIENCE: z.string().optional(),
104
+ /** OAuth: The URI of the JWKS endpoint. */
105
+ OAUTH_JWKS_URI: z.string().url().optional(),
123
106
  /** Optional. OAuth provider authorization endpoint URL. */
124
107
  OAUTH_PROXY_AUTHORIZATION_URL: z
125
108
  .string()
@@ -261,26 +244,14 @@ export const config = {
261
244
  .filter(Boolean),
262
245
  /** Auth secret key (JWTs, http transport). From `MCP_AUTH_SECRET_KEY`. CRITICAL. */
263
246
  mcpAuthSecretKey: env.MCP_AUTH_SECRET_KEY,
264
- /** OpenRouter App URL. From `OPENROUTER_APP_URL`. Default: "http://localhost:3000". */
265
- openrouterAppUrl: env.OPENROUTER_APP_URL || "http://localhost:3000",
266
- /** OpenRouter App Name. From `OPENROUTER_APP_NAME`. Defaults to `mcpServerName`. */
267
- openrouterAppName: env.OPENROUTER_APP_NAME || pkg.name || "MCP TS App",
268
- /** OpenRouter API Key. From `OPENROUTER_API_KEY`. */
269
- openrouterApiKey: env.OPENROUTER_API_KEY,
270
- /** Default LLM model. From `LLM_DEFAULT_MODEL`. */
271
- llmDefaultModel: env.LLM_DEFAULT_MODEL,
272
- /** Default LLM temperature. From `LLM_DEFAULT_TEMPERATURE`. */
273
- llmDefaultTemperature: env.LLM_DEFAULT_TEMPERATURE,
274
- /** Default LLM top_p. From `LLM_DEFAULT_TOP_P`. */
275
- llmDefaultTopP: env.LLM_DEFAULT_TOP_P,
276
- /** Default LLM max tokens. From `LLM_DEFAULT_MAX_TOKENS`. */
277
- llmDefaultMaxTokens: env.LLM_DEFAULT_MAX_TOKENS,
278
- /** Default LLM top_k. From `LLM_DEFAULT_TOP_K`. */
279
- llmDefaultTopK: env.LLM_DEFAULT_TOP_K,
280
- /** Default LLM min_p. From `LLM_DEFAULT_MIN_P`. */
281
- llmDefaultMinP: env.LLM_DEFAULT_MIN_P,
282
- /** Gemini API Key. From `GEMINI_API_KEY`. */
283
- geminiApiKey: env.GEMINI_API_KEY,
247
+ /** Auth mode ('jwt' or 'oauth'). From `MCP_AUTH_MODE`. */
248
+ mcpAuthMode: env.MCP_AUTH_MODE,
249
+ /** OAuth Issuer URL. From `OAUTH_ISSUER_URL`. */
250
+ oauthIssuerUrl: env.OAUTH_ISSUER_URL,
251
+ /** OAuth Audience. From `OAUTH_AUDIENCE`. */
252
+ oauthAudience: env.OAUTH_AUDIENCE,
253
+ /** OAuth JWKS URI. From `OAUTH_JWKS_URI`. */
254
+ oauthJwksUri: env.OAUTH_JWKS_URI,
284
255
  // NCBI Configuration
285
256
  /** NCBI API Key. From `NCBI_API_KEY`. */
286
257
  ncbiApiKey: env.NCBI_API_KEY,
package/dist/index.js CHANGED
@@ -22,16 +22,21 @@
22
22
  * @module src/index
23
23
  */
24
24
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
25
+ import http from "http"; // Import http module
25
26
  import { config, environment } from "./config/index.js";
26
27
  import { initializeAndStartServer } from "./mcp-server/server.js";
27
28
  import { requestContextService } from "./utils/index.js";
28
29
  import { logger } from "./utils/internal/logger.js";
29
30
  /**
30
31
  * Holds the main MCP server instance, primarily for STDIO transport.
31
- * For HTTP transport, server instances are typically managed per session.
32
32
  * @private
33
33
  */
34
- let server;
34
+ let mcpStdioServer;
35
+ /**
36
+ * Holds the Node.js HTTP server instance if HTTP transport is used.
37
+ * @private
38
+ */
39
+ let actualHttpServer;
35
40
  /**
36
41
  * Gracefully shuts down the main MCP server and associated resources.
37
42
  * Called on process termination signals or critical unhandled errors.
@@ -46,25 +51,70 @@ const shutdown = async (signal) => {
46
51
  triggerEvent: signal,
47
52
  });
48
53
  logger.info(`Received ${signal}. Initiating graceful shutdown...`, shutdownContext);
49
- try {
50
- if (server) {
51
- logger.info("Attempting to close main MCP server...", shutdownContext);
52
- await server.close();
53
- logger.info("Main MCP server closed successfully.", shutdownContext);
54
+ let mcpClosed = false;
55
+ let httpClosed = false;
56
+ let closeError = null;
57
+ const checkAndExit = () => {
58
+ if (closeError) {
59
+ logger.error("Critical error encountered during shutdown process.", {
60
+ ...shutdownContext,
61
+ errorMessage: closeError.message,
62
+ errorStack: closeError.stack,
63
+ });
64
+ process.exit(1);
54
65
  }
55
- else {
56
- logger.notice("No global server instance found to close during shutdown (this may be normal for HTTP transport).", shutdownContext);
66
+ else if (mcpClosed && httpClosed) {
67
+ logger.info("Graceful shutdown completed successfully. Exiting.", shutdownContext);
68
+ process.exit(0);
57
69
  }
58
- logger.info("Graceful shutdown completed successfully. Exiting.", shutdownContext);
59
- process.exit(0);
70
+ };
71
+ if (mcpStdioServer) {
72
+ logger.info("Attempting to close main MCP server (STDIO)...", shutdownContext);
73
+ mcpStdioServer
74
+ .close()
75
+ .then(() => {
76
+ logger.info("Main MCP server (STDIO) closed successfully.", shutdownContext);
77
+ mcpClosed = true;
78
+ checkAndExit();
79
+ })
80
+ .catch((err) => {
81
+ logger.error("Error closing MCP server (STDIO).", {
82
+ ...shutdownContext,
83
+ error: err,
84
+ });
85
+ mcpClosed = true; // Consider it closed even on error to allow exit
86
+ if (!closeError)
87
+ closeError = err;
88
+ checkAndExit();
89
+ });
60
90
  }
61
- catch (error) {
62
- logger.error("Critical error encountered during shutdown process.", {
63
- ...shutdownContext,
64
- errorMessage: error instanceof Error ? error.message : String(error),
65
- errorStack: error instanceof Error ? error.stack : undefined,
91
+ else {
92
+ mcpClosed = true; // No STDIO McpServer to close
93
+ }
94
+ if (actualHttpServer) {
95
+ logger.info("Attempting to close HTTP server...", shutdownContext);
96
+ actualHttpServer.close((err) => {
97
+ if (err) {
98
+ logger.error("Error closing HTTP server.", {
99
+ ...shutdownContext,
100
+ error: err,
101
+ });
102
+ if (!closeError)
103
+ closeError = err;
104
+ }
105
+ else {
106
+ logger.info("HTTP server closed successfully.", shutdownContext);
107
+ }
108
+ httpClosed = true;
109
+ checkAndExit();
66
110
  });
67
- process.exit(1);
111
+ }
112
+ else {
113
+ httpClosed = true; // No HTTP server to close
114
+ }
115
+ // Initial check in case no servers needed closing
116
+ if (mcpClosed && httpClosed) {
117
+ checkAndExit();
68
118
  }
69
119
  };
70
120
  /**
@@ -100,13 +150,6 @@ const start = async () => {
100
150
  }
101
151
  await logger.initialize(validatedMcpLogLevel);
102
152
  logger.info(`Logger has been initialized by start(). Effective MCP logging level set to: ${validatedMcpLogLevel}.`);
103
- // Configure RequestContextService once globally
104
- requestContextService.configure({
105
- appName: config.mcpServerName,
106
- appVersion: config.mcpServerVersion,
107
- environment,
108
- });
109
- logger.debug("RequestContextService configured with app name, version, and environment.");
110
153
  const transportType = config.mcpTransportType;
111
154
  const startupContext = requestContextService.createRequestContext({
112
155
  operation: `ServerStartupSequence_${transportType}`,
@@ -129,14 +172,19 @@ const start = async () => {
129
172
  logger.info(`Starting ${config.mcpServerName} (Version: ${config.mcpServerVersion}, Transport: ${transportType}, Env: ${environment})...`, startupContext);
130
173
  try {
131
174
  logger.debug("Calling initializeAndStartServer to set up MCP transport...", startupContext);
132
- const potentialServerInstance = await initializeAndStartServer();
133
- if (transportType === "stdio" &&
134
- potentialServerInstance instanceof McpServer) {
135
- server = potentialServerInstance;
175
+ const serverInstance = await initializeAndStartServer();
176
+ if (transportType === "stdio" && serverInstance instanceof McpServer) {
177
+ mcpStdioServer = serverInstance;
136
178
  logger.info("STDIO McpServer instance stored globally for shutdown.", startupContext);
137
179
  }
180
+ else if (transportType === "http" &&
181
+ serverInstance instanceof http.Server) {
182
+ actualHttpServer = serverInstance;
183
+ logger.info("HTTP transport initialized, http.Server instance stored globally for shutdown.", startupContext);
184
+ }
138
185
  else if (transportType === "http") {
139
- logger.info("HTTP transport initialized. Server lifecycle managed by HTTP listener and session handlers.", startupContext);
186
+ // This case should ideally not be reached if initializeAndStartServer correctly returns http.Server
187
+ logger.warning("HTTP transport initialized, but no http.Server instance was returned to index.ts. Shutdown might be incomplete.", startupContext);
140
188
  }
141
189
  logger.info(`${config.mcpServerName} is now running and ready to accept connections via ${transportType} transport.`, {
142
190
  ...startupContext,
@@ -13,6 +13,7 @@
13
13
  * - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
14
14
  * @module src/mcp-server/server
15
15
  */
16
+ import { ServerType } from "@hono/node-server";
16
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
18
  /**
18
19
  * Main application entry point. Initializes and starts the MCP server.
@@ -22,7 +23,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
23
  * - Manages server startup, leading to a server ready for MCP messages.
23
24
  * - Handles critical startup failures, ensuring appropriate process exit.
24
25
  *
25
- * @returns For 'stdio', resolves with `McpServer`. For 'http', runs indefinitely.
26
+ * @returns For 'stdio', resolves with `McpServer`. For 'http', resolves with `http.Server`.
26
27
  * Rejects on critical failure, leading to process exit.
27
28
  */
28
- export declare function initializeAndStartServer(): Promise<void | McpServer>;
29
+ export declare function initializeAndStartServer(): Promise<void | McpServer | ServerType>;
@@ -14,14 +14,12 @@
14
14
  * @module src/mcp-server/server
15
15
  */
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
- import { config } from "../config/index.js";
17
+ import { config, environment } from "../config/index.js";
18
18
  import { ErrorHandler, logger, requestContextService } from "../utils/index.js";
19
- // import { registerEchoResource } from "./resources/echoResource/index.js"; // To be removed after resource implementations.
20
19
  import { registerFetchPubMedContentTool } from "./tools/fetchPubMedContent/index.js";
21
- // Removed: import { registerFetchImageTestTool } from "./tools/imageTest/index.js";
22
- import { registerGeneratePubMedChartTool } from "./tools/generatePubMedChart/index.js"; // Added import
20
+ import { registerGeneratePubMedChartTool } from "./tools/generatePubMedChart/index.js";
23
21
  import { registerGetPubMedArticleConnectionsTool } from "./tools/getPubMedArticleConnections/index.js";
24
- import { registerPubMedResearchAgentTool } from "./tools/pubmedResearchAgent/index.js"; // Added import
22
+ import { registerPubMedResearchAgentTool } from "./tools/pubmedResearchAgent/index.js";
25
23
  import { registerSearchPubMedArticlesTool } from "./tools/searchPubMedArticles/index.js";
26
24
  import { startHttpTransport } from "./transports/httpTransport.js";
27
25
  import { connectStdioTransport } from "./transports/stdioTransport.js";
@@ -47,6 +45,11 @@ async function createMcpServerInstance() {
47
45
  operation: "createMcpServerInstance",
48
46
  });
49
47
  logger.info("Initializing MCP server instance", context);
48
+ requestContextService.configure({
49
+ appName: config.mcpServerName,
50
+ appVersion: config.mcpServerVersion,
51
+ environment,
52
+ });
50
53
  logger.debug("Instantiating McpServer with capabilities", {
51
54
  ...context,
52
55
  serverInfo: {
@@ -70,10 +73,9 @@ async function createMcpServerInstance() {
70
73
  logger.debug("Registering resources and tools...", context);
71
74
  // IMPORTANT: Keep tool registrations in alphabetical order. Do not remove this comment.
72
75
  await registerFetchPubMedContentTool(server);
73
- // Removed: await registerFetchImageTestTool(server);
74
- await registerGeneratePubMedChartTool(server); // Added new tool registration
76
+ await registerGeneratePubMedChartTool(server);
75
77
  await registerGetPubMedArticleConnectionsTool(server);
76
- await registerPubMedResearchAgentTool(server); // Added new tool registration
78
+ await registerPubMedResearchAgentTool(server);
77
79
  await registerSearchPubMedArticlesTool(server);
78
80
  // Add other tool/resource registrations here
79
81
  logger.info("Resources and tools registered successfully", context);
@@ -96,7 +98,7 @@ async function createMcpServerInstance() {
96
98
  * - Transport Connection: Calls dedicated functions for chosen transport.
97
99
  * - Server Instance Lifecycle: Single instance for 'stdio', per-session for 'http'.
98
100
  *
99
- * @returns Resolves with `McpServer` for 'stdio', or `void` for 'http'.
101
+ * @returns Resolves with `McpServer` for 'stdio', `http.Server` for 'http', or `void` if http transport manages its own lifecycle without returning a server.
100
102
  * @throws {Error} If transport type is unsupported or setup fails.
101
103
  * @private
102
104
  */
@@ -109,16 +111,16 @@ async function startTransport() {
109
111
  logger.info(`Starting transport: ${transportType}`, context);
110
112
  if (transportType === "http") {
111
113
  logger.debug("Delegating to startHttpTransport...", context);
112
- // For HTTP, startHttpTransport manages its own lifecycle and server instances per session.
113
- await startHttpTransport(createMcpServerInstance, context);
114
- return; // HTTP server runs indefinitely, no single server instance returned here.
114
+ // For HTTP, startHttpTransport now returns the http.Server instance.
115
+ const httpServerInstance = await startHttpTransport(createMcpServerInstance, context);
116
+ return httpServerInstance;
115
117
  }
116
118
  if (transportType === "stdio") {
117
119
  logger.debug("Creating single McpServer instance for stdio transport...", context);
118
120
  const server = await createMcpServerInstance();
119
121
  logger.debug("Delegating to connectStdioTransport...", context);
120
122
  await connectStdioTransport(server, context);
121
- return server; // Return the single server instance for stdio.
123
+ return server; // Return the single McpServer instance for stdio.
122
124
  }
123
125
  // Should not be reached if config validation is effective.
124
126
  logger.fatal(`Unsupported transport type configured: ${transportType}`, context);
@@ -132,7 +134,7 @@ async function startTransport() {
132
134
  * - Manages server startup, leading to a server ready for MCP messages.
133
135
  * - Handles critical startup failures, ensuring appropriate process exit.
134
136
  *
135
- * @returns For 'stdio', resolves with `McpServer`. For 'http', runs indefinitely.
137
+ * @returns For 'stdio', resolves with `McpServer`. For 'http', resolves with `http.Server`.
136
138
  * Rejects on critical failure, leading to process exit.
137
139
  */
138
140
  export async function initializeAndStartServer() {
@@ -6,7 +6,7 @@
6
6
  * @module src/mcp-server/tools/fetchPubMedContent/logic
7
7
  */
8
8
  import { z } from "zod";
9
- import { ncbiService } from "../../../services/NCBI/ncbiService.js";
9
+ import { getNcbiService } from "../../../services/NCBI/ncbiService.js";
10
10
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
11
11
  import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
12
12
  import { ensureArray, extractAbstractText, extractArticleDates, extractAuthors, extractDoi, extractGrants, extractJournalInfo, extractKeywords, extractMeshTerms, extractPmid, extractPublicationTypes, getText, } from "../../../utils/parsing/ncbi-parsing/index.js";
@@ -292,6 +292,7 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
292
292
  isError: true,
293
293
  };
294
294
  }
295
+ const ncbiService = getNcbiService();
295
296
  const toolLogicContext = requestContextService.createRequestContext({
296
297
  parentRequestId: parentRequestContext.requestId,
297
298
  operation: "fetchPubMedContentLogic",
@@ -5,11 +5,11 @@ export function registerGeneratePubMedChartTool(server) {
5
5
  const operation = "registerGeneratePubMedChartTool";
6
6
  const regContext = requestContextService.createRequestContext({ operation });
7
7
  try {
8
- server.tool("generate_pubmed_chart", "Generates a customizable chart (SVG) from structured data. " +
8
+ server.tool("generate_pubmed_chart", "Generates a customizable chart (PNG) from structured data. " +
9
9
  "Supports 'bar', 'line', and 'scatter' plots. " +
10
10
  "Requires data values and field mappings for axes. " +
11
11
  "Optional parameters allow for titles, dimensions, and color/size/series encoding. " +
12
- "Internally uses Vega-Lite to produce an SVG image.", GeneratePubMedChartInputSchema.shape, async (validatedInput, mcpProvidedContext) => {
12
+ "Internally uses Vega-Lite and a canvas renderer to produce a Base64-encoded PNG image.", GeneratePubMedChartInputSchema.shape, async (validatedInput, mcpProvidedContext) => {
13
13
  const handlerRequestContext = requestContextService.createRequestContext({
14
14
  parentRequestId: regContext.requestId,
15
15
  operation: "generatePubMedChartToolHandler",
@@ -3,7 +3,7 @@
3
3
  * Fetches article details using EFetch and formats them into various citation styles.
4
4
  * @module src/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter
5
5
  */
6
- import { ncbiService } from "../../../../services/NCBI/ncbiService.js";
6
+ import { getNcbiService } from "../../../../services/NCBI/ncbiService.js";
7
7
  import { logger, requestContextService, } from "../../../../utils/index.js";
8
8
  import { extractAuthors, extractDoi, extractJournalInfo, extractPmid, getText, } from "../../../../utils/parsing/ncbi-parsing/index.js";
9
9
  // Main handler for citation formats
@@ -17,6 +17,7 @@ export async function handleCitationFormats(input, outputData, context) {
17
17
  const eFetchBaseUrl = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
18
18
  const searchParamsString = new URLSearchParams(eFetchParams).toString();
19
19
  outputData.eUtilityUrl = `${eFetchBaseUrl}?${searchParamsString}`;
20
+ const ncbiService = getNcbiService();
20
21
  const eFetchResult = await ncbiService.eFetch(eFetchParams, context);
21
22
  if (!eFetchResult?.PubmedArticleSet?.PubmedArticle?.[0]) {
22
23
  outputData.message =
@@ -3,7 +3,7 @@
3
3
  * for the getPubMedArticleConnections tool.
4
4
  * @module src/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler
5
5
  */
6
- import { ncbiService } from "../../../../services/NCBI/ncbiService.js";
6
+ import { getNcbiService } from "../../../../services/NCBI/ncbiService.js";
7
7
  import { logger } from "../../../../utils/index.js";
8
8
  import { extractBriefSummaries } from "../../../../utils/parsing/ncbi-parsing/index.js";
9
9
  import { ensureArray } from "../../../../utils/parsing/ncbi-parsing/xmlGenericHelpers.js"; // Added import
@@ -33,6 +33,7 @@ export async function handleELinkRelationships(input, outputData, context) {
33
33
  const tempUrl = new URL("https://dummy.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi");
34
34
  Object.keys(eLinkParams).forEach((key) => tempUrl.searchParams.append(key, String(eLinkParams[key])));
35
35
  outputData.eUtilityUrl = `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/elink.fcgi?${tempUrl.search.substring(1)}`;
36
+ const ncbiService = getNcbiService();
36
37
  const eLinkResult = await ncbiService.eLink(eLinkParams, context);
37
38
  // Log the full eLinkResult for debugging
38
39
  logger.debug("Raw eLinkResult from ncbiService:", {
@@ -141,7 +141,7 @@ export const PubMedResearchAgentInputSchema = z.object({
141
141
  .optional()
142
142
  .describe("Ethical considerations, IRB/IACUC approval plans, data privacy, RCR training."),
143
143
  // Meta-parameter from previous iterations, still useful
144
- include_detailed_prompts_for_agent: z // Renamed from include_edge_cases_and_challenges_in_plan
144
+ include_detailed_prompts_for_agent: z
145
145
  .boolean()
146
146
  .optional()
147
147
  .default(false) // Default to false, meaning the tool primarily structures the detailed input.
@@ -27,39 +27,50 @@ function G(notes, includePrompts) {
27
27
  function allPropertiesUndefined(obj) {
28
28
  return Object.values(obj).every((value) => value === undefined);
29
29
  }
30
- // Helper function to recursively remove keys with empty object values
30
+ // Helper function to recursively remove keys with empty object or empty array values
31
31
  function removeEmptyObjectsRecursively(obj) {
32
32
  // Base cases for recursion
33
33
  if (typeof obj !== "object" || obj === null) {
34
34
  return obj; // Not an object or array, return as is
35
35
  }
36
36
  if (Array.isArray(obj)) {
37
- // If it's an array, recurse on each element
38
- // And filter out any elements that become empty objects after recursion
39
- return obj.map(removeEmptyObjectsRecursively).filter((item) => {
37
+ // If it's an array, recurse on each element and filter out empty objects/arrays
38
+ const newArr = obj
39
+ .map(removeEmptyObjectsRecursively)
40
+ .filter((item) => {
41
+ if (item === null || item === undefined)
42
+ return false;
43
+ if (Array.isArray(item) && item.length === 0)
44
+ return false; // Filter out empty arrays
40
45
  if (typeof item === "object" &&
41
- item !== null &&
42
46
  !Array.isArray(item) &&
43
47
  Object.keys(item).length === 0) {
44
- return false; // Filter out empty objects from arrays
48
+ return false; // Filter out empty objects
45
49
  }
46
50
  return true;
47
51
  });
52
+ return newArr;
48
53
  }
49
54
  // If it's an object, create a new object with non-empty properties
50
55
  const newObj = {};
51
56
  for (const key in obj) {
52
57
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
53
58
  const value = removeEmptyObjectsRecursively(obj[key]);
54
- // Check if the recursed value is an empty object
59
+ // Skip null or undefined values
60
+ if (value === null || value === undefined) {
61
+ continue;
62
+ }
63
+ // Skip empty arrays
64
+ if (Array.isArray(value) && value.length === 0) {
65
+ continue;
66
+ }
67
+ // Skip empty objects
55
68
  if (typeof value === "object" &&
56
- value !== null &&
57
69
  !Array.isArray(value) &&
58
70
  Object.keys(value).length === 0) {
59
- // It's an empty object, so we don't add this key-value pair to newObj
60
71
  continue;
61
72
  }
62
- // If value is not an empty object (or not an object at all), add it
73
+ // If value is not empty, add it
63
74
  newObj[key] = value;
64
75
  }
65
76
  }
@@ -5,7 +5,7 @@
5
5
  * @module src/mcp-server/tools/searchPubMedArticles/logic
6
6
  */
7
7
  import { z } from "zod";
8
- import { ncbiService } from "../../../services/NCBI/ncbiService.js";
8
+ import { getNcbiService } from "../../../services/NCBI/ncbiService.js";
9
9
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
10
10
  import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
11
11
  import { extractBriefSummaries } from "../../../utils/parsing/ncbi-parsing/index.js";
@@ -70,6 +70,7 @@ export const SearchPubMedArticlesInputSchema = z.object({
70
70
  * @returns A promise resolving to a CallToolResult.
71
71
  */
72
72
  export async function searchPubMedArticlesLogic(input, parentRequestContext) {
73
+ const ncbiService = getNcbiService();
73
74
  const toolLogicContext = requestContextService.createRequestContext({
74
75
  parentRequestId: parentRequestContext.requestId,
75
76
  operation: "searchPubMedArticlesLogic",
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @fileoverview Defines the AsyncLocalStorage context for authentication information.
3
+ * This module provides a mechanism to store and retrieve authentication details
4
+ * (like scopes and client ID) across asynchronous operations, making it available
5
+ * from the middleware layer down to the tool and resource handlers without
6
+ * drilling props.
7
+ *
8
+ * @module src/mcp-server/transports/authentication/authContext
9
+ */
10
+ import { AsyncLocalStorage } from "async_hooks";
11
+ import type { AuthInfo } from "./types.js";
12
+ /**
13
+ * Defines the structure of the store used within the AsyncLocalStorage.
14
+ * It holds the authentication information for the current request context.
15
+ */
16
+ interface AuthStore {
17
+ authInfo: AuthInfo;
18
+ }
19
+ /**
20
+ * An instance of AsyncLocalStorage to hold the authentication context (`AuthStore`).
21
+ * This allows `authInfo` to be accessible throughout the async call chain of a request
22
+ * after being set in the authentication middleware.
23
+ *
24
+ * @example
25
+ * // In middleware:
26
+ * await authContext.run({ authInfo }, next);
27
+ *
28
+ * // In a deeper handler:
29
+ * const store = authContext.getStore();
30
+ * const scopes = store?.authInfo.scopes;
31
+ */
32
+ export declare const authContext: AsyncLocalStorage<AuthStore>;
33
+ export {};
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @fileoverview Defines the AsyncLocalStorage context for authentication information.
3
+ * This module provides a mechanism to store and retrieve authentication details
4
+ * (like scopes and client ID) across asynchronous operations, making it available
5
+ * from the middleware layer down to the tool and resource handlers without
6
+ * drilling props.
7
+ *
8
+ * @module src/mcp-server/transports/authentication/authContext
9
+ */
10
+ import { AsyncLocalStorage } from "async_hooks";
11
+ /**
12
+ * An instance of AsyncLocalStorage to hold the authentication context (`AuthStore`).
13
+ * This allows `authInfo` to be accessible throughout the async call chain of a request
14
+ * after being set in the authentication middleware.
15
+ *
16
+ * @example
17
+ * // In middleware:
18
+ * await authContext.run({ authInfo }, next);
19
+ *
20
+ * // In a deeper handler:
21
+ * const store = authContext.getStore();
22
+ * const scopes = store?.authInfo.scopes;
23
+ */
24
+ export const authContext = new AsyncLocalStorage();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
2
+ * @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
3
3
  *
4
4
  * This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
5
5
  * using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
@@ -7,23 +7,29 @@
7
7
  * in the configuration (`config.mcpAuthSecretKey`).
8
8
  *
9
9
  * If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
10
- * (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
11
- * If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
10
+ * is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
11
+ * request object is for compatibility with the underlying SDK transport, which is
12
+ * not Hono-context-aware.
13
+ * If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
12
14
  *
13
15
  * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
14
16
  * @module src/mcp-server/transports/authentication/authMiddleware
15
17
  */
16
- import { NextFunction, Request, Response } from "express";
17
- import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
18
- declare global {
19
- namespace Express {
20
- interface Request {
21
- /** Authentication information derived from the JWT, conforming to MCP SDK's AuthInfo. */
22
- auth?: AuthInfo;
23
- }
24
- }
25
- }
18
+ import { HttpBindings } from "@hono/node-server";
19
+ import { Context, Next } from "hono";
26
20
  /**
27
- * Express middleware for verifying JWT Bearer token authentication.
21
+ * Validates the presence of the MCP_AUTH_SECRET_KEY at startup.
22
+ * This should be called once when the application is initializing.
28
23
  */
29
- export declare function mcpAuthMiddleware(req: Request, res: Response, next: NextFunction): void;
24
+ export declare function initializeAuthMiddleware(): void;
25
+ /**
26
+ * Hono middleware for verifying JWT Bearer token authentication.
27
+ * It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
28
+ */
29
+ export declare function mcpAuthMiddleware(c: Context<{
30
+ Bindings: HttpBindings;
31
+ }>, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
32
+ error: string;
33
+ }, 500, "json">) | (Response & import("hono").TypedResponse<{
34
+ error: string;
35
+ }, 401, "json">)>;