pubmed_generate_chart
Transform structured data from PubMed into customizable charts. Specify chart type, axes, and dimensions to generate a Base64-encoded PNG image for analysis and visualization.
Instructions
Generates a customizable chart (PNG) from structured data. Supports various plot types and requires data values and field mappings for axes. Returns a Base64-encoded PNG image.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| chartType | Yes | Specifies the type of chart to generate. | |
| dataValues | Yes | An array of data objects to plot the chart (e.g., [{ 'year': '2020', 'articles': 150 }]). | |
| height | No | The height of the chart canvas in pixels. | |
| outputFormat | No | Specifies the output format for the chart. | png |
| seriesField | No | The field name for creating multiple data series on the same chart. | |
| sizeField | No | For bubble charts, the field name for encoding bubble size. | |
| title | No | The main title displayed above the chart. | |
| width | No | The width of the chart canvas in pixels. | |
| xField | Yes | The field name from `dataValues` for the X-axis. | |
| yField | Yes | The field name from `dataValues` for the Y-axis. |
Implementation Reference
- Main handler function that implements the core logic for generating charts from input data using Chart.js and chartjs-node-canvas, returning base64 PNG.export async function pubmedGenerateChartLogic( input: PubMedGenerateChartInput, parentRequestContext: RequestContext, ): Promise<PubMedGenerateChartOutput> { const operationContext = requestContextService.createRequestContext({ parentRequestId: parentRequestContext.requestId, operation: "pubmedGenerateChartLogicExecution", input: sanitizeInputForLogging(input), }); logger.info( `Executing 'pubmed_generate_chart' with Chart.js. Chart type: ${input.chartType}`, operationContext, ); const { width, height, chartType, dataValues, xField, yField, title, seriesField, sizeField, } = input; const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, chartCallback: (ChartJS) => { ChartJS.defaults.responsive = false; ChartJS.defaults.maintainAspectRatio = false; }, }); const labels = [ ...new Set(dataValues.map((item) => item[xField])), ] as string[]; let datasets: ChartConfiguration["data"]["datasets"]; if (seriesField) { const groupedData = groupDataBySeries( dataValues, xField, yField, seriesField, ); datasets = Array.from(groupedData.entries()).map(([seriesName, data]) => ({ label: seriesName, data: labels.map((label) => { const point = data.find((p) => p.x === label); return point ? (point.y as number) : null; }), })); } else { datasets = [ { label: yField, data: labels.map((label) => { const item = dataValues.find((d) => d[xField] === label); return item ? (item[yField] as number) : null; }), }, ]; } // For scatter and bubble charts, the data format is different if (chartType === "scatter" || chartType === "bubble") { if (seriesField) { const groupedData = groupDataBySeries( dataValues, xField, yField, seriesField, ); datasets = Array.from(groupedData.entries()).map( ([seriesName, data]) => ({ label: seriesName, data: data.map((point) => ({ x: point.x as number, y: point.y as number, r: chartType === "bubble" && sizeField ? (dataValues.find((d) => d[xField] === point.x)![ sizeField ] as number) : undefined, })), }), ); } else { datasets = [ { label: yField, data: dataValues.map((item) => ({ x: item[xField] as number, y: item[yField] as number, r: chartType === "bubble" && sizeField ? (item[sizeField] as number) : undefined, })), }, ]; } } const configuration: ChartConfiguration = { type: chartType, data: { labels: chartType !== "scatter" && chartType !== "bubble" ? labels : undefined, datasets: datasets, }, options: { plugins: { title: { display: !!title, text: title, }, }, scales: chartType === "pie" || chartType === "doughnut" || chartType === "polarArea" ? undefined : { x: { title: { display: true, text: xField, }, }, y: { title: { display: true, text: yField, }, }, }, }, }; try { const imageBuffer = await chartJSNodeCanvas.renderToBuffer(configuration); const base64Data = imageBuffer.toString("base64"); logger.notice("Successfully generated chart with Chart.js.", { ...operationContext, chartType: input.chartType, dataPoints: input.dataValues.length, }); return { base64Data, chartType: input.chartType, dataPoints: input.dataValues.length, }; } catch (error: unknown) { const err = error as Error; throw new McpError( BaseErrorCode.INTERNAL_ERROR, `Chart generation failed: ${err.message || "Internal server error during chart generation."}`, { ...operationContext, originalErrorName: err.name, originalErrorMessage: err.message, }, ); } }
- Zod input schema defining parameters for chart generation: type, dimensions, data, axes fields.export const PubMedGenerateChartInputSchema = z.object({ chartType: z .enum([ "bar", "line", "scatter", "pie", "doughnut", "bubble", "radar", "polarArea", ]) .describe("Specifies the type of chart to generate."), title: z .string() .optional() .describe("The main title displayed above the chart."), width: z .number() .int() .positive() .optional() .default(800) .describe("The width of the chart canvas in pixels."), height: z .number() .int() .positive() .optional() .default(600) .describe("The height of the chart canvas in pixels."), dataValues: z .array(z.record(z.string(), z.any())) .min(1) .describe( "An array of data objects to plot the chart (e.g., [{ 'year': '2020', 'articles': 150 }]).", ), outputFormat: z .enum(["png"]) .default("png") .describe("Specifies the output format for the chart."), xField: z .string() .describe("The field name from `dataValues` for the X-axis."), yField: z .string() .describe("The field name from `dataValues` for the Y-axis."), seriesField: z .string() .optional() .describe( "The field name for creating multiple data series on the same chart.", ), sizeField: z .string() .optional() .describe("For bubble charts, the field name for encoding bubble size."), });
- Registers the 'pubmed_generate_chart' tool on the MCP server with description, input schema, and handler that calls the logic function.export async function registerPubMedGenerateChartTool( server: McpServer, ): Promise<void> { const operation = "registerPubMedGenerateChartTool"; const toolName = "pubmed_generate_chart"; const toolDescription = "Generates a customizable chart (PNG) from structured data. Supports various plot types and requires data values and field mappings for axes. Returns a Base64-encoded PNG image."; const context = requestContextService.createRequestContext({ operation }); await ErrorHandler.tryCatch( async () => { server.tool( toolName, toolDescription, PubMedGenerateChartInputSchema.shape, async ( input: PubMedGenerateChartInput, mcpProvidedContext: unknown, ): Promise<CallToolResult> => { const richContext: RequestContext = requestContextService.createRequestContext({ parentRequestId: context.requestId, operation: "pubmedGenerateChartToolHandler", mcpToolContext: mcpProvidedContext, input, }); try { const result = await pubmedGenerateChartLogic(input, richContext); return { content: [ { type: "image", data: result.base64Data, mimeType: "image/png", }, ], isError: false, }; } catch (error) { const handledError = ErrorHandler.handleError(error, { operation: "pubmedGenerateChartToolHandler", context: richContext, input, rethrow: false, }); const mcpError = handledError instanceof McpError ? handledError : new McpError( BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while generating the chart.", { originalErrorName: handledError.name, originalErrorMessage: handledError.message, }, ); return { content: [ { type: "text", text: JSON.stringify({ error: { code: mcpError.code, message: mcpError.message, details: mcpError.details, }, }), }, ], isError: true, }; } }, ); logger.notice(`Tool '${toolName}' registered.`, context); }, { operation, context, errorCode: BaseErrorCode.INITIALIZATION_FAILED, critical: true, }, ); }
- Helper utility to group input data by series field for creating multiple datasets in the chart.function groupDataBySeries( data: Record<string, unknown>[], xField: string, yField: string, seriesField: string, ) { const series = new Map<string, { x: unknown; y: unknown }[]>(); for (const item of data) { const seriesName = item[seriesField] as string; if (!series.has(seriesName)) { series.set(seriesName, []); } series.get(seriesName)!.push({ x: item[xField], y: item[yField] }); } return series; }