本教程教你从零构建一个能返回图像的 MCP(Model Context Protocol)服务器,让 Claude Code 不仅能"算出数据",还能"看到渲染好的图表"。绝大多数 MCP 教程只演示返回文本或 JSON,但 MCP 工具结果支持 image 内容块——把 base64 编码的 PNG 直接喂给多模态模型。本教程适合已跑通过第一个 MCP 服务器、想掌握非文本返回与本地调试、注册全流程的中级开发者。
claude --version 可用)uv(推荐)或 pipMCP 工具调用的返回值不是一个字符串,而是一个内容块数组(content blocks)。规范定义了三种核心块类型:
| 类型 | 字段 | 作用 |
|---|---|---|
text |
text |
普通文本,最常见 |
image |
data(base64)、mimeType |
让模型"看见"一张图 |
resource |
resource(含 uri) |
引用一段资源(如文件、数据库行) |
关键在于:当工具返回 image 块时,Claude 会把它当作真实图像来"看",可以做视觉推理("柱状图里 B 柱最高"、"散点呈正相关")。这一能力在纯文本 MCP 里完全缺失。我们要造的 chart-mcp 正是利用它:接收数据 → matplotlib 渲染 → base64 → 以 image 块返回。
预期结果:你能在脑中画出"数据进来 → 图像出去"的数据流,并清楚 data 必须是 base64、mimeType 必须填写(如 image/png)。
常见问题:有人把图像 base64 拼进 text 块返回——模型会把这一大坨字符当文本,既费 token 又"看不见"。记住:图像必须走 image 块。
新建目录 chart-mcp,准备两个文件。
requirements.txt:
fastmcp>=2.3.0
matplotlib>=3.8
用 uv 创建隔离环境(推荐):
mkdir chart-mcp && cd chart-mcp
uv venv
uv pip install -r requirements.txt
或用 pip:
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
预期结果:uv pip list 能看到 fastmcp 与 matplotlib。matplotlib 是重量级依赖,安装可能需要几十秒。
常见问题:
- 安装报 externally-managed-environment:改用 uv 或加 --break-system-packages(不推荐污染系统环境)。
- 网络慢导致 matplotlib 下载失败:换国内 PyPI 镜像。
创建 server.py:
import base64
import io
import logging
# 必须在 import pyplot 之前设置无头后端,否则在 stdio 进程里会报缺 display
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from fastmcp import FastMCP
from mcp.types import TextContent, ImageContent
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("chart-mcp")
mcp = FastMCP("chart-mcp")
def _render(fig) -> str:
"""把 matplotlib Figure 渲染成 base64 PNG 字符串。"""
buf = io.BytesIO()
# dpi 控制清晰度,也直接决定 token 消耗;110 是清晰与体积的平衡点
fig.savefig(buf, format="png", dpi=110, bbox_inches="tight")
plt.close(fig)
return base64.b64encode(buf.getvalue()).decode("ascii")
@mcp.tool()
def bar_chart(categories: list[str], values: list[float], title: str = "Bar Chart") -> list:
"""Render a bar chart from categories and values, returned as a viewable image."""
if len(categories) != len(values):
raise ValueError("categories 与 values 长度必须一致")
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.bar(categories, values, color="#4C78A8")
ax.set_title(title)
ax.set_ylabel("value")
b64 = _render(fig)
logger.info("rendered bar_chart: %d bars", len(categories))
return [
TextContent(type="text", text=f"已生成柱状图,共 {len(categories)} 个类别。"),
ImageContent(type="image", data=b64, mimeType="image/png"),
]
@mcp.tool()
def line_chart(x: list[float], y: list[float], title: str = "Line Chart") -> list:
"""Render a line chart, returned as a viewable image."""
fig, ax = plt.subplots(figsize=(6, 3.5))
ax.plot(x, y, marker="o", color="#F58518")
ax.set_title(title)
ax.grid(alpha=0.3)
b64 = _render(fig)
return [
TextContent(type="text", text=f"折线图已生成,{len(x)} 个数据点。"),
ImageContent(type="image", data=b64, mimeType="image/png"),
]
if __name__ == "__main__":
mcp.run()
几个必须注意的细节:
matplotlib.use("Agg") 要在 import pyplot 之前——stdio 服务器没有显示设备,否则会报 no display name。print() 到 stdout——stdio 通道走的是 JSON-RPC,任何额外的 stdout 文本都会破坏协议。要用 logging(默认输出到 stderr)。return 一个内容块列表:TextContent + ImageContent 混排是合规且推荐的,文字给上下文、图像给视觉。Image 快捷方式:也可以 from fastmcp.utilities.types import Image,然后 return Image(data=png_bytes, format="png"),框架自动转成 ImageContent。但混排文字时用上面的列表写法更直观。预期结果:python server.py 启动后会阻塞等待(监听 stdio),没有任何输出到终端,但 stderr 会出现(如果开了 logging)。这是正常状态。
常见问题:
- 中文标题显示成"豆腐块"方块:服务器所在机器缺中文字体。加上:
python
plt.rcParams["font.sans-serif"] = ["Noto Sans CJK SC", "Arial Unicode MS", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False
- 工具签名里的 list[str] 在旧 Python 上报错:要求 3.9+(3.10+ 更稳)。
在把它接进 Claude Code 之前,先用 Inspector 单独验证图像块格式正确。FastMCP 自带开发模式:
fastmcp dev server.py
或用官方 Inspector(基于 Node):
npx @modelcontextprotocol/inspector python server.py
打开浏览器中的 Inspector 面板 → Tools → 选 bar_chart → 填入 categories=["A","B","C"]、values=[30,50,20] → 点 Run Tool。
预期结果:返回 JSON 里能看到两个内容块,image 块的 data 是一大段 base64,mimeType 为 image/png。Inspector 通常会直接把图像渲染出来预览——如果图正常显示,说明图像块格式正确。
常见问题:
- Inspector 里图像不显示但 JSON 有数据:多半是 mimeType 拼错或漏写。
- 工具列表为空:确认函数用了 @mcp.tool() 装饰器,且类型注解齐全(FastMCP 靠注解生成 JSON Schema)。
调试通过后,把服务器注册进 Claude Code。进入任意工作目录执行:
claude mcp add chart-mcp -- uv run --directory /绝对路径/chart-mcp server.py
要点:
- -- 是分隔符,其后才是真正启动服务器的命令与参数。
- 推荐用 uv run,它会自动用项目虚拟环境;如果用裸 python,要写绝对路径:-- python /绝对路径/chart-mcp/server.py。
- 作用域(scope)默认是 local(仅当前项目、当前用户)。若要团队共享,加 --scope project,配置会写进项目根的 .mcp.json,可提交到 git;若要全机器所有项目可用,加 --scope user,写入 ~/.claude.json。
验证注册:
claude mcp list # 列出所有服务器
claude mcp get chart-mcp # 查看某个服务器的详细配置
预期结果:claude mcp list 显示 chart-mcp ✓ connected。
常见问题:
- 状态是 ✗ failed:八成是启动命令路径不对或依赖没装在 uv run 能找到的环境里。先在终端单独跑 uv run --directory /path server.py,确认能启动。
- 远程团队拉取 .mcp.json 后连不上:依赖未安装、字体缺失或 Python 版本不一致。在 .mcp.json 同级补一份 requirements.txt 并写好 README。
启动 Claude Code:
claude
进入对话后,先输入 /mcp,应该能看到 chart-mcp 及其两个工具 bar_chart、line_chart。然后直接用自然语言提问:
帮我画个柱状图:北京 32、上海 45、广州 28、深圳 38,标题"一线城市气温"。
预期结果:Claude 自动调用 bar_chart,传入对应参数,随后终端里会渲染出图像,模型还会基于"看到"的图做描述("上海柱子最高,约 45")。这正印证了图像块被真正送进了多模态推理。
常见问题:
- 模型调用了工具但"没看图"就开始编数据:确认返回的是 image 块而非把 base64 塞进 text。
- 上下文突然变得很贵:单张大 dpi 图的 base64 体积可观。对策是控制 figsize、dpi(80–110 足够),或在工具里先缩放再编码。
下面用一个最小闭环演示它的实战价值——把"算数据"和"看数据"合在一处。
在 Claude Code 里说:
我有一份
sales.csv(字段:month, revenue)。先读它,算出每月同比变化,再用 chart-mcp 画一条 revenue 折线图,最后根据图告诉我增长趋势是否在加速。
模型会:① 读 CSV → ② 计算 → ③ 调 line_chart 出图 → ④ 基于图像判断斜率变化。整个过程你只说了一句话,模型却在"看图说话",而不是仅凭表格数字猜测。
你也可以扩展成多工具服务站:加 scatter、histogram、pie,再补一个 Resources 原语暴露预设配色主题、一个 Prompts 原语给出"如何选图"的建议模板。一个文件、几十行代码,就构成了完整的"可视化即工具"能力。
整体数据流与调用链如下:
flowchart LR
U[用户自然语言] --> CC[Claude Code]
CC -->|选择工具| MC[MCP 客户端]
MC -->|stdio JSON-RPC| SRV[chart-mcp 进程]
SRV -->|调用工具函数| MPL[matplotlib 渲染]
MPL --> B64[base64 编码]
B64 -->|image 内容块| MC
MC -->|多模态输入| CC
CC -->|看图回答| U
一次完整工具调用的时序:
sequenceDiagram
participant U as 用户
participant CC as Claude Code
participant S as chart-mcp
participant M as matplotlib
U->>CC: 画柱状图 A=30 B=50
CC->>S: tools/call bar_chart
S->>M: plt.bar + savefig
M-->>S: PNG bytes
S->>S: base64 编码
S-->>CC: [TextContent, ImageContent]
CC->>CC: 视觉理解图像
CC-->>U: 输出图 + 文字解读
modelcontextprotocol.io 官方对 text / image / resource 块的权威定义deepwiki.com/jlowin/fastmcp,重点看 Type Utilities and Content Conversion,理解 Image 助手与返回值转换规则deepwiki.com/muse-mesh/mume-mcp-servers 的 matplotlib 服务端实现,可作为多图型扩展的范本code.claude.com/docs/en/mcp,覆盖 scope、claude mcp add 全部参数与 .mcp.json 结构mcp.run() 改为 mcp.run(transport="http") 即可升级为可远程访问的 HTTP MCP,配合 OAuth 实现团队共享scatter/histogram 工具;② 用 Resources 暴露配色主题;③ 加 Prompts 给"选图建议";④ 改造为 Streamable HTTP 传输并加 OAuth;⑤ 接 Seaborn/Plotly 输出更精美图表claude mcp add 命令、scope 与配置文件官方说明TextContent/ImageContent 内容块结构与返回值约定Image 助手与返回值自动转换为内容块的机制[!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 等场景全覆盖。
