什么是 MCP 服务器
Model Context Protocol(模型上下文协议,简称 MCP)是一个开放协议,它让 AI 模型可以通过一套标准化的 JSON-RPC 接口来调用外部工具。一个 MCP 服务器做的事情很简单——它暴露一堆工具(tools),每个工具就是一个可以被 AI agent 调用的函数。AI 模型在需要完成某个任务的时候,不用自己硬算,而是说"帮我画一张 sin(x) 的图",然后 MCP 服务器把图画好返回给它。
plot-mcp-worker 暴露了 超过 40 个可视化工具,覆盖的范围非常广:函数绘图、柱状图、散点图、直方图、箱线图、饼图、力分析图、电路原理图、3D 几何图形、韦恩图(Venn diagram)、以及物理和数学的教学模板。从提交第一个 commit 到现在一共 105 次提交。光是 Worker 的入口文件就有 3,471 行 TypeScript 代码,这还不包括一个 1,520 行的独立绘图引擎和一个 1,123 行的独立 SVG 渲染器。这三个文件加起来,就是这个项目全部的核心逻辑。
架构全景
在深入每一个模块之前,先把整条数据管线的全貌看清楚:
AI Agent(Claude / Cursor / 任何支持 MCP 的客户端)
↓ 通过 HTTP POST 发送 JSON-RPC 请求到 /mcp 端点
Cloudflare Worker(TypeScript 编写,运行在 Cloudflare 全球边缘节点上)
├─ 表达式解析器(基于 expr-eval 库,支持分段函数 piecewise)
├─ 数据变换管道(normalize → smooth → filter → rolling_avg → downsample)
├─ 智能坐标轴引擎(nice ticks 算法 + 自动 PI 检测 + 渐近线钳制 + 对数坐标)
├─ SVG 生成器(坐标轴、网格、标签、图例、误差棒、注释标注)
├─ CJK 文字转路径管线(opentype.js + GB2312 一级字库 → 嵌入式 SVG path)
└─ resvg-wasm 栅格化引擎(SVG → PNG,全部在 Workers 的 WASM 运行时内完成)
↓
最终输出:PNG 直链 / SVG 源码 / base64 图片载荷 / 交互式 HTML
↓
Cloudflare 边缘缓存,TTL 5 分钟,同一张图不需要重复渲染
上面这张图里有一个最关键的信息:整条管线没有任何浏览器参与。没有无头 Chrome。没有 Puppeteer。没有 Docker 容器。从用户输入一个数学表达式字符串,到最终输出一张 PNG 图片的二进制数据,全部计算都在 Cloudflare Workers 的 V8 隔离环境中完成——其中 SVG 到 PNG 的栅格化步骤通过编译为 WebAssembly 的 resvg 库来执行,这也是整个管线上唯一一个不是纯 JavaScript 的环节。
1. 表达式解析与函数绘图
绘图引擎的第一个环节是解析用户的数学表达式。buildSinglePlot() 这个函数先把表达式字符串(比如 "sin(x) * exp(-x / 5)")交给 expr-eval 库,由它来构建出一棵抽象语法树(AST)。然后引擎在用户指定的 x 轴定义域上均匀采样——默认 1,000 个采样点,但用户可以要求最高 20,000 个点——在每个采样点对 AST 求值,得到一个 (x, y) 坐标对,连起来就是一条曲线。
1.1 渐近线检测与钳制
但纯粹的均匀采样在遇到渐近线的时候会出大问题。考虑 tan(x) 这个函数:当 x 趋近于 π/2 的时候,tan(x) 的值会从正无穷突然跳到负无穷。如果直接用线段连接相邻的两个采样点,你会得到一条从屏幕顶端垂直贯穿到底端的竖线——这看起来完全不像 tan(x) 的图像,而像一个丑陋的伪影。
引擎通过一个两阶段的检测机制来处理这个问题。首先,遍历所有相邻的采样点对 (y[i], y[i+1]),检查两个条件是否同时成立:一是 y[i] 和 y[i+1] 的符号相反(y[i] * y[i+1] < 0),说明函数在两点之间穿越了零点或者跨越了渐近线;二是两点之间的 y 值跳跃幅度超过了基于 IQR(四分位距)计算出来的阈值——如果是正常的零点穿越,跳跃幅度通常很小;如果跳跃幅度异常大,那就是遇到渐近线了。满足这两个条件的时候,引擎在这两个采样点之间插入一个 NaN(Not a Number)断点。SVG 渲染器在遇到 NaN 的时候会自动断开路径,这样曲线在渐近线处就不会错误地连接起来了。
此外,即使检测到了渐近线,曲线上被钳制的那一段也不能完全丢弃——否则函数的"形状"就不完整了。引擎的做法是用 IQR 方法计算出一个合理的 y 轴视觉上限和下限,把超出这个范围的极值钳制到边界上。这样 tan(x) 的图像会在渐近线附近陡然上升但不会冲出画面,看起来就像你在数学课本上见到的那种规范的渐近线图像。
1.2 分段函数
很多数学问题需要分段定义函数——不同区间用不同的表达式。比如一个分段函数可能长这样:x 小于 0 的时候是 x²,x 在 0 到 3 之间是 sqrt(x),x 大于 3 的时候是 sin(x) + 2。引擎支持这种用法:每个分段有自己的表达式(expr)、自己的 x 定义域上下界(x_min 和 x_max)、自己的显示标签(label)和自己的颜色(color)。引擎在每个分段的内部独立采样,采样点数按分段长度占总定义域的比例来分配。在分段之间的边界点处,引擎插入 NaN 断点,防止不同分段的曲线被错误地连在一起。每个分段的中点位置还会渲染一个小的标注标签,让人一眼就能看出哪个颜色对应哪个分段表达式。
2. 数据变换管道
在完成采样之后、实际渲染之前,数据可以经过一条由五个步骤组成的可组合变换管道。这条管道的设计理念是:用户声明他想要的变换,管道按顺序执行每一步,每一步都可以查看上一步的输出。管道中的每一步都是可选的——如果你不需要任何变换,数据就直接以原始采样结果进入渲染阶段。
- Normalize(归一化):把数据缩放到一个标准范围内。支持三种归一化方法——minmax(线性映射到 [0, 1] 区间,保留原始数据的相对大小关系)、zscore(转换为均值为 0、标准差为 1 的标准正态分布,适用于需要比较不同量纲数据的场景)、maxabs(除以数据中绝对值最大的那个数,结果落在 [-1, 1] 区间内,保留了正负号信息)。你可以选择只归一化 x 轴、只归一化 y 轴,或两个轴都归一化。如果输入数据全为零(比如一条水平直线),归一化操作会跳过并在结构化警告中说明原因,而不是除以零导致 NaN 泛滥。
- Smooth(平滑):使用中心移动平均来消除数据中的高频噪声。窗口大小可以配置(默认是 5 个点)。对于序列开头和结尾的边界点——它们没有足够的邻居来做完整的窗口平均——算法使用截断窗口,有几个邻居就用几个。窗口内的 NaN 值被替换为最近的有效邻居值,防止一个坏点污染整个窗口。
- Filter(过滤):按照比较运算符来删除不符合条件的数据点。支持的运算符包括大于、大于等于、小于、小于等于、等于、不等于。比如你可以设置"只保留 y 值大于 0 的数据点"。有一个防呆设计:如果过滤条件太严格以至于整个序列都被清空了,过滤器会回退并保留全部原始数据点,不会返回一个空序列导致后续步骤报错。
- Rolling average(滚动平均):功能和 Smooth 基本一样,但在语义上被独立出来——Smooth 通常用于"去除噪声",Rolling average 通常用于"观察趋势"。两者的实现分开更有助于管道的清晰性和调试。
- Downsample(降采样):当采样点太多(比如 20,000 个点)的时候,渲染出来的 SVG 会非常庞大,resvg 处理起来也很慢。降采样有两种策略:均匀降采样(每隔 N 个点取一个,简单粗暴但速度快)和 minmax 降采样(先把数据点分配到若干个桶中,每个桶保留一个最小值和最大值,然后按原始索引排序并去重)。minmax 的好处是它不会丢失数据的极值信息——对于有尖峰的数据,均匀降采样可能刚好跳过了尖峰,而 minmax 保证每个桶的尖峰都被保留。
管道中每个变换步骤在遇到异常情况时都会产出结构化的警告信息。比如你在柱状图上尝试 Smooth——柱状图的数据是离散的分类数据,平滑是没有意义的——管道不会崩溃,而是跳过这个步骤并在调试日志中标记一个 warning。如果开启了 trace 模式,每个步骤的输入点数和输出点数都会被记录下来,方便你排查"为什么我的图数据点变少了"这类问题。
3. 智能坐标轴引擎
坐标轴是图表中最容易被忽视但也最容易出问题的部分。一个图表的函数曲线画得再漂亮,如果坐标轴上的刻度值全是"0.3333、0.6667、1.0000"这种垃圾数字,整个图看起来就是业余水平。plot-mcp-worker 的坐标轴引擎有五层智能化处理:
- Nice ticks 算法:这是坐标轴引擎的核心。步长不是随意选取的——它只能从集合 {1, 2, 2.5, 5} 乘以 10 的某个幂次(10ⁿ)中选取。比如数据范围是 0 到 37,原始步长大约是 37/6 ≈ 6.17,这个数字不好看。算法找到最接近 6.17 的 nice 步长是 5(来自 5 × 10⁰),所以实际步长变成 5,坐标轴上的刻度就是 0、5、10、15、20、25、30、35、40——每一个都是人一眼就能看懂的数字。算法确保每个轴上大约有 5 到 8 个刻度,不会太密也不会太稀。
- 自动 PI 检测模式:当 x 轴的定义域是 π 的整数倍时(比如 [-2π, 2π]),引擎检测到
max / PI的值接近一个整数(容差 0.01),自动切换到 π 格式化模式。刻度标签不再显示小数,而是显示 -2π、-π、0、π、2π。这对于三角函数图像来说是标准做法,但很多国产图表库都做不好这个。 - 三角函数 y 轴特化:sin(x) 和 cos(x) 的值域天然是 [-1, 1]。引擎检测到函数是三角函数时,y 轴刻度自动设为 -1、-0.5、0、0.5、1,而不是从数据范围推导出来的类似 -0.983、0.997 这种让人摸不着头脑的数字。
- 0 轴对称:数学函数图像通常需要 y 轴关于零点对称——向上和向下的范围应该一致。引擎计算 ymin 和 ymax,然后取 max(|ymin|, |ymax|) 作为对称轴范围,y 轴就变成了 [-M, M] 的形式,零点正好在正中间。这个行为可以通过参数关闭,如果你确实需要不等距的 y 轴。
- 对数坐标:对于跨越多个数量级的数据(比如 1 到 100000),线性坐标轴完全没法看——小的值全部挤在左下角。对数 y 轴模式下,引擎自动检测数据跨越的数量级范围,网格线标注在 10 的幂次位置(1、10、100、1000……),而不是在线性位置。对数坐标下的 nice ticks 算法和线性坐标略有不同,使用的是 10 的幂次作为候选步长。
4. CJK 文字转路径管线
这是整个项目中最硬核的工程环节。Cloudflare Workers 的运行环境是一个 V8 隔离沙箱——里面没有操作系统,没有文件系统,没有系统字体。你写一行 <text>正弦函数图像</text> 到 SVG 里面,在用户的浏览器里打开,浏览器会使用操作系统的字体来渲染这段文字,看起来一切正常。但如果你在 Worker 里面用 resvg 来把这个 SVG 渲染成 PNG,resvg 会去找系统字体——而 Worker 里面根本没有字体。结果就是所有的中文、日文、韩文字符全部变成一个个空心豆腐块(tofu),图片完全不可用。
解决方案说起来简单做起来极其繁琐:在构建 Worker 的时候,把一个中文字体文件里面每一个字的轮廓都预先提取出来,转换成 SVG 的 <path> 数据,存在一个大的查找表里。当渲染器在标题或标签中遇到中文文本时,它不再输出 <text>正</text>,而是直接输出这个字的轮廓路径:<path d="M 123 456 C 789 012 ... Z">。因为路径数据是 SVG 原生支持的,resvg 不需要任何字体就可以渲染它,也不依赖客户端的系统字体——生成的 PNG 在任何设备上看起来都一模一样。
具体来说,这条管线处理的是 GB2312 一级字符集——这是中国国家标准中定义的最常用汉字集合,包含 3,755 个汉字,加上全角标点符号和拉丁字母,一共对应 4,532 个字形(glyph)。每个字形通过 opentype.js 这个 JavaScript 字体解析库来加载——opentype.js 能够读取 .ttf 或 .otf 字体文件,从中提取出任意字符的贝塞尔曲线轮廓——然后把轮廓序列化为 SVG path 的 d 属性字符串。这个过程中有很多细节需要处理:TrueType 字体使用二次贝塞尔曲线,而 SVG path 使用的是三次贝塞尔曲线,需要做转换;某些复杂字形包含多个闭合轮廓(比如"回"字有两个圈),需要正确处理 moveto 指令来分隔它们。
字体文件的选择也经历了一次迭代。最初使用的是 PingFang SC,这是苹果系统中文字体的标准选择,字形质量很高,但文件体积达到 442 KB——对于一个 Cloudflare Worker 的免费套餐(1 MB 脚本体积限制)来说太大了。后来替换为 Heiti SC Medium,同等覆盖范围下体积降到了 284 KB,减少了 36%。后来又做了一个架构决策:把字体数据从 Worker bundle 中移到了 Cloudflare KV 存储中。Worker 在首次需要渲染中文时从 KV 加载字体数据,加载后缓存 5 分钟。这样 Worker bundle 本身保持在免费套餐限制之内,而 KV 的读取延迟(通常在 10ms 以内)对用户体验几乎没有影响。
5. SVG 到 PNG 的栅格化:resvg-wasm
渲染管线先生成 SVG——在这个阶段,所有东西都是矢量的:坐标轴线是精确的直线,文字是精确的字形路径,网格线是精确的虚线。SVG 的输出本身就已经是一张合法的图表,你可以直接在浏览器里打开它,或者导入到 Figma 里面继续编辑。但大多数使用场景下——特别是 AI agent 想要把图表展示给用户看的时候——需要的是 PNG 格式的位图。
SVG 到 PNG 的转换是通过 resvg-wasm 来完成的。resvg 是一个用 Rust 语言编写的 SVG 渲染库,它在正确性和性能方面比浏览器的内置 SVG 渲染器要可靠得多,特别是在处理复杂的 CSS 样式、clip-path、mask、pattern 等高级 SVG 特性时。这个 Rust 库通过 wasm-pack 编译为 WebAssembly 模块(约 1.4 MB),在 Worker 启动时加载到 WASM 运行时中,之后每次冷启动周期只需要初始化一次。
SVG 输出支持 8 种预设的背景风格——亮色(白色背景,适合嵌入文档)、暗色(深色背景,适合暗色模式的 UI)、透明(适合叠加到其他图像上),以及 5 种颜色变体。默认的输出尺寸是 1200×720 像素,但可以通过参数调整。输出格式有四种选择:png(直接返回 PNG 二进制数据,适合在聊天界面中展示)、svg(返回原始 SVG 文本,适合需要继续编辑的场景)、link(返回一个带有 5 分钟缓存 TTL 的直链 URL,适合需要被多个客户端重复访问的场景)、html(返回一个交互式 3D 几何体的 HTML 页面,用户可以在浏览器中旋转、缩放、平移 3D 图形)。
6. MCP JSON-RPC 协议层
plot-mcp-worker 完整实现了 MCP 2024-11-05 版本的协议规范。Worker 监听一个 POST /mcp 端点,接受标准的 JSON-RPC 2.0 请求。协议支持三种核心方法:
- initialize:MCP 客户端在连接建立后首先调用这个方法。服务器返回自己的能力声明(capabilities)、协议版本号、以及服务器信息——包括 name("plot-mcp-worker")和 version("0.4.14")。客户端根据这个响应来决定后续可以调用哪些功能。
- tools/list:返回全部 40 多个工具的定义清单。每个工具的定义使用 JSON Schema 来描述——包括工具名称、功能描述、以及所有输入参数的名称、类型、是否必填、默认值、约束条件(比如"采样点数必须在 10 到 20000 之间")。客户端拿到这份清单后,就可以在 AI 模型的 function calling 上下文中注册这些工具。
- tools/call:这是实际执行工具调用的方法。客户端发来工具名称和参数,服务器端先根据 JSON Schema 验证参数是否合法(类型对不对、必填参数有没有缺、数值有没有超出范围),验证通过后调用对应的 plot 或 render 函数,返回结果。返回值有三种可能的格式——一个可以直接访问的 PNG URL(附带 5 分钟缓存)、一个 SVG 字符串、或者一个 base64 编码的图片数据。
所有的 HTTP 响应都设置了正确的 CORS 头,允许从任何来源的网页中调用(因为 MCP 客户端可能运行在浏览器扩展、Web IDE 或桌面应用中)。/mcp/health 端点提供了一个简单的健康检查接口,返回服务器当前是否正常运行。
7. 专业示意图生成器
除了标准的统计图表,plot-mcp-worker 还能生成几类高度专业化的示意图,这些工具的设计来自实际的教学和工程需求:
- 力分析图(Force analysis):物理中的受力分析图——在一个物体上画出所有作用力的箭头,包括重力、支持力、摩擦力、拉力、张力等。每个力由三个参数定义:角度(相对于水平面的角度)、大小(箭头的长度)、标签(比如 "F_N" 表示法向力)。引擎自动计算每个力的水平和垂直分量,画出虚线表示分量的投影。还支持合力的计算和显示。内置了 6 种常见物理场景的模板——斜面上的物体、悬挂的重物、水平面上的物体、滑轮系统、弹簧连接体、简谐振动系统——你不需要手动指定每个力的角度和大小,选一个模板就有了预设配置。
- 电路原理图(Circuit schematics):使用 SVG 绘制电子电路图。支持 21 种元件类型——包括电池、电阻、电容、电感、二极管、LED、晶体管、运算放大器、继电器、蜂鸣器、开关、电流表、电压表、接地符号等等。每个元件可以配置方向(水平或垂直)和颜色。电路布局通过"级"(stages)来组织——你可以定义一个串联级(元件从左到右排列)和一个并联级(元件在多个分支中排列),引擎自动计算每个元件的坐标位置。内置了 10 个常见教学电路模板——串联电路、并联电路、带开关的灯泡电路、带电阻的电源电路、LED 限流电路、仪表测量回路、晶体管开关、继电器驱动、蜂鸣器回路、运放跟随器。
- 3D 几何体查看器:生成交互式 HTML 页面,用户可以在浏览器中旋转、缩放、移动 3D 几何图形。支持的几何体类型包括立方体、球体、圆柱体、圆锥体、3D 矢量箭头。更强大的是参数曲面——你可以输入一个数学表达式(比如
"sin(sqrt(x² + y²))"),引擎在 xy 平面上采样,计算每个采样点的 z 值,构建出一个 3D 曲面网格。最多支持 6 个曲面同时显示在同一个场景中,每个曲面可以独立设置颜色、透明度、色阶(colorscale)和等高线。采样密度最高可达每轴 80 个点(即每个曲面最多 6,400 个采样点)。支持 5 种预设色阶(Viridis、Cividis、Turbo、Jet、Plasma)。 - 韦恩图(Venn diagrams):支持 2 集合和 3 集合的韦恩图。每个集合可以自定义颜色和标签。图中的每一个区域——A 独有的区域、B 独有的区域、A 和 B 的交集区域等等——都可以单独设置显示文字。这在概率论教学和集合论教学中非常有用。
- C 语言内存图:这是为 C 语言指针教学专门设计的一种图表。图中显示多个内存块,每个块标注了变量名、数据类型、内存地址、存储的值、以及各字节的内容。可以直观地展示指针指向关系——比如"指针 p 存储在地址 0x1000,它的值是 0x2000,即它指向地址 0x2000 处的整数 42"。
8. 资源限制与安全边界
作为一个运行在 Cloudflare Workers 免费套餐上的公共服务,必须对所有输入做严格的限制,防止恶意或意外的大规模请求耗尽配额。所有限制都是硬编码在 constants 中的:
- 单个数学表达式的长度上限 400 字符
- 采样点数范围 10 ~ 20,000(默认 1,000)
- 每张图最多 12 条数据序列——再多的话图例会挤满整个画面
- 力分析图最多 16 个力、8 个物体、6 个曲面
- 电路图最多 24 个元件、48 条导线
- 3D 场景最多 6 个曲面、每轴 80 个采样点、8 条自定义线段、32 个标记点
- 图表标题文本限制在 120 字符以内,坐标轴标签限制在 80 字符以内——超长的标题除了破坏排版之外没有意义
- 多子图(multi-plot)布局的行列数不受硬性限制,但总子图数受总元素数限制的约束
超过任何一项限制的请求会被立即拒绝,返回一个带有具体错误信息的 JSON-RPC error 响应,而不是让请求挂起直到超时。这样 AI agent 可以解析错误信息,调整参数后重试。
自行部署
plot-mcp-worker 设计为一个可以完全自行部署的开源项目。你只需要一个 Cloudflare 账户(免费套餐就够),把代码 clone 下来,安装 wrangler CLI 工具,运行一条命令就能部署到你自己的 *.workers.dev 子域名下。部署完成后,在任意 MCP 客户端的配置文件中加入几行 JSON,你的 AI agent 就可以开始画图了。
项目使用 CC BY-NC-SA 4.0 协议开源——你可以自由使用、修改和分享,但需要署名、不能用于商业目的、且衍生作品必须使用相同协议。
Star on GitHub · 部署到你自己的 Cloudflare 账户
参见:plot-mcp 操作文档——涵盖全部 40+ 工具、所有 API 端点、本地开发环境和常见问题排查的分步用户指南。