Skip to main content
Glama

quickchart-mcp-server

chart.ts14 kB
import { Tool, McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import * as fs from "fs"; import * as path from "path"; import { getDownloadPath } from "../utils/file.js"; import { QuickChartUrls } from "../utils/config.js"; /** * Tool description */ export const CREATE_CHART_USING_CHARTJS_TOOL: Tool = { name: "create-chart-using-chartjs", description: "Create a chart using QuickChart.io - get chart image URL or save chart image to file", inputSchema: { type: "object", properties: { action: { type: "string", enum: ["get_url", "save_file"], description: "Whether to get chart URL or save chart as file", }, outputPath: { type: "string", description: "Path where to save the file (only used with action=save_file)", }, width: { type: "integer", description: "Pixel width (default: 500)", }, height: { type: "integer", description: "Pixel height (default: 300)", }, devicePixelRatio: { type: "integer", enum: [1, 2], description: "Pixel ratio for Retina support (default: 2)", }, format: { type: "string", enum: ["png", "webp", "jpg", "svg", "pdf", "base64"], description: "Output format (default: png)", }, backgroundColor: { type: "string", description: "Canvas background color - rgb, hex, hsl, or color names (default: transparent)", }, version: { type: "string", description: "Chart.js version - '2', '3', '4', or specific version (default: '2.9.4')", }, encoding: { type: "string", enum: ["url", "base64"], description: "Chart configuration encoding method (default: url)", }, key: { type: "string", description: "API key (optional)", }, chart: { type: "object", additionalProperties: true, description: "Chart.js configuration object", properties: { type: { type: "string", enum: [ "bar", "line", "pie", "doughnut", "radar", "polarArea", "scatter", "bubble", "radialGauge", "speedometer", ], description: "The type of chart to generate", }, data: { type: "object", additionalProperties: true, description: "Chart data", properties: { labels: { type: "array", items: {}, description: "Labels for the data points", }, datasets: { type: "array", items: { type: "object", additionalProperties: true, properties: { data: { description: "Data points", }, label: { type: "string", description: "Label for this dataset", }, backgroundColor: { description: "Background color(s)", }, borderColor: { description: "Border color(s)", }, }, required: ["data"], }, description: "Datasets to display in the chart", }, }, required: ["datasets"], }, options: { type: "object", additionalProperties: true, description: "Chart.js options", }, }, required: ["type", "data"], }, }, required: ["action", "chart"], }, }; /** * Validates */ function validateChart(chart: any): void { if (!chart || typeof chart !== "object") { throw new McpError( ErrorCode.InvalidParams, "Chart must be a non-empty object" ); } } function validateChartType(type: string): void { const validTypes = [ "bar", "line", "pie", "doughnut", "radar", "polarArea", "scatter", "bubble", "radialGauge", "speedometer", ]; if (!validTypes.includes(type)) { throw new McpError( ErrorCode.InvalidParams, `Invalid chart type: ${type}. Valid types are: ${validTypes.join(", ")}` ); } } function validateDatasets(datasets: any[]): void { if (!Array.isArray(datasets) || datasets.length === 0) { throw new McpError( ErrorCode.InvalidParams, "Datasets must be a non-empty array" ); } datasets.forEach((dataset, index) => { if (!dataset.data) { throw new McpError( ErrorCode.InvalidParams, `Dataset at index ${index} must have a 'data' property` ); } }); } function validateAction(action: string): void { if (!action || typeof action !== "string" || action.trim().length === 0) { throw new McpError( ErrorCode.InvalidParams, "Action must be a non-empty string" ); } const validActions = ["get_url", "save_file"]; if (!validActions.includes(action)) { throw new McpError( ErrorCode.InvalidParams, `Invalid action: ${action}. Valid actions are: ${validActions.join(", ")}` ); } } function validateOutputPath( outputPath: string | undefined, action: string ): void { if ( action === "save_file" && (!outputPath || typeof outputPath !== "string" || outputPath.trim().length === 0) ) { throw new McpError( ErrorCode.InvalidParams, "Output path is required for save_file action" ); } } function validateFormat(format?: string): void { if (format !== undefined) { const validFormats = ["png", "webp", "jpg", "svg", "pdf", "base64"]; if (!validFormats.includes(format)) { throw new McpError( ErrorCode.InvalidParams, `Invalid format: ${format}. Valid formats are: ${validFormats.join( ", " )}` ); } } } function validateDimensions(width?: number, height?: number): void { if (width !== undefined) { if (!Number.isInteger(width) || width <= 0 || width > 10000) { throw new McpError( ErrorCode.InvalidParams, "Width must be a positive integer between 1 and 10000" ); } } if (height !== undefined) { if (!Number.isInteger(height) || height <= 0 || height > 10000) { throw new McpError( ErrorCode.InvalidParams, "Height must be a positive integer between 1 and 10000" ); } } } function validateDevicePixelRatio(devicePixelRatio?: number): void { if (devicePixelRatio !== undefined) { const validRatios = [1, 2]; if (!validRatios.includes(devicePixelRatio)) { throw new McpError( ErrorCode.InvalidParams, `Invalid device pixel ratio: ${devicePixelRatio}. Valid ratios are: ${validRatios.join( ", " )}` ); } } } function validateBackgroundColor(backgroundColor?: string): void { if (backgroundColor !== undefined) { if ( typeof backgroundColor !== "string" || backgroundColor.trim().length === 0 ) { throw new McpError( ErrorCode.InvalidParams, "Background color must be a non-empty string" ); } } } function validateVersion(version?: string): void { if (version !== undefined) { if (typeof version !== "string" || version.trim().length === 0) { throw new McpError( ErrorCode.InvalidParams, "Version must be a non-empty string" ); } } } function validateEncoding(encoding?: string): void { if (encoding !== undefined) { const validEncodings = ["url", "base64"]; if (!validEncodings.includes(encoding)) { throw new McpError( ErrorCode.InvalidParams, `Invalid encoding: ${encoding}. Valid encodings are: ${validEncodings.join( ", " )}` ); } } } function validateKey(key?: string): void { if (key !== undefined) { if (typeof key !== "string" || key.trim().length === 0) { throw new McpError( ErrorCode.InvalidParams, "API key must be a non-empty string" ); } } } /** * Fetches */ function buildChartConfig( chartConfig: any, options: { format?: string; width?: number; height?: number; backgroundColor?: string; devicePixelRatio?: number; version?: string; encoding?: string; key?: string; } = {} ): any { return { width: options.width || 500, height: options.height || 300, devicePixelRatio: options.devicePixelRatio || 2, format: options.format || "png", backgroundColor: options.backgroundColor || "transparent", version: options.version || "2.9.4", ...(options.encoding && { encoding: options.encoding }), ...(options.key && { key: options.key }), chart: chartConfig, }; } function buildChartUrls(chartConfig: any): { chartUrl: string; editorUrl: string; } { const chartOnlyJson = JSON.stringify(chartConfig); const encodedChart = encodeURIComponent(chartOnlyJson); return { chartUrl: `${QuickChartUrls.chart()}?c=${encodedChart}`, editorUrl: `${QuickChartUrls.chart().replace('/chart', '/sandbox')}#${encodedChart}`, }; } async function fetchChartContent( postConfig: any, format: string = "png" ): Promise<any> { const config = { ...postConfig, format }; const isSvg = format === "svg"; const axiosConfig = { responseType: (isSvg ? "text" : "arraybuffer") as any, timeout: 30000, headers: { "Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", Accept: isSvg ? "image/svg+xml,*/*" : "image/*,*/*", "Accept-Language": "en-US,en;q=0.9", "Accept-Encoding": "gzip, deflate, br", Connection: "keep-alive", }, validateStatus: (status: number) => status >= 200 && status < 300, }; try { const response = await axios.post( QuickChartUrls.chart(), config, axiosConfig ); return response.data; } catch (error) { const axiosError = error as any; const message = axiosError.response ? `Failed to fetch chart content from QuickChart - Status: ${axiosError.response.status}` : `Failed to fetch chart content from QuickChart - ${axiosError.message}`; throw new McpError(ErrorCode.InternalError, message); } } /** * Tool handler */ export async function handleChartTool(args: any): Promise<any> { const chartConfig = args.chart as any; const action = args.action as string; validateChart(chartConfig); validateChartType(chartConfig.type as string); validateDatasets(chartConfig.data?.datasets as any[]); validateAction(action); validateOutputPath(args.outputPath, action); validateFormat(args.format); validateDimensions(args.width, args.height); validateDevicePixelRatio(args.devicePixelRatio); validateBackgroundColor(args.backgroundColor); validateVersion(args.version); validateEncoding(args.encoding); validateKey(args.key); const postConfig = buildChartConfig(chartConfig, { format: args.format as string, width: args.width as number, height: args.height as number, backgroundColor: args.backgroundColor as string, devicePixelRatio: args.devicePixelRatio as number, version: args.version as string, encoding: args.encoding as string, key: args.key as string, }); const { chartUrl, editorUrl } = buildChartUrls(chartConfig); const result: any = { content: [ { type: "text", text: "Below is the chart URL:", }, { type: "text", text: chartUrl, }, { type: "text", text: "Below is the editor URL:", }, { type: "text", text: editorUrl, }, ], metadata: { chartType: chartConfig.type, generatedAt: new Date().toISOString(), chartUrl: chartUrl, editableUrl: editorUrl, }, }; try { const pngData = await fetchChartContent(postConfig, "png"); const pngBase64 = Buffer.from(pngData).toString("base64"); result.content.push( { type: "text", text: "Below is the PNG image:", }, { type: "image", data: pngBase64, mimeType: "image/png", } ); result.metadata.pngBase64 = pngBase64; } catch (error) { result.content.unshift({ type: "text", text: "⚠️ Failed to fetch chart image", }); result.content.push({ type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }); result.metadata.error = error instanceof Error ? error.message : String(error); } if (action === "get_url") { return result; } const format = (args.format as string) || "png"; validateFormat(format); const outputPath = getDownloadPath( args.outputPath as string | undefined, format ); try { const dir = path.dirname(outputPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } const data = await fetchChartContent(postConfig, format); if (format === "svg") { fs.writeFileSync(outputPath, data, "utf8"); } else { fs.writeFileSync(outputPath, data); } result.metadata.savedPath = outputPath; result.content.push({ type: "text", text: "Below is the saved file path:", }); result.content.push({ type: "text", text: outputPath, }); return result; } catch (error) { throw new McpError( ErrorCode.InternalError, `Failed to save chart: ${ error instanceof Error ? error.message : String(error) }` ); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/TakanariShimbo/quickchart-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server