首页 › tutorials › 造可视化MCP:数据秒变图表

造可视化MCP:让 Claude Code 把数据秒变 ECharts 图表

概述

本教程带你用 Model Context Protocol(MCP) 的 TypeScript SDK,从零构建一个「图表 MCP 服务器」:Claude Code 只要把一串数据交给它,它就能吐出一张可交互的 ECharts 图表。学完你将掌握 MCP 工具(Tool)的完整开发闭环——项目骨架、Zod 输入校验、stdio 传输、错误处理、Inspector 调试、以及接入 Claude Code 实际调用。适合已会基本 TypeScript、想把 Claude Code 能力从「能聊天」升级到「能干活」的开发者。


一、先搞懂一件事:MCP 到底解决了什么

很多新手会问:我直接让 Claude 写一段 ECharts 代码不就行了,为什么要折腾一个 MCP 服务器?

区别在于可靠性边界。让模型每次现场生成 HTML,会面临三个不稳定因素:ECharts 的 option 字段名偶尔写错、CDN 地址版本飘移、数据序列化时引号转义出错。而把「生成图表」封装成一个确定性工具——输入固定、输出固定、逻辑用代码写死——模型只负责决定「调用哪个工具、传什么参数」,脏活累活由代码兜底。这正是 MCP 的设计哲学:把「提供上下文/执行动作」这件事,从 LLM 的概率世界里剥离出来,交给确定性的服务端。

MCP 在 2026 年已经从「Anthropic 的实验」长成了开放标准,TypeScript SDK 每月下载量过千万,公共服务器过万。本文使用的官方 SDK 信息如下(截至 2026 年 7 月):

本教程统一采用 v2 API(registerTool + serveStdio),并在文末给出 v1 的等价写法,方便你在两条路径间迁移。


二、前置条件

项目 要求
Node.js 20 或更高版本(SDK 仅发布 ESM 包,老版本 Node 会报 import 错误)
运行时 任选:Node.js / Bun / Deno
Claude Code 已安装并能正常启动
浏览器 用于打开生成的 HTML 图表
基础 会读 TypeScript、了解 async/await

执行 node -v 确认版本 ≥ 20。若版本过低,用 nvm 升级:nvm install 20 && nvm use 20。


三、第一步:搭项目骨架

MCP 服务器本质上就是一个 Node 程序,骨架和普通脚手架没区别,唯一重点是 type=module。

mkdir chart-mcp && cd chart-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src

三条命令逐条解释:

预期结果:package.json 出现 "type": "module",node_modules 下能找到 @modelcontextprotocol/server。

常见问题


四、第二步:注册第一个工具(Tool)

工具是 MCP 最核心的原语。一个工具 = 一个名字 + 一份入参 schema + 一个异步处理函数。创建 src/index.ts:

import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
import { writeFile, mkdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';

const server = new McpServer({ name: 'charts', version: '1.0.0' });

server.registerTool(
  'render_chart',
  {
    description:
      '根据数据生成一张可交互的 ECharts 图表,保存为独立 HTML 文件。返回文件绝对路径。',
    inputSchema: z.object({
      title: z.string().default('Chart').describe('图表标题'),
      type: z.enum(['bar', 'line', 'pie']).describe('图表类型:柱状/折线/饼图'),
      labels: z.array(z.string()).min(1).describe('分类标签数组'),
      values: z.array(z.number()).min(1).describe('与 labels 一一对应的数值'),
      outputPath: z.string().optional().describe('自定义输出路径(.html);不填则写临时目录'),
    }),
  },
  async ({ title, type, labels, values, outputPath }) => {
    // 1. 业务校验:标签数与数值数必须一致
    if (labels.length !== values.length) {
      return {
        isError: true,
        content: [{
          type: 'text',
          text: `labels 与 values 数量不一致:${labels.length} vs ${values.length}`,
        }],
      };
    }
    // 2. 拼 ECharts option
    const option = buildOption(type, title, labels, values);
    // 3. 渲染独立 HTML
    const html = renderHtml(option);
    // 4. 落盘
    const absPath = outputPath ?? join(tmpdir(), `chart-${randomBytes(4).toString('hex')}.html`);
    await mkdir(join(absPath, '..'), { recursive: true });
    await writeFile(absPath, html, 'utf8');
    // 5. 回传结果
    return {
      content: [{
        type: 'text',
        text: `✅ 图表已生成:${absPath}\n类型:${type}|数据点:${labels.length}\n在浏览器打开即可查看交互式图表。`,
      }],
    };
  }
);

这段代码的关键认知点:

  1. inputSchema 是你唯一要写的 schema。 它是 Zod 对象。SDK 会自动把它转成模型能看懂的 JSON Schema,并在调用你的 handler 之前完成参数校验。也就是说,模型如果把 type 传成了 "scatter",SDK 会直接挡掉,你的 handler 根本不会执行。
  2. handler 返回 content,一个类型化块数组。 这里用 text 块;如果你返回的是图片,可以用 { type:'image', data: base64, mimeType:'image/png' }。
  3. isError: true 不是抛异常,而是「告诉模型这次失败了」。 模型能读到失败原因并决定是否重试。这比抛 throw new Error() 优雅得多——抛异常会让连接进入异常状态。

buildOption 与 renderHtml 两个纯函数放到下一步实现。先在文件末尾接上启动逻辑:

function buildOption(type, title, labels, values) {
  if (type === 'pie') {
    return {
      title: { text: title, left: 'center' },
      tooltip: {},
      legend: { bottom: 0 },
      series: [{
        type: 'pie',
        radius: '60%',
        data: labels.map((l, i) => ({ name: l, value: values[i] })),
      }],
    };
  }
  return {
    title: { text: title, left: 'center' },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: labels },
    yAxis: { type: 'value' },
    series: [{ type, data: values, smooth: type === 'line' }],
  };
}

