@cyanheads/pubmed-mcp-server 1.1.2 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +25 -24
  2. package/dist/config/index.js +39 -1
  3. package/dist/mcp-server/server.d.ts +0 -7
  4. package/dist/mcp-server/server.js +17 -53
  5. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +6 -2
  6. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +102 -311
  7. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +1 -1
  8. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +50 -18
  9. package/dist/mcp-server/tools/generatePubMedChart/logic.d.ts +9 -28
  10. package/dist/mcp-server/tools/generatePubMedChart/logic.js +137 -198
  11. package/dist/mcp-server/tools/generatePubMedChart/registration.d.ts +1 -1
  12. package/dist/mcp-server/tools/generatePubMedChart/registration.js +62 -27
  13. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +1 -1
  14. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +3 -3
  15. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +1 -1
  16. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +27 -4
  17. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +59 -51
  18. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +1 -1
  19. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +1 -26
  20. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +58 -62
  21. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +2 -5
  22. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -40
  23. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +1 -1
  24. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +55 -19
  25. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +12 -10
  26. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +68 -121
  27. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +1 -1
  28. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +54 -21
  29. package/dist/mcp-server/transports/httpTransport.d.ts +0 -8
  30. package/dist/mcp-server/transports/httpTransport.js +57 -345
  31. package/dist/utils/security/rateLimiter.d.ts +4 -0
  32. package/dist/utils/security/rateLimiter.js +4 -0
  33. package/package.json +7 -15
  34. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
  35. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
  36. package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
  37. package/dist/mcp-server/resources/echoResource/index.js +0 -13
  38. package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
  39. package/dist/mcp-server/resources/echoResource/registration.js +0 -168
  40. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
  41. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +0 -6
@@ -61,14 +61,6 @@ export const SearchPubMedArticlesInputSchema = z.object({
61
61
  .default(0)
62
62
  .describe("Number of top PMIDs for which to fetch brief summaries using ESummary v2.0. Set to 0 to disable. Maximum is 50 for this tool. Default is 0."),
63
63
  });
