@cyanheads/pubmed-mcp-server 1.1.4 → 1.2.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 (47) hide show
  1. package/README.md +10 -12
  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/{authentication → auth/core}/authContext.d.ts +2 -2
  24. package/dist/mcp-server/transports/{authentication → auth/core}/authContext.js +1 -1
  25. package/dist/mcp-server/transports/{authentication/types.d.ts → auth/core/authTypes.d.ts} +1 -1
  26. package/dist/mcp-server/transports/{authentication/types.js → auth/core/authTypes.js} +1 -1
  27. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.d.ts +1 -1
  28. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.js +3 -3
  29. package/dist/mcp-server/transports/auth/index.d.ts +10 -0
  30. package/dist/mcp-server/transports/auth/index.js +9 -0
  31. package/dist/mcp-server/transports/{authentication/authMiddleware.d.ts → auth/strategies/jwt/jwtMiddleware.d.ts} +4 -12
  32. package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +36 -43
  33. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts +2 -6
  34. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js +33 -18
  35. package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
  36. package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
  37. package/dist/mcp-server/transports/httpTransport.d.ts +11 -14
  38. package/dist/mcp-server/transports/httpTransport.js +91 -389
  39. package/package.json +13 -21
  40. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
  41. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
  42. package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
  43. package/dist/mcp-server/resources/echoResource/index.js +0 -13
  44. package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
  45. package/dist/mcp-server/resources/echoResource/registration.js +0 -168
  46. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
  47. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +0 -6
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # PubMed MCP Server
2
2
 
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
- [![Model Context Protocol](https://img.shields.io/badge/MCP%20SDK-^1.12.1-green.svg)](https://modelcontextprotocol.io/)
5
- [![Version](https://img.shields.io/badge/Version-1.1.4-blue.svg)](./CHANGELOG.md)
4
+ [![Model Context Protocol](https://img.shields.io/badge/MCP%20SDK-^1.13.0-green.svg)](https://modelcontextprotocol.io/)
5
+ [![Version](https://img.shields.io/badge/Version-1.2.2-blue.svg)](./CHANGELOG.md)
6
6
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
7
7
  [![Status](https://img.shields.io/badge/Status-Stable-green.svg)](https://github.com/cyanheads/pubmed-mcp-server/issues)
8
8
  [![GitHub](https://img.shields.io/github/stars/cyanheads/pubmed-mcp-server?style=social)](https://github.com/cyanheads/pubmed-mcp-server)
@@ -23,7 +23,7 @@ This server equips your AI with specialized tools to interact with PubMed:
23
23
  | [`fetch_pubmed_content`](./src/mcp-server/tools/fetchPubMedContent/) | Retrieves detailed information for PubMed articles. Can use a list of PMIDs or ESearch history (queryKey/webEnv) with pagination. (See [Example](./examples/fetch_pubmed_content_example.md)) | - Flexible `detailLevel`: `abstract_plus` (parsed details, optional MeSH/grant), `full_xml` (JSON representation of the PubMedArticle XML structure), `medline_text` (MEDLINE format), `citation_data` (minimal for citations).<br/>- Supports direct PMID list or `queryKey`/`webEnv` from ESearch history.<br/>- Supports `retstart`/`retmax` for pagination with history.<br/>- Uses NCBI EFetch. |
24
24
  | [`get_pubmed_article_connections`](./src/mcp-server/tools/getPubMedArticleConnections/) | Finds related articles (cited by, similar, references) or formats citations for a PMID. (See [Ex.1](./examples/get_pubmed_article_connections_1.md), [Ex.2](./examples/get_pubmed_article_connections_2.md)) | - Uses NCBI ELink for relationships.<br/>- Uses NCBI EFetch for citation data (RIS, BibTeX, APA, MLA).<br/>- Filter by max related results. |
25
25
  | [`pubmed_research_agent`](./src/mcp-server/tools/pubmedResearchAgent/) | Generates a standardized JSON research plan outline from component details. (See [Example](./examples/pubmed_research_agent_example.md)) | - Accepts granular inputs for all research phases.<br/>- Optionally embeds instructive prompts for agent execution.<br/>- Structures rough ideas into a formal, machine-readable plan for further processing. |
26
- | [`generate_pubmed_chart`](./src/mcp-server/tools/generatePubMedChart/) | Generates a chart image (PNG) from given input data. (See [Examples](./examples/)) | - Supports 'bar', 'line', and 'scatter' chart types.<br/>- Takes data values and field specifications for axes and encoding.<br/>- Constructs a Vega-Lite specification internally and renders it as a PNG using a canvas renderer. |
26
+ | [`generate_pubmed_chart`](./src/mcp-server/tools/generatePubMedChart/) | Generates a chart image (PNG) from given input data. (See [Examples](./examples/generate_pubmed_chart)) | - Supports a wide range of chart types: `bar`, `line`, `scatter`, `pie`, `doughnut`, `bubble`, `radar`, and `polarArea`.<br/>- Takes data values and field specifications for axes and series.<br/>- Uses `Chart.js` and `chartjs-node-canvas` for direct server-side rendering. |
27
27
 
28
28
  ---
29
29
 
@@ -62,7 +62,7 @@ Leverages the robust utilities provided by the `mcp-ts-template`:
62
62
  - **Input Validation/Sanitization**: Uses `zod` for schema validation and custom sanitization logic.
63
63
  - **Request Context**: Tracking and correlation of operations via unique request IDs using `AsyncLocalStorage`.
64
64
  - **Type Safety**: Strong typing enforced by TypeScript and Zod schemas.
65
- - **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management with garbage collection, CORS, and IP-based rate limiting.
65
+ - **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management with garbage collection and CORS support.
66
66
  - **Authentication**: Robust authentication layer supporting JWT and OAuth 2.1, with fine-grained scope enforcement.
67
67
  - **Deployment**: Multi-stage `Dockerfile` for creating small, secure production images with native dependency support.
68
68
 
@@ -73,7 +73,7 @@ Leverages the robust utilities provided by the `mcp-ts-template`:
73
73
  - **Full Article Metadata**: Retrieve complete publication data including abstracts, authors, affiliations, journal information, DOIs, and citation data.
74
74
  - **Citation Network Analysis**: Find related articles, citing articles, and reference lists through ELink integration.
75
75
  - **Research Planning**: Generate structured research plans with automated literature search strategies.
76
- - **Data Visualization**: Create PNG charts from publication metadata (bar charts, line graphs, scatter plots).
76
+ - **Data Visualization**: Create PNG charts from publication metadata (bar, line, scatter, pie, bubble, radar, polarArea).
77
77
  - **Multiple Output Formats**: Support for JSON, MEDLINE text, full XML, and formatted citations (RIS, BibTeX, APA, MLA).
78
78
  - **Batch Processing**: Efficient handling of multiple PMIDs with pagination support.
79
79
 
@@ -135,14 +135,14 @@ Configure the server using environment variables. These environmental variables
135
135
 
136
136
  ### MCP Client Settings
137
137
 
138
- Add to your MCP client settings (e.g., `cline_mcp_settings.json`):
138
+ Add the following to your MCP client's configuration file (e.g., `cline_mcp_settings.json`). This configuration uses `npx` to run the server, which will automatically install the package if not already present:
139
139
 
140
140
  ```json
141
141
  {
142
142
  "mcpServers": {
143
143
  "pubmed-mcp-server": {
144
- "command": "node",
145
- "args": ["/path/to/your/pubmed-mcp-server/dist/index.js"],
144
+ "command": "npx",
145
+ "args": ["@cyanheads/pubmed-mcp-server"],
146
146
  "env": {
147
147
  "NCBI_API_KEY": "your_ncbi_api_key_here"
148
148
  },
@@ -153,8 +153,6 @@ Add to your MCP client settings (e.g., `cline_mcp_settings.json`):
153
153
  }
154
154
  ```
155
155
 
156
- **Note**: You can see [mcp.json](mcp.json) for an example MCP client configuration file that includes the PubMed MCP Server.
157
-
158
156
  ## Project Structure
159
157
 
160
158
  The codebase follows a modular structure within the `src/` directory:
@@ -187,7 +185,7 @@ The PubMed MCP Server provides a comprehensive suite of tools for biomedical lit
187
185
  | `fetch_pubmed_content` | Fetches detailed article information using PMIDs or search history. | `pmids?`, `queryKey?`, `webEnv?`, `detailLevel?`, `includeMeshTerms?`, `includeGrantInfo?` |
188
186
  | `get_pubmed_article_connections` | Finds related articles, citations, and references for a given PMID. | `sourcePmid`, `relationshipType?`, `maxRelatedResults?`, `citationStyles?` |
189
187
  | `pubmed_research_agent` | Generates structured research plans with literature search strategies. | `project_title_suggestion`, `primary_research_goal`, `research_keywords`, `organism_focus?`, `p1_*`, etc. |
190
- | `generate_pubmed_chart` | Creates customizable PNG charts from structured publication data. | `chartType`, `dataValues`, `xField`, `yField`, `title?`, `colorField?`, `seriesField?`, `sizeField?` |
188
+ | `generate_pubmed_chart` | Creates customizable PNG charts from structured publication data. | `chartType`, `dataValues`, `xField`, `yField`, `title?`, `seriesField?`, `sizeField?` |
191
189
 
192
190
  _Note: All tools support comprehensive error handling and return structured JSON responses._
193
191
 
@@ -199,7 +197,7 @@ Comprehensive usage examples are available in the [`examples/`](examples/) direc
199
197
  - [Fetch Article Content](examples/fetch_pubmed_content_example.md)
200
198
  - [Article Connections](examples/get_pubmed_article_connections_1.md)
201
199
  - [Research Planning](examples/pubmed_research_agent_example.md)
202
- - [Chart Generation](examples/) - Generated chart examples (bar, line, scatter) are available in the `examples/` directory.
200
+ - [Chart Generation](./examples/generate_pubmed_chart/) - Generated chart examples (bar, line, scatter, pie, bubble, radar, polarArea) are available in the `examples/generate_pubmed_chart/` directory.
203
201
 
204
202
  ## Development
205
203
 
@@ -18,12 +18,5 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
18
  /**
19
19
  * Main application entry point. Initializes and starts the MCP server.
20
20
  * Orchestrates server startup, transport selection, and top-level error handling.
21
- *
22
- * MCP Spec Relevance:
23
- * - Manages server startup, leading to a server ready for MCP messages.
24
- * - Handles critical startup failures, ensuring appropriate process exit.
25
- *
26
- * @returns For 'stdio', resolves with `McpServer`. For 'http', resolves with `http.Server`.
27
- * Rejects on critical failure, leading to process exit.
28
21
  */
29
22
  export declare function initializeAndStartServer(): Promise<void | McpServer | ServerType>;
@@ -16,6 +16,7 @@
16
16
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
17
  import { config, environment } from "../config/index.js";
18
18
  import { ErrorHandler, logger, requestContextService } from "../utils/index.js";
19
+ import { BaseErrorCode } from "../types-global/errors.js";
19
20
  import { registerFetchPubMedContentTool } from "./tools/fetchPubMedContent/index.js";
20
21
  import { registerGeneratePubMedChartTool } from "./tools/generatePubMedChart/index.js";
21
22
  import { registerGetPubMedArticleConnectionsTool } from "./tools/getPubMedArticleConnections/index.js";
@@ -34,10 +35,8 @@ import { connectStdioTransport } from "./transports/stdioTransport.js";
34
35
  * - Capabilities Declaration: Declares supported features (logging, dynamic resources/tools).
35
36
  * - Resource/Tool Registration: Makes capabilities discoverable and invocable.
36
37
  *
37
- * Design Note: This factory is called once for 'stdio' transport and per session for 'http' transport.
38
- *
39
38
  * @returns A promise resolving with the configured `McpServer` instance.
40
- * @throws {Error} If any resource or tool registration fails.
39
+ * @throws {McpError} If any resource or tool registration fails.
41
40
  * @private
42
41
  */
43
42
  async function createMcpServerInstance() {
@@ -50,18 +49,6 @@ async function createMcpServerInstance() {
50
49
  appVersion: config.mcpServerVersion,
51
50
  environment,
52
51
  });
53
- logger.debug("Instantiating McpServer with capabilities", {
54
- ...context,
55
- serverInfo: {
56
- name: config.mcpServerName,
57
- version: config.mcpServerVersion,
58
- },
59
- capabilities: {
60
- logging: {},
61
- resources: { listChanged: true },
62
- tools: { listChanged: true },
63
- },
64
- });
65
52
  const server = new McpServer({ name: config.mcpServerName, version: config.mcpServerVersion }, {
66
53
  capabilities: {
67
54
  logging: {}, // Server can receive logging/setLevel and send notifications/message
@@ -69,7 +56,7 @@ async function createMcpServerInstance() {
69
56
  tools: { listChanged: true }, // Server supports dynamic tool lists
70
57
  },
71
58
  });
72
- try {
59
+ await ErrorHandler.tryCatch(async () => {
73
60
  logger.debug("Registering resources and tools...", context);
74
61
  // IMPORTANT: Keep tool registrations in alphabetical order. Do not remove this comment.
75
62
  await registerFetchPubMedContentTool(server);
@@ -77,17 +64,13 @@ async function createMcpServerInstance() {
77
64
  await registerGetPubMedArticleConnectionsTool(server);
78
65
  await registerPubMedResearchAgentTool(server);
79
66
  await registerSearchPubMedArticlesTool(server);
80
- // Add other tool/resource registrations here
81
67
  logger.info("Resources and tools registered successfully", context);
82
- }
83
- catch (err) {
84
- logger.error("Failed to register resources/tools", {
85
- ...context,
86
- error: err instanceof Error ? err.message : String(err),
87
- stack: err instanceof Error ? err.stack : undefined,
88
- });
89
- throw err;
90
- }
68
+ }, {
69
+ operation: "registerAllTools",
70
+ context,
71
+ errorCode: BaseErrorCode.INITIALIZATION_FAILED,
72
+ critical: true,
73
+ });
91
74
  return server;
92
75
  }
93
76
  /**
@@ -98,7 +81,7 @@ async function createMcpServerInstance() {
98
81
  * - Transport Connection: Calls dedicated functions for chosen transport.
99
82
  * - Server Instance Lifecycle: Single instance for 'stdio', per-session for 'http'.
100
83
  *
101
- * @returns Resolves with `McpServer` for 'stdio', `http.Server` for 'http', or `void` if http transport manages its own lifecycle without returning a server.
84
+ * @returns Resolves with `McpServer` for 'stdio', `http.Server` for 'http', or `void`.
102
85
  * @throws {Error} If transport type is unsupported or setup fails.
103
86
  * @private
104
87
  */
@@ -110,32 +93,18 @@ async function startTransport() {
110
93
  });
111
94
  logger.info(`Starting transport: ${transportType}`, context);
112
95
  if (transportType === "http") {
113
- logger.debug("Delegating to startHttpTransport...", context);
114
- // For HTTP, startHttpTransport now returns the http.Server instance.
115
- const httpServerInstance = await startHttpTransport(createMcpServerInstance, context);
116
- return httpServerInstance;
96
+ return startHttpTransport(createMcpServerInstance, context);
117
97
  }
118
98
  if (transportType === "stdio") {
119
- logger.debug("Creating single McpServer instance for stdio transport...", context);
120
99
  const server = await createMcpServerInstance();
121
- logger.debug("Delegating to connectStdioTransport...", context);
122
100
  await connectStdioTransport(server, context);
123
- return server; // Return the single McpServer instance for stdio.
101
+ return server;
124
102
  }
125
- // Should not be reached if config validation is effective.
126
- logger.fatal(`Unsupported transport type configured: ${transportType}`, context);
127
103
  throw new Error(`Unsupported transport type: ${transportType}. Must be 'stdio' or 'http'.`);
128
104
  }
129
105
  /**
130
106
  * Main application entry point. Initializes and starts the MCP server.
131
107
  * Orchestrates server startup, transport selection, and top-level error handling.
132
- *
133
- * MCP Spec Relevance:
134
- * - Manages server startup, leading to a server ready for MCP messages.
135
- * - Handles critical startup failures, ensuring appropriate process exit.
136
- *
137
- * @returns For 'stdio', resolves with `McpServer`. For 'http', resolves with `http.Server`.
138
- * Rejects on critical failure, leading to process exit.
139
108
  */
140
109
  export async function initializeAndStartServer() {
141
110
  const context = requestContextService.createRequestContext({
@@ -148,18 +117,13 @@ export async function initializeAndStartServer() {
148
117
  return result;
149
118
  }
150
119
  catch (err) {
151
- logger.fatal("Critical error during MCP server initialization.", {
152
- ...context,
153
- error: err instanceof Error ? err.message : String(err),
154
- stack: err instanceof Error ? err.stack : undefined,
155
- });
156
- // Ensure the error is handled by our centralized handler, which might log more details or perform cleanup.
157
120
  ErrorHandler.handleError(err, {
158
- operation: "initializeAndStartServer", // More specific operation
159
- context: context, // Pass the existing context
160
- critical: true, // This is a critical failure
121
+ operation: "initializeAndStartServer",
122
+ context: context,
123
+ critical: true,
124
+ rethrow: false, // Ensure we don't rethrow, so we can exit gracefully.
161
125
  });
162
126
  logger.info("Exiting process due to critical initialization error.", context);
163
- process.exit(1); // Exit with a non-zero code to indicate failure.
127
+ process.exit(1);
164
128
  }
165
129
  }
@@ -5,7 +5,6 @@
5
5
  * MEDLINE text, and citation data.
6
6
  * @module src/mcp-server/tools/fetchPubMedContent/logic
7
7
  */
8
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
9
8
  import { z } from "zod";
10
9
  import { RequestContext } from "../../../utils/index.js";
11
10
  export declare const FetchPubMedContentInputSchema: z.ZodEffects<z.ZodObject<{
@@ -60,4 +59,9 @@ export declare const FetchPubMedContentInputSchema: z.ZodEffects<z.ZodObject<{
60
59
  outputFormat?: "json" | "raw_text" | undefined;
61
60
  }>;
62
61
  export type FetchPubMedContentInput = z.infer<typeof FetchPubMedContentInputSchema>;
63
- export declare function fetchPubMedContentLogic(input: FetchPubMedContentInput, parentRequestContext: RequestContext): Promise<CallToolResult>;
62
+ export type FetchPubMedContentOutput = {
63
+ content: string;
64
+ articlesReturned: number;
65
+ eFetchUrl: string;
66
+ };
67
+ export declare function fetchPubMedContentLogic(input: FetchPubMedContentInput, parentRequestContext: RequestContext): Promise<FetchPubMedContentOutput>;
@@ -106,48 +106,28 @@ function parsePubMedArticleSet(xmlData, input, parentContext) {
106
106
  if (!xmlData ||
107
107
  typeof xmlData !== "object" ||
108
108
  !("PubmedArticleSet" in xmlData)) {
109
- logger.warning("Invalid or unexpected structure for xmlData in parsePubMedArticleSet.", {
109
+ throw new McpError(BaseErrorCode.PARSING_ERROR, "Invalid or unexpected structure for xmlData in parsePubMedArticleSet.", {
110
110
  ...operationContext,
111
111
  xmlDataType: typeof xmlData,
112
112
  xmlDataPreview: sanitizeInputForLogging(JSON.stringify(xmlData).substring(0, 200)),
113
113
  });
114
- return articles;
115
114
  }
116
115
  const typedXmlData = xmlData;
117
116
  const articleSet = typedXmlData.PubmedArticleSet;
118
117
  if (!articleSet || !articleSet.PubmedArticle) {
119
- logger.warning("PubmedArticleSet or PubmedArticle array not found in EFetch XML response.", {
120
- ...operationContext,
121
- xmlDataPreview: sanitizeInputForLogging(JSON.stringify(typedXmlData).substring(0, 200)),
122
- });
118
+ logger.warning("PubmedArticleSet or PubmedArticle array not found in EFetch XML response.", operationContext);
123
119
  return articles;
124
120
  }
125
121
  const pubmedArticlesXml = ensureArray(articleSet.PubmedArticle);
126
- const totalArticlesInXml = pubmedArticlesXml.length;
127
122
  for (const articleXml of pubmedArticlesXml) {
128
- if (!articleXml || typeof articleXml !== "object") {
129
- logger.warning("Skipping invalid articleXml item in pubmedArticlesXml array", {
130
- ...operationContext,
131
- articleXmlItem: sanitizeInputForLogging(articleXml),
132
- });
123
+ if (!articleXml || typeof articleXml !== "object")
133
124
  continue;
134
- }
135
125
  const medlineCitation = articleXml.MedlineCitation;
136
- if (!medlineCitation) {
137
- logger.warning("MedlineCitation not found in articleXml, skipping.", {
138
- ...operationContext,
139
- articleXmlPreview: sanitizeInputForLogging(JSON.stringify(articleXml).substring(0, 200)),
140
- });
126
+ if (!medlineCitation)
141
127
  continue;
142
- }
143
128
  const pmid = extractPmid(medlineCitation);
144
- if (!pmid) {
145
- logger.warning("Could not extract PMID from MedlineCitation, skipping.", {
146
- ...operationContext,
147
- medlineCitationPreview: sanitizeInputForLogging(JSON.stringify(medlineCitation).substring(0, 200)),
148
- });
129
+ if (!pmid)
149
130
  continue;
150
- }
151
131
  const articleNode = medlineCitation.Article;
152
132
  const parsedArticle = {
153
133
  pmid: pmid,
@@ -186,11 +166,6 @@ function parsePubMedArticleSet(xmlData, input, parentContext) {
186
166
  }
187
167
  articles.push(parsedArticle);
188
168
  }
189
- logger.debug(`Successfully parsed ${articles.length} of ${totalArticlesInXml} articles from XML.`, {
190
- ...operationContext,
191
- parsedCount: articles.length,
192
- totalInXml: totalArticlesInXml,
193
- });
194
169
  return articles;
195
170
  }
196
171
  export async function fetchPubMedContentLogic(input, parentRequestContext) {
@@ -199,26 +174,9 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
199
174
  operation: "fetchPubMedContentLogic",
200
175
  input: sanitizeInputForLogging(input),
201
176
  });
202
- // Manual validation safeguard
203
177
  const validationResult = FetchPubMedContentInputSchema.safeParse(input);
204
178
  if (!validationResult.success) {
205
- const errorMessage = validationResult.error.errors[0]?.message || "Invalid input";
206
- logger.warning(`Input validation failed pre-check: ${errorMessage}`, toolLogicContext);
207
- return {
208
- content: [
209
- {
210
- type: "text",
211
- text: JSON.stringify({
212
- error: {
213
- code: BaseErrorCode.VALIDATION_ERROR,
214
- message: errorMessage,
215
- details: validationResult.error.flatten(),
216
- },
217
- }),
218
- },
219
- ],
220
- isError: true,
221
- };
179
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, validationResult.error.errors[0]?.message || "Invalid input", { ...toolLogicContext, details: validationResult.error.flatten() });
222
180
  }
223
181
  const ncbiService = getNcbiService();
224
182
  logger.info("Executing fetch_pubmed_content tool", toolLogicContext);
@@ -228,12 +186,10 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
228
186
  usingHistory = true;
229
187
  eFetchParams.query_key = input.queryKey;
230
188
  eFetchParams.WebEnv = input.webEnv;
231
- if (input.retstart !== undefined) {
189
+ if (input.retstart !== undefined)
232
190
  eFetchParams.retstart = String(input.retstart);
233
- }
234
- if (input.retmax !== undefined) {
191
+ if (input.retmax !== undefined)
235
192
  eFetchParams.retmax = String(input.retmax);
236
- }
237
193
  }
238
194
  else if (input.pmids && input.pmids.length > 0) {
239
195
  eFetchParams.id = input.pmids.join(",");
@@ -256,163 +212,99 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
256
212
  eFetchParams.retmode = serviceRetmode;
257
213
  if (rettype)
258
214
  eFetchParams.rettype = rettype;
259
- let eFetchUrl = "";
260
- try {
261
- const eFetchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
262
- const eFetchQueryString = new URLSearchParams(eFetchParams).toString();
263
- eFetchUrl = `${eFetchBase}?${eFetchQueryString}`;
264
- const shouldReturnRawXml = input.detailLevel === "full_xml" && input.outputFormat === "raw_text";
265
- const eFetchResponseData = await ncbiService.eFetch(eFetchParams, toolLogicContext, { retmode: serviceRetmode, rettype, returnRawXml: shouldReturnRawXml });
266
- let finalOutputText;
267
- let structuredResponseData;
268
- let articlesCount = 0;
269
- if (input.detailLevel === "medline_text") {
270
- const medlineText = String(eFetchResponseData);
271
- const foundPmidsInMedline = new Set();
272
- const pmidRegex = /^PMID- (\d+)/gm;
273
- let match;
274
- while ((match = pmidRegex.exec(medlineText)) !== null) {
275
- foundPmidsInMedline.add(match[1]);
276
- }
277
- articlesCount = foundPmidsInMedline.size;
278
- let notFoundPmids = [];
279
- if (input.pmids && input.pmids.length > 0) {
280
- notFoundPmids = input.pmids.filter((pmid) => !foundPmidsInMedline.has(pmid));
281
- }
282
- structuredResponseData = {
283
- requestedPmids: input.pmids || "N/A (used history query)",
284
- articles: [
285
- {
286
- pmids: input.pmids || "N/A (used history query)",
287
- medlineText: medlineText,
288
- },
289
- ],
290
- notFoundPmids: usingHistory
291
- ? "N/A (used history query)"
292
- : notFoundPmids,
293
- eFetchDetails: {
294
- urls: [eFetchUrl],
295
- requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
296
- ? "POST"
297
- : "GET",
298
- },
299
- };
300
- finalOutputText =
301
- input.outputFormat === "raw_text"
302
- ? medlineText
303
- : JSON.stringify(structuredResponseData);
215
+ const eFetchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
216
+ const eFetchQueryString = new URLSearchParams(eFetchParams).toString();
217
+ const eFetchUrl = `${eFetchBase}?${eFetchQueryString}`;
218
+ const shouldReturnRawXml = input.detailLevel === "full_xml" && input.outputFormat === "raw_text";
219
+ const eFetchResponseData = await ncbiService.eFetch(eFetchParams, toolLogicContext, { retmode: serviceRetmode, rettype, returnRawXml: shouldReturnRawXml });
220
+ let finalOutputText;
221
+ let articlesCount = 0;
222
+ if (input.detailLevel === "medline_text") {
223
+ const medlineText = String(eFetchResponseData);
224
+ const foundPmidsInMedline = new Set();
225
+ const pmidRegex = /^PMID- (\d+)/gm;
226
+ let match;
227
+ while ((match = pmidRegex.exec(medlineText)) !== null) {
228
+ foundPmidsInMedline.add(match[1]);
304
229
  }
305
- else if (input.detailLevel === "full_xml") {
306
- if (input.outputFormat === "raw_text") {
307
- finalOutputText = String(eFetchResponseData);
308
- articlesCount = (finalOutputText.match(/<PubmedArticle>/g) || []).length;
309
- }
310
- else {
311
- const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
312
- articlesCount = articlesXml.length;
313
- const articlesPayload = [];
314
- const foundPmidsInXml = new Set();
315
- for (const articleXml of articlesXml) {
316
- const pmid = extractPmid(articleXml.MedlineCitation) || "unknown_pmid";
317
- if (pmid !== "unknown_pmid")
318
- foundPmidsInXml.add(pmid);
319
- articlesPayload.push({ pmid, fullXmlContent: articleXml });
320
- }
321
- const notFoundPmids = input.pmids && input.pmids.length > 0
322
- ? input.pmids.filter((pmid) => !foundPmidsInXml.has(pmid))
323
- : "N/A (used history query)";
324
- structuredResponseData = {
325
- requestedPmids: input.pmids || "N/A (used history query)",
326
- articles: articlesPayload,
327
- notFoundPmids,
328
- eFetchDetails: {
329
- urls: [eFetchUrl],
330
- requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
331
- ? "POST"
332
- : "GET",
333
- },
334
- };
335
- finalOutputText = JSON.stringify(structuredResponseData);
336
- }
230
+ articlesCount = foundPmidsInMedline.size;
231
+ if (input.outputFormat === "raw_text") {
232
+ finalOutputText = medlineText;
337
233
  }
338
234
  else {
339
- // abstract_plus or citation_data
340
- const parsedArticles = parsePubMedArticleSet(eFetchResponseData, input, toolLogicContext);
341
- articlesCount = parsedArticles.length;
342
- const foundPmids = new Set(parsedArticles.map((p) => p.pmid));
343
- const notFoundPmids = input.pmids && input.pmids.length > 0
344
- ? input.pmids.filter((pmid) => !foundPmids.has(pmid))
345
- : "N/A (used history query)";
346
- structuredResponseData = {
347
- requestedPmids: input.pmids || "N/A (used history query)",
348
- articles: parsedArticles,
235
+ const notFoundPmids = input.pmids?.filter((pmid) => !foundPmidsInMedline.has(pmid)) || [];
236
+ finalOutputText = JSON.stringify({
237
+ requestedPmids: input.pmids || "N/A (history query)",
238
+ articles: [{ medlineText }],
349
239
  notFoundPmids,
350
- eFetchDetails: {
351
- urls: [eFetchUrl],
352
- requestMethod: input.pmids && input.pmids.length > 200 && !usingHistory
353
- ? "POST"
354
- : "GET",
355
- },
356
- };
357
- if (input.detailLevel === "citation_data") {
358
- structuredResponseData.articles = structuredResponseData.articles.map((article) => ({
359
- pmid: article.pmid,
360
- title: article.title,
361
- authors: article.authors?.map((a) => ({
362
- lastName: a.lastName,
363
- initials: a.initials,
364
- })),
365
- journalInfo: {
366
- title: article.journalInfo?.title,
367
- isoAbbreviation: article.journalInfo?.isoAbbreviation,
368
- volume: article.journalInfo?.volume,
369
- issue: article.journalInfo?.issue,
370
- pages: article.journalInfo?.pages,
371
- year: article.journalInfo?.publicationDate?.year,
372
- },
373
- doi: article.doi,
374
- ...(input.includeMeshTerms && { meshTerms: article.meshTerms }),
375
- }));
376
- }
377
- finalOutputText = JSON.stringify(structuredResponseData);
240
+ eFetchDetails: { urls: [eFetchUrl] },
241
+ });
378
242
  }
379
- logger.notice("Successfully executed fetch_pubmed_content tool.", {
380
- ...toolLogicContext,
381
- detailLevel: input.detailLevel,
382
- outputFormat: input.outputFormat,
383
- articlesReturned: articlesCount,
384
- usingHistory,
385
- });
386
- return {
387
- content: [{ type: "text", text: finalOutputText }],
388
- isError: false,
389
- };
390
243
  }
391
- catch (error) {
392
- logger.error("Error in fetch_pubmed_content logic", error, toolLogicContext);
393
- const mcpError = error instanceof McpError
394
- ? error
395
- : new McpError(BaseErrorCode.INTERNAL_ERROR, "Failed to fetch PubMed content.", {
396
- originalErrorName: error.name,
397
- originalErrorMessage: error.message,
398
- requestId: toolLogicContext.requestId,
244
+ else if (input.detailLevel === "full_xml") {
245
+ if (input.outputFormat === "raw_text") {
246
+ finalOutputText = String(eFetchResponseData);
247
+ articlesCount = (finalOutputText.match(/<PubmedArticle>/g) || []).length;
248
+ }
249
+ else {
250
+ const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
251
+ articlesCount = articlesXml.length;
252
+ const foundPmidsInXml = new Set();
253
+ const articlesPayload = articlesXml.map((articleXml) => {
254
+ const pmid = extractPmid(articleXml.MedlineCitation) || "unknown_pmid";
255
+ if (pmid !== "unknown_pmid")
256
+ foundPmidsInXml.add(pmid);
257
+ return { pmid, fullXmlContent: articleXml };
258
+ });
259
+ const notFoundPmids = input.pmids?.filter((pmid) => !foundPmidsInXml.has(pmid)) || [];
260
+ finalOutputText = JSON.stringify({
261
+ requestedPmids: input.pmids || "N/A (history query)",
262
+ articles: articlesPayload,
263
+ notFoundPmids,
264
+ eFetchDetails: { urls: [eFetchUrl] },
399
265
  });
400
- return {
401
- content: [
402
- {
403
- type: "text",
404
- text: JSON.stringify({
405
- error: {
406
- code: mcpError.code,
407
- message: mcpError.message,
408
- details: mcpError.details,
409
- },
410
- requestedPmids: input.pmids || "N/A (used history query)",
411
- eFetchUrl,
412
- }),
266
+ }
267
+ }
268
+ else {
269
+ const parsedArticles = parsePubMedArticleSet(eFetchResponseData, input, toolLogicContext);
270
+ articlesCount = parsedArticles.length;
271
+ const foundPmids = new Set(parsedArticles.map((p) => p.pmid));
272
+ const notFoundPmids = input.pmids?.filter((pmid) => !foundPmids.has(pmid)) || [];
273
+ let articlesToReturn = parsedArticles;
274
+ if (input.detailLevel === "citation_data") {
275
+ articlesToReturn = parsedArticles.map((article) => ({
276
+ pmid: article.pmid,
277
+ title: article.title,
278
+ authors: article.authors?.map((a) => ({
279
+ lastName: a.lastName,
280
+ initials: a.initials,
281
+ })),
282
+ journalInfo: {
283
+ title: article.journalInfo?.title,
284
+ isoAbbreviation: article.journalInfo?.isoAbbreviation,
285
+ volume: article.journalInfo?.volume,
286
+ issue: article.journalInfo?.issue,
287
+ pages: article.journalInfo?.pages,
288
+ year: article.journalInfo?.publicationDate?.year,
413
289
  },
414
- ],
415
- isError: true,
416
- };
290
+ doi: article.doi,
291
+ ...(input.includeMeshTerms && { meshTerms: article.meshTerms }),
292
+ }));
293
+ }
294
+ finalOutputText = JSON.stringify({
295
+ requestedPmids: input.pmids || "N/A (history query)",
296
+ articles: articlesToReturn,
297
+ notFoundPmids,
298
+ eFetchDetails: { urls: [eFetchUrl] },
299
+ });
417
300
  }
301
+ logger.notice("Successfully executed fetch_pubmed_content tool.", {
302
+ ...toolLogicContext,
303
+ articlesReturned: articlesCount,
304
+ });
305
+ return {
306
+ content: finalOutputText,
307
+ articlesReturned: articlesCount,
308
+ eFetchUrl,
309
+ };
418
310
  }