@cyanheads/pubmed-mcp-server 1.1.1 → 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.
Files changed (26) hide show
  1. package/README.md +19 -16
  2. package/dist/config/index.d.ts +4 -2
  3. package/dist/config/index.js +52 -7
  4. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +59 -160
  5. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +1 -1
  6. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +11 -15
  7. package/dist/mcp-server/tools/generatePubMedChart/logic.js +21 -22
  8. package/dist/mcp-server/tools/generatePubMedChart/registration.d.ts +1 -1
  9. package/dist/mcp-server/tools/generatePubMedChart/registration.js +10 -19
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +3 -3
  11. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +9 -6
  12. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +1 -2
  13. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +16 -26
  14. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +1 -3
  15. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +1 -2
  16. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -6
  17. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +1 -1
  18. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +9 -14
  19. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +9 -4
  20. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +1 -1
  21. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +13 -20
  22. package/dist/mcp-server/transports/httpTransport.js +9 -0
  23. package/dist/utils/internal/logger.js +49 -51
  24. package/dist/utils/security/rateLimiter.d.ts +4 -0
  25. package/dist/utils/security/rateLimiter.js +4 -0
  26. package/package.json +2 -3
@@ -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({
@@ -83,9 +86,8 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
83
86
  input: sanitizeInputForLogging(input),
84
87
  });
85
88
  logger.info(`Executing 'generate_pubmed_chart'. Chart type: ${input.chartType}, Output format: ${input.outputFormat}`, operationContext);
86
- if (input.outputFormat !== "png") { // Changed from svg to png
87
- const unsupportedFormatError = new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported output format: ${input.outputFormat}. Currently, only 'png' is supported.`, // Changed message
88
- { requestedFormat: input.outputFormat });
89
+ if (input.outputFormat !== "png") {
90
+ const unsupportedFormatError = new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported output format: ${input.outputFormat}. Currently, only 'png' is supported.`, { requestedFormat: input.outputFormat });
89
91
  logger.warning(unsupportedFormatError.message, operationContext);
90
92
  return {
91
93
  content: [
@@ -104,15 +106,14 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
104
106
  };
105
107
  }
106
108
  try {
107
- let vegaLiteSpec = {
109
+ const vegaLiteSpec = {
108
110
  $schema: "https://vega.github.io/schema/vega-lite/v5.json",
109
111
  title: input.title,
110
112
  width: input.width,
111
113
  height: input.height,
112
114
  data: { values: input.dataValues },
113
- encoding: {}, // To be populated by chart type specific logic
115
+ encoding: {},
114
116
  };
115
- // Default field types if not provided
116
117
  const yEncType = input.yFieldType || "quantitative";
117
118
  const colorEncType = input.colorFieldType || "nominal";
118
119
  const seriesEncType = input.seriesFieldType || "nominal";
@@ -157,14 +158,12 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
157
158
  },
158
159
  };
159
160
  if (input.seriesField) {
160
- // For line charts, seriesField is typically used for color
161
161
  vegaLiteSpec.encoding.color = {
162
162
  field: input.seriesField,
163
163
  type: seriesEncType,
164
164
  };
165
165
  }
