@cyanheads/pubmed-mcp-server 1.1.4 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +10 -12
  2. package/dist/mcp-server/server.d.ts +0 -7
  3. package/dist/mcp-server/server.js +17 -53
  4. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +6 -2
  5. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +94 -202
  6. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +43 -7
  7. package/dist/mcp-server/tools/generatePubMedChart/logic.d.ts +9 -28
  8. package/dist/mcp-server/tools/generatePubMedChart/logic.js +133 -192
  9. package/dist/mcp-server/tools/generatePubMedChart/registration.js +57 -13
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +1 -1
  11. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +1 -1
  12. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +27 -4
  13. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +50 -45
  14. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +1 -1
  15. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +0 -24
  16. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +51 -45
  17. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +2 -4
  18. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +7 -41
  19. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +49 -8
  20. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +12 -10
  21. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +68 -126
  22. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +46 -6
  23. package/dist/mcp-server/transports/{authentication → auth/core}/authContext.d.ts +2 -2
  24. package/dist/mcp-server/transports/{authentication → auth/core}/authContext.js +1 -1
  25. package/dist/mcp-server/transports/{authentication/types.d.ts → auth/core/authTypes.d.ts} +1 -1
  26. package/dist/mcp-server/transports/{authentication/types.js → auth/core/authTypes.js} +1 -1
  27. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.d.ts +1 -1
  28. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.js +3 -3
  29. package/dist/mcp-server/transports/auth/index.d.ts +10 -0
  30. package/dist/mcp-server/transports/auth/index.js +9 -0
  31. package/dist/mcp-server/transports/{authentication/authMiddleware.d.ts → auth/strategies/jwt/jwtMiddleware.d.ts} +4 -12
  32. package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +36 -43
  33. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts +2 -6
  34. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js +33 -18
  35. package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
  36. package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
  37. package/dist/mcp-server/transports/httpTransport.d.ts +11 -14
  38. package/dist/mcp-server/transports/httpTransport.js +91 -389
  39. package/package.json +13 -21
  40. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +0 -79
  41. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +0 -82
  42. package/dist/mcp-server/resources/echoResource/index.d.ts +0 -13
  43. package/dist/mcp-server/resources/echoResource/index.js +0 -13
  44. package/dist/mcp-server/resources/echoResource/registration.d.ts +0 -30
  45. package/dist/mcp-server/resources/echoResource/registration.js +0 -168
  46. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +0 -6
  47. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +0 -6
@@ -2,7 +2,7 @@
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 } from "../../../types-global/errors.js";
5
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
6
6
  import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
7
7
  import { FetchPubMedContentInputSchema, fetchPubMedContentLogic, } from "./logic.js";
8
8
  /**
@@ -11,19 +11,55 @@ import { FetchPubMedContentInputSchema, fetchPubMedContentLogic, } from "./logic
11
11
  */
