Skip to content

Commit a9739d4

Browse files
authored
feat: use MinIO to storage chart image (#12)
* feat(图表生成): 实现MinIO集成和统一图片处理逻辑 添加MinIO对象存储支持,将图表图片存储为URL而非Base64数据 重构所有图表工具使用统一的generateChartImageForTool方法 更新文档说明MinIO配置和故障排查指南 添加环境变量配置和调试日志支持 * refactor(imageHandler): 修改图表图片返回格式为纯文本地址 将返回的HTML img标签改为纯文本文件地址显示,提高兼容性 * refactor(utils): 统一图表生成函数并优化MinIO处理逻辑 将`generateChartImageForTool`合并到`generateChartImage`中,简化代码结构 将MinIO相关逻辑抽离到单独模块 更新所有工具文件使用新的统一函数 优化README文档并移除过时的TROUBLESHOOTING文件 * docs: 添加主要云服务提供商集成说明到README * docs: 更新README中的对象存储提供商集成说明 更新对象存储提供商集成部分,将标题改为更通用的描述并添加MinIO的详细集成信息 * docs: 更新贡献者列表 * test(echarts): 为宽度和高度添加最小最大值限制
1 parent 0a49365 commit a9739d4

27 files changed

+558
-218
lines changed

.env.example

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# MinIO Configuration
2+
# Copy this file to .env and update with your MinIO settings
3+
4+
# MinIO server endpoint (without protocol)
5+
MINIO_ENDPOINT=localhost
6+
7+
# MinIO server port
8+
MINIO_PORT=9000
9+
10+
# Use SSL/HTTPS connection (true/false)
11+
MINIO_USE_SSL=false
12+
13+
# MinIO access credentials
14+
MINIO_ACCESS_KEY=minioadmin
15+
MINIO_SECRET_KEY=minioadmin
16+
17+
# Bucket name for storing charts
18+
MINIO_BUCKET_NAME=mcp-echarts

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,11 @@ coverage
3131
package-lock.json
3232
pnpm-lock.yaml
3333
bun.lock
34+
35+
# Environment variables
36+
.env
37+
.env.*
38+
!.env.example
39+
40+
# Test files
41+
test-*.js

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Generate <img src="https://echarts.apache.org/zh/images/favicon.png" height="14"
1414

1515
- Fully support all features and syntax of `ECharts`, include data, style, theme and so on.
1616
- Support exporting to `png`, `svg`, and `option` formats, with validation for `ECharts` to facilitate the model's multi-round output of correct syntax and graphics.
17+
- **MinIO Integration**: Store charts in MinIO object storage and return URLs instead of Base64 data for better performance and sharing capabilities.
1718
- Lightweight, we can install it with `zero dependence`.
1819
- Extremely `secure`, fully generated locally, without relying on any remote services.
1920

@@ -56,6 +57,46 @@ On Window system:
5657

5758
Also, you can use it on aliyun, modelscope, glama.ai, smithery.ai or others with HTTP, SSE Protocol.
5859

60+
## 🗂️ MinIO Configuration (Optional)
61+
62+
For better performance and sharing capabilities, you can configure MinIO object storage to store chart images as URLs instead of Base64 data.
63+
64+
### Setup MinIO
65+
66+
1. **Install and start MinIO locally:**
67+
```bash
68+
# Download MinIO (macOS example)
69+
brew install minio/stable/minio
70+
71+
# Start MinIO server
72+
minio server ~/minio-data --console-address :9001
73+
```
74+
75+
2. **Configure environment variables:**
76+
```bash
77+
# Copy the example environment file
78+
cp .env.example .env
79+
80+
# Edit .env with your MinIO settings
81+
MINIO_ENDPOINT=localhost
82+
MINIO_PORT=9000
83+
MINIO_USE_SSL=false
84+
MINIO_ACCESS_KEY=minioadmin
85+
MINIO_SECRET_KEY=minioadmin
86+
MINIO_BUCKET_NAME=mcp-echarts
87+
```
88+
89+
3. **Integration with Object Storage Providers:**
90+
- **[MinIO](https://min.io/)**: High-performance, S3-compatible object storage. Use [MinIO JavaScript Client](https://min.io/docs/minio/linux/developers/javascript/minio-javascript.html) for direct integration.
91+
- **[Amazon S3](https://aws.amazon.com/s3/)**: Use [AWS SDK](https://aws.amazon.com/sdk-for-javascript/) with compatible API endpoint.
92+
- **[Alibaba Cloud OSS](https://www.alibabacloud.com/product/object-storage-service)**: Use the [Alibaba Cloud SDK](https://www.alibabacloud.com/help/en/sdk) for OSS services.
93+
- **[Google Cloud Storage](https://cloud.google.com/storage)**: Integrate using [Google Cloud SDK](https://cloud.google.com/sdk) or compatible API.
94+
- **[Microsoft Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs)**: Use [Azure SDK](https://azure.microsoft.com/en-us/downloads/) for Blob storage access.
95+
- **[Tencent Cloud COS](https://intl.cloud.tencent.com/product/cos)**: Use the [Tencent Cloud SDK](https://intl.cloud.tencent.com/document/product/436/6474) for COS integration.
96+
### Fallback Behavior
97+
98+
If MinIO is not configured or unavailable, the system automatically falls back to `Base64` data output, ensuring compatibility.
99+
59100

60101
## 🔨 Development
61102

@@ -81,6 +122,7 @@ npm run start
81122
## 🧑🏻‍💻 Contributors
82123

83124
- [lyw405](https://github.com/lyw405): Supports 15+ charting MCP tool. [#2](https://github.com/hustcc/mcp-echarts/issues/2)
125+
- [susuperli](https://github.com/susuperli): use MinIO to save the chart image base64 and return the url. [#10](https://github.com/hustcc/mcp-echarts/issues/10)
84126
- [BQXBQX](https://github.com/BQXBQX): Use @napi-rs/canvas instead node-canvas. [#3](https://github.com/hustcc/mcp-echarts/issues/3)
85127
- [hustcc](https://github.com/hustcc): Initial the repo.
86128

__tests__/tools/echarts.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@
1212
"width": {
1313
"type": "number",
1414
"description": "The width of the ECharts in pixels. Default is 800.",
15-
"default": 800
15+
"default": 800,
16+
"minimum": 50,
17+
"maximum": 5000
1618
},
1719
"height": {
1820
"type": "number",
1921
"description": "The height of the ECharts in pixels. Default is 600.",
20-
"default": 600
22+
"default": 600,
23+
"minimum": 50,
24+
"maximum": 5000
2125
},
2226
"theme": {
2327
"type": "string",

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
"dependencies": {
2020
"@modelcontextprotocol/sdk": "^1.12.0",
2121
"@napi-rs/canvas": "^0.1.73",
22+
"dotenv": "^17.2.1",
2223
"echarts": "^5.6.0",
24+
"minio": "^8.0.5",
2325
"zod": "^3.25.16"
2426
},
2527
"devDependencies": {

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22
import process from "node:process";
33
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5+
import { config } from "dotenv";
56
import { tools } from "./tools";
67

8+
// Load environment variables from .env file (completely silent to avoid stdout contamination)
9+
process.env.DOTENV_CONFIG_QUIET = "true";
10+
config({ override: false, debug: false });
11+
712
/**
813
* MCP Server for ECharts.
914
* This server provides tools for generating ECharts visualizations and validate ECharts configurations.
@@ -65,6 +70,6 @@ async function main(): Promise<void> {
6570

6671
// Start application
6772
main().catch((error) => {
68-
console.error("Failed to start application:", error);
73+
// Don't use console.error in MCP servers as it interferes with JSON protocol
6974
process.exit(1);
7075
});

src/tools/bar.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { EChartsOption, SeriesOption } from "echarts";
22
import { z } from "zod";
3-
import { renderECharts } from "../utils/render";
3+
import { generateChartImage } from "../utils";
44
import {
55
AxisXTitleSchema,
66
AxisYTitleSchema,
@@ -55,7 +55,7 @@ export const generateBarChartTool = {
5555
title: TitleSchema,
5656
width: WidthSchema,
5757
}),
58-
run: (params: {
58+
run: async (params: {
5959
axisXTitle?: string;
6060
axisYTitle?: string;
6161
data: Array<{ category: string; value: number; group?: string }>;
@@ -164,15 +164,13 @@ export const generateBarChartTool = {
164164
},
165165
};
166166

167-
const imageBase64 = renderECharts(echartsOption, width, height, theme);
168-
return {
169-
content: [
170-
{
171-
data: imageBase64,
172-
mimeType: "image/png",
173-
type: "image",
174-
},
175-
],
176-
};
167+
return await generateChartImage(
168+
echartsOption,
169+
width,
170+
height,
171+
theme,
172+
"png",
173+
"generate_bar_chart",
174+
);
177175
},
178176
};

src/tools/boxplot.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { EChartsOption, SeriesOption } from "echarts";
22
import { z } from "zod";
3-
import { renderECharts } from "../utils/render";
3+
import { generateChartImage } from "../utils";
44
import {
55
AxisXTitleSchema,
66
AxisYTitleSchema,
@@ -42,7 +42,7 @@ export const generateBoxplotChartTool = {
4242
title: TitleSchema,
4343
width: WidthSchema,
4444
}),
45-
run: (params: {
45+
run: async (params: {
4646
axisXTitle?: string;
4747
axisYTitle?: string;
4848
data: Array<{ category: string; value: number; group?: string }>;
@@ -181,15 +181,13 @@ export const generateBoxplotChartTool = {
181181
return [min, Q1, median, Q3, max];
182182
}
183183

184-
const imageBase64 = renderECharts(echartsOption, width, height, theme);
185-
return {
186-
content: [
187-
{
188-
data: imageBase64,
189-
mimeType: "image/png",
190-
type: "image",
191-
},
192-
],
193-
};
184+
return await generateChartImage(
185+
echartsOption,
186+
width,
187+
height,
188+
theme,
189+
"png",
190+
"generate_boxplot_chart",
191+
);
194192
},
195193
};

src/tools/candlestick.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { EChartsOption, SeriesOption } from "echarts";
22
import { z } from "zod";
3-
import { renderECharts } from "../utils/render";
3+
import { generateChartImage } from "../utils";
44
import {
55
HeightSchema,
66
ThemeSchema,
@@ -41,7 +41,7 @@ export const generateCandlestickChartTool = {
4141
title: TitleSchema,
4242
width: WidthSchema,
4343
}),
44-
run: (params: {
44+
run: async (params: {
4545
data: Array<{
4646
date: string;
4747
open: number;
@@ -208,15 +208,13 @@ export const generateCandlestickChartTool = {
208208
},
209209
};
210210

211-
const imageBase64 = renderECharts(echartsOption, width, height, theme);
212-
return {
213-
content: [
214-
{
215-
data: imageBase64,
216-
mimeType: "image/png",
217-
type: "image",
218-
},
219-
],
220-
};
211+
return await generateChartImage(
212+
echartsOption,
213+
width,
214+
height,
215+
theme,
216+
"png",
217+
"generate_candlestick_chart",
218+
);
221219
},
222220
};

src/tools/echarts.ts

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,30 @@
11
import { z } from "zod";
2-
import { renderECharts } from "../utils";
2+
import { generateChartImage } from "../utils";
33

44
function isValidEChartsOption(option: string): boolean {
55
try {
66
const parsedOption = JSON.parse(option);
7-
return typeof parsedOption === "object" && parsedOption !== null;
7+
if (typeof parsedOption !== "object" || parsedOption === null) {
8+
return false;
9+
}
10+
11+
// Basic validation for common chart types that require axes
12+
if (parsedOption.series && Array.isArray(parsedOption.series)) {
13+
const hasCartesianSeries = parsedOption.series.some(
14+
(series: { type?: string }) =>
15+
series.type && ["bar", "line", "scatter"].includes(series.type),
16+
);
17+
18+
// If chart has cartesian series, it should have proper axis configuration
19+
if (hasCartesianSeries && !parsedOption.xAxis && !parsedOption.yAxis) {
20+
console.error(
21+
"[DEBUG] Chart validation failed: Cartesian chart missing axis configuration",
22+
);
23+
return false;
24+
}
25+
}
26+
27+
return true;
828
} catch {
929
return false;
1030
}
@@ -46,11 +66,21 @@ ATTENTION: A valid ECharts option must be a valid JSON string, and cannot be emp
4666
}),
4767
width: z
4868
.number()
69+
.min(
70+
50,
71+
"Width must be at least 50 pixels to ensure proper chart rendering",
72+
)
73+
.max(5000, "Width cannot exceed 5000 pixels")
4974
.describe("The width of the ECharts in pixels. Default is 800.")
5075
.optional()
5176
.default(800),
5277
height: z
5378
.number()
79+
.min(
80+
50,
81+
"Height must be at least 50 pixels to ensure proper chart rendering",
82+
)
83+
.max(5000, "Height cannot exceed 5000 pixels")
5484
.describe("The height of the ECharts in pixels. Default is 600.")
5585
.optional()
5686
.default(600),
@@ -67,7 +97,7 @@ ATTENTION: A valid ECharts option must be a valid JSON string, and cannot be emp
6797
.optional()
6898
.default("png"),
6999
}),
70-
run: (params: {
100+
run: async (params: {
71101
echartsOption: string;
72102
width?: number;
73103
height?: number;
@@ -76,6 +106,17 @@ ATTENTION: A valid ECharts option must be a valid JSON string, and cannot be emp
76106
}) => {
77107
const { width, height, echartsOption, theme, outputType } = params;
78108

109+
// Debug logging (writes to stderr, won't interfere with MCP protocol)
110+
if (process.env.DEBUG_MCP_ECHARTS) {
111+
console.error("[DEBUG] ECharts tool called with params:", {
112+
echartsOptionLength: echartsOption?.length,
113+
width,
114+
height,
115+
theme,
116+
outputType,
117+
});
118+
}
119+
79120
if (!isValidEChartsOption(echartsOption)) {
80121
throw new Error(
81122
"Invalid ECharts option, a valid ECharts option must be a valid JSON string, and cannot be empty.",
@@ -84,23 +125,14 @@ ATTENTION: A valid ECharts option must be a valid JSON string, and cannot be emp
84125

85126
const option = JSON.parse(echartsOption);
86127

87-
const r = renderECharts(option, width, height, theme, outputType);
88-
89-
const isImage = outputType !== "svg" && outputType !== "option";
90-
91-
const result = isImage
92-
? {
93-
type: "image",
94-
data: r,
95-
mimeType: "image/png",
96-
}
97-
: {
98-
type: "text",
99-
text: r,
100-
};
101-
102-
return {
103-
content: [result],
104-
};
128+
// Use the unified image generation method
129+
return await generateChartImage(
130+
option,
131+
width,
132+
height,
133+
theme,
134+
outputType,
135+
"generate_echarts_chart",
136+
);
105137
},
106138
};

0 commit comments

Comments
 (0)