166
166
  else if (input.colorField) {
167
- // Allow direct colorField as well
168
167
  vegaLiteSpec.encoding.color = {
169
168
  field: input.colorField,
170
169
  type: colorEncType,
@@ -173,7 +172,7 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
173
172
  break;
174
173
  case "scatter":
175
174
  xEncType = input.xFieldType || "quantitative";
176
- vegaLiteSpec.mark = "point"; // "circle" is also an option
175
+ vegaLiteSpec.mark = "point";
177
176
  vegaLiteSpec.encoding = {
178
177
  x: {
179
178
  field: input.xField,
@@ -198,28 +197,28 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
198
197
  type: sizeEncType,
199
198
  };
200
199
  }
201
- // Add shape encoding here if shapeField is implemented
202
200
  break;
203
- // No default case needed as chartType is an enum and Zod validates it.
204
201
  }
205
202
  const compiledVegaSpec = vegaLite.compile(vegaLiteSpec).spec;
206
203
  const view = new vega.View(vega.parse(compiledVegaSpec), {
207
- renderer: "canvas", // Explicitly set renderer to 'canvas'
204
+ renderer: "canvas",
208
205
  });
209
- // const svgString = await view.toSVG(); // Old SVG method
210
- // New PNG method
211
- // Initialize the view to ensure canvas is ready
212
- await view.runAsync(); // Initialize and run the view
213
- const canvas = await view.toCanvas(); // Render to canvas
214
- // Cast to 'any' to access toBuffer, assuming it's a Node Canvas instance at runtime
215
- 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");
216
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
+ });
217
216
  return {
218
217
  content: [
219
218
  {
220
219
  type: "image",
221
220
  data: base64Data,
222
- mimeType: "image/png", // Changed MIME type to image/png
221
+ mimeType: "image/png",
223
222
  },
224
223
  ],
225
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, McpError } from "../../../types-global/errors.js";
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
- try {
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(`Tool 'generate_pubmed_chart' registered with updated schema (added scatter).`, regContext);
21
- }
22
- catch (error) {
23
- const mcpError = error instanceof McpError
24
- ? error
25
- : new McpError(BaseErrorCode.INITIALIZATION_FAILED, `Failed to register 'generate_pubmed_chart': ${error instanceof Error ? error.message : String(error)}`, {
26
- originalErrorName: error instanceof Error ? error.name : "UnknownError",
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 ? `<em>${journalInfo.title}</em>` : "N/A";
203
- const volume = journalInfo?.volume ? `<em>${journalInfo.volume}</em>` : "";
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 ? `<em>${journalInfo.title}</em>` : "N/A";
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, // Link to parent context
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, // Initialize
31
- message: undefined, // Initialize
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, McpError } from "../../../types-global/errors.js"; // Import McpError
8
- import { ErrorHandler, requestContextService, } from "../../../utils/index.js"; // Import requestContextService
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 (uses EFetch)."),
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
- try {
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, // Pass .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, // Link to registration context
53
+ parentRequestId: registrationContext.requestId,
57
54
  operation: "getPubMedArticleConnectionsToolHandler",
58
- mcpToolContext: toolContext, // Include MCP-provided context
55
+ mcpToolContext: toolContext,
59
56
  });
60
- return await handleGetPubMedArticleConnections(validatedInput, richLogicContext);
57
+ return handleGetPubMedArticleConnections(validatedInput, richLogicContext);
61
58
  });
62
- // Consistent with other tools, explicit success logging here might be omitted,
63
- // relying on ErrorHandler for issues or higher-level logging.
64
- }
65
- catch (error) {
66
- ErrorHandler.handleError(new McpError(// Create an McpError for consistent handling
67
- BaseErrorCode.INITIALIZATION_FAILED, "Failed to register 'get_pubmed_article_connections' tool.", {
68
- originalError: error instanceof Error ? error.message : String(error),
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
  }
@@ -35,9 +35,7 @@ function removeEmptyObjectsRecursively(obj) {
35
35
  }
36
36
  if (Array.isArray(obj)) {
37
37
  // If it's an array, recurse on each element and filter out empty objects/arrays
38
- const newArr = obj
39
- .map(removeEmptyObjectsRecursively)
40
- .filter((item) => {
38
+ const newArr = obj.map(removeEmptyObjectsRecursively).filter((item) => {
41
39
  if (item === null || item === undefined)
42
40
  return false;
43
41
  if (Array.isArray(item) && item.length === 0)
@@ -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 { // Re-export for registration
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, // Re-export for registration
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, // Using a generic 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, // Adding some input context to error
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, McpError } from "../../../types-global/errors.js";
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
- try {
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
- catch (error) {
27
- ErrorHandler.handleError(new McpError(BaseErrorCode.INITIALIZATION_FAILED, "Failed to register pubmed_research_agent tool", {
28
- originalError: error instanceof Error ? error.message : String(error),
29
- }), {
30
- operation,
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; // Use history with retmax
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, // These are the PMIDs from ESearch, limited by input.maxResults
189
- briefSummaries, // These should now be limited by currentFetchBriefSummaries
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, McpError } from "../../../types-global/errors.js"; // Import McpError
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
- 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
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, // Include MCP-provided context if any
20
+ mcpToolContext: toolContext,
23
21
  });
24
22
  return searchPubMedArticlesLogic(input, richContext);
25
23
  });
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
- }
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
  */