首页 › tutorials › 造可视化MCP:模型直接看图表

概述

本教程教你从零构建一个能返回图像的 MCP(Model Context Protocol)服务器,让 Claude Code 不仅能"算出数据",还能"看到渲染好的图表"。绝大多数 MCP 教程只演示返回文本或 JSON,但 MCP 工具结果支持 image 内容块——把 base64 编码的 PNG 直接喂给多模态模型。本教程适合已跑通过第一个 MCP 服务器、想掌握非文本返回与本地调试、注册全流程的中级开发者。

前置条件

第一步:理解"图像内容块"为什么是新鲜事

MCP 工具调用的返回值不是一个字符串,而是一个内容块数组(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()

几个必须注意的细节:

  1. matplotlib.use("Agg") 要在 import pyplot 之前——stdio 服务器没有显示设备,否则会报 no display name。
  2. 不要 print() 到 stdout——stdio 通道走的是 JSON-RPC,任何额外的 stdout 文本都会破坏协议。要用 logging(默认输出到 stderr)。
  3. return 一个内容块列表:TextContent + ImageContent 混排是合规且推荐的,文字给上下文、图像给视觉。
  4. FastMCP 的 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+ 更稳)。

第四步:用 MCP Inspector 本地调试

在把它接进 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 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 足够),或在工具里先缩放再编码。

实战案例:把 chart-mcp 接进数据分析闭环

下面用一个最小闭环演示它的实战价值——把"算数据"和"看数据"合在一处。

在 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: 输出图 + 文字解读

延伸阅读

训练营适用性评估

参考来源

  1. Connect to MCP servers — Claude Code Docs — claude mcp add 命令、scope 与配置文件官方说明
  2. Content Types and Responses — mcp-server-python template (DeepWiki) — TextContent/ImageContent 内容块结构与返回值约定
  3. Type Utilities and Content Conversion — jlowin/fastmcp (DeepWiki) — FastMCP Image 助手与返回值自动转换为内容块的机制
  4. matplotlib-mcp Server — mume-mcp-servers (DeepWiki) — 基于 FastMCP 的 matplotlib 服务端参考实现
  5. Tool results Image content supports url — modelcontextprotocol Discussions #794 — 图像内容块 base64 与 url 两种形式的社区讨论与规范演进

[!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学通信知识星球海报-扫码加入