#
tokens: 4537/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

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

# Files

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

```
node_modules/
build/
*.log
.env*
```

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

```
node_modules/
src/
tsconfig.json
.gitignore
.git
```

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

```markdown
# quickchart-server MCP Server

![image](https://github.com/user-attachments/assets/1093570f-7c6b-4e5f-ad69-f8a9f950376a)
<a href="https://glama.ai/mcp/servers/y17zluizso">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/y17zluizso/badge" alt="Quickchart-MCP-Server MCP server" />
</a>

<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')

A Model Context Protocol server for generating charts using QuickChart.io

This is a TypeScript-based MCP server that provides chart generation capabilities. It allows you to create various types of charts through MCP tools.

## Overview

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.

## Features

### Tools
- `generate_chart` - Generate a chart URL using QuickChart.io
  - Supports multiple chart types: bar, line, pie, doughnut, radar, polarArea, scatter, bubble, radialGauge, speedometer
  - Customizable with labels, datasets, colors, and additional options
  - Returns a URL to the generated chart

- `download_chart` - Download a chart image to a local file
  - Takes chart configuration and output path as parameters
  - Saves the chart image to the specified location
![image](https://github.com/user-attachments/assets/c6864098-dd9a-48ff-b53a-d897427748f7)

![image](https://github.com/user-attachments/assets/c008adbb-55ec-4432-bfe7-5644a0fccfae)


## Supported Chart Types
- Bar charts: For comparing values across categories
- Line charts: For showing trends over time
- Pie charts: For displaying proportional data
- Doughnut charts: Similar to pie charts with a hollow center
- Radar charts: For showing multivariate data
- Polar Area charts: For displaying proportional data with fixed-angle segments
- Scatter plots: For showing data point distributions
- Bubble charts: For three-dimensional data visualization
- Radial Gauge: For displaying single values within a range
- Speedometer: For speedometer-style value display

## Usage

### Chart Configuration
The server uses Chart.js configuration format. Here's a basic example:

```javascript
{
  "type": "bar",
  "data": {
    "labels": ["January", "February", "March"],
    "datasets": [{
      "label": "Sales",
      "data": [65, 59, 80],
      "backgroundColor": "rgb(75, 192, 192)"
    }]
  },
  "options": {
    "title": {
      "display": true,
      "text": "Monthly Sales"
    }
  }
}
```

### URL Generation
The server converts your configuration into a QuickChart URL:
```
https://quickchart.io/chart?c={...encoded configuration...}
```

## Development

Install dependencies:
```bash
npm install
```

Build the server:
```bash
npm run build
```

## Installation

### Installing

 ```bash
 npm install @gongrzhe/quickchart-mcp-server
 ```

### Installing via Smithery
 
 To install QuickChart Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@GongRzhe/Quickchart-MCP-Server):
 
 ```bash
 npx -y @smithery/cli install @gongrzhe/quickchart-mcp-server --client claude
 ```

To use with Claude Desktop, add the server config:

On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`

```json
{
  "mcpServers": {
    "quickchart-server": {
      "command": "node",
      "args": ["/path/to/quickchart-server/build/index.js"]
    }
  }
}
```

or

```json
{
  "mcpServers": {
    "quickchart-server": {
      "command": "npx",
      "args": [
        "-y",
        "@gongrzhe/quickchart-mcp-server"
      ]
    }
  }
}
```


