@cyanheads/pubmed-mcp-server 1.1.2 → 1.1.4
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 +19 -16
- package/dist/config/index.js +39 -1
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +59 -160
- package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +1 -1
- package/dist/mcp-server/tools/fetchPubMedContent/registration.js +11 -15
- package/dist/mcp-server/tools/generatePubMedChart/logic.js +20 -22
- package/dist/mcp-server/tools/generatePubMedChart/registration.d.ts +1 -1
- package/dist/mcp-server/tools/generatePubMedChart/registration.js +10 -19
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +3 -3
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +9 -6
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +1 -2
- package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +16 -26
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +1 -2
- package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -6
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +1 -1
- package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +9 -14
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +9 -4
- package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +1 -1
- package/dist/mcp-server/tools/searchPubMedArticles/registration.js +13 -20
- package/dist/mcp-server/transports/httpTransport.js +9 -0
- package/dist/utils/security/rateLimiter.d.ts +4 -0
- package/dist/utils/security/rateLimiter.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
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)
|
|
@@ -17,13 +17,13 @@ Built on the [`cyanheads/mcp-ts-template`](https://github.com/cyanheads/mcp-ts-t
|
|
|
17
17
|
|
|
18
18
|
This server equips your AI with specialized tools to interact with PubMed:
|
|
19
19
|
|
|
20
|
-
| Tool Name | Description
|
|
21
|
-
| :-------------------------------------------------------------------------------------- |
|
|
22
|
-
| [`search_pubmed_articles`](./src/mcp-server/tools/searchPubMedArticles/) | Searches PubMed for articles based on your query. (See [Example](./examples/search_pubmed_articles_example.md))
|
|
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))
|
|
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))
|
|
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))
|
|
26
|
-
| [`generate_pubmed_chart`](./src/mcp-server/tools/generatePubMedChart/) | Generates a chart image (PNG) from given input data. (See [
|
|
20
|
+
| Tool Name | Description | Key Features |
|
|
21
|
+
| :-------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
|
+
| [`search_pubmed_articles`](./src/mcp-server/tools/searchPubMedArticles/) | Searches PubMed for articles based on your query. (See [Example](./examples/search_pubmed_articles_example.md)) | - Filter by max results, sort order, date range, publication types.<br/>- Uses NCBI ESearch for PMIDs.<br/>- Optionally fetches brief summaries (title, authors, source, dates) via ESummary. |
|
|
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
|
+
| [`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
|
+
| [`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. |
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
@@ -56,14 +56,15 @@ Built on the robust `mcp-ts-template`, this server provides a standardized, secu
|
|
|
56
56
|
|
|
57
57
|
Leverages the robust utilities provided by the `mcp-ts-template`:
|
|
58
58
|
|
|
59
|
-
- **Logging**: Structured, configurable logging (file rotation,
|
|
59
|
+
- **Logging**: Structured, configurable logging (file rotation, stdout JSON, MCP notifications) with sensitive data redaction.
|
|
60
60
|
- **Error Handling**: Centralized error processing, standardized error types (`McpError`), and automatic logging.
|
|
61
61
|
- **Configuration**: Environment variable loading (`dotenv`) with comprehensive validation.
|
|
62
62
|
- **Input Validation/Sanitization**: Uses `zod` for schema validation and custom sanitization logic.
|
|
63
|
-
- **Request Context**: Tracking and correlation of operations via unique request IDs
|
|
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
|
|
66
|
-
- **
|
|
65
|
+
- **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management with garbage collection, CORS, and IP-based rate limiting.
|
|
66
|
+
- **Authentication**: Robust authentication layer supporting JWT and OAuth 2.1, with fine-grained scope enforcement.
|
|
67
|
+
- **Deployment**: Multi-stage `Dockerfile` for creating small, secure production images with native dependency support.
|
|
67
68
|
|
|
68
69
|
### PubMed Integration
|
|
69
70
|
|
|
@@ -82,6 +83,7 @@ Leverages the robust utilities provided by the `mcp-ts-template`:
|
|
|
82
83
|
|
|
83
84
|
- [Node.js (>=18.0.0)](https://nodejs.org/)
|
|
84
85
|
- [npm](https://www.npmjs.com/) (comes with Node.js)
|
|
86
|
+
- [Docker](https://www.docker.com/) (optional, for containerized deployment)
|
|
85
87
|
- **NCBI API Key** (recommended for higher rate limits) - [Get one here](https://ncbiinsights.ncbi.nlm.nih.gov/2017/11/02/new-api-keys-for-the-e-utilities/)
|
|
86
88
|
|
|
87
89
|
### Install via npm (recommended)
|
|
@@ -124,10 +126,12 @@ Configure the server using environment variables. These environmental variables
|
|
|
124
126
|
| `MCP_HTTP_HOST` | Host address for the HTTP server (if `MCP_TRANSPORT_TYPE=http`). | `127.0.0.1` |
|
|
125
127
|
| `MCP_ALLOWED_ORIGINS` | Comma-separated list of allowed origins for CORS (if `MCP_TRANSPORT_TYPE=http`). | (none) |
|
|
126
128
|
| `MCP_LOG_LEVEL` | Logging level (`debug`, `info`, `notice`, `warning`, `error`, `crit`, `alert`, `emerg`). | `debug` |
|
|
127
|
-
| `
|
|
129
|
+
| `LOG_OUTPUT_MODE` | Logging output mode: `file` or `stdout`. | `file` |
|
|
130
|
+
| `MCP_AUTH_MODE` | Authentication mode for HTTP: `jwt` or `oauth`. | `jwt` |
|
|
131
|
+
| `MCP_AUTH_SECRET_KEY` | **Required for `jwt` auth.** Minimum 32-character secret key for JWT authentication. | (none) |
|
|
128
132
|
| `NCBI_API_KEY` | **Recommended.** Your NCBI API Key for higher rate limits and reliable access. | (none) |
|
|
129
133
|
| `NCBI_TOOL_IDENTIFIER` | Tool identifier for NCBI E-utility requests. | `@cyanheads/pubmed-mcp-server` |
|
|
130
|
-
| `LOGS_DIR` | Directory for log file storage.
|
|
134
|
+
| `LOGS_DIR` | Directory for log file storage (if `LOG_OUTPUT_MODE=file`). | `logs/` |
|
|
131
135
|
|
|
132
136
|
### MCP Client Settings
|
|
133
137
|
|
|
@@ -166,8 +170,7 @@ src/
|
|
|
166
170
|
│ ├── resources/ # MCP Resource implementations
|
|
167
171
|
│ └── tools/ # MCP Tool implementations (subdirs per tool)
|
|
168
172
|
├── services/ # External service integrations
|
|
169
|
-
│
|
|
170
|
-
│ └── llm-providers/ # LLM provider integrations (optional)
|
|
173
|
+
│ └── NCBI/ # NCBI E-utilities API client and parsing
|
|
171
174
|
├── types-global/ # Shared TypeScript type definitions
|
|
172
175
|
└── utils/ # Common utility functions (logger, error handler, etc.)
|
|
173
176
|
```
|
package/dist/config/index.js
CHANGED
|
@@ -71,7 +71,8 @@ catch (error) {
|
|
|
71
71
|
* Provides type safety, validation, defaults, and clear error messages.
|
|
72
72
|
* @private
|
|
73
73
|
*/
|
|
74
|
-
const EnvSchema = z
|
|
74
|
+
const EnvSchema = z
|
|
75
|
+
.object({
|
|
75
76
|
/** Optional. The desired name for the MCP server. Defaults to `package.json` name. */
|
|
76
77
|
MCP_SERVER_NAME: z.string().optional(),
|
|
77
78
|
/** Optional. The version of the MCP server. Defaults to `package.json` version. */
|
|
@@ -146,6 +147,43 @@ const EnvSchema = z.object({
|
|
|
146
147
|
NCBI_REQUEST_DELAY_MS: z.coerce.number().int().positive().optional(), // Default will be set conditionally
|
|
147
148
|
/** Maximum number of retries for failed NCBI requests. Default: 3. */
|
|
148
149
|
NCBI_MAX_RETRIES: z.coerce.number().int().nonnegative().default(3),
|
|
150
|
+
})
|
|
151
|
+
.superRefine((data, ctx) => {
|
|
152
|
+
// Rule 1: MCP_AUTH_SECRET_KEY is required for http transport in production with jwt auth
|
|
153
|
+
if (data.NODE_ENV === "production" &&
|
|
154
|
+
data.MCP_TRANSPORT_TYPE === "http" &&
|
|
155
|
+
data.MCP_AUTH_MODE === "jwt" &&
|
|
156
|
+
!data.MCP_AUTH_SECRET_KEY) {
|
|
157
|
+
ctx.addIssue({
|
|
158
|
+
code: z.ZodIssueCode.custom,
|
|
159
|
+
path: ["MCP_AUTH_SECRET_KEY"],
|
|
160
|
+
message: "MCP_AUTH_SECRET_KEY is required for 'jwt' auth with 'http' transport in a 'production' environment.",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Rule 2: Core OAuth variables are required when MCP_AUTH_MODE is 'oauth'
|
|
164
|
+
if (data.MCP_AUTH_MODE === "oauth") {
|
|
165
|
+
if (!data.OAUTH_ISSUER_URL) {
|
|
166
|
+
ctx.addIssue({
|
|
167
|
+
code: z.ZodIssueCode.custom,
|
|
168
|
+
path: ["OAUTH_ISSUER_URL"],
|
|
169
|
+
message: "OAUTH_ISSUER_URL is required when MCP_AUTH_MODE is 'oauth'.",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if (!data.OAUTH_AUDIENCE) {
|
|
173
|
+
ctx.addIssue({
|
|
174
|
+
code: z.ZodIssueCode.custom,
|
|
175
|
+
path: ["OAUTH_AUDIENCE"],
|
|
176
|
+
message: "OAUTH_AUDIENCE is required when MCP_AUTH_MODE is 'oauth'.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (!data.OAUTH_JWKS_URI) {
|
|
180
|
+
ctx.addIssue({
|
|
181
|
+
code: z.ZodIssueCode.custom,
|
|
182
|
+
path: ["OAUTH_JWKS_URI"],
|
|
183
|
+
message: "OAUTH_JWKS_URI is required when MCP_AUTH_MODE is 'oauth'.",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
149
187
|
});
|
|
150
188
|
const parsedEnv = EnvSchema.safeParse(process.env);
|
|
151
189
|
if (!parsedEnv.success) {
|
|
@@ -97,14 +97,12 @@ export const FetchPubMedContentInputSchema = z
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
|
-
function parsePubMedArticleSet(xmlData,
|
|
101
|
-
input, parentContext) {
|
|
100
|
+
function parsePubMedArticleSet(xmlData, input, parentContext) {
|
|
102
101
|
const articles = [];
|
|
103
102
|
const operationContext = requestContextService.createRequestContext({
|
|
104
103
|
parentRequestId: parentContext.requestId,
|
|
105
104
|
operation: "parsePubMedArticleSet",
|
|
106
105
|
});
|
|
107
|
-
// Type guard for xmlData
|
|
108
106
|
if (!xmlData ||
|
|
109
107
|
typeof xmlData !== "object" ||
|
|
110
108
|
!("PubmedArticleSet" in xmlData)) {
|
|
@@ -115,30 +113,17 @@ input, parentContext) {
|
|
|
115
113
|
});
|
|
116
114
|
return articles;
|
|
117
115
|
}
|
|
118
|
-
const typedXmlData = xmlData;
|
|
116
|
+
const typedXmlData = xmlData;
|
|
119
117
|
const articleSet = typedXmlData.PubmedArticleSet;
|
|
120
118
|
if (!articleSet || !articleSet.PubmedArticle) {
|
|
121
|
-
logger.warning("PubmedArticleSet or PubmedArticle array not found in EFetch XML response.",
|
|
119
|
+
logger.warning("PubmedArticleSet or PubmedArticle array not found in EFetch XML response.", {
|
|
122
120
|
...operationContext,
|
|
123
121
|
xmlDataPreview: sanitizeInputForLogging(JSON.stringify(typedXmlData).substring(0, 200)),
|
|
124
|
-
})
|
|
122
|
+
});
|
|
125
123
|
return articles;
|
|
126
124
|
}
|
|
127
125
|
const pubmedArticlesXml = ensureArray(articleSet.PubmedArticle);
|
|
128
|
-
|
|
129
|
-
...operationContext,
|
|
130
|
-
pubmedArticlesXmlPreview: sanitizeInputForLogging(JSON.stringify(pubmedArticlesXml).substring(0, 500)),
|
|
131
|
-
isPubmedArticlesXmlArray: Array.isArray(pubmedArticlesXml),
|
|
132
|
-
pubmedArticlesXmlLength: Array.isArray(pubmedArticlesXml)
|
|
133
|
-
? pubmedArticlesXml.length
|
|
134
|
-
: undefined,
|
|
135
|
-
});
|
|
136
|
-
if (Array.isArray(pubmedArticlesXml) && pubmedArticlesXml.length > 0) {
|
|
137
|
-
logger.debug("First item of pubmedArticlesXml:", {
|
|
138
|
-
...operationContext,
|
|
139
|
-
firstItemPreview: sanitizeInputForLogging(JSON.stringify(pubmedArticlesXml[0]).substring(0, 500)),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
126
|
+
const totalArticlesInXml = pubmedArticlesXml.length;
|
|
142
127
|
for (const articleXml of pubmedArticlesXml) {
|
|
143
128
|
if (!articleXml || typeof articleXml !== "object") {
|
|
144
129
|
logger.warning("Skipping invalid articleXml item in pubmedArticlesXml array", {
|
|
@@ -156,13 +141,13 @@ input, parentContext) {
|
|
|
156
141
|
continue;
|
|
157
142
|
}
|
|
158
143
|
const pmid = extractPmid(medlineCitation);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (!pmid)
|
|
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
|
+
});
|
|
165
149
|
continue;
|
|
150
|
+
}
|
|
166
151
|
const articleNode = medlineCitation.Article;
|
|
167
152
|
const parsedArticle = {
|
|
168
153
|
pmid: pmid,
|
|
@@ -201,82 +186,24 @@ input, parentContext) {
|
|
|
201
186
|
}
|
|
202
187
|
articles.push(parsedArticle);
|
|
203
188
|
}
|
|
189
|
+
logger.debug(`Successfully parsed ${articles.length} of ${totalArticlesInXml} articles from XML.`, {
|
|
190
|
+
...operationContext,
|
|
191
|
+
parsedCount: articles.length,
|
|
192
|
+
totalInXml: totalArticlesInXml,
|
|
193
|
+
});
|
|
204
194
|
return articles;
|
|
205
195
|
}
|
|
206
196
|
export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
message: "webEnv is required if queryKey is provided.",
|
|
218
|
-
},
|
|
219
|
-
}),
|
|
220
|
-
},
|
|
221
|
-
],
|
|
222
|
-
isError: true,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
if (!input.queryKey && input.webEnv) {
|
|
226
|
-
return {
|
|
227
|
-
content: [
|
|
228
|
-
{
|
|
229
|
-
type: "text",
|
|
230
|
-
text: JSON.stringify({
|
|
231
|
-
error: {
|
|
232
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
233
|
-
message: "queryKey is required if webEnv is provided.",
|
|
234
|
-
},
|
|
235
|
-
}),
|
|
236
|
-
},
|
|
237
|
-
],
|
|
238
|
-
isError: true,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
if (input.pmids &&
|
|
242
|
-
input.pmids.length > 0 &&
|
|
243
|
-
(input.queryKey || input.webEnv)) {
|
|
244
|
-
return {
|
|
245
|
-
content: [
|
|
246
|
-
{
|
|
247
|
-
type: "text",
|
|
248
|
-
text: JSON.stringify({
|
|
249
|
-
error: {
|
|
250
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
251
|
-
message: "Cannot use pmids and queryKey/webEnv simultaneously. Please choose one method.",
|
|
252
|
-
},
|
|
253
|
-
}),
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
isError: true,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
if ((input.retstart !== undefined || input.retmax !== undefined) &&
|
|
260
|
-
!(input.queryKey && input.webEnv)) {
|
|
261
|
-
return {
|
|
262
|
-
content: [
|
|
263
|
-
{
|
|
264
|
-
type: "text",
|
|
265
|
-
text: JSON.stringify({
|
|
266
|
-
error: {
|
|
267
|
-
code: BaseErrorCode.VALIDATION_ERROR,
|
|
268
|
-
message: "retstart/retmax can only be used with queryKey and webEnv.",
|
|
269
|
-
},
|
|
270
|
-
}),
|
|
271
|
-
},
|
|
272
|
-
],
|
|
273
|
-
isError: true,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
// SuperRefine also checks: if ((!data.pmids || data.pmids.length === 0) && !(data.queryKey && data.webEnv))
|
|
277
|
-
// This should ideally be caught before handler, but as a safeguard:
|
|
278
|
-
if ((!input.pmids || input.pmids.length === 0) &&
|
|
279
|
-
!(input.queryKey && input.webEnv)) {
|
|
197
|
+
const toolLogicContext = requestContextService.createRequestContext({
|
|
198
|
+
parentRequestId: parentRequestContext.requestId,
|
|
199
|
+
operation: "fetchPubMedContentLogic",
|
|
200
|
+
input: sanitizeInputForLogging(input),
|
|
201
|
+
});
|
|
202
|
+
// Manual validation safeguard
|
|
203
|
+
const validationResult = FetchPubMedContentInputSchema.safeParse(input);
|
|
204
|
+
if (!validationResult.success) {
|
|
205
|
+
const errorMessage = validationResult.error.errors[0]?.message || "Invalid input";
|
|
206
|
+
logger.warning(`Input validation failed pre-check: ${errorMessage}`, toolLogicContext);
|
|
280
207
|
return {
|
|
281
208
|
content: [
|
|
282
209
|
{
|
|
@@ -284,7 +211,8 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
284
211
|
text: JSON.stringify({
|
|
285
212
|
error: {
|
|
286
213
|
code: BaseErrorCode.VALIDATION_ERROR,
|
|
287
|
-
message:
|
|
214
|
+
message: errorMessage,
|
|
215
|
+
details: validationResult.error.flatten(),
|
|
288
216
|
},
|
|
289
217
|
}),
|
|
290
218
|
},
|
|
@@ -293,18 +221,13 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
293
221
|
};
|
|
294
222
|
}
|
|
295
223
|
const ncbiService = getNcbiService();
|
|
296
|
-
const toolLogicContext = requestContextService.createRequestContext({
|
|
297
|
-
parentRequestId: parentRequestContext.requestId,
|
|
298
|
-
operation: "fetchPubMedContentLogic",
|
|
299
|
-
input: sanitizeInputForLogging(input),
|
|
300
|
-
});
|
|
301
224
|
logger.info("Executing fetch_pubmed_content tool", toolLogicContext);
|
|
302
225
|
const eFetchParams = { db: "pubmed" };
|
|
303
226
|
let usingHistory = false;
|
|
304
227
|
if (input.queryKey && input.webEnv) {
|
|
305
228
|
usingHistory = true;
|
|
306
229
|
eFetchParams.query_key = input.queryKey;
|
|
307
|
-
eFetchParams.WebEnv = input.webEnv;
|
|
230
|
+
eFetchParams.WebEnv = input.webEnv;
|
|
308
231
|
if (input.retstart !== undefined) {
|
|
309
232
|
eFetchParams.retstart = String(input.retstart);
|
|
310
233
|
}
|
|
@@ -315,10 +238,8 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
315
238
|
else if (input.pmids && input.pmids.length > 0) {
|
|
316
239
|
eFetchParams.id = input.pmids.join(",");
|
|
317
240
|
}
|
|
318
|
-
|
|
319
|
-
let serviceRetmode = "xml"; // Renamed to avoid conflict with local retmode variable if any
|
|
241
|
+
let serviceRetmode = "xml";
|
|
320
242
|
let rettype;
|
|
321
|
-
// responseContentType is determined by input.outputFormat at the end
|
|
322
243
|
switch (input.detailLevel) {
|
|
323
244
|
case "full_xml":
|
|
324
245
|
serviceRetmode = "xml";
|
|
@@ -329,7 +250,7 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
329
250
|
break;
|
|
330
251
|
case "abstract_plus":
|
|
331
252
|
case "citation_data":
|
|
332
|
-
serviceRetmode = "xml";
|
|
253
|
+
serviceRetmode = "xml";
|
|
333
254
|
break;
|
|
334
255
|
}
|
|
335
256
|
eFetchParams.retmode = serviceRetmode;
|
|
@@ -340,11 +261,11 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
340
261
|
const eFetchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi";
|
|
341
262
|
const eFetchQueryString = new URLSearchParams(eFetchParams).toString();
|
|
342
263
|
eFetchUrl = `${eFetchBase}?${eFetchQueryString}`;
|
|
343
|
-
// Determine if raw XML should be fetched
|
|
344
264
|
const shouldReturnRawXml = input.detailLevel === "full_xml" && input.outputFormat === "raw_text";
|
|
345
265
|
const eFetchResponseData = await ncbiService.eFetch(eFetchParams, toolLogicContext, { retmode: serviceRetmode, rettype, returnRawXml: shouldReturnRawXml });
|
|
346
266
|
let finalOutputText;
|
|
347
|
-
let structuredResponseData;
|
|
267
|
+
let structuredResponseData;
|
|
268
|
+
let articlesCount = 0;
|
|
348
269
|
if (input.detailLevel === "medline_text") {
|
|
349
270
|
const medlineText = String(eFetchResponseData);
|
|
350
271
|
const foundPmidsInMedline = new Set();
|
|
@@ -353,6 +274,7 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
353
274
|
while ((match = pmidRegex.exec(medlineText)) !== null) {
|
|
354
275
|
foundPmidsInMedline.add(match[1]);
|
|
355
276
|
}
|
|
277
|
+
articlesCount = foundPmidsInMedline.size;
|
|
356
278
|
let notFoundPmids = [];
|
|
357
279
|
if (input.pmids && input.pmids.length > 0) {
|
|
358
280
|
notFoundPmids = input.pmids.filter((pmid) => !foundPmidsInMedline.has(pmid));
|
|
@@ -375,48 +297,30 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
375
297
|
: "GET",
|
|
376
298
|
},
|
|
377
299
|
};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
finalOutputText = JSON.stringify(structuredResponseData);
|
|
383
|
-
}
|
|
300
|
+
finalOutputText =
|
|
301
|
+
input.outputFormat === "raw_text"
|
|
302
|
+
? medlineText
|
|
303
|
+
: JSON.stringify(structuredResponseData);
|
|
384
304
|
}
|
|
385
305
|
else if (input.detailLevel === "full_xml") {
|
|
386
306
|
if (input.outputFormat === "raw_text") {
|
|
387
|
-
// eFetchResponseData is already the raw XML string due to returnRawXml: true
|
|
388
307
|
finalOutputText = String(eFetchResponseData);
|
|
389
|
-
|
|
390
|
-
// For now, returning the direct XML string as per user expectation for "raw_text"
|
|
308
|
+
articlesCount = (finalOutputText.match(/<PubmedArticle>/g) || []).length;
|
|
391
309
|
}
|
|
392
310
|
else {
|
|
393
|
-
// outputFormat is 'json', so eFetchResponseData is the parsed XML object
|
|
394
311
|
const articlesXml = ensureArray(eFetchResponseData?.PubmedArticleSet?.PubmedArticle || []);
|
|
312
|
+
articlesCount = articlesXml.length;
|
|
395
313
|
const articlesPayload = [];
|
|
396
314
|
const foundPmidsInXml = new Set();
|
|
397
315
|
for (const articleXml of articlesXml) {
|
|
398
|
-
|
|
399
|
-
if (
|
|
400
|
-
const extracted = extractPmid(articleXml.MedlineCitation);
|
|
401
|
-
if (extracted) {
|
|
402
|
-
pmid = extracted;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
if (pmid !== "unknown_pmid") {
|
|
316
|
+
const pmid = extractPmid(articleXml.MedlineCitation) || "unknown_pmid";
|
|
317
|
+
if (pmid !== "unknown_pmid")
|
|
406
318
|
foundPmidsInXml.add(pmid);
|
|
407
|
-
}
|
|
408
|
-
articlesPayload.push({
|
|
409
|
-
pmid: pmid,
|
|
410
|
-
fullXmlContent: articleXml,
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
let notFoundPmids = [];
|
|
414
|
-
if (input.pmids && input.pmids.length > 0) {
|
|
415
|
-
notFoundPmids = input.pmids.filter((pmid) => !foundPmidsInXml.has(pmid));
|
|
416
|
-
}
|
|
417
|
-
else if (usingHistory) {
|
|
418
|
-
notFoundPmids = "N/A (used history query)";
|
|
319
|
+
articlesPayload.push({ pmid, fullXmlContent: articleXml });
|
|
419
320
|
}
|
|
321
|
+
const notFoundPmids = input.pmids && input.pmids.length > 0
|
|
322
|
+
? input.pmids.filter((pmid) => !foundPmidsInXml.has(pmid))
|
|
323
|
+
: "N/A (used history query)";
|
|
420
324
|
structuredResponseData = {
|
|
421
325
|
requestedPmids: input.pmids || "N/A (used history query)",
|
|
422
326
|
articles: articlesPayload,
|
|
@@ -432,17 +336,13 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
432
336
|
}
|
|
433
337
|
}
|
|
434
338
|
else {
|
|
435
|
-
// abstract_plus or citation_data
|
|
436
|
-
// eFetchResponseData is the parsed XML object
|
|
339
|
+
// abstract_plus or citation_data
|
|
437
340
|
const parsedArticles = parsePubMedArticleSet(eFetchResponseData, input, toolLogicContext);
|
|
341
|
+
articlesCount = parsedArticles.length;
|
|
438
342
|
const foundPmids = new Set(parsedArticles.map((p) => p.pmid));
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
else if (usingHistory) {
|
|
444
|
-
notFoundPmids = "N/A (used history query)";
|
|
445
|
-
}
|
|
343
|
+
const notFoundPmids = input.pmids && input.pmids.length > 0
|
|
344
|
+
? input.pmids.filter((pmid) => !foundPmids.has(pmid))
|
|
345
|
+
: "N/A (used history query)";
|
|
446
346
|
structuredResponseData = {
|
|
447
347
|
requestedPmids: input.pmids || "N/A (used history query)",
|
|
448
348
|
articles: parsedArticles,
|
|
@@ -471,21 +371,20 @@ export async function fetchPubMedContentLogic(input, parentRequestContext) {
|
|
|
471
371
|
year: article.journalInfo?.publicationDate?.year,
|
|
472
372
|
},
|
|
473
373
|
doi: article.doi,
|
|
474
|
-
// Conditionally include meshTerms if the input flag was set (it defaults to true)
|
|
475
374
|
...(input.includeMeshTerms && { meshTerms: article.meshTerms }),
|
|
476
375
|
}));
|
|
477
376
|
}
|
|
478
|
-
// For abstract_plus and citation_data, outputFormat 'raw_text' doesn't make sense,
|
|
479
|
-
// as the data is inherently structured. So, always output JSON.
|
|
480
377
|
finalOutputText = JSON.stringify(structuredResponseData);
|
|
481
378
|
}
|
|
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
|
+
});
|
|
482
386
|
return {
|
|
483
|
-
content: [
|
|
484
|
-
{
|
|
485
|
-
type: "text",
|
|
486
|
-
text: finalOutputText,
|
|
487
|
-
},
|
|
488
|
-
],
|
|
387
|
+
content: [{ type: "text", text: finalOutputText }],
|
|
489
388
|
isError: false,
|
|
490
389
|
};
|
|
491
390
|
}
|
|
@@ -7,4 +7,4 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
7
7
|
* Registers the fetch_pubmed_content tool with the MCP server.
|
|
8
8
|
* @param server - The McpServer instance.
|
|
9
9
|
*/
|
|
10
|
-
export declare function registerFetchPubMedContentTool(server: McpServer): void
|
|
10
|
+
export declare function registerFetchPubMedContentTool(server: McpServer): Promise<void>;
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
* @fileoverview Registration for the fetch_pubmed_content MCP tool.
|
|
3
3
|
* @module src/mcp-server/tools/fetchPubMedContent/registration
|
|
4
4
|
*/
|
|
5
|
-
import { BaseErrorCode
|
|
6
|
-
import { ErrorHandler, requestContextService } from "../../../utils/index.js";
|
|
5
|
+
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
7
7
|
import { FetchPubMedContentInputSchema, fetchPubMedContentLogic, } from "./logic.js";
|
|
8
8
|
/**
|
|
9
9
|
* Registers the fetch_pubmed_content tool with the MCP server.
|
|
10
10
|
* @param server - The McpServer instance.
|
|
11
11
|
*/
|
|
12
|
-
export function registerFetchPubMedContentTool(server) {
|
|
12
|
+
export async function registerFetchPubMedContentTool(server) {
|
|
13
13
|
const operation = "registerFetchPubMedContentTool";
|
|
14
14
|
const context = requestContextService.createRequestContext({ operation });
|
|
15
|
-
|
|
15
|
+
await ErrorHandler.tryCatch(() => {
|
|
16
16
|
server.tool("fetch_pubmed_content", "Fetches detailed information from PubMed using NCBI EFetch. Can be used with a direct list of PMIDs or with queryKey/webEnv from an ESearch history entry. Supports pagination (retstart, retmax) when using history. Available 'detailLevel' options: 'abstract_plus' (parsed title, abstract, authors, journal, keywords, DOI, optional MeSH/grant info), 'full_xml' (JSON representation of the PubMedArticle XML structure), 'medline_text' (MEDLINE format), or 'citation_data' (minimal data for citations). Returns a JSON object containing results, any PMIDs not found (if applicable), and EFetch details.", FetchPubMedContentInputSchema._def.schema.shape, // Access .shape from the ZodObject before superRefine
|
|
17
17
|
async (input, toolContext) => {
|
|
18
18
|
// Added 'any' type for toolContext
|
|
@@ -23,15 +23,11 @@ export function registerFetchPubMedContentTool(server) {
|
|
|
23
23
|
});
|
|
24
24
|
return fetchPubMedContentLogic(input, richContext);
|
|
25
25
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
34
|
-
critical: true,
|
|
35
|
-
});
|
|
36
|
-
}
|
|
26
|
+
logger.notice("Tool 'fetch_pubmed_content' registered.", context);
|
|
27
|
+
}, {
|
|
28
|
+
operation,
|
|
29
|
+
context,
|
|
30
|
+
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
31
|
+
critical: true,
|
|
32
|
+
});
|
|
37
33
|
}
|
|
@@ -2,6 +2,11 @@ import * as vega from "vega";
|
|
|
2
2
|
import * as vegaLite from "vega-lite";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
5
|
+
// IMPORTANT DEPLOYMENT NOTE:
|
|
6
|
+
// This tool uses the 'canvas' package to render PNG images on the server.
|
|
7
|
+
// The 'canvas' package has native system dependencies (e.g., cairo, pango, libjpeg).
|
|
8
|
+
// Ensure these are installed on the deployment environment.
|
|
9
|
+
// See: https://www.npmjs.com/package/canvas#compiling
|
|
5
10
|
import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
|
|
6
11
|
export const GeneratePubMedChartInputSchema = z.object({
|
|
7
12
|
chartType: z
|
|
@@ -73,8 +78,6 @@ export const GeneratePubMedChartInputSchema = z.object({
|
|
|
73
78
|
.enum(["quantitative", "ordinal"])
|
|
74
79
|
.optional()
|
|
75
80
|
.describe("Optional. Specifies the data type of the `sizeField`. Options: 'quantitative', 'ordinal'. Defaults to 'quantitative' if `sizeField` is provided and this is omitted."),
|
|
76
|
-
// shapeField: z.string().optional().describe("Optional field for encoding point shape in scatter plots."), // Future enhancement
|
|
77
|
-
// shapeFieldType: z.enum(["nominal", "ordinal"]).optional().describe("Type of the shape field."), // Future enhancement
|
|
78
81
|
});
|
|
79
82
|
export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
80
83
|
const operationContext = requestContextService.createRequestContext({
|
|
@@ -84,9 +87,7 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
84
87
|
});
|
|
85
88
|
logger.info(`Executing 'generate_pubmed_chart'. Chart type: ${input.chartType}, Output format: ${input.outputFormat}`, operationContext);
|
|
86
89
|
if (input.outputFormat !== "png") {
|
|
87
|
-
|
|
88
|
-
const unsupportedFormatError = new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported output format: ${input.outputFormat}. Currently, only 'png' is supported.`, // Changed message
|
|
89
|
-
{ requestedFormat: input.outputFormat });
|
|
90
|
+
const unsupportedFormatError = new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported output format: ${input.outputFormat}. Currently, only 'png' is supported.`, { requestedFormat: input.outputFormat });
|
|
90
91
|
logger.warning(unsupportedFormatError.message, operationContext);
|
|
91
92
|
return {
|
|
92
93
|
content: [
|
|
@@ -105,15 +106,14 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
try {
|
|
108
|
-
|
|
109
|
+
const vegaLiteSpec = {
|
|
109
110
|
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
|
110
111
|
title: input.title,
|
|
111
112
|
width: input.width,
|
|
112
113
|
height: input.height,
|
|
113
114
|
data: { values: input.dataValues },
|
|
114
|
-
encoding: {},
|
|
115
|
+
encoding: {},
|
|
115
116
|
};
|
|
116
|
-
// Default field types if not provided
|
|
117
117
|
const yEncType = input.yFieldType || "quantitative";
|
|
118
118
|
const colorEncType = input.colorFieldType || "nominal";
|
|
119
119
|
const seriesEncType = input.seriesFieldType || "nominal";
|
|
@@ -158,14 +158,12 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
158
158
|
},
|
|
159
159
|
};
|
|
160
160
|
if (input.seriesField) {
|
|
161
|
-
// For line charts, seriesField is typically used for color
|
|
162
161
|
vegaLiteSpec.encoding.color = {
|
|
163
162
|
field: input.seriesField,
|
|
164
163
|
type: seriesEncType,
|
|
165
164
|
};
|
|
166
165
|
}
|
|
167
166
|
else if (input.colorField) {
|
|
168
|
-
// Allow direct colorField as well
|
|
169
167
|
vegaLiteSpec.encoding.color = {
|
|
170
168
|
field: input.colorField,
|
|
171
169
|
type: colorEncType,
|
|
@@ -174,7 +172,7 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
174
172
|
break;
|
|
175
173
|
case "scatter":
|
|
176
174
|
xEncType = input.xFieldType || "quantitative";
|
|
177
|
-
vegaLiteSpec.mark = "point";
|
|
175
|
+
vegaLiteSpec.mark = "point";
|
|
178
176
|
vegaLiteSpec.encoding = {
|
|
179
177
|
x: {
|
|
180
178
|
field: input.xField,
|
|
@@ -199,28 +197,28 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
|
|
|
199
197
|
type: sizeEncType,
|
|
200
198
|
};
|
|
201
199
|
}
|
|
202
|
-
// Add shape encoding here if shapeField is implemented
|
|
203
200
|
break;
|
|
204
|
-
// No default case needed as chartType is an enum and Zod validates it.
|
|
205
201
|
}
|
|
206
202
|
const compiledVegaSpec = vegaLite.compile(vegaLiteSpec).spec;
|
|
207
203
|
const view = new vega.View(vega.parse(compiledVegaSpec), {
|
|
208
|
-
renderer: "canvas",
|
|
204
|
+
renderer: "canvas",
|
|
209
205
|
});
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await view.runAsync(); // Initialize and run the view
|
|
214
|
-
const canvas = await view.toCanvas(); // Render to canvas
|
|
215
|
-
// Cast to 'any' to access toBuffer, assuming it's a Node Canvas instance at runtime
|
|
216
|
-
const imageBuffer = await canvas.toBuffer("image/png"); // Get PNG buffer from canvas
|
|
206
|
+
await view.runAsync();
|
|
207
|
+
const canvas = await view.toCanvas();
|
|
208
|
+
const imageBuffer = await canvas.toBuffer("image/png");
|
|
217
209
|
const base64Data = imageBuffer.toString("base64");
|
|
210
|
+
logger.notice("Successfully generated chart.", {
|
|
211
|
+
...operationContext,
|
|
212
|
+
chartType: input.chartType,
|
|
213
|
+
outputFormat: input.outputFormat,
|
|
214
|
+
dataPoints: input.dataValues.length,
|
|
215
|
+
});
|
|
218
216
|
return {
|
|
219
217
|
content: [
|
|
220
218
|
{
|
|
221
219
|
type: "image",
|
|
222
220
|
data: base64Data,
|
|
223
|
-
mimeType: "image/png",
|
|
221
|
+
mimeType: "image/png",
|
|
224
222
|
},
|
|
225
223
|
],
|
|
226
224
|
isError: false,
|
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* @module src/mcp-server/tools/generatePubMedChart/registration
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
-
export declare function registerGeneratePubMedChartTool(server: McpServer): void
|
|
7
|
+
export declare function registerGeneratePubMedChartTool(server: McpServer): Promise<void>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { BaseErrorCode
|
|
1
|
+
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
2
2
|
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
3
3
|
import { GeneratePubMedChartInputSchema, generatePubMedChartLogic, } from "./logic.js";
|
|
4
|
-
export function registerGeneratePubMedChartTool(server) {
|
|
4
|
+
export async function registerGeneratePubMedChartTool(server) {
|
|
5
5
|
const operation = "registerGeneratePubMedChartTool";
|
|
6
6
|
const regContext = requestContextService.createRequestContext({ operation });
|
|
7
|
-
|
|
7
|
+
await ErrorHandler.tryCatch(() => {
|
|
8
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. " +
|
|
@@ -17,20 +17,11 @@ export function registerGeneratePubMedChartTool(server) {
|
|
|
17
17
|
});
|
|
18
18
|
return generatePubMedChartLogic(validatedInput, handlerRequestContext);
|
|
19
19
|
});
|
|
20
|
-
logger.notice(
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
details: "Error during server.tool() call for generate_pubmed_chart.",
|
|
28
|
-
});
|
|
29
|
-
ErrorHandler.handleError(mcpError, {
|
|
30
|
-
operation,
|
|
31
|
-
context: regContext,
|
|
32
|
-
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
33
|
-
critical: true,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
20
|
+
logger.notice("Tool 'generate_pubmed_chart' registered.", regContext);
|
|
21
|
+
}, {
|
|
22
|
+
operation,
|
|
23
|
+
context: regContext,
|
|
24
|
+
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
25
|
+
critical: true,
|
|
26
|
+
});
|
|
36
27
|
}
|
|
@@ -199,8 +199,8 @@ function formatAsAPA(article, context) {
|
|
|
199
199
|
}
|
|
200
200
|
const year = journalInfo?.publicationDate?.year || "n.d.";
|
|
201
201
|
const apaTitle = titleText.charAt(0).toUpperCase() + titleText.slice(1); // APA typically sentence case for article titles.
|
|
202
|
-
const journal = journalInfo?.title
|
|
203
|
-
const volume = journalInfo?.volume
|
|
202
|
+
const journal = journalInfo?.title || "N/A";
|
|
203
|
+
const volume = journalInfo?.volume || "";
|
|
204
204
|
const issue = journalInfo?.issue ? `(${journalInfo.issue})` : "";
|
|
205
205
|
const pages = journalInfo?.pages || "";
|
|
206
206
|
const doiLink = doi ? ` https://doi.org/${doi}` : "";
|
|
@@ -253,7 +253,7 @@ function formatAsMLA(article, context) {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
const title = titleText ? `"${titleText}."` : "N/A.";
|
|
256
|
-
const journal = journalInfo?.title
|
|
256
|
+
const journal = journalInfo?.title || "N/A";
|
|
257
257
|
let publicationDateString = journalInfo?.publicationDate?.year || "";
|
|
258
258
|
if (journalInfo?.publicationDate?.month && journalInfo.publicationDate.year) {
|
|
259
259
|
const month = journalInfo.publicationDate.month.substring(0, 3) + "."; // Abbreviate month
|
|
@@ -15,7 +15,7 @@ import { handleELinkRelationships } from "./elinkHandler.js";
|
|
|
15
15
|
*/
|
|
16
16
|
export async function handleGetPubMedArticleConnections(input, context) {
|
|
17
17
|
const toolLogicContext = requestContextService.createRequestContext({
|
|
18
|
-
parentRequestId: context.requestId,
|
|
18
|
+
parentRequestId: context.requestId,
|
|
19
19
|
operation: "handleGetPubMedArticleConnections",
|
|
20
20
|
toolName: "get_pubmed_article_connections",
|
|
21
21
|
input: sanitizeInputForLogging(input),
|
|
@@ -27,8 +27,8 @@ export async function handleGetPubMedArticleConnections(input, context) {
|
|
|
27
27
|
relatedArticles: [],
|
|
28
28
|
citations: {},
|
|
29
29
|
retrievedCount: 0,
|
|
30
|
-
eUtilityUrl: undefined,
|
|
31
|
-
message: undefined,
|
|
30
|
+
eUtilityUrl: undefined,
|
|
31
|
+
message: undefined,
|
|
32
32
|
};
|
|
33
33
|
try {
|
|
34
34
|
switch (input.relationshipType) {
|
|
@@ -41,10 +41,8 @@ export async function handleGetPubMedArticleConnections(input, context) {
|
|
|
41
41
|
await handleCitationFormats(input, outputData, toolLogicContext);
|
|
42
42
|
break;
|
|
43
43
|
default:
|
|
44
|
-
// This case should ideally be caught by Zod schema validation beforehand
|
|
45
44
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported relationshipType: ${input.relationshipType}`, { receivedType: input.relationshipType });
|
|
46
45
|
}
|
|
47
|
-
// Ensure message is set if no specific error but also no results from handlers
|
|
48
46
|
if (outputData.retrievedCount === 0 &&
|
|
49
47
|
!outputData.message &&
|
|
50
48
|
(input.relationshipType !== "citation_formats" ||
|
|
@@ -58,7 +56,6 @@ export async function handleGetPubMedArticleConnections(input, context) {
|
|
|
58
56
|
error instanceof McpError
|
|
59
57
|
? error.message
|
|
60
58
|
: "An unexpected error occurred while processing the request.";
|
|
61
|
-
// Ensure critical details from McpError are passed through if available
|
|
62
59
|
const errorDetails = error instanceof McpError
|
|
63
60
|
? error.details
|
|
64
61
|
: { originalError: error.message };
|
|
@@ -75,6 +72,12 @@ export async function handleGetPubMedArticleConnections(input, context) {
|
|
|
75
72
|
];
|
|
76
73
|
return { content, isError: true };
|
|
77
74
|
}
|
|
75
|
+
logger.notice("Successfully executed get_pubmed_article_connections tool.", {
|
|
76
|
+
...toolLogicContext,
|
|
77
|
+
relationshipType: input.relationshipType,
|
|
78
|
+
retrievedCount: outputData.retrievedCount,
|
|
79
|
+
citationsGenerated: Object.keys(outputData.citations).length,
|
|
80
|
+
});
|
|
78
81
|
const responseContent = [
|
|
79
82
|
{ type: "text", text: JSON.stringify(outputData) },
|
|
80
83
|
];
|
|
@@ -31,6 +31,5 @@ export type GetPubMedArticleConnectionsInput = z.infer<typeof GetPubMedArticleCo
|
|
|
31
31
|
/**
|
|
32
32
|
* Registers the 'get_pubmed_article_connections' tool with the given MCP server instance.
|
|
33
33
|
* @param {McpServer} server - The MCP server instance.
|
|
34
|
-
* @param {RequestContext} registrationContext - The context for this registration operation.
|
|
35
34
|
*/
|
|
36
|
-
export declare function registerGetPubMedArticleConnectionsTool(server: McpServer): void
|
|
35
|
+
export declare function registerGetPubMedArticleConnectionsTool(server: McpServer): Promise<void>;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* @module src/mcp-server/tools/getPubMedArticleConnections/registration
|
|
5
5
|
*/
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { BaseErrorCode
|
|
8
|
-
import { ErrorHandler, requestContextService, } from "../../../utils/index.js";
|
|
7
|
+
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
8
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
9
9
|
import { handleGetPubMedArticleConnections } from "./logic.js";
|
|
10
10
|
/**
|
|
11
11
|
* Zod schema for the input parameters of the 'get_pubmed_article_connections' tool.
|
|
@@ -23,7 +23,7 @@ export const GetPubMedArticleConnectionsInputSchema = z.object({
|
|
|
23
23
|
"citation_formats",
|
|
24
24
|
])
|
|
25
25
|
.default("pubmed_similar_articles")
|
|
26
|
-
.describe("Specifies the type of connection or action: \n- 'pubmed_similar_articles': Finds articles similar to the source PMID (uses ELink `cmd=neighbor`). \n- 'pubmed_citedin': Finds articles in PubMed that cite the source PMID (uses ELink `linkname=pubmed_pubmed_citedin`). \n- 'pubmed_references': Finds articles in PubMed referenced by the source PMID (uses ELink `linkname=pubmed_pubmed_refs`). \n- 'citation_formats': Retrieves data for the source PMID and formats it into specified citation styles (
|
|
26
|
+
.describe("Specifies the type of connection or action: \n- 'pubmed_similar_articles': Finds articles similar to the source PMID (uses ELink `cmd=neighbor`). \n- 'pubmed_citedin': Finds articles in PubMed that cite the source PMID (uses ELink `linkname=pubmed_pubmed_citedin`). \n- 'pubmed_references': Finds articles in PubMed referenced by the source PMID (uses ELink `linkname=pubmed_pubmed_refs`). \n- 'citation_formats': Retrieves data for the source PMID and formats it into specified citation styles (RIS, BibTeX, APA, MLA via NCBI EFetch and server-side formatting)."),
|
|
27
27
|
maxRelatedResults: z
|
|
28
28
|
.number()
|
|
29
29
|
.int()
|
|
@@ -41,36 +41,26 @@ export const GetPubMedArticleConnectionsInputSchema = z.object({
|
|
|
41
41
|
/**
|
|
42
42
|
* Registers the 'get_pubmed_article_connections' tool with the given MCP server instance.
|
|
43
43
|
* @param {McpServer} server - The MCP server instance.
|
|
44
|
-
* @param {RequestContext} registrationContext - The context for this registration operation.
|
|
45
44
|
*/
|
|
46
|
-
export function registerGetPubMedArticleConnectionsTool(server) {
|
|
45
|
+
export async function registerGetPubMedArticleConnectionsTool(server) {
|
|
47
46
|
const operation = "registerGetPubMedArticleConnectionsTool";
|
|
48
47
|
const registrationContext = requestContextService.createRequestContext({
|
|
49
48
|
operation,
|
|
50
49
|
});
|
|
51
|
-
|
|
52
|
-
server.tool("get_pubmed_article_connections", "Finds articles related to a source PubMed ID (PMID) or retrieves formatted citations for it. Supports finding similar articles, articles that cite the source, articles referenced by the source (via NCBI ELink), or fetching data to generate citations in various styles (RIS, BibTeX, APA, MLA via NCBI EFetch and server-side formatting). Returns a JSON object detailing the connections or formatted citations.", GetPubMedArticleConnectionsInputSchema.shape,
|
|
53
|
-
async (validatedInput, toolContext) => {
|
|
54
|
-
// Create a new rich context for the logic handler
|
|
50
|
+
await ErrorHandler.tryCatch(() => {
|
|
51
|
+
server.tool("get_pubmed_article_connections", "Finds articles related to a source PubMed ID (PMID) or retrieves formatted citations for it. Supports finding similar articles, articles that cite the source, articles referenced by the source (via NCBI ELink), or fetching data to generate citations in various styles (RIS, BibTeX, APA, MLA via NCBI EFetch and server-side formatting). Returns a JSON object detailing the connections or formatted citations.", GetPubMedArticleConnectionsInputSchema.shape, async (validatedInput, toolContext) => {
|
|
55
52
|
const richLogicContext = requestContextService.createRequestContext({
|
|
56
|
-
parentRequestId: registrationContext.requestId,
|
|
53
|
+
parentRequestId: registrationContext.requestId,
|
|
57
54
|
operation: "getPubMedArticleConnectionsToolHandler",
|
|
58
|
-
mcpToolContext: toolContext,
|
|
55
|
+
mcpToolContext: toolContext,
|
|
59
56
|
});
|
|
60
|
-
return
|
|
57
|
+
return handleGetPubMedArticleConnections(validatedInput, richLogicContext);
|
|
61
58
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}), {
|
|
70
|
-
operation,
|
|
71
|
-
context: registrationContext, // Use the context of the registration operation
|
|
72
|
-
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
73
|
-
critical: true, // Registration failure is critical
|
|
74
|
-
});
|
|
75
|
-
}
|
|
59
|
+
logger.notice("Tool 'get_pubmed_article_connections' registered.", registrationContext);
|
|
60
|
+
}, {
|
|
61
|
+
operation,
|
|
62
|
+
context: registrationContext,
|
|
63
|
+
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
64
|
+
critical: true,
|
|
65
|
+
});
|
|
76
66
|
}
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import { RequestContext } from "../../../utils/index.js";
|
|
9
|
-
import {
|
|
10
|
-
PubMedResearchAgentInput, PubMedResearchAgentInputSchema } from "./logic/index.js";
|
|
9
|
+
import { PubMedResearchAgentInput, PubMedResearchAgentInputSchema } from "./logic/index.js";
|
|
11
10
|
export { PubMedResearchAgentInput, PubMedResearchAgentInputSchema };
|
|
12
11
|
export declare function pubmedResearchAgentLogic(input: PubMedResearchAgentInput, parentRequestContext: RequestContext): Promise<CallToolResult>;
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
8
8
|
import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
|
|
9
|
-
import { generateFullResearchPlanOutline, PubMedResearchAgentInputSchema,
|
|
10
|
-
} from "./logic/index.js";
|
|
11
|
-
// Re-export schema and input type for easy access by registration.ts
|
|
9
|
+
import { generateFullResearchPlanOutline, PubMedResearchAgentInputSchema, } from "./logic/index.js";
|
|
12
10
|
export { PubMedResearchAgentInputSchema };
|
|
13
11
|
export async function pubmedResearchAgentLogic(input, parentRequestContext) {
|
|
14
12
|
const operationContext = requestContextService.createRequestContext({
|
|
@@ -19,6 +17,10 @@ export async function pubmedResearchAgentLogic(input, parentRequestContext) {
|
|
|
19
17
|
logger.info(`Executing 'pubmed_research_agent' to generate research plan outline. Keywords: ${input.research_keywords.join(", ")}`, operationContext);
|
|
20
18
|
try {
|
|
21
19
|
const researchPlanOutline = generateFullResearchPlanOutline(input, operationContext);
|
|
20
|
+
logger.notice("Successfully generated research plan outline.", {
|
|
21
|
+
...operationContext,
|
|
22
|
+
projectTitle: input.project_title_suggestion,
|
|
23
|
+
});
|
|
22
24
|
return {
|
|
23
25
|
content: [
|
|
24
26
|
{ type: "text", text: JSON.stringify(researchPlanOutline, null, 2) },
|
|
@@ -30,11 +32,10 @@ export async function pubmedResearchAgentLogic(input, parentRequestContext) {
|
|
|
30
32
|
logger.error("Execution failed for 'pubmed_research_agent'", error, operationContext);
|
|
31
33
|
const mcpError = error instanceof McpError
|
|
32
34
|
? error
|
|
33
|
-
: new McpError(BaseErrorCode.INTERNAL_ERROR,
|
|
34
|
-
`'pubmed_research_agent' tool failed during plan outline generation: ${error.message || "Internal server error."}`, {
|
|
35
|
+
: new McpError(BaseErrorCode.INTERNAL_ERROR, `'pubmed_research_agent' tool failed during plan outline generation: ${error.message || "Internal server error."}`, {
|
|
35
36
|
originalErrorName: error.name,
|
|
36
37
|
requestId: operationContext.requestId,
|
|
37
|
-
inputKeywords: input.research_keywords,
|
|
38
|
+
inputKeywords: input.research_keywords,
|
|
38
39
|
});
|
|
39
40
|
return {
|
|
40
41
|
content: [
|
|
@@ -7,4 +7,4 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
7
7
|
* Registers the pubmed_research_agent tool with the MCP server.
|
|
8
8
|
* @param server - The McpServer instance.
|
|
9
9
|
*/
|
|
10
|
-
export declare function registerPubMedResearchAgentTool(server: McpServer): void
|
|
10
|
+
export declare function registerPubMedResearchAgentTool(server: McpServer): Promise<void>;
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
* @fileoverview Registration for the pubmed_research_agent tool.
|
|
3
3
|
* @module pubmedResearchAgent/registration
|
|
4
4
|
*/
|
|
5
|
-
import { BaseErrorCode
|
|
5
|
+
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
6
|
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
7
7
|
import { PubMedResearchAgentInputSchema, pubmedResearchAgentLogic, } from "./logic.js";
|
|
8
8
|
/**
|
|
9
9
|
* Registers the pubmed_research_agent tool with the MCP server.
|
|
10
10
|
* @param server - The McpServer instance.
|
|
11
11
|
*/
|
|
12
|
-
export function registerPubMedResearchAgentTool(server) {
|
|
12
|
+
export async function registerPubMedResearchAgentTool(server) {
|
|
13
13
|
const operation = "registerPubMedResearchAgentTool";
|
|
14
14
|
const context = requestContextService.createRequestContext({ operation });
|
|
15
|
-
|
|
15
|
+
await ErrorHandler.tryCatch(() => {
|
|
16
16
|
server.tool("pubmed_research_agent", "Generates a standardized JSON research plan outline from component details you provide. It accepts granular inputs for all research phases (conception, data collection, analysis, dissemination, cross-cutting concerns). If `include_detailed_prompts_for_agent` is true, the output plan will embed instructive prompts and detailed guidance notes to aid the research agent. The tool's primary function is to organize and structure your rough ideas into a formal, machine-readable plan. This plan is intended for further processing; as the research agent, you should then utilize your full suite of tools (e.g., file manipulation, `get_pubmed_article_connections` for literature/data search via PMID) to execute the outlined research, tailored to the user's request.", PubMedResearchAgentInputSchema.shape, async (validatedInput, mcpProvidedContext) => {
|
|
17
17
|
const handlerRequestContext = requestContextService.createRequestContext({
|
|
18
18
|
parentRequestId: context.requestId,
|
|
@@ -22,15 +22,10 @@ export function registerPubMedResearchAgentTool(server) {
|
|
|
22
22
|
return pubmedResearchAgentLogic(validatedInput, handlerRequestContext);
|
|
23
23
|
});
|
|
24
24
|
logger.notice(`Tool 'pubmed_research_agent' registered.`, context);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
context,
|
|
32
|
-
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
33
|
-
critical: true,
|
|
34
|
-
});
|
|
35
|
-
}
|
|
25
|
+
}, {
|
|
26
|
+
operation,
|
|
27
|
+
context,
|
|
28
|
+
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
29
|
+
critical: true,
|
|
30
|
+
});
|
|
36
31
|
}
|
|
@@ -140,10 +140,9 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
|
|
|
140
140
|
if (esResult.WebEnv && esResult.QueryKey) {
|
|
141
141
|
eSummaryParams.WebEnv = esResult.WebEnv;
|
|
142
142
|
eSummaryParams.query_key = esResult.QueryKey;
|
|
143
|
-
eSummaryParams.retmax = currentFetchBriefSummaries;
|
|
143
|
+
eSummaryParams.retmax = currentFetchBriefSummaries;
|
|
144
144
|
}
|
|
145
145
|
else {
|
|
146
|
-
// Fallback to using explicit IDs if history is not available (should not happen if usehistory='y' was successful)
|
|
147
146
|
const pmidsForSummary = pmids
|
|
148
147
|
.slice(0, currentFetchBriefSummaries)
|
|
149
148
|
.join(",");
|
|
@@ -185,13 +184,19 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
|
|
|
185
184
|
effectiveESearchTerm: effectiveQuery,
|
|
186
185
|
totalFound,
|
|
187
186
|
retrievedPmidCount,
|
|
188
|
-
pmids,
|
|
189
|
-
briefSummaries,
|
|
187
|
+
pmids,
|
|
188
|
+
briefSummaries,
|
|
190
189
|
eSearchUrl,
|
|
191
190
|
eSummaryUrl: currentFetchBriefSummaries > 0 && pmids.length > 0
|
|
192
191
|
? eSummaryUrl
|
|
193
192
|
: undefined,
|
|
194
193
|
};
|
|
194
|
+
logger.notice("Successfully executed searchPubMedArticles tool.", {
|
|
195
|
+
...toolLogicContext,
|
|
196
|
+
totalFound,
|
|
197
|
+
retrievedPmidCount,
|
|
198
|
+
summariesFetched: briefSummaries.length,
|
|
199
|
+
});
|
|
195
200
|
return {
|
|
196
201
|
content: [{ type: "text", text: JSON.stringify(resultPayload) }],
|
|
197
202
|
isError: false,
|
|
@@ -7,4 +7,4 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
7
7
|
* Registers the searchPubMedArticles tool with the MCP server.
|
|
8
8
|
* @param server - The McpServer instance.
|
|
9
9
|
*/
|
|
10
|
-
export declare function registerSearchPubMedArticlesTool(server: McpServer): void
|
|
10
|
+
export declare function registerSearchPubMedArticlesTool(server: McpServer): Promise<void>;
|
|
@@ -2,37 +2,30 @@
|
|
|
2
2
|
* @fileoverview Registration for the searchPubMedArticles MCP tool.
|
|
3
3
|
* @module src/mcp-server/tools/searchPubMedArticles/registration
|
|
4
4
|
*/
|
|
5
|
-
import { BaseErrorCode
|
|
6
|
-
import { ErrorHandler, requestContextService } from "../../../utils/index.js";
|
|
5
|
+
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
6
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
7
7
|
import { SearchPubMedArticlesInputSchema, searchPubMedArticlesLogic, } from "./logic.js";
|
|
8
8
|
/**
|
|
9
9
|
* Registers the searchPubMedArticles tool with the MCP server.
|
|
10
10
|
* @param server - The McpServer instance.
|
|
11
11
|
*/
|
|
12
|
-
export function registerSearchPubMedArticlesTool(server) {
|
|
12
|
+
export async function registerSearchPubMedArticlesTool(server) {
|
|
13
13
|
const operation = "registerSearchPubMedArticlesTool";
|
|
14
14
|
const context = requestContextService.createRequestContext({ operation });
|
|
15
|
-
|
|
16
|
-
server.tool("search_pubmed_articles", "Searches PubMed for articles using a query term and optional filters (max results, sort, date range, publication types). Uses NCBI ESearch to find PMIDs and ESummary (optional) for brief summaries. Returns a JSON object with search parameters, ESearch term, result counts, PMIDs, optional summaries (PMID, title, authors, source, dates), and E-utility URLs.", SearchPubMedArticlesInputSchema.shape,
|
|
17
|
-
async (input, toolContext) => {
|
|
18
|
-
// Explicitly type input
|
|
15
|
+
await ErrorHandler.tryCatch(() => {
|
|
16
|
+
server.tool("search_pubmed_articles", "Searches PubMed for articles using a query term and optional filters (max results, sort, date range, publication types). Uses NCBI ESearch to find PMIDs and ESummary (optional) for brief summaries. Returns a JSON object with search parameters, ESearch term, result counts, PMIDs, optional summaries (PMID, title, authors, source, dates), and E-utility URLs.", SearchPubMedArticlesInputSchema.shape, async (input, toolContext) => {
|
|
19
17
|
const richContext = requestContextService.createRequestContext({
|
|
20
18
|
parentRequestId: context.requestId,
|
|
21
19
|
operation: "searchPubMedArticlesToolHandler",
|
|
22
|
-
mcpToolContext: toolContext,
|
|
20
|
+
mcpToolContext: toolContext,
|
|
23
21
|
});
|
|
24
22
|
return searchPubMedArticlesLogic(input, richContext);
|
|
25
23
|
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
context,
|
|
34
|
-
errorCode: BaseErrorCode.INITIALIZATION_FAILED, // errorCode is still useful for categorization
|
|
35
|
-
critical: true,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
24
|
+
logger.notice("Tool 'search_pubmed_articles' registered.", context);
|
|
25
|
+
}, {
|
|
26
|
+
operation,
|
|
27
|
+
context,
|
|
28
|
+
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
29
|
+
critical: true,
|
|
30
|
+
});
|
|
38
31
|
}
|
|
@@ -54,6 +54,11 @@ const MAX_PORT_RETRIES = 15;
|
|
|
54
54
|
* Stores active `StreamableHTTPServerTransport` instances from the SDK, keyed by their session ID.
|
|
55
55
|
* This is essential for routing subsequent HTTP requests (GET, DELETE, non-initialize POST)
|
|
56
56
|
* to the correct stateful session transport instance.
|
|
57
|
+
*
|
|
58
|
+
* @scalability Note: This is an in-memory store. For horizontal scaling across
|
|
59
|
+
* multiple processes or machines, this state would need to be moved to a shared,
|
|
60
|
+
* distributed store like Redis or a database.
|
|
61
|
+
*
|
|
57
62
|
* @type {Record<string, StreamableHTTPServerTransport>}
|
|
58
63
|
* @private
|
|
59
64
|
*/
|
|
@@ -61,6 +66,10 @@ const httpTransports = {};
|
|
|
61
66
|
/**
|
|
62
67
|
* Stores the last activity timestamp for each session, keyed by session ID.
|
|
63
68
|
* Used for garbage collecting stale/abandoned sessions.
|
|
69
|
+
*
|
|
70
|
+
* @scalability Note: This is an in-memory store. Like `httpTransports`, this
|
|
71
|
+
* would need to be moved to a distributed store for multi-instance deployments.
|
|
72
|
+
*
|
|
64
73
|
* @type {Record<string, number>}
|
|
65
74
|
* @private
|
|
66
75
|
*/
|
|
@@ -28,6 +28,10 @@ export interface RateLimitEntry {
|
|
|
28
28
|
/**
|
|
29
29
|
* A generic rate limiter class using an in-memory store.
|
|
30
30
|
* Controls frequency of operations based on unique keys.
|
|
31
|
+
*
|
|
32
|
+
* @scalability Note: This is an in-memory store. For horizontal scaling across
|
|
33
|
+
* multiple processes or machines, this state would need to be moved to a shared,
|
|
34
|
+
* distributed store like Redis or a database.
|
|
31
35
|
*/
|
|
32
36
|
export declare class RateLimiter {
|
|
33
37
|
private config;
|
|
@@ -9,6 +9,10 @@ import { logger, requestContextService } from "../index.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* A generic rate limiter class using an in-memory store.
|
|
11
11
|
* Controls frequency of operations based on unique keys.
|
|
12
|
+
*
|
|
13
|
+
* @scalability Note: This is an in-memory store. For horizontal scaling across
|
|
14
|
+
* multiple processes or machines, this state would need to be moved to a shared,
|
|
15
|
+
* distributed store like Redis or a database.
|
|
12
16
|
*/
|
|
13
17
|
export class RateLimiter {
|
|
14
18
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/pubmed-mcp-server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|