├── .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 | ![image](https://github.com/user-attachments/assets/1093570f-7c6b-4e5f-ad69-f8a9f950376a) 4 | 5 | Quickchart-MCP-Server MCP server 6 | 7 | 8 | Smithery Badge ![](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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------