├── .gitignore
├── .npmignore
├── Dockerfile
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── smithery.yaml
├── src
└── index.ts
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | src/
3 | tsconfig.json
4 | .gitignore
5 | .git
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 GongRzhe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # quickchart-server MCP Server
2 |
3 | 
4 |
5 |
6 |
7 |
8 |

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 | 
30 |
31 | 
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 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@gongrzhe/quickchart-mcp-server",
3 | "version": "1.0.5",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@gongrzhe/quickchart-mcp-server",
9 | "version": "1.0.5",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@modelcontextprotocol/sdk": "^0.6.0",
13 | "@types/getenv": "^1.0.3",
14 | "axios": "^1.7.9",
15 | "getenv": "^1.0.0"
16 | },
17 | "bin": {
18 | "quickchart-mcp-server": "build/index.js"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^20.11.24",
22 | "typescript": "^5.3.3"
23 | },
24 | "engines": {
25 | "node": ">=14.0.0"
26 | }
27 | },
28 | "node_modules/@modelcontextprotocol/sdk": {
29 | "version": "0.6.1",
30 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-0.6.1.tgz",
31 | "integrity": "sha512-OkVXMix3EIbB5Z6yife2XTrSlOnVvCLR1Kg91I4pYFEsV9RbnoyQVScXCuVhGaZHOnTZgso8lMQN1Po2TadGKQ==",
32 | "license": "MIT",
33 | "dependencies": {
34 | "content-type": "^1.0.5",
35 | "raw-body": "^3.0.0",
36 | "zod": "^3.23.8"
37 | }
38 | },
39 | "node_modules/@types/getenv": {
40 | "version": "1.0.3",
41 | "resolved": "https://registry.npmjs.org/@types/getenv/-/getenv-1.0.3.tgz",
42 | "integrity": "sha512-aCe60hc2U/xtdnGYbcZBFm6Xkm/MFuWqKMbhupcGt7DJ1QojADIXFKthtlgTwyXkEi6vr8lMXtOI218Ua9qx6w==",
43 | "license": "MIT",
44 | "dependencies": {
45 | "@types/node": "*"
46 | }
47 | },
48 | "node_modules/@types/node": {
49 | "version": "20.17.46",
50 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.46.tgz",
51 | "integrity": "sha512-0PQHLhZPWOxGW4auogW0eOQAuNIlCYvibIpG67ja0TOJ6/sehu+1en7sfceUn+QQtx4Rk3GxbLNwPh0Cav7TWw==",
52 | "license": "MIT",
53 | "dependencies": {
54 | "undici-types": "~6.19.2"
55 | }
56 | },
57 | "node_modules/asynckit": {
58 | "version": "0.4.0",
59 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
60 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
61 | "license": "MIT"
62 | },
63 | "node_modules/axios": {
64 | "version": "1.9.0",
65 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
66 | "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
67 | "license": "MIT",
68 | "dependencies": {
69 | "follow-redirects": "^1.15.6",
70 | "form-data": "^4.0.0",
71 | "proxy-from-env": "^1.1.0"
72 | }
73 | },
74 | "node_modules/bytes": {
75 | "version": "3.1.2",
76 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
77 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
78 | "license": "MIT",
79 | "engines": {
80 | "node": ">= 0.8"
81 | }
82 | },
83 | "node_modules/call-bind-apply-helpers": {
84 | "version": "1.0.2",
85 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
86 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
87 | "license": "MIT",
88 | "dependencies": {
89 | "es-errors": "^1.3.0",
90 | "function-bind": "^1.1.2"
91 | },
92 | "engines": {
93 | "node": ">= 0.4"
94 | }
95 | },
96 | "node_modules/combined-stream": {
97 | "version": "1.0.8",
98 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
99 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
100 | "license": "MIT",
101 | "dependencies": {
102 | "delayed-stream": "~1.0.0"
103 | },
104 | "engines": {
105 | "node": ">= 0.8"
106 | }
107 | },
108 | "node_modules/content-type": {
109 | "version": "1.0.5",
110 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
111 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
112 | "license": "MIT",
113 | "engines": {
114 | "node": ">= 0.6"
115 | }
116 | },
117 | "node_modules/delayed-stream": {
118 | "version": "1.0.0",
119 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
120 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
121 | "license": "MIT",
122 | "engines": {
123 | "node": ">=0.4.0"
124 | }
125 | },
126 | "node_modules/depd": {
127 | "version": "2.0.0",
128 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
129 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
130 | "license": "MIT",
131 | "engines": {
132 | "node": ">= 0.8"
133 | }
134 | },
135 | "node_modules/dunder-proto": {
136 | "version": "1.0.1",
137 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
138 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
139 | "license": "MIT",
140 | "dependencies": {
141 | "call-bind-apply-helpers": "^1.0.1",
142 | "es-errors": "^1.3.0",
143 | "gopd": "^1.2.0"
144 | },
145 | "engines": {
146 | "node": ">= 0.4"
147 | }
148 | },
149 | "node_modules/es-define-property": {
150 | "version": "1.0.1",
151 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
152 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
153 | "license": "MIT",
154 | "engines": {
155 | "node": ">= 0.4"
156 | }
157 | },
158 | "node_modules/es-errors": {
159 | "version": "1.3.0",
160 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
161 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
162 | "license": "MIT",
163 | "engines": {
164 | "node": ">= 0.4"
165 | }
166 | },
167 | "node_modules/es-object-atoms": {
168 | "version": "1.1.1",
169 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
170 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
171 | "license": "MIT",
172 | "dependencies": {
173 | "es-errors": "^1.3.0"
174 | },
175 | "engines": {
176 | "node": ">= 0.4"
177 | }
178 | },
179 | "node_modules/es-set-tostringtag": {
180 | "version": "2.1.0",
181 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
182 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
183 | "license": "MIT",
184 | "dependencies": {
185 | "es-errors": "^1.3.0",
186 | "get-intrinsic": "^1.2.6",
187 | "has-tostringtag": "^1.0.2",
188 | "hasown": "^2.0.2"
189 | },
190 | "engines": {
191 | "node": ">= 0.4"
192 | }
193 | },
194 | "node_modules/follow-redirects": {
195 | "version": "1.15.9",
196 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
197 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
198 | "funding": [
199 | {
200 | "type": "individual",
201 | "url": "https://github.com/sponsors/RubenVerborgh"
202 | }
203 | ],
204 | "license": "MIT",
205 | "engines": {
206 | "node": ">=4.0"
207 | },
208 | "peerDependenciesMeta": {
209 | "debug": {
210 | "optional": true
211 | }
212 | }
213 | },
214 | "node_modules/form-data": {
215 | "version": "4.0.2",
216 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
217 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
218 | "license": "MIT",
219 | "dependencies": {
220 | "asynckit": "^0.4.0",
221 | "combined-stream": "^1.0.8",
222 | "es-set-tostringtag": "^2.1.0",
223 | "mime-types": "^2.1.12"
224 | },
225 | "engines": {
226 | "node": ">= 6"
227 | }
228 | },
229 | "node_modules/function-bind": {
230 | "version": "1.1.2",
231 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
232 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
233 | "license": "MIT",
234 | "funding": {
235 | "url": "https://github.com/sponsors/ljharb"
236 | }
237 | },
238 | "node_modules/get-intrinsic": {
239 | "version": "1.3.0",
240 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
241 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
242 | "license": "MIT",
243 | "dependencies": {
244 | "call-bind-apply-helpers": "^1.0.2",
245 | "es-define-property": "^1.0.1",
246 | "es-errors": "^1.3.0",
247 | "es-object-atoms": "^1.1.1",
248 | "function-bind": "^1.1.2",
249 | "get-proto": "^1.0.1",
250 | "gopd": "^1.2.0",
251 | "has-symbols": "^1.1.0",
252 | "hasown": "^2.0.2",
253 | "math-intrinsics": "^1.1.0"
254 | },
255 | "engines": {
256 | "node": ">= 0.4"
257 | },
258 | "funding": {
259 | "url": "https://github.com/sponsors/ljharb"
260 | }
261 | },
262 | "node_modules/get-proto": {
263 | "version": "1.0.1",
264 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
265 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
266 | "license": "MIT",
267 | "dependencies": {
268 | "dunder-proto": "^1.0.1",
269 | "es-object-atoms": "^1.0.0"
270 | },
271 | "engines": {
272 | "node": ">= 0.4"
273 | }
274 | },
275 | "node_modules/getenv": {
276 | "version": "1.0.0",
277 | "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz",
278 | "integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==",
279 | "license": "MIT",
280 | "engines": {
281 | "node": ">=6"
282 | }
283 | },
284 | "node_modules/gopd": {
285 | "version": "1.2.0",
286 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
287 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
288 | "license": "MIT",
289 | "engines": {
290 | "node": ">= 0.4"
291 | },
292 | "funding": {
293 | "url": "https://github.com/sponsors/ljharb"
294 | }
295 | },
296 | "node_modules/has-symbols": {
297 | "version": "1.1.0",
298 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
299 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
300 | "license": "MIT",
301 | "engines": {
302 | "node": ">= 0.4"
303 | },
304 | "funding": {
305 | "url": "https://github.com/sponsors/ljharb"
306 | }
307 | },
308 | "node_modules/has-tostringtag": {
309 | "version": "1.0.2",
310 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
311 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
312 | "license": "MIT",
313 | "dependencies": {
314 | "has-symbols": "^1.0.3"
315 | },
316 | "engines": {
317 | "node": ">= 0.4"
318 | },
319 | "funding": {
320 | "url": "https://github.com/sponsors/ljharb"
321 | }
322 | },
323 | "node_modules/hasown": {
324 | "version": "2.0.2",
325 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
326 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
327 | "license": "MIT",
328 | "dependencies": {
329 | "function-bind": "^1.1.2"
330 | },
331 | "engines": {
332 | "node": ">= 0.4"
333 | }
334 | },
335 | "node_modules/http-errors": {
336 | "version": "2.0.0",
337 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
338 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
339 | "license": "MIT",
340 | "dependencies": {
341 | "depd": "2.0.0",
342 | "inherits": "2.0.4",
343 | "setprototypeof": "1.2.0",
344 | "statuses": "2.0.1",
345 | "toidentifier": "1.0.1"
346 | },
347 | "engines": {
348 | "node": ">= 0.8"
349 | }
350 | },
351 | "node_modules/iconv-lite": {
352 | "version": "0.6.3",
353 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
354 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
355 | "license": "MIT",
356 | "dependencies": {
357 | "safer-buffer": ">= 2.1.2 < 3.0.0"
358 | },
359 | "engines": {
360 | "node": ">=0.10.0"
361 | }
362 | },
363 | "node_modules/inherits": {
364 | "version": "2.0.4",
365 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
366 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
367 | "license": "ISC"
368 | },
369 | "node_modules/math-intrinsics": {
370 | "version": "1.1.0",
371 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
372 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
373 | "license": "MIT",
374 | "engines": {
375 | "node": ">= 0.4"
376 | }
377 | },
378 | "node_modules/mime-db": {
379 | "version": "1.52.0",
380 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
381 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
382 | "license": "MIT",
383 | "engines": {
384 | "node": ">= 0.6"
385 | }
386 | },
387 | "node_modules/mime-types": {
388 | "version": "2.1.35",
389 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
390 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
391 | "license": "MIT",
392 | "dependencies": {
393 | "mime-db": "1.52.0"
394 | },
395 | "engines": {
396 | "node": ">= 0.6"
397 | }
398 | },
399 | "node_modules/proxy-from-env": {
400 | "version": "1.1.0",
401 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
402 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
403 | "license": "MIT"
404 | },
405 | "node_modules/raw-body": {
406 | "version": "3.0.0",
407 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
408 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
409 | "license": "MIT",
410 | "dependencies": {
411 | "bytes": "3.1.2",
412 | "http-errors": "2.0.0",
413 | "iconv-lite": "0.6.3",
414 | "unpipe": "1.0.0"
415 | },
416 | "engines": {
417 | "node": ">= 0.8"
418 | }
419 | },
420 | "node_modules/safer-buffer": {
421 | "version": "2.1.2",
422 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
423 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
424 | "license": "MIT"
425 | },
426 | "node_modules/setprototypeof": {
427 | "version": "1.2.0",
428 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
429 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
430 | "license": "ISC"
431 | },
432 | "node_modules/statuses": {
433 | "version": "2.0.1",
434 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
435 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
436 | "license": "MIT",
437 | "engines": {
438 | "node": ">= 0.8"
439 | }
440 | },
441 | "node_modules/toidentifier": {
442 | "version": "1.0.1",
443 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
444 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
445 | "license": "MIT",
446 | "engines": {
447 | "node": ">=0.6"
448 | }
449 | },
450 | "node_modules/typescript": {
451 | "version": "5.8.3",
452 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
453 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
454 | "dev": true,
455 | "license": "Apache-2.0",
456 | "bin": {
457 | "tsc": "bin/tsc",
458 | "tsserver": "bin/tsserver"
459 | },
460 | "engines": {
461 | "node": ">=14.17"
462 | }
463 | },
464 | "node_modules/undici-types": {
465 | "version": "6.19.8",
466 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
467 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
468 | "license": "MIT"
469 | },
470 | "node_modules/unpipe": {
471 | "version": "1.0.0",
472 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
473 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
474 | "license": "MIT",
475 | "engines": {
476 | "node": ">= 0.8"
477 | }
478 | },
479 | "node_modules/zod": {
480 | "version": "3.24.4",
481 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz",
482 | "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==",
483 | "license": "MIT",
484 | "funding": {
485 | "url": "https://github.com/sponsors/colinhacks"
486 | }
487 | }
488 | }
489 | }
490 |
--------------------------------------------------------------------------------
/package.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 |
--------------------------------------------------------------------------------
/smithery.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 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
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 {
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;
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 |
--------------------------------------------------------------------------------
/tsconfig.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 |
--------------------------------------------------------------------------------