64
- /**
65
- * Logic for the searchPubMedArticles tool.
66
- * Constructs and executes ESearch and optionally ESummary queries via NcbiService,
67
- * then formats the results into a CallToolResult.
68
- * @param input - Validated input arguments for the tool.
69
- * @param parentRequestContext - The parent request context for logging and correlation.
70
- * @returns A promise resolving to a CallToolResult.
71
- */
72
64
  export async function searchPubMedArticlesLogic(input, parentRequestContext) {
73
65
  const ncbiService = getNcbiService();
74
66
  const toolLogicContext = requestContextService.createRequestContext({
@@ -107,124 +99,79 @@ export async function searchPubMedArticlesLogic(input, parentRequestContext) {
107
99
  sort: input.sortBy,
108
100
  usehistory: currentFetchBriefSummaries > 0 ? "y" : "n",
109
101
  };
110
- let eSearchUrl = "";
111
- let eSummaryUrl = "";
112
- try {
113
- const eSearchResponse = await ncbiService.eSearch(eSearchParams, toolLogicContext);
114
- const eSearchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
115
- const eSearchQueryStringParams = {};
116
- for (const key in eSearchParams) {
117
- if (eSearchParams[key] !== undefined) {
118
- eSearchQueryStringParams[key] = String(eSearchParams[key]);
119
- }
102
+ const eSearchResponse = await ncbiService.eSearch(eSearchParams, toolLogicContext);
103
+ const eSearchBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi";
104
+ const eSearchQueryStringParams = {};
105
+ for (const key in eSearchParams) {
106
+ if (eSearchParams[key] !== undefined) {
107
+ eSearchQueryStringParams[key] = String(eSearchParams[key]);
120
108
  }
121
- const eSearchQueryString = new URLSearchParams(eSearchQueryStringParams).toString();
122
- eSearchUrl = `${eSearchBase}?${eSearchQueryString}`;
123
- if (!eSearchResponse || !eSearchResponse.eSearchResult) {
124
- throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Invalid or empty ESearch response from NCBI.", {
125
- responsePreview: sanitizeInputForLogging(JSON.stringify(eSearchResponse).substring(0, 200)),
126
- requestId: toolLogicContext.requestId,
127
- });
109
+ }
110
+ const eSearchQueryString = new URLSearchParams(eSearchQueryStringParams).toString();
111
+ const eSearchUrl = `${eSearchBase}?${eSearchQueryString}`;
112
+ if (!eSearchResponse || !eSearchResponse.eSearchResult) {
113
+ throw new McpError(BaseErrorCode.NCBI_PARSING_ERROR, "Invalid or empty ESearch response from NCBI.", {
114
+ ...toolLogicContext,
115
+ responsePreview: sanitizeInputForLogging(JSON.stringify(eSearchResponse).substring(0, 200)),
116
+ });
117
+ }
118
+ const esResult = eSearchResponse.eSearchResult;
119
+ const pmids = esResult.IdList?.Id || [];
120
+ const totalFound = parseInt(esResult.Count || "0", 10);
121
+ const retrievedPmidCount = pmids.length;
122
+ let briefSummaries = [];
123
+ let eSummaryUrl;
124
+ if (currentFetchBriefSummaries > 0 && pmids.length > 0) {
125
+ const eSummaryParams = {
126
+ db: "pubmed",
127
+ version: "2.0",
128
+ retmode: "xml",
129
+ };
130
+ if (esResult.WebEnv && esResult.QueryKey) {
131
+ eSummaryParams.WebEnv = esResult.WebEnv;
132
+ eSummaryParams.query_key = esResult.QueryKey;
133
+ eSummaryParams.retmax = currentFetchBriefSummaries;
128
134
  }
129
- const esResult = eSearchResponse.eSearchResult;
130
- const pmids = esResult.IdList?.Id || [];
131
- const totalFound = parseInt(esResult.Count || "0", 10);
132
- const retrievedPmidCount = pmids.length;
133
- let briefSummaries = [];
134
- if (currentFetchBriefSummaries > 0 && pmids.length > 0) {
135
- const eSummaryParams = {
136
- db: "pubmed",
137
- version: "2.0",
138
- retmode: "xml",
139
- };
140
- if (esResult.WebEnv && esResult.QueryKey) {
141
- eSummaryParams.WebEnv = esResult.WebEnv;
142
- eSummaryParams.query_key = esResult.QueryKey;
143
- eSummaryParams.retmax = currentFetchBriefSummaries; // Use history with retmax
144
- }
145
- else {
146
- // Fallback to using explicit IDs if history is not available (should not happen if usehistory='y' was successful)
147
- const pmidsForSummary = pmids
148
- .slice(0, currentFetchBriefSummaries)
149
- .join(",");
150
- eSummaryParams.id = pmidsForSummary;
151
- }
152
- const eSummaryBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi";
153
- const eSummaryQueryStringParams = {};
154
- for (const key in eSummaryParams) {
155
- if (eSummaryParams[key] !== undefined) {
156
- eSummaryQueryStringParams[key] = String(eSummaryParams[key]);
157
- }
135
+ else {
136
+ const pmidsForSummary = pmids
137
+ .slice(0, currentFetchBriefSummaries)
138
+ .join(",");
139
+ eSummaryParams.id = pmidsForSummary;
140
+ }
141
+ const eSummaryBase = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi";
142
+ const eSummaryQueryStringParams = {};
143
+ for (const key in eSummaryParams) {
144
+ if (eSummaryParams[key] !== undefined) {
145
+ eSummaryQueryStringParams[key] = String(eSummaryParams[key]);
158
146
  }
159
- const eSummaryQueryString = new URLSearchParams(eSummaryQueryStringParams).toString();
160
- eSummaryUrl = `${eSummaryBase}?${eSummaryQueryString}`;
161
- const eSummaryResponseXml = await ncbiService.eSummary(eSummaryParams, toolLogicContext);
162
- logger.debug("Raw ESummary Response (XML parsed by ncbiService):", {
147
+ }
148
+ const eSummaryQueryString = new URLSearchParams(eSummaryQueryStringParams).toString();
149
+ eSummaryUrl = `${eSummaryBase}?${eSummaryQueryString}`;
150
+ const eSummaryResponseXml = await ncbiService.eSummary(eSummaryParams, toolLogicContext);
151
+ if (eSummaryResponseXml && eSummaryResponseXml.eSummaryResult) {
152
+ briefSummaries = await extractBriefSummaries(eSummaryResponseXml.eSummaryResult, toolLogicContext);
153
+ }
154
+ else if (eSummaryResponseXml && eSummaryResponseXml.ERROR) {
155
+ logger.warning("ESummary returned a top-level error", {
163
156
  ...toolLogicContext,
164
- eSummaryResponse: sanitizeInputForLogging(eSummaryResponseXml),
157
+ errorDetails: eSummaryResponseXml.ERROR,
165
158
  });
166
- if (eSummaryResponseXml && eSummaryResponseXml.eSummaryResult) {
167
- briefSummaries = await extractBriefSummaries(eSummaryResponseXml.eSummaryResult, toolLogicContext);
168
- }
169
- else if (eSummaryResponseXml && eSummaryResponseXml.ERROR) {
170
- logger.warning("ESummary returned a top-level error", {
171
- ...toolLogicContext,
172
- errorDetails: eSummaryResponseXml.ERROR,
173
- });
174
- }
175
159
  }
176
- const resultPayload = {
177
- searchParameters: {
178
- queryTerm: input.queryTerm,
179
- maxResults: input.maxResults,
180
- sortBy: input.sortBy,
181
- dateRange: input.dateRange,
182
- filterByPublicationTypes: input.filterByPublicationTypes,
183
- fetchBriefSummaries: currentFetchBriefSummaries,
184
- },
185
- effectiveESearchTerm: effectiveQuery,
186
- totalFound,
187
- retrievedPmidCount,
188
- pmids, // These are the PMIDs from ESearch, limited by input.maxResults
189
- briefSummaries, // These should now be limited by currentFetchBriefSummaries
190
- eSearchUrl,
191
- eSummaryUrl: currentFetchBriefSummaries > 0 && pmids.length > 0
192
- ? eSummaryUrl
193
- : undefined,
194
- };
195
- return {
196
- content: [{ type: "text", text: JSON.stringify(resultPayload) }],
197
- isError: false,
198
- };
199
- }
200
- catch (error) {
201
- logger.error("Error in searchPubMedArticlesLogic", error, toolLogicContext);
202
- const mcpError = error instanceof McpError
203
- ? error
204
- : new McpError(BaseErrorCode.INTERNAL_ERROR, "Failed to search PubMed articles due to an unexpected error.", {
205
- originalErrorName: error.name,
206
- originalErrorMessage: error.message,
207
- requestId: toolLogicContext.requestId,
208
- });
209
- return {
210
- content: [
211
- {
212
- type: "text",
213
- text: JSON.stringify({
214
- error: {
215
- code: mcpError.code,
216
- message: mcpError.message,
217
- details: mcpError.details,
218
- },
219
- searchParameters: sanitizeInputForLogging(input),
220
- eSearchUrl,
221
- eSummaryUrl: (input.fetchBriefSummaries ?? 0) > 0 && eSummaryUrl
222
- ? eSummaryUrl
223
- : undefined,
224
- }),
225
- },
226
- ],
227
- isError: true,
228
- };
229
160
  }
161
+ logger.notice("Successfully executed searchPubMedArticles tool.", {
162
+ ...toolLogicContext,
163
+ totalFound,
164
+ retrievedPmidCount,
165
+ summariesFetched: briefSummaries.length,
166
+ });
167
+ return {
168
+ searchParameters: input,
169
+ effectiveESearchTerm: effectiveQuery,
170
+ totalFound,
171
+ retrievedPmidCount,
172
+ pmids,
173
+ briefSummaries,
174
+ eSearchUrl,
175
+ eSummaryUrl,
176
+ };
230
177
  }
@@ -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,70 @@
2
2
  * @fileoverview Registration for the searchPubMedArticles MCP tool.
3
3
  * @module src/mcp-server/tools/searchPubMedArticles/registration
4
4
  */
5
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js"; // Import McpError
6
- import { ErrorHandler, requestContextService } from "../../../utils/index.js";
5
+ import { BaseErrorCode, McpError } 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
+ const toolName = "search_pubmed_articles";
15
+ const toolDescription = "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.";
14
16
  const context = requestContextService.createRequestContext({ operation });
15
- try {
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, // Pass the .shape for ZodRawShape
17
- async (input, toolContext) => {
18
- // Explicitly type input
17
+ await ErrorHandler.tryCatch(async () => {
18
+ server.tool(toolName, toolDescription, SearchPubMedArticlesInputSchema.shape, async (input, mcpProvidedContext) => {
19
19
  const richContext = requestContextService.createRequestContext({
20
20
  parentRequestId: context.requestId,
21
21
  operation: "searchPubMedArticlesToolHandler",
22
- mcpToolContext: toolContext, // Include MCP-provided context if any
22
+ mcpToolContext: mcpProvidedContext,
23
+ input,
23
24
  });
24
- return searchPubMedArticlesLogic(input, richContext);
25
+ try {
26
+ const result = await searchPubMedArticlesLogic(input, richContext);
27
+ return {
28
+ content: [
29
+ { type: "text", text: JSON.stringify(result, null, 2) },
30
+ ],
31
+ isError: false,
32
+ };
33
+ }
34
+ catch (error) {
35
+ const handledError = ErrorHandler.handleError(error, {
36
+ operation: "searchPubMedArticlesToolHandler",
37
+ context: richContext,
38
+ input,
39
+ rethrow: false,
40
+ });
41
+ const mcpError = handledError instanceof McpError
42
+ ? handledError
43
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while searching PubMed articles.", {
44
+ originalErrorName: handledError.name,
45
+ originalErrorMessage: handledError.message,
46
+ });
47
+ return {
48
+ content: [
49
+ {
50
+ type: "text",
51
+ text: JSON.stringify({
52
+ error: {
53
+ code: mcpError.code,
54
+ message: mcpError.message,
55
+ details: mcpError.details,
56
+ },
57
+ }),
58
+ },
59
+ ],
60
+ isError: true,
61
+ };
62
+ }
25
63
  });
26
- }
27
- catch (error) {
28
- ErrorHandler.handleError(new McpError(// Create an McpError for consistent handling
29
- BaseErrorCode.INITIALIZATION_FAILED, "Failed to register searchPubMedArticles tool", {
30
- originalError: error instanceof Error ? error.message : String(error),
31
- }), {
32
- operation,
33
- context,
34
- errorCode: BaseErrorCode.INITIALIZATION_FAILED, // errorCode is still useful for categorization
35
- critical: true,
36
- });
37
- }
64
+ logger.notice(`Tool '${toolName}' registered.`, context);
65
+ }, {
66
+ operation,
67
+ context,
68
+ errorCode: BaseErrorCode.INITIALIZATION_FAILED,
69
+ critical: true,
70
+ });
38
71
  }
@@ -13,12 +13,4 @@
13
13
  import { ServerType } from "@hono/node-server";
14
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
15
  import { RequestContext } from "../../utils/index.js";
16
- /**
17
- * Sets up and starts the Streamable HTTP transport layer for the MCP server.
18
- *
19
- * @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
20
- * @param parentContext - Logging context from the main server startup process.
21
- * @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
22
- * @throws {Error} If the server fails to start after all port retries.
23
- */
24
16
  export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<ServerType>;