void serveStdio(() => server);
console.error('charts MCP server running on stdio');

预期结果:文件能通过类型检查(npx tsx --eval "import './src/index.ts'" 不报语法错)。

常见问题


五、第三步:把数据变成一张真正的图

renderHtml 负责把 option 塞进一个自带 ECharts 的独立 HTML:

function renderHtml(option) {
  const json = JSON.stringify(option);
  return `<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="utf-8">
<title>${option.title?.text ?? 'Chart'}</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<style>html,body,#main{margin:0;height:100%}</style>
</head><body><div id="main"></div>
<script>
  const chart = echarts.init(document.getElementById('main'));
  chart.setOption(${json});
  window.addEventListener('resize', () => chart.resize());
</script></body></html>`;
}

设计取舍说明: 这里用 CDN 引入 ECharts,是为了让产物保持「单文件、零依赖、双击即看」。如果你的部署环境不能联网,可改成 npm install echarts 后用打包器内联,或预生成 PNG(需要 headless 浏览器,超出本文范围)。


六、第四步:跑起来——stdio 协议的黄金法则

npx tsx src/index.ts

预期结果: 终端打印一行:

charts MCP server running on stdio

然后就「卡住」了。这是正常的——stdio 服务器在 stdin 上等待客户端发起握手,没有客户端连进来它就永远等。

这里有一条会让 90% 新手翻车的铁律,官方文档原话警告:

stdout 是协议通道。日志请用 console.error——一次 console.log 就会污染 JSON-RPC 流。

stdio 服务器把请求读自 stdin、响应写到 stdout。一旦你手贱 console.log('调试一下'),这行字符串就会和合法的 JSON-RPC 报文混在一起,客户端解析时报错,连接直接崩。所以全文唯一安全的打印方式是 console.error(走 stderr,不进协议流)。

常见问题


七、第五步:用 MCP Inspector 调试,免装客户端

在你把服务器接进 Claude Code 之前,先用官方调试器单独验证工具逻辑。Inspector 是个本地 Web 应用,能直接调用任何 stdio 服务器:

npx @modelcontextprotocol/inspector npx tsx src/index.ts

