Quickchart-MCP-Server
by GongRzhe
- src
#!/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';
const 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 {
const { type, labels, datasets, title, options = {} } = args;
this.validateChartType(type);
const config: ChartConfig = {
type,
data: {
labels: labels || [],
datasets: datasets.map((dataset: any) => ({
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'
}
},
required: ['config', 'outputPath']
}
}
]
}));
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 } = request.params.arguments as {
config: Record<string, unknown>;
outputPath: string;
};
const chartConfig = this.generateChartConfig(config);
const url = await this.generateChartUrl(chartConfig);
const response = await axios.get(url, { responseType: 'arraybuffer' });
const fs = await import('fs');
await fs.promises.writeFile(outputPath, response.data);
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);