@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.
- package/README.md +10 -12
- package/dist/mcp-server/server.d.ts +0 -7
- package/dist/mcp-server/server.js +17 -53
- package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +6 -2
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +94 -202
- package/dist/mcp-server/tools/fetchPubMedContent/registration.js +43 -7
- package/dist/mcp-server/tools/generatePubMedChart/logic.d.ts +9 -28
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +133 -192
- package/dist/mcp-server/tools/generatePubMedChart/registration.js +57 -13
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +1 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +1 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +27 -4
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +50 -45
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +1 -1
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +0 -24
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +51 -45
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +2 -4
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -41
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +49 -8
- package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +12 -10
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +68 -126
- package/dist/mcp-server/tools/searchPubMedArticles/registration.js +46 -6
- package/dist/mcp-server/transports/{authentication → auth/core}/authContext.d.ts +2 -2
- package/dist/mcp-server/transports/{authentication → auth/core}/authContext.js +1 -1
- package/dist/mcp-server/transports/{authentication/types.d.ts → auth/core/authTypes.d.ts} +1 -1
- package/dist/mcp-server/transports/{authentication/types.js → auth/core/authTypes.js} +1 -1
- package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.d.ts +1 -1
- package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.js +3 -3
- package/dist/mcp-server/transports/auth/index.d.ts +10 -0
- package/dist/mcp-server/transports/auth/index.js +9 -0
- package/dist/mcp-server/transports/{authentication/authMiddleware.d.ts → auth/strategies/jwt/jwtMiddleware.d.ts} +4 -12
- package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +36 -43
- package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts +2 -6
- package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js +33 -18
- package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
- package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +11 -14
- package/dist/mcp-server/transports/httpTransport.js +91 -389
- package/package.json +13 -21
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
- package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
- package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
- package/dist/mcp-server/resources/echoResource/index.js +0 -13
- package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
- package/dist/mcp-server/resources/echoResource/registration.js +0 -168
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
- 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
|
[](https://www.typescriptlang.org/)
|
|
4
|
-
[](https://modelcontextprotocol.io/)
|
|
5
|
+
[](./CHANGELOG.md)
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://github.com/cyanheads/pubmed-mcp-server/issues)
|
|
8
8
|
[](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/))
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
145
|
-
"args": ["/
|
|
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?`, `
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
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;
|
|
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",
|
|
159
|
-
context: context,
|
|
160
|
-
critical: true,
|
|
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);
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
}
|