@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
@@ -25,8 +25,10 @@ export declare const config: {
25
25
  mcpServerVersion: string;
26
26
  /** Logging level. From `MCP_LOG_LEVEL` env var. Default: "debug". */
27
27
  logLevel: string;
28
- /** Absolute path to the logs directory. From `LOGS_DIR` env var. */
29
- logsPath: string;
28
+ /** Defines the logging output mode ('file' or 'stdout'). From `LOG_OUTPUT_MODE`. */
29
+ logOutputMode: "file" | "stdout";
30
+ /** Absolute path to the logs directory (if logOutputMode is 'file'). From `LOGS_DIR`. */
31
+ logsPath: string | null;
30
32
  /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
31
33
  environment: string;
32
34
  /** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
@@ -39,26 +41,14 @@ export declare const config: {
39
41
  mcpAllowedOrigins: string[] | undefined;
40
42
  /** Auth secret key (JWTs, http transport). From `MCP_AUTH_SECRET_KEY`. CRITICAL. */
41
43
  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;
44
+ /** Auth mode ('jwt' or 'oauth'). From `MCP_AUTH_MODE`. */
45
+ mcpAuthMode: "jwt" | "oauth";
46
+ /** OAuth Issuer URL. From `OAUTH_ISSUER_URL`. */
47
+ oauthIssuerUrl: string | undefined;
48
+ /** OAuth Audience. From `OAUTH_AUDIENCE`. */
49
+ oauthAudience: string | undefined;
50
+ /** OAuth JWKS URI. From `OAUTH_JWKS_URI`. */
51
+ oauthJwksUri: string | undefined;
62
52
  /** NCBI API Key. From `NCBI_API_KEY`. */
63
53
  ncbiApiKey: string | undefined;
64
54
  /** NCBI Tool Identifier. From `NCBI_TOOL_IDENTIFIER`. Defaults to server name/version. */
@@ -80,6 +80,8 @@ const EnvSchema = z.object({
80
80
  MCP_LOG_LEVEL: z.string().default("debug"),
81
81
  /** Directory for log files. Defaults to "logs" in project root. */
82
82
  LOGS_DIR: z.string().default(path.join(projectRoot, "logs")),
83
+ /** Defines the logging output mode. "file" for logs in LOGS_DIR, "stdout" for console logging. */
84
+ LOG_OUTPUT_MODE: z.enum(["file", "stdout"]).default("file"),
83
85
  /** Runtime environment (e.g., "development", "production"). Default: "development". */
84
86
  NODE_ENV: z.string().default("development"),
85
87
  /** MCP communication transport ("stdio" or "http"). Default: "stdio". */
@@ -95,31 +97,14 @@ const EnvSchema = z.object({
95
97
  .string()
96
98
  .min(32, "MCP_AUTH_SECRET_KEY must be at least 32 characters long for security reasons.")
97
99
  .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(),
100
+ /** Authentication mode ('jwt' or 'oauth'). Default: 'jwt'. */
101
+ MCP_AUTH_MODE: z.enum(["jwt", "oauth"]).default("jwt"),
102
+ /** OAuth: The expected issuer of the JWT. */
103
+ OAUTH_ISSUER_URL: z.string().url().optional(),
104
+ /** OAuth: The expected audience of the JWT. */
105
+ OAUTH_AUDIENCE: z.string().optional(),
106
+ /** OAuth: The URI of the JWKS endpoint. */
107
+ OAUTH_JWKS_URI: z.string().url().optional(),
123
108
  /** Optional. OAuth provider authorization endpoint URL. */
124
109
  OAUTH_PROXY_AUTHORIZATION_URL: z
125
110
  .string()
@@ -226,12 +211,15 @@ const ensureDirectory = (dirPath, rootDir, dirName) => {
226
211
  };
227
212
  // --- End Directory Ensurance Function ---
228
213
  // --- Logs Directory Handling ---
229
- const validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
230
- if (!validatedLogsPath) {
231
- if (process.stdout.isTTY) {
232
- console.error("FATAL: Logs directory configuration is invalid or could not be created. Please check permissions and path. Exiting.");
214
+ let validatedLogsPath = null;
215
+ if (env.LOG_OUTPUT_MODE === "file") {
216
+ validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
217
+ if (!validatedLogsPath) {
218
+ if (process.stdout.isTTY) {
219
+ console.error("FATAL: Log mode is 'file' but logs directory is invalid or could not be created. Please check LOGS_DIR, permissions, and path. Exiting.");
220
+ }
221
+ process.exit(1); // Exit if file logging is configured but directory is not usable
233
222
  }
234
- process.exit(1); // Exit if logs directory is not usable
235
223
  }
236
224
  // --- End Logs Directory Handling ---
237
225
  /**
@@ -245,7 +233,9 @@ export const config = {
245
233
  mcpServerVersion: env.MCP_SERVER_VERSION || pkg.version,
246
234
  /** Logging level. From `MCP_LOG_LEVEL` env var. Default: "debug". */
247
235
  logLevel: env.MCP_LOG_LEVEL,
248
- /** Absolute path to the logs directory. From `LOGS_DIR` env var. */
236
+ /** Defines the logging output mode ('file' or 'stdout'). From `LOG_OUTPUT_MODE`. */
237
+ logOutputMode: env.LOG_OUTPUT_MODE,
238
+ /** Absolute path to the logs directory (if logOutputMode is 'file'). From `LOGS_DIR`. */
249
239
  logsPath: validatedLogsPath,
250
240
  /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
251
241
  environment: env.NODE_ENV,
@@ -261,26 +251,14 @@ export const config = {
261
251
  .filter(Boolean),
262
252
  /** Auth secret key (JWTs, http transport). From `MCP_AUTH_SECRET_KEY`. CRITICAL. */
263
253
  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,
254
+ /** Auth mode ('jwt' or 'oauth'). From `MCP_AUTH_MODE`. */
255
+ mcpAuthMode: env.MCP_AUTH_MODE,
256
+ /** OAuth Issuer URL. From `OAUTH_ISSUER_URL`. */
257
+ oauthIssuerUrl: env.OAUTH_ISSUER_URL,
258
+ /** OAuth Audience. From `OAUTH_AUDIENCE`. */
259
+ oauthAudience: env.OAUTH_AUDIENCE,
260
+ /** OAuth JWKS URI. From `OAUTH_JWKS_URI`. */
261
+ oauthJwksUri: env.OAUTH_JWKS_URI,
284
262
  // NCBI Configuration
285
263
  /** NCBI API Key. From `NCBI_API_KEY`. */
286
264
  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",
@@ -83,7 +83,8 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
83
83
  input: sanitizeInputForLogging(input),
84
84
  });
85
85
  logger.info(`Executing 'generate_pubmed_chart'. Chart type: ${input.chartType}, Output format: ${input.outputFormat}`, operationContext);
86
- if (input.outputFormat !== "png") { // Changed from svg to png
86
+ if (input.outputFormat !== "png") {
87
+ // Changed from svg to png
87
88
  const unsupportedFormatError = new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported output format: ${input.outputFormat}. Currently, only 'png' is supported.`, // Changed message
88
89
  { requestedFormat: input.outputFormat });
89
90
  logger.warning(unsupportedFormatError.message, operationContext);
@@ -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,48 @@ 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.map(removeEmptyObjectsRecursively).filter((item) => {
39
+ if (item === null || item === undefined)
40
+ return false;
41
+ if (Array.isArray(item) && item.length === 0)
42
+ return false; // Filter out empty arrays
40
43
  if (typeof item === "object" &&
41
- item !== null &&
42
44
  !Array.isArray(item) &&
43
45
  Object.keys(item).length === 0) {
44
- return false; // Filter out empty objects from arrays
46
+ return false; // Filter out empty objects
45
47
  }
46
48
  return true;
47
49
  });
50
+ return newArr;
48
51
  }
49
52
  // If it's an object, create a new object with non-empty properties
50
53
  const newObj = {};
51
54
  for (const key in obj) {
52
55
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
53
56
  const value = removeEmptyObjectsRecursively(obj[key]);
54
- // Check if the recursed value is an empty object
57
+ // Skip null or undefined values
58
+ if (value === null || value === undefined) {
59
+ continue;
60
+ }
61
+ // Skip empty arrays
62
+ if (Array.isArray(value) && value.length === 0) {
63
+ continue;
64
+ }
65
+ // Skip empty objects
55
66
  if (typeof value === "object" &&
56
- value !== null &&
57
67
  !Array.isArray(value) &&
58
68
  Object.keys(value).length === 0) {
59
- // It's an empty object, so we don't add this key-value pair to newObj
60
69
  continue;
61
70
  }
62
- // If value is not an empty object (or not an object at all), add it
71
+ // If value is not empty, add it
63
72
  newObj[key] = value;
64
73
  }
65
74
  }
@@ -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 {};