12
12
  export async function registerFetchPubMedContentTool(server) {
13
13
  const operation = "registerFetchPubMedContentTool";
14
+ const toolName = "fetch_pubmed_content";
15
+ const toolDescription = "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.";
14
16
  const context = requestContextService.createRequestContext({ operation });
15
- await ErrorHandler.tryCatch(() => {
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
- async (input, toolContext) => {
18
- // Added 'any' type for toolContext
17
+ await ErrorHandler.tryCatch(async () => {
18
+ server.tool(toolName, toolDescription, FetchPubMedContentInputSchema._def.schema.shape, async (input, toolContext) => {
19
19
  const richContext = requestContextService.createRequestContext({
20
20
  parentRequestId: context.requestId,
21
21
  operation: "fetchPubMedContentToolHandler",
22
22
  mcpToolContext: toolContext,
23
+ input,
23
24
  });
24
- return fetchPubMedContentLogic(input, richContext);
25
+ try {
26
+ const result = await fetchPubMedContentLogic(input, richContext);
27
+ return {
28
+ content: [{ type: "text", text: result.content }],
29
+ isError: false,
30
+ };
31
+ }
32
+ catch (error) {
33
+ const handledError = ErrorHandler.handleError(error, {
34
+ operation: "fetchPubMedContentToolHandler",
35
+ context: richContext,
36
+ input,
37
+ rethrow: false,
38
+ });
39
+ const mcpError = handledError instanceof McpError
40
+ ? handledError
41
+ : new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while fetching PubMed content.", {
42
+ originalErrorName: handledError.name,
43
+ originalErrorMessage: handledError.message,
44
+ });
45
+ return {
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: JSON.stringify({
50
+ error: {
51
+ code: mcpError.code,
52
+ message: mcpError.message,
53
+ details: mcpError.details,
54
+ },
55
+ }),
56
+ },
57
+ ],
58
+ isError: true,
59
+ };
60
+ }
25
61
  });
26
- logger.notice("Tool 'fetch_pubmed_content' registered.", context);
62
+ logger.notice(`Tool '${toolName}' registered.`, context);
27
63
  }, {
28
64
  operation,
29
65
  context,
@@ -1,13 +1,7 @@
1
- /**
2
- * @fileoverview Core logic for the generate_pubmed_chart tool.
3
- * Generates charts from parameterized input by creating Vega-Lite specifications.
4
- * @module src/mcp-server/tools/generatePubMedChart/logic
5
- */
6
- import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
7
1
  import { z } from "zod";
8
2
  import { RequestContext } from "../../../utils/index.js";
9
3
  export declare const GeneratePubMedChartInputSchema: z.ZodObject<{
10
- chartType: z.ZodEnum<["bar", "line", "scatter"]>;
4
+ chartType: z.ZodEnum<["bar", "line", "scatter", "pie", "doughnut", "bubble", "radar", "polarArea"]>;
11
5
  title: z.ZodOptional<z.ZodString>;
12
6
  width: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
13
7
  height: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -15,33 +9,21 @@ export declare const GeneratePubMedChartInputSchema: z.ZodObject<{
15
9
  outputFormat: z.ZodDefault<z.ZodEnum<["png"]>>;
16
10
  xField: z.ZodString;
17
11
  yField: z.ZodString;
18
- xFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
19
- yFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
20
- colorField: z.ZodOptional<z.ZodString>;
21
- colorFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
22
12
  seriesField: z.ZodOptional<z.ZodString>;
23
- seriesFieldType: z.ZodOptional<z.ZodEnum<["nominal", "ordinal", "quantitative", "temporal"]>>;
24
13
  sizeField: z.ZodOptional<z.ZodString>;
25
- sizeFieldType: z.ZodOptional<z.ZodEnum<["quantitative", "ordinal"]>>;
26
14
  }, "strip", z.ZodTypeAny, {
27
15
  width: number;
28
16
  height: number;
29
17
  outputFormat: "png";
30
- chartType: "bar" | "line" | "scatter";
18
+ chartType: "bar" | "line" | "scatter" | "pie" | "doughnut" | "bubble" | "radar" | "polarArea";
31
19
  dataValues: Record<string, any>[];
32
20
  xField: string;
33
21
  yField: string;
34
22
  title?: string | undefined;
35
- xFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
36
- yFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
37
- colorField?: string | undefined;
38
- colorFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
39
23
  seriesField?: string | undefined;
40
- seriesFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
41
24
  sizeField?: string | undefined;
42
- sizeFieldType?: "ordinal" | "quantitative" | undefined;
43
25
  }, {
44
- chartType: "bar" | "line" | "scatter";
26
+ chartType: "bar" | "line" | "scatter" | "pie" | "doughnut" | "bubble" | "radar" | "polarArea";
45
27
  dataValues: Record<string, any>[];
46
28
  xField: string;
47
29
  yField: string;
@@ -49,14 +31,13 @@ export declare const GeneratePubMedChartInputSchema: z.ZodObject<{
49
31
  width?: number | undefined;
50
32
  height?: number | undefined;
51
33
  outputFormat?: "png" | undefined;
52
- xFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
53
- yFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
54
- colorField?: string | undefined;
55
- colorFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
56
34
  seriesField?: string | undefined;
57
- seriesFieldType?: "nominal" | "ordinal" | "quantitative" | "temporal" | undefined;
58
35
  sizeField?: string | undefined;
59
- sizeFieldType?: "ordinal" | "quantitative" | undefined;
60
36
  }>;
61
37
  export type GeneratePubMedChartInput = z.infer<typeof GeneratePubMedChartInputSchema>;
62
- export declare function generatePubMedChartLogic(input: GeneratePubMedChartInput, parentRequestContext: RequestContext): Promise<CallToolResult>;
38
+ export type GeneratePubMedChartOutput = {
39
+ base64Data: string;
40
+ chartType: string;
41
+ dataPoints: number;
42
+ };
43
+ export declare function generatePubMedChartLogic(input: GeneratePubMedChartInput, parentRequestContext: RequestContext): Promise<GeneratePubMedChartOutput>;
@@ -1,17 +1,26 @@
1
- import * as vega from "vega";
2
- import * as vegaLite from "vega-lite";
1
+ /**
2
+ * @fileoverview Core logic for the generate_pubmed_chart tool.
3
+ * Generates charts from parameterized input by creating Chart.js configurations
4
+ * and rendering them on the server using chartjs-node-canvas.
5
+ * @module src/mcp-server/tools/generatePubMedChart/logic
6
+ */
7
+ import { ChartJSNodeCanvas } from "chartjs-node-canvas";
3
8
  import { z } from "zod";
4
9
  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
10
10
  import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
11
11
  export const GeneratePubMedChartInputSchema = z.object({
12
12
  chartType: z
13
- .enum(["bar", "line", "scatter"])
14
- .describe("Required. Specifies the type of chart to generate. Options: 'bar', 'line', 'scatter'."),
13
+ .enum([
14
+ "bar",
15
+ "line",
16
+ "scatter",
17
+ "pie",
18
+ "doughnut",
19
+ "bubble",
20
+ "radar",
21
+ "polarArea",
22
+ ])
23
+ .describe("Required. Specifies the type of chart to generate. Options: 'bar', 'line', 'scatter', 'pie', 'doughnut', 'bubble', 'radar', 'polarArea'."),
15
24
  title: z
16
25
  .string()
17
26
  .optional()
@@ -21,22 +30,22 @@ export const GeneratePubMedChartInputSchema = z.object({
21
30
  .int()
22
31
  .positive()
23
32
  .optional()
24
- .default(400)
25
- .describe("Optional. The width of the chart canvas in pixels. Must be a positive integer. Default: 400."),
33
+ .default(800)
34
+ .describe("Optional. The width of the chart canvas in pixels. Must be a positive integer. Default: 800."),
26
35
  height: z
27
36
  .number()
28
37
  .int()
29
38
  .positive()
30
39
  .optional()
31
- .default(300)
32
- .describe("Optional. The height of the chart canvas in pixels. Must be a positive integer. Default: 300."),
40
+ .default(600)
41
+ .describe("Optional. The height of the chart canvas in pixels. Must be a positive integer. Default: 600."),
33
42
  dataValues: z
34
43
  .array(z.record(z.string(), z.any()))
35
44
  .min(1)
36
45
  .describe("Required. An array of data objects used to plot the chart. Each object represents a data point or bar, structured as key-value pairs (e.g., [{ 'year': '2020', 'articles': 150 }, { 'year': '2021', 'articles': 180 }]). Must contain at least one data object."),
37
46
  outputFormat: z
38
- .enum(["png"]) // Changed from svg to png
39
- .default("png") // Changed default to png
47
+ .enum(["png"])
48
+ .default("png")
40
49
  .describe("Specifies the output format for the chart. Currently, only 'png' (Portable Network Graphics) is supported and is the default."),
41
50
  xField: z
42
51
  .string()
@@ -44,209 +53,141 @@ export const GeneratePubMedChartInputSchema = z.object({
44
53
  yField: z
45
54
  .string()
46
55
  .describe("Required. The name of the field in `dataValues` to be used for the Y-axis (vertical). This field determines the values plotted upwards on the chart (e.g., 'articles', 'expressionLevel', 'citationCount')."),
47
- xFieldType: z
48
- .enum(["nominal", "ordinal", "quantitative", "temporal"])
49
- .optional()
50
- .describe("Optional. Specifies the data type of the X-axis field. Options: 'nominal' (categories), 'ordinal' (ordered categories), 'quantitative' (numerical), 'temporal' (dates/times). If omitted, a suitable default is chosen based on `chartType` (e.g., 'nominal' for bar charts, 'temporal' for line charts, 'quantitative' for scatter plots)."),
51
- yFieldType: z
52
- .enum(["nominal", "ordinal", "quantitative", "temporal"])
53
- .optional()
54
- .describe("Optional. Specifies the data type of the Y-axis field. Options: 'nominal', 'ordinal', 'quantitative', 'temporal'. Defaults to 'quantitative' if omitted."),
55
- // Optional fields for various chart types
56
- colorField: z
57
- .string()
58
- .optional()
59
- .describe("Optional. The name of the field in `dataValues` to use for color encoding. This can differentiate bars, lines, or points by color based on the values in this field (e.g., 'studyType', 'country')."),
60
- colorFieldType: z
61
- .enum(["nominal", "ordinal", "quantitative", "temporal"])
62
- .optional()
63
- .describe("Optional. Specifies the data type of the `colorField`. Options: 'nominal', 'ordinal', 'quantitative', 'temporal'. Defaults to 'nominal' if `colorField` is provided and this is omitted."),
64
56
  seriesField: z
65
57
  .string()
66
58
  .optional()
67
- .describe("Optional. Primarily for line charts. The name of the field in `dataValues` used to create multiple distinct lines (series) on the same chart. Each unique value in this field will correspond to a separate line (e.g., 'drugName' to plot different drug efficacy trends). Often used with `colorField` implicitly or explicitly."),
68
- seriesFieldType: z
69
- .enum(["nominal", "ordinal", "quantitative", "temporal"])
70
- .optional()
71
- .describe("Optional. Specifies the data type of the `seriesField`. Options: 'nominal', 'ordinal', 'quantitative', 'temporal'. Defaults to 'nominal' if `seriesField` is provided and this is omitted."),
72
- // Scatter plot specific optional fields (can be expanded)
59
+ .describe("Optional. The name of the field in `dataValues` used to create multiple distinct lines or bar groups (series) on the same chart. Each unique value in this field will correspond to a separate dataset."),
73
60
  sizeField: z
74
61
  .string()
75
62
  .optional()
76
- .describe("Optional. For scatter plots. The name of the field in `dataValues` to use for encoding the size of the points. Larger values in this field will result in larger points on the scatter plot (e.g., 'sampleSize', 'effectMagnitude')."),
77
- sizeFieldType: z
78
- .enum(["quantitative", "ordinal"])
79
- .optional()
80
- .describe("Optional. Specifies the data type of the `sizeField`. Options: 'quantitative', 'ordinal'. Defaults to 'quantitative' if `sizeField` is provided and this is omitted."),
63
+ .describe("Optional. For bubble charts. The name of the field in `dataValues` to use for encoding the size of the bubbles. Larger values in this field will result in larger bubbles (e.g., 'sampleSize', 'effectMagnitude')."),
81
64
  });
65
+ // Helper to group data by a series field
66
+ function groupDataBySeries(data, xField, yField, seriesField) {
67
+ const series = new Map();
68
+ for (const item of data) {
69
+ const seriesName = item[seriesField];
70
+ if (!series.has(seriesName)) {
71
+ series.set(seriesName, []);
72
+ }
73
+ series.get(seriesName).push({ x: item[xField], y: item[yField] });
74
+ }
75
+ return series;
76
+ }
82
77
  export async function generatePubMedChartLogic(input, parentRequestContext) {
83
78
  const operationContext = requestContextService.createRequestContext({
84
79
  parentRequestId: parentRequestContext.requestId,
85
80
  operation: "generatePubMedChartLogicExecution",
86
81
  input: sanitizeInputForLogging(input),
87
82
  });
88
- logger.info(`Executing 'generate_pubmed_chart'. Chart type: ${input.chartType}, Output format: ${input.outputFormat}`, operationContext);
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 });
91
- logger.warning(unsupportedFormatError.message, operationContext);
92
- return {
93
- content: [
94
- {
95
- type: "text",
96
- text: JSON.stringify({
97
- error: {
98
- code: unsupportedFormatError.code,
99
- message: unsupportedFormatError.message,
100
- details: unsupportedFormatError.details,
101
- },
102
- }),
103
- },
104
- ],
105
- isError: true,
106
- };
83
+ logger.info(`Executing 'generate_pubmed_chart' with Chart.js. Chart type: ${input.chartType}`, operationContext);
84
+ const { width, height, chartType, dataValues, xField, yField, title, seriesField, sizeField, } = input;
85
+ const chartJSNodeCanvas = new ChartJSNodeCanvas({
86
+ width,
87
+ height,
88
+ chartCallback: (ChartJS) => {
89
+ ChartJS.defaults.responsive = false;
90
+ ChartJS.defaults.maintainAspectRatio = false;
91
+ },
92
+ });
93
+ const labels = [...new Set(dataValues.map((item) => item[xField]))];
94
+ let datasets;
95
+ if (seriesField) {
96
+ const groupedData = groupDataBySeries(dataValues, xField, yField, seriesField);
97
+ datasets = Array.from(groupedData.entries()).map(([seriesName, data]) => ({
98
+ label: seriesName,
99
+ data: labels.map(label => {
100
+ const point = data.find(p => p.x === label);
101
+ return point ? point.y : null;
102
+ }),
103
+ // You can add backgroundColor, borderColor etc. here for styling
104
+ }));
107
105
  }
108
- try {
109
- const vegaLiteSpec = {
110
- $schema: "https://vega.github.io/schema/vega-lite/v5.json",
111
- title: input.title,
112
- width: input.width,
113
- height: input.height,
114
- data: { values: input.dataValues },
115
- encoding: {},
116
- };
117
- const yEncType = input.yFieldType || "quantitative";
118
- const colorEncType = input.colorFieldType || "nominal";
119
- const seriesEncType = input.seriesFieldType || "nominal";
120
- const sizeEncType = input.sizeFieldType || "quantitative";
121
- let xEncType;
122
- switch (input.chartType) {
123
- case "bar":
124
- xEncType = input.xFieldType || "nominal";
125
- vegaLiteSpec.mark = "bar";
126
- vegaLiteSpec.encoding = {
127
- x: {
128
- field: input.xField,
129
- type: xEncType,
130
- axis: { title: input.xField },
131
- },
132
- y: {
133
- field: input.yField,
134
- type: yEncType,
135
- axis: { title: input.yField },
136
- },
137
- };
138
- if (input.colorField) {
139
- vegaLiteSpec.encoding.color = {
140
- field: input.colorField,
141
- type: colorEncType,
142
- };
143
- }
144
- break;
145
- case "line":
146
- xEncType = input.xFieldType || "temporal";
147
- vegaLiteSpec.mark = "line";
148
- vegaLiteSpec.encoding = {
149
- x: {
150
- field: input.xField,
151
- type: xEncType,
152
- axis: { title: input.xField },
153
- },
154
- y: {
155
- field: input.yField,
156
- type: yEncType,
157
- axis: { title: input.yField },
158
- },
159
- };
160
- if (input.seriesField) {
161
- vegaLiteSpec.encoding.color = {
162
- field: input.seriesField,
163
- type: seriesEncType,
164
- };
165
- }
166
- else if (input.colorField) {
167
- vegaLiteSpec.encoding.color = {
168
- field: input.colorField,
169
- type: colorEncType,
170
- };
171
- }
172
- break;
173
- case "scatter":
174
- xEncType = input.xFieldType || "quantitative";
175
- vegaLiteSpec.mark = "point";
176
- vegaLiteSpec.encoding = {
106
+ else {
107
+ datasets = [
108
+ {
109
+ label: yField,
110
+ data: labels.map(label => {
111
+ const item = dataValues.find(d => d[xField] === label);
112
+ return item ? item[yField] : null;
113
+ }),
114
+ },
115
+ ];
116
+ }
117
+ // For scatter and bubble charts, the data format is different
118
+ if (chartType === 'scatter' || chartType === 'bubble') {
119
+ if (seriesField) {
120
+ const groupedData = groupDataBySeries(dataValues, xField, yField, seriesField);
121
+ datasets = Array.from(groupedData.entries()).map(([seriesName, data]) => ({
122
+ label: seriesName,
123
+ data: data.map(point => ({
124
+ x: point.x,
125
+ y: point.y,
126
+ r: chartType === 'bubble' && sizeField ? dataValues.find(d => d[xField] === point.x)[sizeField] : undefined
127
+ })),
128
+ }));
129
+ }
130
+ else {
131
+ datasets = [{
132
+ label: yField,
133
+ data: dataValues.map(item => ({
134
+ x: item[xField],
135
+ y: item[yField],
136
+ r: chartType === 'bubble' && sizeField ? item[sizeField] : undefined
137
+ })),
138
+ }];
139
+ }
140
+ }
141
+ const configuration = {
142
+ type: chartType,
143
+ data: {
144
+ labels: (chartType !== 'scatter' && chartType !== 'bubble') ? labels : undefined,
145
+ datasets: datasets,
146
+ },
147
+ options: {
148
+ plugins: {
149
+ title: {
150
+ display: !!title,
151
+ text: title,
152
+ },
153
+ },
154
+ scales: chartType === "pie" || chartType === "doughnut" || chartType === "polarArea"
155
+ ? undefined
156
+ : {
177
157
  x: {
178
- field: input.xField,
179
- type: xEncType,
180
- axis: { title: input.xField },
158
+ title: {
159
+ display: true,
160
+ text: xField,
161
+ },
181
162
  },
182
163
  y: {
183
- field: input.yField,
184
- type: yEncType,
185
- axis: { title: input.yField },
164
+ title: {
165
+ display: true,
166
+ text: yField,
167
+ },
186
168
  },
187
- };
188
- if (input.colorField) {
189
- vegaLiteSpec.encoding.color = {
190
- field: input.colorField,
191
- type: colorEncType,
192
- };
193
- }
194
- if (input.sizeField) {
195
- vegaLiteSpec.encoding.size = {
196
- field: input.sizeField,
197
- type: sizeEncType,
198
- };
199
- }
200
- break;
201
- }
202
- const compiledVegaSpec = vegaLite.compile(vegaLiteSpec).spec;
203
- const view = new vega.View(vega.parse(compiledVegaSpec), {
204
- renderer: "canvas",
205
- });
206
- await view.runAsync();
207
- const canvas = await view.toCanvas();
208
- const imageBuffer = await canvas.toBuffer("image/png");
169
+ },
170
+ },
171
+ };
172
+ try {
173
+ const imageBuffer = await chartJSNodeCanvas.renderToBuffer(configuration);
209
174
  const base64Data = imageBuffer.toString("base64");
210
- logger.notice("Successfully generated chart.", {
175
+ logger.notice("Successfully generated chart with Chart.js.", {
211
176
  ...operationContext,
212
177
  chartType: input.chartType,
213
- outputFormat: input.outputFormat,
214
178
  dataPoints: input.dataValues.length,
215
179
  });
216
180
  return {
217
- content: [
218
- {
219
- type: "image",
220
- data: base64Data,
221
- mimeType: "image/png",
222
- },
223
- ],
224
- isError: false,
181
+ base64Data,
182
+ chartType: input.chartType,
183
+ dataPoints: input.dataValues.length,
225
184
  };
226
185
  }
227
186
  catch (error) {
228
- logger.error("Execution failed for 'generate_pubmed_chart'", error, operationContext);
229
- const mcpError = error instanceof McpError
230
- ? error
231
- : new McpError(BaseErrorCode.INTERNAL_ERROR, `'generate_pubmed_chart' failed: ${error.message || "Internal server error during chart generation."}`, {
232
- originalErrorName: error.name,
233
- originalErrorMessage: error.message,
234
- requestId: operationContext.requestId,
235
- });
236
- return {
237
- content: [
238
- {
239
- type: "text",
240
- text: JSON.stringify({
241
- error: {
242
- code: mcpError.code,
243
- message: mcpError.message,
244
- details: mcpError.details,
245
- },
246
- }),
247
- },
248
- ],
249
- isError: true,
250
- };
187
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, `Chart generation failed: ${error.message || "Internal server error during chart generation."}`, {
188
+ ...operationContext,
189
+ originalErrorName: error.name,
190
+ originalErrorMessage: error.message,
191
+ });
251
192
  }
252
193
  }
@@ -1,26 +1,70 @@
1
- import { BaseErrorCode } from "../../../types-global/errors.js";
1
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
2
2
  import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
3
3
  import { GeneratePubMedChartInputSchema, generatePubMedChartLogic, } from "./logic.js";
4
4
  export async function registerGeneratePubMedChartTool(server) {
5
5
  const operation = "registerGeneratePubMedChartTool";
6
- const regContext = requestContextService.createRequestContext({ operation });
7
- await ErrorHandler.tryCatch(() => {
8
- server.tool("generate_pubmed_chart", "Generates a customizable chart (PNG) from structured data. " +
9
- "Supports 'bar', 'line', and 'scatter' plots. " +
10
- "Requires data values and field mappings for axes. " +
11
- "Optional parameters allow for titles, dimensions, and color/size/series encoding. " +
12
- "Internally uses Vega-Lite and a canvas renderer to produce a Base64-encoded PNG image.", GeneratePubMedChartInputSchema.shape, async (validatedInput, mcpProvidedContext) => {
13
- const handlerRequestContext = requestContextService.createRequestContext({
14
- parentRequestId: regContext.requestId,
6
+ const toolName = "generate_pubmed_chart";
7
+ const toolDescription = "Generates a customizable chart (PNG) from structured data. " +
8
+ "Supports 'bar', 'line', 'scatter', 'pie', 'doughnut', 'bubble', 'radar', and 'polarArea' plots. " +
9
+ "Requires data values and field mappings for axes. " +
10
+ "Optional parameters allow for titles and dimensions. " +
11
+ "Internally uses Chart.js and chartjs-node-canvas to produce a Base64-encoded PNG image.";
12
+ const context = requestContextService.createRequestContext({ operation });
13
+ await ErrorHandler.tryCatch(async () => {
14
+ server.tool(toolName, toolDescription, GeneratePubMedChartInputSchema.shape, async (input, mcpProvidedContext) => {
15
+ const richContext = requestContextService.createRequestContext({
16
+ parentRequestId: context.requestId,
15
17
  operation: "generatePubMedChartToolHandler",
16
18
  mcpToolContext: mcpProvidedContext,
19
+ input,
17
20
  });
18
- return generatePubMedChartLogic(validatedInput, handlerRequestContext);
21
+ try {
22
+ const result = await generatePubMedChartLogic(input, richContext);
23
+ return {
24
+ content: [
25
+ {
26
+ type: "image",
27
+ data: result.base64Data,
28
+ mimeType: "image/png",
29
+ },
30
+ ],
31
+ isError: false,
32
+ };
33
+ }
34
+ catch (error) {
35
+ const handledError = ErrorHandler.handleError(error, {
36
+ operation: "generatePubMedChartToolHandler",
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 generating the chart.", {
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
+ }
19
63
  });
20
- logger.notice("Tool 'generate_pubmed_chart' registered.", regContext);
64
+ logger.notice(`Tool '${toolName}' registered.`, context);
21
65
  }, {
22
66
  operation,
23
- context: regContext,
67
+ context,
24
68
  errorCode: BaseErrorCode.INITIALIZATION_FAILED,
25
69
  critical: true,
26
70
  });
@@ -4,6 +4,6 @@
4
4
  * @module src/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter
5
5
  */
6
6
  import { RequestContext } from "../../../../utils/index.js";
7
- import type { GetPubMedArticleConnectionsInput } from "../registration.js";
7
+ import type { GetPubMedArticleConnectionsInput } from "./index.js";
8
8
  import type { ToolOutputData } from "./types.js";
9
9
  export declare function handleCitationFormats(input: GetPubMedArticleConnectionsInput, outputData: ToolOutputData, context: RequestContext): Promise<void>;
@@ -4,6 +4,6 @@
4
4
  * @module src/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler
5
5
  */
6
6
  import { RequestContext } from "../../../../utils/index.js";
7
- import type { GetPubMedArticleConnectionsInput } from "../registration.js";
7
+ import type { GetPubMedArticleConnectionsInput } from "./index.js";
8
8
  import type { ToolOutputData } from "./types.js";
9
9
  export declare function handleELinkRelationships(input: GetPubMedArticleConnectionsInput, outputData: ToolOutputData, context: RequestContext): Promise<void>;