操作步骤:

  1. 命令会启动一个本地服务并自动打开浏览器(通常在 http://localhost:6274)。
  2. 点 Connect,握手成功后左侧出现服务器信息。
  3. 切到 Tools 标签,选中 render_chart。
  4. 在右侧表单填入测试参数:
{
  "title": "Q1 各产品线营收(万元)",
  "type": "bar",
  "labels": ["网优", "核心网", "传输", "数据通信"],
  "values": [320, 580, 210, 460]
}
  1. 点 Run。

预期结果: 返回一段文本,类似:

✅ 图表已生成:/tmp/chart-a3f1b2c9.html
类型:bar|数据点:4

把这个路径在浏览器打开,就能看到一张带 tooltip 的柱状图。

调试技巧:


八、第六步:接入 Claude Code

服务器调通了,接下来让它真正被 Claude Code 调用。两种方式任选其一。

方式 A:命令行注册(推荐,最快)

claude mcp add chart-mcp -- npx tsx /绝对路径/chart-mcp/src/index.ts

注意 -- 后面是启动命令本身。用绝对路径,避免 Claude Code 的工作目录不在项目里时找不到文件。

方式 B:项目级 .mcp.json(适合团队共享)

在项目根目录创建 .mcp.json:

{
  "mcpServers": {
    "chart-mcp": {
      "command": "npx",
      "args": ["tsx", "/绝对路径/chart-mcp/src/index.ts"]
    }
  }
}

提交进 Git,队友 clone 后 Claude Code 会自动发现该服务器(首次需确认信任)。

验证接入: 启动 Claude Code,输入 /mcp,应看到 chart-mcp 状态为 connected,并列出 render_chart 工具。

实战对话:

你:帮我模拟一组 7 天的接口调用量数据,画成折线图。

Claude Code:(决定调用 render_chart)
→ render_chart(type=line, labels=["周一"..."周日"], values=[1200,1500,...])
← /tmp/chart-7d3e9a01.html

Claude Code:已生成折线图,路径如上, depicts 本周调用量趋势……

你打开那个 HTML,就是一张可缩放、带 tooltip 的折线图。整个过程中,模型只负责「编数据 + 决定图表类型」,确定性的渲染逻辑由你的 MCP 服务器保证——这就是 MCP 的价值兑现。


九、实战案例:完整可运行项目

把前面所有片段拼起来,这是一个复制即可跑的最小完整项目。文件清单:

chart-mcp/
├── package.json        # type=module,已装 server/zod/tsx
└── src/
    └── index.ts        # 完整服务器

src/index.ts 完整版:

import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
import { writeFile, mkdir } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { randomBytes } from 'node:crypto';

const server = new McpServer({ name: 'charts', version: '1.0.0' });

function buildOption(type, title, labels, values) {
  if (type === 'pie') {
    return {
      title: { text: title, left: 'center' },
      tooltip: {}, legend: { bottom: 0 },
      series: [{ type: 'pie', radius: '60%',
        data: labels.map((l, i) => ({ name: l, value: values[i] })) }],
    };
  }
  return {
    title: { text: title, left: 'center' },
    tooltip: { trigger: 'axis' },
    xAxis: { type: 'category', data: labels },
    yAxis: { type: 'value' },
    series: [{ type, data: values, smooth: type === 'line' }],
  };
}

function renderHtml(option) {
  const json = JSON.stringify(option);
  return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">
<title>${option.title?.text ?? 'Chart'}</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<style>html,body,#main{margin:0;height:100%}</style></head>
<body><div id="main"></div><script>
const c=echarts.init(document.getElementById('main'));
c.setOption(${json});
window.addEventListener('resize',()=>c.resize());
</script></body></html>`;
}

server.registerTool(
  'render_chart',
  {
    description: '根据数据生成可交互 ECharts 图表,保存为独立 HTML,返回路径。',
    inputSchema: z.object({
      title: z.string().default('Chart'),
      type: z.enum(['bar', 'line', 'pie']),
      labels: z.array(z.string()).min(1),
      values: z.array(z.number()).min(1),
      outputPath: z.string().optional(),
    }),
  },
  async ({ title, type, labels, values, outputPath }) => {
    if (labels.length !== values.length) {
      return { isError: true, content: [{ type: 'text',
        text: `labels 与 values 数量不一致:${labels.length} vs ${values.length}` }] };
    }
    const html = renderHtml(buildOption(type, title, labels, values));
    const absPath = outputPath ??
      join(tmpdir(), `chart-${randomBytes(4).toString('hex')}.html`);
    await mkdir(join(absPath, '..'), { recursive: true });
    await writeFile(absPath, html, 'utf8');
    return { content: [{ type: 'text',
      text: `✅ 图表已生成:${absPath}\n类型:${type}|数据点:${labels.length}` }] };
  }
);

void serveStdio(() => server);
console.error('charts MCP server running on stdio');

一键验证流程:

npx tsx src/index.ts                                  # 确认能启动
npx @modelcontextprotocol/inspector npx tsx src/index.ts   # Inspector 调用
claude mcp add chart-mcp -- npx tsx $(pwd)/src/index.ts    # 接入 Claude Code

十、进阶:错误处理、输出类型与安全清单

工具从「能跑」到「能上线」,差在三件事:

  1. 分级错误。 参数错(schema 挡)→ 业务错(isError:true + 文本说明)→ 系统错(throw,仅用于真正不可恢复的故障,如磁盘满)。三者不要混用。
  2. 善用多种 content 块。 除 text 外,image 块可直接把 PNG 喂给模型看(需配合 headless 渲染);resource 块可让模型按 URI 取更大数据。本例用 text 回传路径,是权衡「产物可复用」与「协议轻量」后的选择。
  3. 路径安全。 outputPath 直接来自模型输入——务必警惕路径穿越(如 ../../../etc/xxx)。生产化时应对路径做 path.resolve + 前缀白名单校验,限制只能写到指定目录。临时文件名用 randomBytes 而非时间戳,避免并发碰撞与可猜测。

十一、工作流程与架构图

flowchart LR
    U[用户: 画个柱状图] --> CC[Claude Code]
    CC -->|JSON-RPC over stdio| SVR[chart-mcp 服务器]
    SVR -->|Zod 校验参数| OK{合法?}
    OK -->|否| REJ[SDK 拦截返回校验错误]
    OK -->|是| HDL[render_chart handler]
    HDL --> OPT[拼 ECharts option]
    OPT --> FS[写独立 HTML 文件]
    FS --> PATH[/tmp/chart-xxxx.html]
    PATH -->|text 块回传路径| CC
    CC --> U2[用户: 浏览器打开图表]
sequenceDiagram
    participant CC as Claude Code
    participant S as chart-mcp
    participant FS as 文件系统
    CC->>S: tools/call render_chart {type,labels,values}
    S->>S: Zod 校验 + 一致性检查
    alt 参数非法
        S-->>CC: isError:true(文本原因)
    else 合法
        S->>S: buildOption + renderHtml
        S->>FS: writeFile(/tmp/chart-xxx.html)
        FS-->>S: ok
        S-->>CC: text 块(文件路径)
    end

十二、v1 等价写法(生产稳定版参考)

如果你的项目还在 v1.x(@modelcontextprotocol/sdk),核心差异是包名与注册 API:

// v1.x 写法
import { Server } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new Server({ name: 'charts', version: '1.0.0' });
server.tool('render_chart', { title: z.string(), type: z.enum(['bar','line','pie']), /* ... */ },
  async (args) => ({ content: [{ type: 'text', text: '...' }] }));

await server.connect(new StdioServerTransport());

v2 把 Server 改名 McpServer、server.tool 改为 server.registerTool、手动 connect(transport) 封装成 serveStdio(factory)。迁移时可参考官方 v2 文档的迁移章节。


十三、训练营适用性评估

维度 评估
适合的训练营类型 AI 应用开发、LLM 工具链、前端工程化、自动化测试
学员基础 会 TypeScript 基本语法、用过 npm
预计教学时长 2.5~3 小时(搭骨架 30min + 写工具 60min + Inspector 调试 45min + 接入 Claude Code 30min)
动手产出 每人一个可运行的图表 MCP,带回自己电脑继续用
进阶扩展作业 加 PNG 输出(headless 渲染)、加 Resources 提供「图表主题模板」、改成 Streamable HTTP 远程部署

本教程刻意只聚焦 Tool 一个原语并讲透,避免和「三原语」「远程 HTTP」等主题重叠。适合放在训练营的第二阶段——学员已装通 Claude Code、做完入门工具后,用它建立「从工具到协议」的完整心智模型。


延伸阅读


参考来源

  1. MCP TypeScript SDK 官方仓库 README(v2 beta) — 权威说明了 v2 拆包、@modelcontextprotocol/server、registerTool API、Zod v4 schema 与 serveStdio,以及 2026-07-28 规范时间线
  2. Build your first server — 官方分步教程 — 提供 npm init 到工具调用的完整流程、stdio「stdout 即协议」警告、Inspector 调用方式与错误返回约定
  3. MCP Inspector — 官方文档 — 交互式调试工具的使用方法与连接 stdio 服务器的细节
  4. Add Custom Tools to TypeScript MCP Servers — MCPcat 指南 — Zod 校验、错误处理、自定义工具的最佳实践补充
  5. Build an MCP Server with TypeScript: 2026 Tutorial — dev.to — 2026 年社区教程,佐证 SDK 下载量过千万、公共服务器过万的生态现状

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入