Skip to main content
Glama

ECharts MCP

by apache
index.js8.46 kB
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import express from 'express'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'; import dotenv from 'dotenv'; import fs from 'fs'; import { getChartBase64 } from './chart.js'; import { isTreelike, seriesTypes } from './util.js'; import { saveImage } from './storage.js'; dotenv.config(); class EChartsServer { constructor() { // Throw an error if there is no .env file if (!fs.existsSync('.env')) { throw new Error('Missing .env file. Please create a .env file with your configuration. See .env.example for reference.'); } this.server = new Server( { name: 'echarts', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.server.onerror = (error) => console.error('[MCP Error]', error); } validateChartType(type) { if (!seriesTypes.includes(type)) { throw new McpError( ErrorCode.InvalidParams, `Invalid chart type. Must be one of: ${seriesTypes.join(', ')}` ); } } validateChartData(data, type) { if (isTreelike(type)) { if (data.length > 0 && data[0].value == null) { throw new McpError( ErrorCode.InvalidParams, type + ' chart data should be like [["A", 100], ["B", 200], ["C", 300]] for bar/line/pie/scatter charts, or [{ "name": "A", "value": 100, "children": [{ "name": "A1", "value": 40}, { "name": "A2", "value": 60}]}]' ); } return; } if (!Array.isArray(data)) { throw new McpError(ErrorCode.InvalidParams, 'Chart data must be an array. Input data: ' + JSON.stringify(data)); } if (data.length > 1) { const firstRow = data[0]; if (!Array.isArray(firstRow)) { throw new McpError( ErrorCode.InvalidParams, 'Chart data must be an array of arrays. For example: [["A", 100], ["B", 200], ["C", 300]]' ); } } } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'get-chart', description: 'Generate an ECharts chart', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Chart title', }, type: { type: 'string', description: `Chart type (${seriesTypes.join(', ')})`, }, seriesName: { type: 'string', description: 'Series name that will be displayed in the legend', }, data: { type: 'array', description: 'Chart data array. For example: [["A", 100], ["B", 200], ["C", 300]] for bar/line/pie/scatter charts, or [{ "name": "A", "value": 100, "children": [{ "name": "A1", "value": 40}, { "name": "A2", "value": 60}]}] for tree charts', }, xAxisName: { type: 'string', description: 'Name of the first dimension (data[0]) including unit, for bar/line/scatter/pie charts. For example, when data is [["Apple", 100], ["Banana", 200], ["Cherry", 300]], xAxisName should be "Fruit".', }, yAxisName: { type: 'string', description: 'Name of the second dimension (data[1]) including unit, for bar/line/scatter/pie charts. For example, when data is [["Apple", 100], ["Banana", 200], ["Cherry", 300]], yAxisName should be "Sales (USD)".', }, }, required: ['type', 'data', 'title', 'seriesName'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'get-chart') { try { const { type, data, title, seriesName, xAxisName, yAxisName } = request.params.arguments; this.validateChartType(type); this.validateChartData(data, type); const base64 = getChartBase64(type, data, title, seriesName, xAxisName, yAxisName); try { const url = await saveImage(base64); return { content: [ { type: 'text', text: url, }, ], }; } catch (error) { console.error(error); throw new McpError(ErrorCode.InternalError, 'Failed to save image'); } } catch (error) { console.error(error); if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Failed to generate chart: ${error?.message || 'Unknown error'}` ); } } throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); }); } async run() { const app = express(); const transports = {}; app.get('/', (_, res) => { res.send('Apache ECharts MCP Server is running'); }); app.get('/sse', async (_, res) => { const transport = new SSEServerTransport('/messages', res); transports[transport.sessionId] = transport; res.on('close', () => { delete transports[transport.sessionId]; }); await this.server.connect(transport); }); app.post('/messages', async (req, res) => { const sessionId = req.query.sessionId; const transport = transports[sessionId]; if (!transport) { if (!res.headersSent) { res.status(400).send('No transport found for sessionId. Please establish SSE connection first.'); } return; } await transport.handlePostMessage(req, res); }); const port = process.env.SERVER_PORT || 8081; app.listen(port, () => { console.log(`Server is running on port ${port}`); }); } } const server = new EChartsServer(); server.run().catch(console.error);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/apache/echarts-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server