download_chart
Save chart images locally by specifying Chart.js configurations and output paths. Ideal for generating and storing customizable data visualizations directly from QuickChart-MCP-Server.
Instructions
Download a chart image to a local file
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| config | Yes | Chart configuration object | |
| outputPath | No | Path where the chart image should be saved. If not provided, the chart will be saved to Desktop or home directory. |
Implementation Reference
- src/index.ts:274-401 (handler)Handler for 'download_chart' tool: normalizes input config, generates chart URL via QuickChart, downloads image with axios, saves to file (defaulting to Desktop/home with timestamped name), handles permissions and errors.case 'download_chart': { try { const { config, outputPath: userProvidedPath } = request.params.arguments as { config: Record<string, unknown>; outputPath?: string; }; // Validate and normalize config first if (!config || typeof config !== 'object') { throw new McpError( ErrorCode.InvalidParams, 'Config must be a valid chart configuration object' ); } // Handle both direct properties and nested properties in 'data' let normalizedConfig: any = { ...config }; // If config has data property with datasets, extract them if (config.data && typeof config.data === 'object' && (config.data as any).datasets && !normalizedConfig.datasets) { normalizedConfig.datasets = (config.data as any).datasets; } // If config has data property with labels, extract them if (config.data && typeof config.data === 'object' && (config.data as any).labels && !normalizedConfig.labels) { normalizedConfig.labels = (config.data as any).labels; } // If type is inside data object but not at root, extract it if (config.data && typeof config.data === 'object' && (config.data as any).type && !normalizedConfig.type) { normalizedConfig.type = (config.data as any).type; } // Final validation after normalization if (!normalizedConfig.type || !normalizedConfig.datasets) { throw new McpError( ErrorCode.InvalidParams, 'Config must include type and datasets properties (either at root level or inside data object)' ); } // Generate default outputPath if not provided const fs = await import('fs'); const path = await import('path'); const os = await import('os'); let outputPath = userProvidedPath; if (!outputPath) { // Get home directory const homeDir = os.homedir(); const desktopDir = path.join(homeDir, 'Desktop'); // Check if Desktop directory exists and is writable let baseDir = homeDir; try { await fs.promises.access(desktopDir, fs.constants.W_OK); baseDir = desktopDir; // Desktop exists and is writable } catch (error) { // Desktop doesn't exist or is not writable, use home directory console.error('Desktop not accessible, using home directory instead'); } // Generate a filename based on chart type and timestamp const timestamp = new Date().toISOString() .replace(/:/g, '-') .replace(/\..+/, '') .replace('T', '_'); const chartType = normalizedConfig.type || 'chart'; outputPath = path.join(baseDir, `${chartType}_${timestamp}.png`); console.error(`No output path provided, using: ${outputPath}`); } // Check if the output directory exists and is writable const outputDir = path.dirname(outputPath); try { await fs.promises.access(outputDir, fs.constants.W_OK); } catch (error) { throw new McpError( ErrorCode.InvalidParams, `Output directory does not exist or is not writable: ${outputDir}` ); } const chartConfig = this.generateChartConfig(normalizedConfig); const url = await this.generateChartUrl(chartConfig); try { const response = await axios.get(url, { responseType: 'arraybuffer' }); await fs.promises.writeFile(outputPath, response.data); } catch (error: any) { if (error.code === 'EACCES' || error.code === 'EROFS') { throw new McpError( ErrorCode.InvalidParams, `Cannot write to ${outputPath}: Permission denied` ); } if (error.code === 'ENOENT') { throw new McpError( ErrorCode.InvalidParams, `Cannot write to ${outputPath}: Directory does not exist` ); } throw error; } return { content: [ { type: 'text', text: `Chart saved to ${outputPath}` } ] }; } catch (error: any) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to download chart: ${error?.message || 'Unknown error'}` ); } }
- src/index.ts:229-245 (registration)Tool registration in listTools response, defining name, description, and input schema requiring 'config' object and optional 'outputPath'.name: 'download_chart', description: 'Download a chart image to a local file', inputSchema: { type: 'object', properties: { config: { type: 'object', description: 'Chart configuration object' }, outputPath: { type: 'string', description: 'Path where the chart image should be saved. If not provided, the chart will be saved to Desktop or home directory.' } }, required: ['config'] } }
- src/index.ts:15-40 (schema)TypeScript interface defining the structure of ChartConfig used in chart generation and validation for download_chart.interface ChartConfig { type: string; data: { labels?: string[]; datasets: Array<{ label?: string; data: number[]; backgroundColor?: string | string[]; borderColor?: string | string[]; [key: string]: any; }>; [key: string]: any; }; options?: { title?: { display: boolean; text: string; }; scales?: { y?: { beginAtZero?: boolean; }; }; [key: string]: any; }; }
- src/index.ts:80-173 (helper)Helper method to generate and validate normalized ChartConfig from input arguments, used by download_chart handler.private generateChartConfig(args: any): ChartConfig { // Add defensive checks to handle possibly malformed input if (!args) { throw new McpError( ErrorCode.InvalidParams, 'No arguments provided to generateChartConfig' ); } if (!args.type) { throw new McpError( ErrorCode.InvalidParams, 'Chart type is required' ); } if (!args.datasets || !Array.isArray(args.datasets)) { throw new McpError( ErrorCode.InvalidParams, 'Datasets must be a non-empty array' ); } const { type, labels, datasets, title, options = {} } = args; this.validateChartType(type); const config: ChartConfig = { type, data: { labels: labels || [], datasets: datasets.map((dataset: any) => { if (!dataset || !dataset.data) { throw new McpError( ErrorCode.InvalidParams, 'Each dataset must have a data property' ); } return { label: dataset.label || '', data: dataset.data, backgroundColor: dataset.backgroundColor, borderColor: dataset.borderColor, ...(dataset.additionalConfig || {}) }; }) }, options: { ...options, ...(title && { title: { display: true, text: title } }) } }; // Special handling for specific chart types switch (type) { case 'radialGauge': case 'speedometer': if (!datasets?.[0]?.data?.[0]) { throw new McpError( ErrorCode.InvalidParams, `${type} requires a single numeric value` ); } config.options = { ...config.options, plugins: { datalabels: { display: true, formatter: (value: number) => value } } }; break; case 'scatter': case 'bubble': datasets.forEach((dataset: any) => { if (!Array.isArray(dataset.data[0])) { throw new McpError( ErrorCode.InvalidParams, `${type} requires data points in [x, y${type === 'bubble' ? ', r' : ''}] format` ); } }); break; } return config; }