## Documentation References
- [QuickChart Documentation](https://quickchart.io/documentation/)
- [Chart Types Reference](https://quickchart.io/documentation/chart-types/)

## 📜 License

This project is licensed under the MIT License.

```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

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

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    properties: {}
  commandFunction:
    # A function that produces the CLI command to start the MCP on stdio.
    |-
    (config) => ({ command: 'node', args: ['./build/index.js'] })
  exampleConfig: {}

```

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

```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine

# Create app directory
WORKDIR /app

# Install app dependencies
COPY package*.json ./
RUN npm install --ignore-scripts

# Bundle app source
COPY . .

# Build the TypeScript source
RUN npm run build

# Expose port if needed (not strictly needed for stdio services)

# Run the MCP server
CMD [ "node", "./build/index.js" ]

```

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

```json
{
  "name": "@gongrzhe/quickchart-mcp-server",
  "version": "1.0.6",
  "description": "A Model Context Protocol server for generating charts using QuickChart.io",
  "type": "module",
  "main": "build/index.js",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js",
    "prepare": "npm run build",
    "prepublishOnly": "npm run build"
  },
  "bin": {
    "quickchart-mcp-server": "./build/index.js"
  },
  "files": [
    "build"
  ],
  "keywords": [
    "mcp",
    "model-context-protocol",
    "quickchart",
    "chart",
    "data-visualization"
  ],
  "author": "gongrzhe",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/GongRzhe/Quickchart-MCP-Server"
  },
  "homepage": "https://github.com/GongRzhe/Quickchart-MCP-Server#readme",
  "publishConfig": {
    "access": "public"
  },
  "engines": {
    "node": ">=14.0.0"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.6.0",
    "@types/getenv": "^1.0.3",
    "axios": "^1.7.9",
    "getenv": "^1.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.11.24",
    "typescript": "^5.3.3"
  }
}

```

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

```typescript
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ErrorCode,
  ListToolsRequestSchema,
  McpError,
} from '@modelcontextprotocol/sdk/types.js';
import axios from 'axios';
import getenv from 'getenv';

const QUICKCHART_BASE_URL = getenv('QUICKCHART_BASE_URL', 'https://quickchart.io/chart');

interface ChartConfig {
  type: string;
  data: {
    labels?: string[];
    datasets: Array<{
      label?: string;
      data: number[];
      backgroundColor?: string | string[];
      borderColor?: string | string[];
      [key: string]: any;
    }>;
    [key: string]: any;
  };
  options?: {
    title?: {
      display: boolean;
      text: string;
    };
    scales?: {
      y?: {
        beginAtZero?: boolean;
      };
    };
    [key: string]: any;
  };
}

class QuickChartServer {
  private server: Server;

  constructor() {
    this.server = new Server(
      {
        name: 'quickchart-server',
        version: '1.0.0',
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupToolHandlers();
    
    this.server.onerror = (error) => console.error('[MCP Error]', error);
    process.on('SIGINT', async () => {
      await this.server.close();
      process.exit(0);
    });
  }

  private validateChartType(type: string): void {
    const validTypes = [
      'bar', 'line', 'pie', 'doughnut', 'radar',
      'polarArea', 'scatter', 'bubble', 'radialGauge', 'speedometer'
    ];
    if (!validTypes.includes(type)) {
      throw new McpError(
        ErrorCode.InvalidParams,
        `Invalid chart type. Must be one of: ${validTypes.join(', ')}`
      );
    }
  }

  private generateChartConfig(args: any): ChartConfig {
    // Add defensive checks to handle possibly malformed input
    if (!args) {
      throw new McpError(
        ErrorCode.InvalidParams,
        'No arguments provided to generateChartConfig'
      );
    }
    
    if (!args.type) {
      throw new McpError(
        ErrorCode.InvalidParams,
        'Chart type is required'
      );
    }
    
    if (!args.datasets || !Array.isArray(args.datasets)) {
      throw new McpError(
        ErrorCode.InvalidParams,
        'Datasets must be a non-empty array'
      );
    }
    
    const { type, labels, datasets, title, options = {} } = args;
    
    this.validateChartType(type);

    const config: ChartConfig = {
      type,
      data: {
        labels: labels || [],
        datasets: datasets.map((dataset: any) => {
          if (!dataset || !dataset.data) {
            throw new McpError(
              ErrorCode.InvalidParams,
              'Each dataset must have a data property'
            );
          }
          return {
            label: dataset.label || '',
            data: dataset.data,
            backgroundColor: dataset.backgroundColor,
            borderColor: dataset.borderColor,
            ...(dataset.additionalConfig || {})
          };
        })
      },
      options: {
        ...options,
        ...(title && {
          title: {
            display: true,
            text: title
          }
        })
      }
    };

    // Special handling for specific chart types
    switch (type) {
      case 'radialGauge':
      case 'speedometer':
        if (!datasets?.[0]?.data?.[0]) {
          throw new McpError(
            ErrorCode.InvalidParams,
            `${type} requires a single numeric value`
          );
        }
        config.options = {
          ...config.options,
          plugins: {
            datalabels: {
              display: true,
              formatter: (value: number) => value
            }
          }
        };
        break;

      case 'scatter':
      case 'bubble':
        datasets.forEach((dataset: any) => {
          if (!Array.isArray(dataset.data[0])) {
            throw new McpError(
              ErrorCode.InvalidParams,
              `${type} requires data points in [x, y${type === 'bubble' ? ', r' : ''}] format`
            );
          }
        });
        break;
    }

    return config;
  }

  private async generateChartUrl(config: ChartConfig): Promise<string> {
    const encodedConfig = encodeURIComponent(JSON.stringify(config));
    return `${QUICKCHART_BASE_URL}?c=${encodedConfig}`;
  }

  private setupToolHandlers() {
    this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
      tools: [
        {
          name: 'generate_chart',
          description: 'Generate a chart using QuickChart',
          inputSchema: {
            type: 'object',
            properties: {
              type: {
                type: 'string',
                description: 'Chart type (bar, line, pie, doughnut, radar, polarArea, scatter, bubble, radialGauge, speedometer)'
              },
              labels: {
                type: 'array',
                items: { type: 'string' },
                description: 'Labels for data points'
              },
              datasets: {
                type: 'array',
                items: {
                  type: 'object',
                  properties: {
                    label: { type: 'string' },
                    data: { type: 'array' },
                    backgroundColor: { 
                      oneOf: [
                        { type: 'string' },
                        { type: 'array', items: { type: 'string' } }
                      ]
                    },
                    borderColor: {
                      oneOf: [
                        { type: 'string' },
                        { type: 'array', items: { type: 'string' } }
                      ]
                    },
                    additionalConfig: { type: 'object' }
                  },
                  required: ['data']
                }
              },
              title: { type: 'string' },
              options: { type: 'object' }
            },
            required: ['type', 'datasets']
          }
        },
        {
          name: 'download_chart',
          description: 'Download a chart image to a local file',
          inputSchema: {
            type: 'object',
            properties: {
              config: {
                type: 'object',
                description: 'Chart configuration object'
              },
              outputPath: {
                type: 'string',
                description: 'Path where the chart image should be saved. If not provided, the chart will be saved to Desktop or home directory.'
              }
            },
            required: ['config']
          }
        }
      ]
    }));

    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      switch (request.params.name) {
        case 'generate_chart': {
          try {
            const config = this.generateChartConfig(request.params.arguments);
            const url = await this.generateChartUrl(config);
            return {
              content: [
                {
                  type: 'text',
                  text: url
                }
              ]
            };
          } catch (error: any) {
            if (error instanceof McpError) {
              throw error;
            }
            throw new McpError(
              ErrorCode.InternalError,
              `Failed to generate chart: ${error?.message || 'Unknown error'}`
            );
          }
        }

        case 'download_chart': {
          try {
            const { config, outputPath: userProvidedPath } = request.params.arguments as { 
              config: Record<string, unknown>;
              outputPath?: string;
            };
            
            // Validate and normalize config first
            if (!config || typeof config !== 'object') {
              throw new McpError(
                ErrorCode.InvalidParams,
                'Config must be a valid chart configuration object'
              );
            }
            
            // Handle both direct properties and nested properties in 'data'
            let normalizedConfig: any = { ...config };
            
            // If config has data property with datasets, extract them
            if (config.data && typeof config.data === 'object' && 
                (config.data as any).datasets && !normalizedConfig.datasets) {
              normalizedConfig.datasets = (config.data as any).datasets;
            }
            
            // If config has data property with labels, extract them
            if (config.data && typeof config.data === 'object' && 
                (config.data as any).labels && !normalizedConfig.labels) {
              normalizedConfig.labels = (config.data as any).labels;
            }
            
            // If type is inside data object but not at root, extract it
            if (config.data && typeof config.data === 'object' && 
                (config.data as any).type && !normalizedConfig.type) {
              normalizedConfig.type = (config.data as any).type;
            }
            
            // Final validation after normalization
            if (!normalizedConfig.type || !normalizedConfig.datasets) {
              throw new McpError(
                ErrorCode.InvalidParams,
                'Config must include type and datasets properties (either at root level or inside data object)'
              );
            }
            
            // Generate default outputPath if not provided
            const fs = await import('fs');
            const path = await import('path');
            const os = await import('os');
            
            let outputPath = userProvidedPath;
            if (!outputPath) {
              // Get home directory
              const homeDir = os.homedir();
              const desktopDir = path.join(homeDir, 'Desktop');
              
              // Check if Desktop directory exists and is writable
              let baseDir = homeDir;
              try {
                await fs.promises.access(desktopDir, fs.constants.W_OK);
                baseDir = desktopDir; // Desktop exists and is writable
              } catch (error) {
                // Desktop doesn't exist or is not writable, use home directory
                console.error('Desktop not accessible, using home directory instead');
              }
              
              // Generate a filename based on chart type and timestamp
              const timestamp = new Date().toISOString()
                .replace(/:/g, '-')
                .replace(/\..+/, '')
                .replace('T', '_');
              const chartType = normalizedConfig.type || 'chart';
              outputPath = path.join(baseDir, `${chartType}_${timestamp}.png`);
              
              console.error(`No output path provided, using: ${outputPath}`);
            }
            
            // Check if the output directory exists and is writable
            const outputDir = path.dirname(outputPath);
            
            try {
              await fs.promises.access(outputDir, fs.constants.W_OK);
            } catch (error) {
              throw new McpError(
                ErrorCode.InvalidParams,
                `Output directory does not exist or is not writable: ${outputDir}`
              );
            }
            
            const chartConfig = this.generateChartConfig(normalizedConfig);
            const url = await this.generateChartUrl(chartConfig);
            
            try {
              const response = await axios.get(url, { responseType: 'arraybuffer' });
              await fs.promises.writeFile(outputPath, response.data);
            } catch (error: any) {
              if (error.code === 'EACCES' || error.code === 'EROFS') {
                throw new McpError(
                  ErrorCode.InvalidParams,
                  `Cannot write to ${outputPath}: Permission denied`
                );
              }
              if (error.code === 'ENOENT') {
                throw new McpError(
                  ErrorCode.InvalidParams,
                  `Cannot write to ${outputPath}: Directory does not exist`
                );
              }
              throw error;
            }
            
            return {
              content: [
                {
                  type: 'text',
                  text: `Chart saved to ${outputPath}`
                }
              ]
            };
          } catch (error: any) {
            if (error instanceof McpError) {
              throw error;
            }
            throw new McpError(
              ErrorCode.InternalError,
              `Failed to download chart: ${error?.message || 'Unknown error'}`
            );
          }
        }

        default:
          throw new McpError(
            ErrorCode.MethodNotFound,
            `Unknown tool: ${request.params.name}`
          );
      }
    });
  }

  async run() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error('QuickChart MCP server running on stdio');
  }
}

const server = new QuickChartServer();
server.run().catch(console.error);

```