#
tokens: 6208/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .npmignore
├── Dockerfile
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
```

--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------

```
1 | node_modules/
2 | src/
3 | tsconfig.json
4 | .gitignore
5 | .git
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # quickchart-server MCP Server
  2 | 
  3 | ![image](https://github.com/user-attachments/assets/1093570f-7c6b-4e5f-ad69-f8a9f950376a)
  4 | <a href="https://glama.ai/mcp/servers/y17zluizso">
  5 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/y17zluizso/badge" alt="Quickchart-MCP-Server MCP server" />
  6 | </a>
  7 | 
  8 | <a href="https://smithery.ai/server/@GongRzhe/Quickchart-MCP-Server"><img alt="Smithery Badge" src="https://smithery.ai/badge/@GongRzhe/Quickchart-MCP-Server"></a> ![](https://badge.mcpx.dev?type=server 'MCP Server')
  9 | 
 10 | A Model Context Protocol server for generating charts using QuickChart.io
 11 | 
 12 | This is a TypeScript-based MCP server that provides chart generation capabilities. It allows you to create various types of charts through MCP tools.
 13 | 
 14 | ## Overview
 15 | 
 16 | This server integrates with QuickChart.io's URL-based chart generation service to create chart images using Chart.js configurations. Users can generate various types of charts by providing data and styling parameters, which the server converts into chart URLs or downloadable images.
 17 | 
 18 | ## Features
 19 | 
 20 | ### Tools
 21 | - `generate_chart` - Generate a chart URL using QuickChart.io
 22 |   - Supports multiple chart types: bar, line, pie, doughnut, radar, polarArea, scatter, bubble, radialGauge, speedometer
 23 |   - Customizable with labels, datasets, colors, and additional options
 24 |   - Returns a URL to the generated chart
 25 | 
 26 | - `download_chart` - Download a chart image to a local file
 27 |   - Takes chart configuration and output path as parameters
 28 |   - Saves the chart image to the specified location
 29 | ![image](https://github.com/user-attachments/assets/c6864098-dd9a-48ff-b53a-d897427748f7)
 30 | 
 31 | ![image](https://github.com/user-attachments/assets/c008adbb-55ec-4432-bfe7-5644a0fccfae)
 32 | 
 33 | 
 34 | ## Supported Chart Types
 35 | - Bar charts: For comparing values across categories
 36 | - Line charts: For showing trends over time
 37 | - Pie charts: For displaying proportional data
 38 | - Doughnut charts: Similar to pie charts with a hollow center
 39 | - Radar charts: For showing multivariate data
 40 | - Polar Area charts: For displaying proportional data with fixed-angle segments
 41 | - Scatter plots: For showing data point distributions
 42 | - Bubble charts: For three-dimensional data visualization
 43 | - Radial Gauge: For displaying single values within a range
 44 | - Speedometer: For speedometer-style value display
 45 | 
 46 | ## Usage
 47 | 
 48 | ### Chart Configuration
 49 | The server uses Chart.js configuration format. Here's a basic example:
 50 | 
 51 | ```javascript
 52 | {
 53 |   "type": "bar",
 54 |   "data": {
 55 |     "labels": ["January", "February", "March"],
 56 |     "datasets": [{
 57 |       "label": "Sales",
 58 |       "data": [65, 59, 80],
 59 |       "backgroundColor": "rgb(75, 192, 192)"
 60 |     }]
 61 |   },
 62 |   "options": {
 63 |     "title": {
 64 |       "display": true,
 65 |       "text": "Monthly Sales"
 66 |     }
 67 |   }
 68 | }
 69 | ```
 70 | 
 71 | ### URL Generation
 72 | The server converts your configuration into a QuickChart URL:
 73 | ```
 74 | https://quickchart.io/chart?c={...encoded configuration...}
 75 | ```
 76 | 
 77 | ## Development
 78 | 
 79 | Install dependencies:
 80 | ```bash
 81 | npm install
 82 | ```
 83 | 
 84 | Build the server:
 85 | ```bash
 86 | npm run build
 87 | ```
 88 | 
 89 | ## Installation
 90 | 
 91 | ### Installing
 92 | 
 93 |  ```bash
 94 |  npm install @gongrzhe/quickchart-mcp-server
 95 |  ```
 96 | 
 97 | ### Installing via Smithery
 98 |  
 99 |  To install QuickChart Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@GongRzhe/Quickchart-MCP-Server):
100 |  
101 |  ```bash
102 |  npx -y @smithery/cli install @gongrzhe/quickchart-mcp-server --client claude
103 |  ```
104 | 
105 | To use with Claude Desktop, add the server config:
106 | 
107 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
108 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
109 | 
110 | ```json
111 | {
112 |   "mcpServers": {
113 |     "quickchart-server": {
114 |       "command": "node",
115 |       "args": ["/path/to/quickchart-server/build/index.js"]
116 |     }
117 |   }
118 | }
119 | ```
120 | 
121 | or
122 | 
123 | ```json
124 | {
125 |   "mcpServers": {
126 |     "quickchart-server": {
127 |       "command": "npx",
128 |       "args": [
129 |         "-y",
130 |         "@gongrzhe/quickchart-mcp-server"
131 |       ]
132 |     }
133 |   }
134 | }
135 | ```
136 | 
137 | 
138 | ## Documentation References
139 | - [QuickChart Documentation](https://quickchart.io/documentation/)
140 | - [Chart Types Reference](https://quickchart.io/documentation/chart-types/)
141 | 
142 | ## 📜 License
143 | 
144 | This project is licensed under the MIT License.
145 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     properties: {}
 9 |   commandFunction:
10 |     # A function that produces the CLI command to start the MCP on stdio.
11 |     |-
12 |     (config) => ({ command: 'node', args: ['./build/index.js'] })
13 |   exampleConfig: {}
14 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM node:lts-alpine
 3 | 
 4 | # Create app directory
 5 | WORKDIR /app
 6 | 
 7 | # Install app dependencies
 8 | COPY package*.json ./
 9 | RUN npm install --ignore-scripts
10 | 
11 | # Bundle app source
12 | COPY . .
13 | 
14 | # Build the TypeScript source
15 | RUN npm run build
16 | 
17 | # Expose port if needed (not strictly needed for stdio services)
18 | 
19 | # Run the MCP server
20 | CMD [ "node", "./build/index.js" ]
21 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "@gongrzhe/quickchart-mcp-server",
 3 |   "version": "1.0.6",
 4 |   "description": "A Model Context Protocol server for generating charts using QuickChart.io",
 5 |   "type": "module",
 6 |   "main": "build/index.js",
 7 |   "scripts": {
 8 |     "build": "tsc",
 9 |     "start": "node build/index.js",
10 |     "prepare": "npm run build",
11 |     "prepublishOnly": "npm run build"
12 |   },
13 |   "bin": {
14 |     "quickchart-mcp-server": "./build/index.js"
15 |   },
16 |   "files": [
17 |     "build"
18 |   ],
19 |   "keywords": [
20 |     "mcp",
21 |     "model-context-protocol",
22 |     "quickchart",
23 |     "chart",
24 |     "data-visualization"
25 |   ],
26 |   "author": "gongrzhe",
27 |   "license": "MIT",
28 |   "repository": {
29 |     "type": "git",
30 |     "url": "https://github.com/GongRzhe/Quickchart-MCP-Server"
31 |   },
32 |   "homepage": "https://github.com/GongRzhe/Quickchart-MCP-Server#readme",
33 |   "publishConfig": {
34 |     "access": "public"
35 |   },
36 |   "engines": {
37 |     "node": ">=14.0.0"
38 |   },
39 |   "dependencies": {
40 |     "@modelcontextprotocol/sdk": "^0.6.0",
41 |     "@types/getenv": "^1.0.3",
42 |     "axios": "^1.7.9",
43 |     "getenv": "^1.0.0"
44 |   },
45 |   "devDependencies": {
46 |     "@types/node": "^20.11.24",
47 |     "typescript": "^5.3.3"
48 |   }
49 | }
50 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import {
  5 |   CallToolRequestSchema,
  6 |   ErrorCode,
  7 |   ListToolsRequestSchema,
  8 |   McpError,
  9 | } from '@modelcontextprotocol/sdk/types.js';
 10 | import axios from 'axios';
 11 | import getenv from 'getenv';
 12 | 
 13 | const QUICKCHART_BASE_URL = getenv('QUICKCHART_BASE_URL', 'https://quickchart.io/chart');
 14 | 
 15 | interface ChartConfig {
 16 |   type: string;
 17 |   data: {
 18 |     labels?: string[];
 19 |     datasets: Array<{
 20 |       label?: string;
 21 |       data: number[];
 22 |       backgroundColor?: string | string[];
 23 |       borderColor?: string | string[];
 24 |       [key: string]: any;
 25 |     }>;
 26 |     [key: string]: any;
 27 |   };
 28 |   options?: {
 29 |     title?: {
 30 |       display: boolean;
 31 |       text: string;
 32 |     };
 33 |     scales?: {
 34 |       y?: {
 35 |         beginAtZero?: boolean;
 36 |       };
 37 |     };
 38 |     [key: string]: any;
 39 |   };
 40 | }
 41 | 
 42 | class QuickChartServer {
 43 |   private server: Server;
 44 | 
 45 |   constructor() {
 46 |     this.server = new Server(
 47 |       {
 48 |         name: 'quickchart-server',
 49 |         version: '1.0.0',
 50 |       },
 51 |       {
 52 |         capabilities: {
 53 |           tools: {},
 54 |         },
 55 |       }
 56 |     );
 57 | 
 58 |     this.setupToolHandlers();
 59 |     
 60 |     this.server.onerror = (error) => console.error('[MCP Error]', error);
 61 |     process.on('SIGINT', async () => {
 62 |       await this.server.close();
 63 |       process.exit(0);
 64 |     });
 65 |   }
 66 | 
 67 |   private validateChartType(type: string): void {
 68 |     const validTypes = [
 69 |       'bar', 'line', 'pie', 'doughnut', 'radar',
 70 |       'polarArea', 'scatter', 'bubble', 'radialGauge', 'speedometer'
 71 |     ];
 72 |     if (!validTypes.includes(type)) {
 73 |       throw new McpError(
 74 |         ErrorCode.InvalidParams,
 75 |         `Invalid chart type. Must be one of: ${validTypes.join(', ')}`
 76 |       );
 77 |     }
 78 |   }
 79 | 
 80 |   private generateChartConfig(args: any): ChartConfig {
 81 |     // Add defensive checks to handle possibly malformed input
 82 |     if (!args) {
 83 |       throw new McpError(
 84 |         ErrorCode.InvalidParams,
 85 |         'No arguments provided to generateChartConfig'
 86 |       );
 87 |     }
 88 |     
 89 |     if (!args.type) {
 90 |       throw new McpError(
 91 |         ErrorCode.InvalidParams,
 92 |         'Chart type is required'
 93 |       );
 94 |     }
 95 |     
 96 |     if (!args.datasets || !Array.isArray(args.datasets)) {
 97 |       throw new McpError(
 98 |         ErrorCode.InvalidParams,
 99 |         'Datasets must be a non-empty array'
100 |       );
101 |     }
102 |     
103 |     const { type, labels, datasets, title, options = {} } = args;
104 |     
105 |     this.validateChartType(type);
106 | 
107 |     const config: ChartConfig = {
108 |       type,
109 |       data: {
110 |         labels: labels || [],
111 |         datasets: datasets.map((dataset: any) => {
112 |           if (!dataset || !dataset.data) {
113 |             throw new McpError(
114 |               ErrorCode.InvalidParams,
115 |               'Each dataset must have a data property'
116 |             );
117 |           }
118 |           return {
119 |             label: dataset.label || '',
120 |             data: dataset.data,
121 |             backgroundColor: dataset.backgroundColor,
122 |             borderColor: dataset.borderColor,
123 |             ...(dataset.additionalConfig || {})
124 |           };
125 |         })
126 |       },
127 |       options: {
128 |         ...options,
129 |         ...(title && {
130 |           title: {
131 |             display: true,
132 |             text: title
133 |           }
134 |         })
135 |       }
136 |     };
137 | 
138 |     // Special handling for specific chart types
139 |     switch (type) {
140 |       case 'radialGauge':
141 |       case 'speedometer':
142 |         if (!datasets?.[0]?.data?.[0]) {
143 |           throw new McpError(
144 |             ErrorCode.InvalidParams,
145 |             `${type} requires a single numeric value`
146 |           );
147 |         }
148 |         config.options = {
149 |           ...config.options,
150 |           plugins: {
151 |             datalabels: {
152 |               display: true,
153 |               formatter: (value: number) => value
154 |             }
155 |           }
156 |         };
157 |         break;
158 | 
159 |       case 'scatter':
160 |       case 'bubble':
161 |         datasets.forEach((dataset: any) => {
162 |           if (!Array.isArray(dataset.data[0])) {
163 |             throw new McpError(
164 |               ErrorCode.InvalidParams,
165 |               `${type} requires data points in [x, y${type === 'bubble' ? ', r' : ''}] format`
166 |             );
167 |           }
168 |         });
169 |         break;
170 |     }
171 | 
172 |     return config;
173 |   }
174 | 
175 |   private async generateChartUrl(config: ChartConfig): Promise<string> {
176 |     const encodedConfig = encodeURIComponent(JSON.stringify(config));
177 |     return `${QUICKCHART_BASE_URL}?c=${encodedConfig}`;
178 |   }
179 | 
180 |   private setupToolHandlers() {
181 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
182 |       tools: [
183 |         {
184 |           name: 'generate_chart',
185 |           description: 'Generate a chart using QuickChart',
186 |           inputSchema: {
187 |             type: 'object',
188 |             properties: {
189 |               type: {
190 |                 type: 'string',
191 |                 description: 'Chart type (bar, line, pie, doughnut, radar, polarArea, scatter, bubble, radialGauge, speedometer)'
192 |               },
193 |               labels: {
194 |                 type: 'array',
195 |                 items: { type: 'string' },
196 |                 description: 'Labels for data points'
197 |               },
198 |               datasets: {
199 |                 type: 'array',
200 |                 items: {
201 |                   type: 'object',
202 |                   properties: {
203 |                     label: { type: 'string' },
204 |                     data: { type: 'array' },
205 |                     backgroundColor: { 
206 |                       oneOf: [
207 |                         { type: 'string' },
208 |                         { type: 'array', items: { type: 'string' } }
209 |                       ]
210 |                     },
211 |                     borderColor: {
212 |                       oneOf: [
213 |                         { type: 'string' },
214 |                         { type: 'array', items: { type: 'string' } }
215 |                       ]
216 |                     },
217 |                     additionalConfig: { type: 'object' }
218 |                   },
219 |                   required: ['data']
220 |                 }
221 |               },
222 |               title: { type: 'string' },
223 |               options: { type: 'object' }
224 |             },
225 |             required: ['type', 'datasets']
226 |           }
227 |         },
228 |         {
229 |           name: 'download_chart',
230 |           description: 'Download a chart image to a local file',
231 |           inputSchema: {
232 |             type: 'object',
233 |             properties: {
234 |               config: {
235 |                 type: 'object',
236 |                 description: 'Chart configuration object'
237 |               },
238 |               outputPath: {
239 |                 type: 'string',
240 |                 description: 'Path where the chart image should be saved. If not provided, the chart will be saved to Desktop or home directory.'
241 |               }
242 |             },
243 |             required: ['config']
244 |           }
245 |         }
246 |       ]
247 |     }));
248 | 
249 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
250 |       switch (request.params.name) {
251 |         case 'generate_chart': {
252 |           try {
253 |             const config = this.generateChartConfig(request.params.arguments);
254 |             const url = await this.generateChartUrl(config);
255 |             return {
256 |               content: [
257 |                 {
258 |                   type: 'text',
259 |                   text: url
260 |                 }
261 |               ]
262 |             };
263 |           } catch (error: any) {
264 |             if (error instanceof McpError) {
265 |               throw error;
266 |             }
267 |             throw new McpError(
268 |               ErrorCode.InternalError,
269 |               `Failed to generate chart: ${error?.message || 'Unknown error'}`
270 |             );
271 |           }
272 |         }
273 | 
274 |         case 'download_chart': {
275 |           try {
276 |             const { config, outputPath: userProvidedPath } = request.params.arguments as { 
277 |               config: Record<string, unknown>;
278 |               outputPath?: string;
279 |             };
280 |             
281 |             // Validate and normalize config first
282 |             if (!config || typeof config !== 'object') {
283 |               throw new McpError(
284 |                 ErrorCode.InvalidParams,
285 |                 'Config must be a valid chart configuration object'
286 |               );
287 |             }
288 |             
289 |             // Handle both direct properties and nested properties in 'data'
290 |             let normalizedConfig: any = { ...config };
291 |             
292 |             // If config has data property with datasets, extract them
293 |             if (config.data && typeof config.data === 'object' && 
294 |                 (config.data as any).datasets && !normalizedConfig.datasets) {
295 |               normalizedConfig.datasets = (config.data as any).datasets;
296 |             }
297 |             
298 |             // If config has data property with labels, extract them
299 |             if (config.data && typeof config.data === 'object' && 
300 |                 (config.data as any).labels && !normalizedConfig.labels) {
301 |               normalizedConfig.labels = (config.data as any).labels;
302 |             }
303 |             
304 |             // If type is inside data object but not at root, extract it
305 |             if (config.data && typeof config.data === 'object' && 
306 |                 (config.data as any).type && !normalizedConfig.type) {
307 |               normalizedConfig.type = (config.data as any).type;
308 |             }
309 |             
310 |             // Final validation after normalization
311 |             if (!normalizedConfig.type || !normalizedConfig.datasets) {
312 |               throw new McpError(
313 |                 ErrorCode.InvalidParams,
314 |                 'Config must include type and datasets properties (either at root level or inside data object)'
315 |               );
316 |             }
317 |             
318 |             // Generate default outputPath if not provided
319 |             const fs = await import('fs');
320 |             const path = await import('path');
321 |             const os = await import('os');
322 |             
323 |             let outputPath = userProvidedPath;
324 |             if (!outputPath) {
325 |               // Get home directory
326 |               const homeDir = os.homedir();
327 |               const desktopDir = path.join(homeDir, 'Desktop');
328 |               
329 |               // Check if Desktop directory exists and is writable
330 |               let baseDir = homeDir;
331 |               try {
332 |                 await fs.promises.access(desktopDir, fs.constants.W_OK);
333 |                 baseDir = desktopDir; // Desktop exists and is writable
334 |               } catch (error) {
335 |                 // Desktop doesn't exist or is not writable, use home directory
336 |                 console.error('Desktop not accessible, using home directory instead');
337 |               }
338 |               
339 |               // Generate a filename based on chart type and timestamp
340 |               const timestamp = new Date().toISOString()
341 |                 .replace(/:/g, '-')
342 |                 .replace(/\..+/, '')
343 |                 .replace('T', '_');
344 |               const chartType = normalizedConfig.type || 'chart';
345 |               outputPath = path.join(baseDir, `${chartType}_${timestamp}.png`);
346 |               
347 |               console.error(`No output path provided, using: ${outputPath}`);
348 |             }
349 |             
350 |             // Check if the output directory exists and is writable
351 |             const outputDir = path.dirname(outputPath);
352 |             
353 |             try {
354 |               await fs.promises.access(outputDir, fs.constants.W_OK);
355 |             } catch (error) {
356 |               throw new McpError(
357 |                 ErrorCode.InvalidParams,
358 |                 `Output directory does not exist or is not writable: ${outputDir}`
359 |               );
360 |             }
361 |             
362 |             const chartConfig = this.generateChartConfig(normalizedConfig);
363 |             const url = await this.generateChartUrl(chartConfig);
364 |             
365 |             try {
366 |               const response = await axios.get(url, { responseType: 'arraybuffer' });
367 |               await fs.promises.writeFile(outputPath, response.data);
368 |             } catch (error: any) {
369 |               if (error.code === 'EACCES' || error.code === 'EROFS') {
370 |                 throw new McpError(
371 |                   ErrorCode.InvalidParams,
372 |                   `Cannot write to ${outputPath}: Permission denied`
373 |                 );
374 |               }
375 |               if (error.code === 'ENOENT') {
376 |                 throw new McpError(
377 |                   ErrorCode.InvalidParams,
378 |                   `Cannot write to ${outputPath}: Directory does not exist`
379 |                 );
380 |               }
381 |               throw error;
382 |             }
383 |             
384 |             return {
385 |               content: [
386 |                 {
387 |                   type: 'text',
388 |                   text: `Chart saved to ${outputPath}`
389 |                 }
390 |               ]
391 |             };
392 |           } catch (error: any) {
393 |             if (error instanceof McpError) {
394 |               throw error;
395 |             }
396 |             throw new McpError(
397 |               ErrorCode.InternalError,
398 |               `Failed to download chart: ${error?.message || 'Unknown error'}`
399 |             );
400 |           }
401 |         }
402 | 
403 |         default:
404 |           throw new McpError(
405 |             ErrorCode.MethodNotFound,
406 |             `Unknown tool: ${request.params.name}`
407 |           );
408 |       }
409 |     });
410 |   }
411 | 
412 |   async run() {
413 |     const transport = new StdioServerTransport();
414 |     await this.server.connect(transport);
415 |     console.error('QuickChart MCP server running on stdio');
416 |   }
417 | }
418 | 
419 | const server = new QuickChartServer();
420 | server.run().catch(console.error);
421 | 
```