@gongrzhe/quickchart-mcp-server
Version:
A Model Context Protocol server for generating charts using QuickChart.io
218 lines (217 loc) • 9.21 kB
JavaScript
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';
class QuickChartServer {
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);
});
}
validateChartType(type) {
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(', ')}`);
}
}
generateChartConfig(args) {
const { type, labels, datasets, title, options = {} } = args;
this.validateChartType(type);
const config = {
type,
data: {
labels: labels || [],
datasets: datasets.map((dataset) => ({
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) => value
}
}
};
break;
case 'scatter':
case 'bubble':
datasets.forEach((dataset) => {
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;
}
async generateChartUrl(config) {
const encodedConfig = encodeURIComponent(JSON.stringify(config));
return `${QUICKCHART_BASE_URL}?c=${encodedConfig}`;
}
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) {
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;
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) {
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);