MCP:标准化的工具暴露与发现

0. 开篇:为什么需要 MCP

智能体(Agent)落地的难点,往往不在对话生成与理解能力,而在受控行动(tool-use)与可编排执行能力。一旦进入工程化与企业级场景,智能体几乎必然需要三类能力:

  • Tools:调用外部能力,如计算、查询、写入、触发任务、调用系统或第三方 API 等
  • Resources:读取可复用资源与上下文,如字典、规则、配置、表结构、知识条目等
  • 边界:明确允许访问的范围,如目录范围、数据范围、权限范围等,以保证可控与可审计

智能体在应用这三类能力时通常面临的问题是:由于缺少统一协议,上述三类能力往往以“各自为政”的方式存在,导致智能体不得不承担大量适配工作。

0.1 缺少标准化时工具接入的典型代价

没有标准化时,在实际工程中常见的现象包括:

  • 外部能力存在多种形态:不同能力可能分别来自 HTTP API、Python 包、脚本或内部服务,智能体需要分别适配。
  • 参数与返回缺乏一致性:不同工具对输入字段命名、错误表示、分页方式、返回结构的约定不同,调用成本随工具数量线性上升。
  • 能力清单被硬编码:工具新增、替换或升级时,智能体需要同步修改,耦合不断加深。
  • 资源分散且不可自描述:字典、规则、表结构等常以文档或配置文件形式散落,难以形成“可发现、可读取、可调试”的统一入口。
  • 边界控制依赖约定:文件读取范围、数据查询范围、脚本可执行范围等缺少可验证的声明机制,安全与治理难以规模化。

这些问题的共同根源是:智能体被迫理解并适配每个工具的“私有协议”。工具形态越多、约定越分散,智能体的胶水逻辑就越厚,编排能力也越难复用。

当工具生态扩大时,智能体往往会逐步拥有一个难以维护的“胶水层”。更换框架、供应商或团队时,这层适配还会被迫重写,形成反复建设。

0.2 MCP 的核心价值:标准化暴露与标准化发现

要解决上述“私有协议导致的胶水化”问题,需要一套可被不同工具与不同智能体共同遵循的最小公共约定。MCP(Model Context Protocol,模型上下文协议)在此背景下应运而生。将视角从“逐个对接工具”拉回到“定义最小公共约定”后,MCP的价值可以简单概括为:

将工具与资源以标准方式暴露,并允许智能体以标准方式发现与调用,从而降低接入成本并提升可组合性。

  • 没有 MCP:每接入一个工具就要写一套适配与错误处理;
  • 有 MCP:只要能发现 Schema,就能用统一方式调用与编排。

MCP 与 AI 应用及各类工具与资源的关系如下图所示:
MCP功能示意图
在 MCP 语义下,智能体包含以下三类分工清晰的角色:

  • Server:负责“声明并提供能力”

    • 声明可调用的 Tools(名称、输入/输出结构)
    • 声明可读取的 Resources(URI、内容类型、语义)
    • 接受并遵循 Roots(访问边界/根目录)
  • Client:负责“与某个 Server 建立并维护会话”

    • 完成协议协商、能力交换,并双向转发协议消息
    • 负责订阅、通知等会话级机制
    • 示例程序中它与 Server 是一对一关系,生产中可按需要实现连接池/多会话管理
  • Host:负责“创建 Client、做路由与做编排”

    • 创建并管理多个 Client 实例(生命周期与权限)
    • 通过 list_tools() / list_resources() 发现能力
    • 通过 read_resource() 读取资源
    • 通过 call_tool() 调用工具
    • 在此基础上完成任务编排与交互呈现

下文如无特别说明,“智能体侧”的对外交互主体均指 Host(负责路由、编排与呈现),而 Client 仅承担协议会话维护。

下图了展示三者的职责边界与交互关系,该图强调的是整体分工,而非某个示例实现细节。
Host-Client-Server
这样,工具接入从“手工对接”转为“运行时发现”,新增工具或替换实现时不再要求 Host 逐个改造,应用程序与工具(乃至生态)的可扩展性与可维护性显著提升。

0.3 Roots:标准化接口之外的边界机制

仅统一工具形态并不足以满足企业落地要求。智能体具备推理与行动能力后,“能访问什么”必须被显式约束,否则易产生两类风险:

  • 越权风险:工具能力增强后,若缺少边界声明,可能访问到不应触达的文件或数据。
  • 排障成本上升:当结果异常时,很难区分是工具逻辑问题还是访问范围配置问题。

因此,MCP 中的 Roots 可被视为“边界的标准化载体”:Host 在建立连接时提供 Roots,Server 应在该范围内工作。这样既支持“同一服务在不同数据集/不同环境下复用”,也提供了明确、可验证的访问边界。

0.4 本篇示例:用“企业术语库”讲清 MCP 的关键元素

为避免引入与协议无关的复杂业务,本篇选用“企业术语库(Terminology)”作为载体:它天然同时具备工具调用(查/荐/比)、资源读取(meta/terms/term)与边界声明(Roots)三要素,足以覆盖 MCP 的关键技术点。

1. 示例全景:你将看到什么

本篇的示例程序“企业术语库(Terminology)”构建了一个最小但完整的 MCP 闭环:Host 连接 Server 后,通过 MCP 的发现与读取机制获取能力目录,并以结构化方式调用工具、读取资源、验证访问边界。示例聚焦协议关键点(Tools/Resources/Roots),避免引入与 MCP 无关的复杂业务逻辑。

1.1 示例能力清单:Tools、Resources 与 Roots

该 Terminology Server 对外暴露三类能力:Tools(可调用动作)Resources(可读取资源)Roots(访问边界/数据源声明)

Tools:三项动作能力(查、荐、比)
  • lookup_term(query: str, mode: str = "exact", top_k: int = 3, ctx: Context | None = None)

    查词入口:按术语(term)或别名(alias)检索条目。

    关键输入为 query/mode/top_k;关键输出为 hits(命中列表)、matched_by(exact/alias/fuzzy/none/invalid)与 version(数据集版本)。其中 ctx 为 FastMCP 注入的上下文参数(用于读取 Roots 与会话缓存),Host 调用时不需要显式传入(下同)。

    FastMCP 是一个面向 Python 的 MCP Server 开发框架,可将普通函数通过装饰器快速暴露为 MCP 的 ToolResource,并自动处理协议层的消息收发与结构化参数绑定。FastMCP 只是实现选择之一;MCP 是协议,换语言/换框架仍可遵循同一规范。

    读者在这一工具上重点关注两个字段:matched_by(命中方式)与 hits(命中列表)。

  • suggest_terms(prefix: str | None = None, context: str | None = None, top_k: int = 10, ctx: Context | None = None)

    推荐入口:用于前缀补全或从自然语言上下文中提取候选术语。

    输入二选一:prefixcontext(可配 top_k);输出 candidates(候选列表,含 term/reason/score)、strategy(prefix/context/none)、可选 tokens(仅 context 模式:从输入中抽取的候选片段列表,用于解释推荐过程)以及 version。

    读者在这一工具上重点关注两个字段:strategy(推荐依据)与 candidates(候选列表)。

  • compare_terms(term_a: str, term_b: str, ctx: Context | None = None)

    对比入口:对两个术语做字段级差异对比,并返回可直接表格化的结构化 diffs。

    关键输入为 term_a/term_b;关键输出为 summarydifferencesreferencesversion

    读者在这一工具上重点关注两个字段:differences(字段级差异)与 references(原始条目引用)。

Resources:三类可读资源接口
  • resource://glossary/meta

    返回结构化的 {meta: ...}(版本、字段说明等),并附带 {roots: [...]} 快照以便诊断与演示。

  • resource://glossary/terms

    返回术语全量列表及计数(示例数据量可控,因此允许全量返回)。

  • resource://glossary/term/{term}

    返回单条术语条目;其实现复用 lookup_term 的精确查询逻辑,确保语义一致。

此外还提供一个诊断辅助资源:

  • roots://list

    返回 {roots: [...]},用于验证会话建立时传入的 Roots 是否按预期生效。

Roots:边界与数据源切换机制
  • Host 在建立连接时传入 Roots(以 file://... URL 表示)

  • Server 优先从 Roots 所指目录加载 terms.jsonmeta.json;若未找到则回退到内置数据目录

  • 由此形成两点能力:

    1) 边界可控:Server 的数据访问限定在 Roots 范围内

    2) 数据可替换:更换 Roots 即可切换到另一份术语数据集,无需改动 Server 代码

本文将 Roots 同时视为:访问边界声明 + 数据集选择入口(示例以文件目录实现)。

1.2 交互主链路:发现 → 读取 → 调用的标准闭环

为了把 MCP 的关键动作串成一条可复现的“主链路”,示例将交互过程固化为五步。它们分别对应连接与边界声明、能力发现、资源读取、工具调用与结构化输出;后文第 6 节会用三个输入把这五步跑通并对齐其链路价值。

1) 连接与边界声明:Host 通过 stdio 启动 server.py,建立会话并传入 Roots。

2) 能力发现:调用 list_tools()list_resources(),获得可调用/可读取能力目录。

3) 读取诊断与自描述资源:读取 roots://listresource://glossary/meta,验证边界并获取版本与字段语义。

4) 调用工具:将输入路由为 tool + args,使用 call_tool() 执行“查/荐/比”。

5) 结构化输出:以稳定 JSON 输出结果,便于复查、回归与编排。

为了便于快速复现,这条主链路可以用最少的命令跑通:

# 单次演示(输出发现 → 读资源 → 调用工具)
python client_demo.py --once "MTM 是什么?"

# 交互探索(可 tools/resources/read <URI>)
python client_demo.py --repl

2. Host 侧设计:一个“最小但像样”的 MCP Host(client_demo.py)

client_demo.py 的定位不是实现一个完整的智能体框架,而是用尽量少的代码把 MCP Host 的关键职责跑通:建立会话、传递 Roots、发现能力、读取资源、路由调用、规范化输出。这一设计使示例既能用于文章复现,也便于迁移到真实工程中作为 Host 的最小骨架。

2.1 传输与会话:用 stdio 启动 Server 并建立连接

示例采用 stdio 作为传输方式,即 Host 以子进程形式启动 server.py,并通过标准输入/输出与其交互。这种形态特别适合本地演示与“可插拔工具”的工程落地,因为它无需端口、无需独立部署进程,启动与回收都由 Host 统一托管。

在生产场景中,Server 往往会采用网络化传输(例如基于 HTTP 的可流式连接)以支持远程部署与多 Host 访问。本文示例选择 stdio 以降低复现成本。

Host 侧的基本动作包括两步:一是定位 server.py 的绝对路径以保证可复现启动;二是创建 MCP 会话并托管生命周期。会话以 async with Client(...) 的方式打开与关闭,确保退出时资源被正确回收。由于本示例需要体现访问边界,Roots 在会话建立时一并传入,使“边界由 Host 提供”成为连接层的固有属性,而不是运行时的额外参数。

上述动作所对应的代码如下:

# client_demo.py
server = _server_script()
roots = [_root_to_file_uri(_default_data_root())]  # client roots

# 使用 stdio 启动服务端脚本,并通过 Roots 传递数据边界
async with Client(PythonStdioTransport(str(server)), roots=roots) as client:
    ...

2.2 Roots 传递:用 file:// 统一表达“允许访问的范围”

MCP 的 Roots 用于声明 Server 允许访问的范围。client_demo.py 将数据目录统一编码为 file://... 形式的URI,并以 roots=[...] 传入 Client。采用 URI 表达 Roots 的好处在于语义明确,其本质是边界声明,而不是普通字符串路径;同时也便于跨平台与跨调用栈的统一处理。

示例默认 Roots 指向工程内置的 data/glossary 目录。这样设计带来一个直接的好处是同一个 Server 不需要改动代码,仅通过切换 Roots 就可以加载另一份 terms.json/meta.json 数据集,既满足演示需要,也体现了 MCP 在“可替换数据源”上的工程价值。

2.3 能力发现:list_tools() / list_resources() 是 Host 的第一职责

会话建立后,Host 不会立刻调用工具,而是先执行能力发现:

  • list_tools():获取 Server 暴露的工具清单及其输入/输出结构
  • list_resources():获取 Server 暴露的资源清单及其 URI 形态
# client_demo.py
tools = await client.list_tools()
resources = await client.list_resources()

能力发现的价值在于解除 Host 与 Server 的硬耦合,Host 不需要提前维护“工具列表配置”,也不需要在代码里硬编码某个工具一定存在。

只要 Server 按 MCP 规范暴露能力,Host 在运行时即可感知变化:工具新增、替换、升级都可以通过发现机制自然承载。进一步走向“多 Server 聚合”“按能力路由”等形态时,发现机制就是基础前提。

提示:先发现,再选择,再调用——这是 MCP Host 的固定工作流。

2.4 资源读取:用 roots://list 与 resource://glossary/meta 验证边界与自描述

为了让 Roots 与 resources 的价值可观察、可验证,Host 在工具调用前主动读取两类资源:

  • roots://list:回显当前会话的 Roots,用于确认边界是否按预期传递与生效
  • resource://glossary/meta:读取数据集元信息(版本、字段定义、说明等),用于理解资源语义并支持调试与治理
# client_demo.py
roots_res = await client.read_resource("roots://list")
meta_res = await client.read_resource("resource://glossary/meta")

这种顺序把 MCP 的“两条腿”分得更清楚:Tools 负责动作执行,Resources 负责资源读取与自描述。

更重要的是,资源读取提供了一条天然的排障路径,当工具结果异常时,先看 roots://list 是否符合预期,再看 resource://glossary/meta 的版本与字段语义,最后才回到工具逻辑本身。

提示:先读边界与元信息,再调工具,问题定位更快捷。

2.5 Host 路由:把自然语言输入映射为 tool + args

client_demo.py 内置一个轻量的意图解析器 _parse_intent(),将用户输入映射为某个 MCP 工具及其参数。其目标不在体现“理解得多聪明”,而在把 Host 的职责边界讲清楚,即把交互输入转换为结构化调用。这段路由逻辑是为了让示例更像一个可交互的 Host(属于 demo 的交互“糖衣”),并不是 MCP 协议的必要组成部分。

# client_demo.py
def _parse_intent(text: str) -> Tuple[str, Dict[str, Any]]:
    """
    轻量意图解析,让 demo 更像一个「Host」:
    - compare:包含“区别/对比/比较”,且有两个术语(由 和 / 与 / vs 分隔)
    - suggest:包含“有哪些/推荐/列出/建议/补全”
    - 其它情况:默认 lookup

    Args:
        text: 输入文本。

    Returns:
        (intent, args) 二元组。
    """
    t = text.strip()
    if not t:
        return "noop", {}

    if any(k in t for k in ["区别", "对比", "比较"]):
        m = re.split(r"(?:\s*(?:和|与|vs|VS|对比|比较)\s*)", t)
        # attempt to pull two uppercase-ish tokens or quoted words
        caps = re.findall(r"[A-Z][A-Z0-9\-]{1,20}", t)
        if len(caps) >= 2:
            return "compare_terms", {"term_a": caps[0], "term_b": caps[1]}
        # fallback: split by common connectors
        parts = re.split(r"(?:和|与|vs|VS)", t)
        parts = [p.strip() for p in parts if p.strip()]
        if len(parts) >= 2:
            return "compare_terms", {"term_a": parts[0], "term_b": parts[1]}
        return "compare_terms", {"term_a": t, "term_b": t}

    if any(k in t for k in ["有哪些", "推荐", "列出", "建议", "补全"]):
        # heuristic: if starts with prefix-like (e.g., "M开头")
        m = re.search(r"([A-Za-z0-9\-]{1,10})\s*开头", t)
        if m:
            return "suggest_terms", {"prefix": m.group(1), "top_k": 10}
        # else treat as context suggestion
        return "suggest_terms", {"context": t, "top_k": 10}

    # lookup: extract a probable term token if possible
    caps = re.findall(r"[A-Z][A-Z0-9\-]{1,20}", t)
    if caps:
        return "lookup_term", {"query": caps[0], "mode": "exact"}
    # fallback to the whole string
    return "lookup_term", {"query": t, "mode": "exact"}

路由规则保持最小化但覆盖典型交互:

  • 输入包含“区别/对比/比较”等对比意图时,优先抽取两个大写缩写片段;不足则尝试用“和/与/vs”分割;仍不足则退化为将原句作为 term_a/term_b 调用 compare_terms
  • 输入包含“有哪些/推荐/列出/建议/补全”等推荐意图时,优先识别“X 开头”并调用 suggest_terms(prefix=...),否则使用 suggest_terms(context=...)
  • 其余情况默认走查词路径,优先从句子中抽取大写缩写片段(如 MTM、RAG、JSON-RPC),再调用 lookup_term(query=...)

在实际落地应用中,为使意图解析器能更灵活地解析意图,且在性能允许的前提下,可以使用大语言模型或微调过的意图分析模型的语义理解能力实现意图解析。

在这一机制下,工具的输入结构由 Server 侧 Schema 决定,Host 只需要产出满足 Schema 的参数即可。路由层可以独立演进,不会牵连工具服务的实现细节。

2.6 输出规范化:结构化先稳定,再友好展示

MCP 工具调用的返回通常是结构化对象,这对后续编排与自动化非常友好,但对命令行阅读不够直观。示例程序的 Host 做了两层输出处理:

  1. 结构化打印:将结果以稳定的 JSON 形式输出,保留字段、层级与编码信息,便于复查与回归测试。
  2. 结果规整:当返回中出现“嵌套 JSON 字符串”时,先尝试解析为对象再输出,避免出现“看似是字符串但实际是 JSON”的二次解析负担。

这种输出策略确保了两点:一是结果字段稳定可被机器消费;二是在人类阅读场景下仍具备可用性与可调试性。更重要的是,展示层不会引入推测性解释,输出只基于返回字段,保证可追溯。

2.7 一键演示与 REPL:兼顾复现与探索

Host 提供两种运行方式,对应两类需求:

  • --once:单次运行,完整输出发现 → 读资源 → 调工具的链路,适合固定用例复现与回归。
  • --repl:交互式输入,支持随时查询 tools/resources 并通过 read <URI> 读取资源,适合探索与验证理解。

至此,Host 侧的最小骨架已具备会话管理、Roots 边界传递、能力发现、资源读取、路由调用与稳定输出的能力。

3. Server 侧设计:如何“标准化暴露”一个术语服务(server.py)

server.py 的核心任务是把“企业术语库”包装成一个可被 MCP Host 发现、读取、调用的标准化服务。它在实现上并不追求复杂业务能力,而是围绕 MCP 的三类关键对象 Roots(边界/数据源)Tools(动作能力)Resources(可读资源) 建立稳定的工程骨架。其中 Roots 与数据装载方式决定了服务的可控性与可复用性;Tools/Resources 则决定了 Host 的可组合能力。

3.1 Roots:数据边界与可替换数据源

在 MCP 语义中,Roots 是访问边界的载体,Host 在建立连接时提供 Roots,Server 只在该范围内工作。

在本示例里,server.py 将 Roots 具体化为“数据集选择机制”,它将 优先从 Roots 指定目录加载 terms.jsonmeta.json,找不到则回退到内置数据集

提示:Roots 既是边界声明,也是数据源切换开关。

这一机制由两部分组成,即 Roots 规范化与数据探测。

下面两段代码把“Roots 优先 + 两种布局探测 + fallback”的规则固化为可复用逻辑,使数据边界与数据源切换都变得可验证。

(1)Roots 规范化:兼容 file:// 与本地路径

Host 侧为了表达清晰,通常以 file://... 形式的 URI 传递 Roots,但某些实现或调试手段也可能直接传递本地路径。Server 侧通过 _normalize_root() 将两种形态统一为 Path

# server.py
def _normalize_root(r: Any) -> Path:
    """FastMCP client roots 可为本地路径或 file:// URL,统一标准化为 Path。"""
    s = str(r)
    if s.startswith("file://"):
        s = s[7:]
    return Path(s).expanduser().resolve()
  • file:// 前缀做剥离
  • 进行 expanduser()resolve(),得到标准化绝对路径

Roots 规范化的价值在于把“边界声明”落到统一的文件系统语义上,从而使后续的数据探测逻辑更稳定、更可预测。

(2)数据探测:支持多种根目录布局并提供 fallback

真实项目中数据目录布局往往并不一致。为降低接入摩擦,Server 采用了“候选路径集合”的探测策略:对每个 root,依次尝试两种常见布局:

# server.py
async def _pick_glossary_paths(ctx: Context) -> GlossaryPaths:
    """优先使用 client roots 指定的数据集;否则回退到内置数据。"""
    try:
        roots = await ctx.list_roots()  # type: ignore[attr-defined]
    except Exception:
        roots = []

    for r in roots or []:
        root = _normalize_root(r)
        candidates = [
            (root / "terms.json", root / "meta.json"),
            (root / "glossary" / "terms.json", root / "glossary" / "meta.json"),
        ]
        for terms_p, meta_p in candidates:
            if terms_p.is_file() and meta_p.is_file():
                return GlossaryPaths(terms_p, meta_p)

    gdir = _bundled_glossary_dir()
    return GlossaryPaths(gdir / "terms.json", gdir / "meta.json")

候选目录布局包含两种:

  • <root>/terms.json<root>/meta.json
  • <root>/glossary/terms.json<root>/glossary/meta.json

只要任一组合存在即认为命中。若所有 Roots 均未命中,则回退到随服务内置的 ./data/glossary 目录。由此,服务具备两项工程特性:

  • 可替换性:更换 Roots 即更换数据集,不需要修改 Server 代码
  • 可控边界:数据读取仅发生在 Roots 或内置目录范围内,边界清晰且可验证

这种设计同时兼顾了“演示可复现”和“接入足够宽容”,演示环境下无需额外准备数据;工程接入时则可通过 Roots 指向外部数据集完成替换。

3.2 会话级缓存:稳定性与性能(兼容 FastMCP 差异)

工具调用与资源读取往往是高频操作,若每次都重新读取 JSON 文件,会产生不必要的 I/O 开销,并且会让调试输出噪声变大。server.py 采用会话级缓存:在同一 MCP 会话内,数据集只加载一次,后续直接复用内存对象。
缓存通过 ctx.get_state() / ctx.set_state() 实现,但需要处理一个现实问题:不同 FastMCP 版本中这两个方法可能是同步或异步。为避免因版本差异导致服务不稳定,server.py 增加了 _maybe_await()

# server.py
async def _maybe_await(value: Any) -> Any:
    # FastMCP 2.x 中 get_state / set_state 可能是同步或异步
    return await value if inspect.isawaitable(value) else value
  • 若返回值是 awaitable,则 await 获取结果
  • 否则直接返回

在此基础上,_load_dataset() 形成稳定的数据加载流程:

# server.py
async def _load_dataset(ctx: Context) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
    cached = await _maybe_await(ctx.get_state("glossary_cache"))  # type: ignore[attr-defined]
    if cached:
        return cached["terms"], cached["meta"]

    paths = await _pick_glossary_paths(ctx)
    terms = json.loads(paths.terms_json.read_text(encoding="utf-8"))
    meta = json.loads(paths.meta_json.read_text(encoding="utf-8"))

    await _maybe_await(ctx.set_state("glossary_cache", {"terms": terms, "meta": meta}))  # type: ignore[attr-defined]
    return terms, meta

这种策略带来以下三点直接收益:

  • 性能可控:避免重复 I/O 与重复解析 JSON。
  • 结果一致:同一会话内数据集版本固定,便于复现与排障。
  • 兼容稳健:对 FastMCP API 行为差异具备适配能力。

提示:同一会话内“只加载一次”,既稳结果也稳性能。

3.3 索引构建:term/alias 双索引支撑“最小可用体验”

术语服务的“可用性”高度依赖检索体验。即使示例数据规模不大,仍需要一个最小的内部结构来保证行为符合直觉。server.py 通过 _build_indexes() 构建两类索引:

  • term_index:以 term.lower() 为 key,快速定位术语条目
  • alias_index:以 alias.lower() 为 key,将别名映射到同一条目

索引构建的设计动机主要有两点:

  • 一致的命中语义:术语与别名在查询入口上应具有同等地位;否则用户体验会出现“明明存在却查不到”的现象。
  • 为多工具复用提供基础lookup_term()suggest_terms()compare_terms() 都需要“给定输入 → 找到条目”的能力;集中构建索引可避免重复实现与语义漂移。

提示:MCP Server 不只是“把函数暴露出去”,还需要提供支撑工具语义的数据结构,让行为稳定、输出可解释。

完成 Roots 解析、数据装载与索引构建后,服务端就具备了稳定的“数据底座”。在此之上,Tools 与 Resources 只需要围绕这套底座提供一致的结构化接口即可。

4. Tools 设计:3 个工具覆盖“查、荐、比”(带稳定的 schema)

在 MCP 体系中,Tools 是“可调用动作”的标准化载体。与传统函数或脚本不同,MCP Tool 的关键要求不是实现多复杂,而是满足两点工程属性:

(1) 输入输出结构稳定:Host 可以基于 Schema 做路由、校验、渲染与编排

(2) 语义可解释:结果不仅给人看,也要支持上层系统据此做决策(例如是否需要二次确认、是否回退策略等)

提示:如果由大语言模型负责意图识别与工具选择(而非 Host 侧的固定规则路由),建议为 Tools 中每一个工具补充更完整的“说明性注释”(类似函数 docstring / OpenAPI 描述),至少覆盖:功能边界、参数含义与约束、返回字段语义、错误/边界条件与示例调用。这样模型在做 tool selection / argument filling 时更稳定,也能显著降低误用与二次追问。

4.1 lookup_term():精确 / 别名 / 模糊三段式

lookup_term() 解决最核心的问题:给定一个查询串,返回对应术语条目。该工具支持两种模式:

  • mode="exact":确定性查询(优先)
  • mode="fuzzy":相似度检索(兜底)
输入字段(稳定且最小化)
  • query: str:待查询术语或别名
  • mode: str"exact""fuzzy"
  • top_k: int:模糊模式返回数量上限(并做上限约束)
输出字段(Host 可直接消费)
  • hits: list[entry]:命中条目列表;每条包含原始字段并补充 score

  • matched_by: str:命中方式标识

    • exact:term 精确命中
    • alias:alias 精确命中
    • fuzzy:相似度命中
    • none/invalid:无命中或参数非法
  • version: str:数据集版本(来自 meta)

这种输出结构有两个明显的收益:

  • Host 不必依赖“hits 是否为空”来猜测命中语义,可以直接读取 matched_by 决定交互策略。
  • version 使结果可追溯:当数据集替换或升级后,日志与回归可以明确区分来源。
三段式匹配策略(确定性优先)

(1) 先查 term_index:保证标准术语有最高优先级

(2) 再查 alias_index:别名与术语同等可用,但语义上仍需标记为 alias 命中

(3) 若 mode="fuzzy":对 term 与 alias 同时计算相似度,按分数排序截断

# server.py(节选)
@mcp.tool()
async def lookup_term(query: str, mode: str = "exact", top_k: int = 3, ctx: Context | None = None) -> Dict[str, Any]:
    if ctx is None:
        raise RuntimeError("Context is required for this tool.")

    q = (query or "").strip()
    if not q:
        return {"hits": [], "matched_by": "invalid", "version": "unknown", "message": "query is empty"}

    terms, meta = await _load_dataset(ctx)
    term_index, alias_index = _build_indexes(terms)

    # 1) 确定性优先:term / alias 精确命中
    if mode == "exact":
        hit = term_index.get(q.lower())
        matched_by = "exact"
        if not hit:
            hit = alias_index.get(q.lower())
            matched_by = "alias" if hit else "none"
        return {
            "hits": ([{**hit, "score": 1.0}] if hit else []),
            "matched_by": matched_by,
            "version": meta.get("version", "unknown"),
        }

    # 2) 兜底:fuzzy 相似度排序 + 阈值过滤
    k = max(1, min(int(top_k), 20))
    scored: List[Tuple[float, Dict[str, Any]]] = []
    for e in terms:
        s = _score(q, str(e.get("term", "")))
        for a in e.get("aliases", []) or []:
            s = max(s, _score(q, str(a)))
        scored.append((s, e))

    scored.sort(key=lambda x: x[0], reverse=True)
    hits = [{**e, "score": round(float(s), 4)} for s, e in scored[:k] if s >= 0.35]

    return {"hits": hits, "matched_by": "fuzzy", "version": meta.get("version", "unknown")}

完整实现代码中包含更详细的返回字段与边界处理逻辑,具体代码请参见代码仓库。

模糊模式使用 SequenceMatcher 计算相似度,并设置阈值(示例为 0.35)过滤噪声。阈值的存在使得即使 top_k 较大,也不会把“几乎无关”的条目塞入结果,降低后续误用概率。这使得 fuzzy 命中更可控。

4.2 suggest_terms():prefix 与 context 两种推荐策略

推荐能力在智能体交互中常用于两类场景:输入补全(prefix)与从自然语言中提取候选术语(context)。suggest_terms() 用一个工具同时覆盖两种策略,并通过 strategy 字段显式标识返回依据,避免 Host 推断。

输入字段

  • prefix: str | None:前缀推荐入口
  • context: str | None:上下文推荐入口
  • top_k: int:候选数量上限(并做上限约束)

输出字段

  • candidates: list[{term, reason, score}]:候选术语列表
  • strategy: str"prefix" / "context" / "none"
  • version: str
  • tokens: list[str](仅 context 模式):从上下文抽取的候选片段,用于解释推荐过程
4.2.1 prefix 模式:面向补全与列表

prefix 模式的逻辑刻意保持确定性与高可解释性:

  • 遍历 term:筛选 term.startswith(prefix)
  • 遍历 alias:筛选 alias.startswith(prefix) 并映射回对应 term
  • 对候选做去重:同一 term 只保留一次

候选条目提供 reasonprefix_matchalias_prefix_match:<alias>。这样 Host 在展示时能说明“为何推荐”,并能在必要时区分“本体命中”与“别名命中”。

4.2.2 context 模式:面向自然语言输入的轻量提取

context 模式分两步:

第一步:抽取候选片段

_extract_candidates_from_context 采用启发式规则抽取两类候选片段:

  • 大写缩写(如 RAGSLAJSON-RPC
  • 中文短语(长度 2~8)

并在抽取后去重、保持顺序。该策略并非 NLP 解析,而是工程上“最小可用”的候选提取器,它覆盖缩写与中文术语两大常见输入形态,成本低且可控。

第二步:匹配与打分(精确优先)

  • 对每个候选片段,先做 term/alias 精确命中,命中则赋最高分(1.0)
  • 未命中则对全量 terms 做模糊匹配,达到阈值(示例为 0.55)则纳入候选并记录分数
  • 汇总去重后按分数排序,截断到 top_k

该流程刻意采用“先精确、再模糊”的顺序,避免模糊推荐压过确定性命中。tokens 会随结果返回,形成可解释链路:推荐结果可追溯到“上下文中被抽取的哪些候选片段”。

suggest_terms() 的具体实现代码参见代码仓库,此处不再列出。

4.3 compare_terms():字段级对比,输出可直接 UI 表格化

compare_terms 的目标不是生成长篇解释,而是输出结构化差异,以便 Host 或上层应用直接表格化展示、用于审计与复核。对比的对象是两条术语条目,入口同时支持 term 与 alias。

输入字段

  • term_a: str
  • term_b: str

输出字段

  • summary: str:对比结论摘要(差异数为核心信息)
  • differences: list[{aspect, a, b}]:字段差异列表
  • references: {a: entry|None, b: entry|None}:原始条目引用(用于调试/追溯)
  • version: str

对比逻辑(稳定且可预测)

  • 通过 get_entry() 先按 term/alias 精确解析到条目
  • 若任一不存在,返回“未找到”的 summary,并将 references 中缺失一方置为 None
  • 若均存在,则仅比较固定字段集合:
    ["definition", "domain", "owner", "source", "updated_at"]
  • 仅当字段值不同才写入 differences,并在 summary 中给出差异数量

该工具的工程价值在于:返回结构天然适合 UI 与自动化处理。对比结果不依赖自然语言生成,因而更稳定、更可测试。若需要面向业务人员的解释文本,可在上层再引入 LLM 对 differences 做二次总结,但底座仍应以结构化差异为准。

compare_terms() 的具体实现代码参见代码仓库,此处亦不再列出。

至此,三个工具实现完毕,它们之间同时形成互补关系:

  • lookup_term() 提供确定性检索与可控的模糊兜底
  • suggest_terms() 提供补全与上下文推荐,并携带解释字段
  • compare_terms() 提供结构化差异,便于展示与审计

5. Resources 设计:把“资源”也标准化(通配路由)

在 MCP 语义中,Resources 与 Tools 同等重要:Tools 负责“做事”,Resources 负责“读资源”。企业级应用落地时,智能体系统往往不仅需要调用动作能力,还需要访问一组稳定的“可读材料”——例如字典、规则、配置、数据集元信息、字段含义与版本说明。若这些资源仍以零散文档或非结构化接口存在,Host 很难实现可解释、可调试与可治理。

企业术语库示例将资源接口设计为三类:数据集元信息术语集合单条术语,并额外提供一个 Roots 回显资源用于诊断与 Demo 分析。实现上采用通配路由(wildcard resource)来集中处理资源分发,减少装饰器数量,同时保证 URI 语义稳定。

5.1 resource://glossary/{item}:用通配路由暴露 meta 与 terms

Server 通过一个通配资源入口统一承载 glossary 相关资源:

  • resource://glossary/meta
  • resource://glossary/terms

这两类资源并不是分别实现两个独立的资源函数,而是统一由一个通配路由承载,当 {item}=="meta"{item}=="terms" 时,Server 在同一个入口内做显式分发并返回对应的结构化结果。

该设计的核心点在于资源的“类别”由 {item} 参数决定,Server 侧做显式分发并返回结构化结果,而不是依赖隐式约定。

# server.py
@mcp.resource("resource://glossary/{item}")
async def glossary_resource(item: str, ctx: Context) -> Dict[str, Any]:
    key = (item or "").strip().lower()
    if key == "meta":
        _, meta = await _load_dataset(ctx)
        try:
            roots = await ctx.list_roots()  # type: ignore[attr-defined]
        except Exception:
            roots = []
        return {"meta": meta, "roots": [str(r) for r in roots]}
    if key == "terms":
        terms, meta = await _load_dataset(ctx)
        return {"count": len(terms), "version": meta.get("version", "unknown"), "terms": terms}
    return {"error": f"unknown glossary resource: {item}"}
5.1.1 meta:数据集自描述 + Roots 快照

{item} == "meta" 时,Server 返回两类信息:

  • meta:来自 meta.json 的数据集元信息(版本、字段说明、notes 等)
  • Roots:当前会话下 Server 感知到的 Roots 列表(用于验证边界与排障)

将 Roots 快照与 meta 一并返回的意义在于:资源读取不仅用于“展示”,也用于“诊断”。当工具结果异常时,可以先读 resource://glossary/meta,以快速确认:

  • 当前使用的数据集版本是否符合预期
  • Roots 是否正确传递,是否指向正确目录
  • 字段语义是否与上层渲染/逻辑一致
5.1.2 terms:术语全量列表(示例规模下的可控返回)

{item} == "terms" 时,Server 返回:

  • count:术语数量
  • version:数据集版本
  • terms:术语条目数组

示例选择全量返回是因为数据规模可控,便于 Demo 展示与调试。在更大规模的生产数据集下,terms 往往需要进一步分页/检索化,但这不改变资源接口的语义,即Resources 用于“读取资源”,而不是必须通过工具才能获取。

5.1.3 未知 item:结构化错误而非抛异常

{item} 不属于已支持的集合(meta/terms)时,Server 返回结构化错误对象,其形如:

{ "error": "unknown glossary resource: xxx" }

该策略避免 Host 因异常中断,并提供可渲染、可记录的错误信息。对于 MCP 服务来说,资源读取的失败也应当尽量保持“可解释与可恢复”。

5.2 resource://glossary/term/{term}:单条术语资源(复用 tool 语义)

单条术语资源用于按 URI 精确读取某个条目,其实现代码如下:

# server.py
@mcp.resource("resource://glossary/term/{term}")
async def glossary_term(term: str, ctx: Context) -> Dict[str, Any]:
    # reuse tool semantics
    res = await lookup_term(term, mode="exact", ctx=ctx)
    hits = res.get("hits", [])
    return {"hit": hits[0] if hits else None, "matched_by": res.get("matched_by")}

例如,以下是两个可能的用于精确读取术语的资源URI:

  • resource://glossary/term/MTM
  • resource://glossary/term/检索增强生成

该资源的实现不重复定义检索逻辑,而是直接复用 lookup_term(term, mode="exact", ctx=ctx) 的行为,并在输出中返回:

  • hit:命中条目(或 None)
  • matched_by:命中方式(exact/alias/none)

这种复用带来两项好处:

  • 语义一致:资源读取与工具查询对“term/alias 命中规则”保持一致,避免出现同一术语在不同接口下结果不一致的问题。
  • 维护成本低:检索规则变化时只需修改 lookup_term(),资源自然同步更新。

在 Host 侧,单条资源适合用于 UI 详情页、调试定位、或在工具调用前做“是否存在”的轻量检查。

5.3 roots://{item}:以资源形式暴露 Roots(Demo与诊断入口)

为了让 Roots 边界“可观察”,Server 提供 Roots 资源通配路由:

  • roots://{item}

{item} == "list" 时,返回当前会话下 Server 感知到的 Roots 列表。若 item 不支持,则返回结构化错误对象。

这一资源在示例中承担两类作用:

  • Demo:直观展示 Roots 从 Host 传递到 Server 的链路,强化“边界由 Host 声明”的心智模型。
  • 诊断:当数据集选择不符合预期时,可以先读取 roots://list 验证根目录是否正确,再进一步排查 terms.json/meta.json 的布局与命名。

这里使用通配路由的意义在于:URI 形态先稳定下来,后续可以在不新增装饰器的情况下逐步扩展更多子资源。即便当前示例仅实现了 list,通配形态仍然让“可扩展点”在接口层是显式的。

通过 resource://glossary/*,Host 能以统一方式获取数据集自描述与内容;通过 roots://list,边界声明也变为可验证事实。至此,Tools/Resources/Roots 在本示例中形成闭环。

6. 端到端演示脚本:用 3 个输入跑通全链路

企业术语库示例的演示目标不是展示“复杂智能”,而是用最少的交互输入,把 MCP 的关键链路一次性跑通并可复查:连接与 Roots → 能力发现 → 资源读取 → 工具调用 → 结构化输出client_demo.py 通过 --once--repl 两种方式覆盖“可复现”与“可探索”两类需求;其中端到端演示以 --once 为主,保证输出稳定并便于对照。

下面截取 client_demo.py 中 run_once() 的主流程代码,用于直观看到“发现 → 读取 → 调用”的串联方式。

# client_demo.py
async def run_once(text: str) -> None:
    server = _server_script()
    roots = [_root_to_file_uri(_default_data_root())]  # client roots

    # 使用 stdio 启动服务端脚本,并通过 roots 传递数据边界
    async with Client(PythonStdioTransport(str(server)), roots=roots) as client:
        tools = await client.list_tools()
        resources = await client.list_resources()

        print("=== Discovered Tools ===")
        print(_pretty([t.model_dump() if hasattr(t, "model_dump") else t for t in tools]))
        print("\n=== Discovered Resources ===")
        print(_pretty([r.model_dump() if hasattr(r, "model_dump") else r for r in resources]))

        # 先读 roots/meta:验证边界与版本,便于排障与追溯
        print("\n=== Read roots://list ===")
        roots_res = await client.read_resource("roots://list")
        print(_pretty([c.model_dump() if hasattr(c, "model_dump") else c for c in roots_res]))

        print("\n=== Read resource://glossary/meta ===")
        meta_res = await client.read_resource("resource://glossary/meta")
        print(_pretty([c.model_dump() if hasattr(c, "model_dump") else c for c in meta_res]))

        intent, args = _parse_intent(text)
        if intent == "noop":
            return

        print(f"\n=== Call Tool: {intent} ===")
        result = await client.call_tool(intent, args)
        # FastMCP returns a ToolResult-like object; normalize for display
        print(_pretty(_as_dict(result)))
        print("\n=== 易读总结 ===")
        print(_human_summary(intent, result))

6.2 输入 1:查词(lookup)——“MTM 是什么?”

输入示例
  • MTM 是什么?
Host 路由
  • 解析到“查词”意图,抽取大写片断 MTM,调用:
    lookup_term(query="MTM", mode="exact")
预期输出要点(结构化)
  • matched_byexactalias(取决于数据集定义)
  • hits 至少包含 1 条,并带有 score=1.0
  • 返回 version,用于标记数据集来源
    查词
链路价值
  • 能力发现对齐:在 list_tools 的结果中选择 lookup_term() 作为本次动作入口,而不是依赖硬编码工具名。
  • 资源读取对齐:以 roots://listresource://glossary/meta 确认数据边界与数据集版本,为本次命中结果提供可追溯上下文。
  • 工具调用对齐:使用 call_tool() 执行 lookup_term(query="MTM", mode="exact"),验证 term/alias 双索引与 matched_by 语义。
  • 结构化输出对齐:输出 hits/matched_by/version 的稳定 JSON 结构,便于复查与回归。
  • 可解释性对齐:用 matched_by 明确命中来源(exact/alias),避免 Host 侧二次猜测。

工程上,这意味着即使更换数据集(Roots)或替换 Server 实现,Host 侧仍可保持同一套调用与展示逻辑。

6.3 输入 2:推荐(suggest)——“M 开头的术语有哪些?”

输入示例
  • M开头的术语有哪些?
Host 路由
  • 解析到“推荐/列出”意图,并识别“X 开头”模式,调用:
    suggest_terms(prefix="M", top_k=10)
预期输出要点(结构化)
  • strategyprefix
  • candidates 返回若干候选项,每项包含:
    • term(候选术语)
    • reasonprefix_matchalias_prefix_match:<alias>
    • score(示例中 prefix 匹配通常为固定分数)
      推荐
链路价值
  • 能力发现对齐:在工具目录中选择 suggest_terms(),体现“发现 → 选择 → 调用”的固定流程。
  • 资源读取对齐:基于 resource://glossary/meta 的字段语义与版本信息,使推荐结果可追溯、可对照。
  • 工具调用对齐:调用 suggest_terms(prefix="M"),验证 strategy=prefixreason 字段能解释候选来源(term/alias)。
  • 结构化输出对齐:以稳定 JSON 输出 candidates/strategy/version,便于 UI 渲染与自动化消费。

工程上,这意味着 UI 渲染与自动化消费可以依赖稳定字段(strategy/candidates/version),而不需要为不同实现写分支适配。

6.4 输入 3:对比(compare)——“MTM 和 MTO 有什么区别?”

输入示例
  • MTM 和 MTO 有什么区别?
Host 路由
  • 解析到“对比/区别”意图,抽取两个术语 token,调用:
    compare_terms(term_a="MTM", term_b="MTO")
预期输出要点(结构化)
  • summary 提示差异数量(例如 Found N differences on key fields.
  • differences 是差异数组,每项包含:
    • aspect(字段名,如 definition/domain/owner/source/updated_at)
    • ab(字段值)
  • references 包含 a/b 原始条目(用于追溯)
    对比
链路价值
  • 能力发现对齐:从工具目录选择 compare_terms(),将“对比”作为独立动作能力对外暴露。
  • 资源读取对齐:在已确认 Roots 与版本的前提下,对比结果可被解释为“同一数据快照下的差异”。
  • 工具调用对齐:调用 compare_terms(term_a="MTM", term_b="MTO"),验证 term/alias 统一入口与字段级差异提取。
  • 结构化输出对齐differences 天然可表格化,references 提供追溯锚点,保证对比结果既可展示也可审计。

工程上,这意味着对比结果可以直接进入表格、工单或审计链路,避免把差异解释完全押在自然语言生成上。

6.5 交互式探索:REPL 的最小命令集

除固定三输入的单次演示外,REPL 模式用于探索与快速验证:

  • tools:查看当前 Server 暴露的工具列表
  • resources:查看当前资源列表
  • read <URI>:读取任意资源(例如 resource://glossary/term/MTM
  • 任意自然语言输入:触发 Host 的路由与工具调用

REPL 模式用于快速验证与探索能力边界:既可查看 tools/resources 目录,也可直接读取资源或触发工具调用。

通过三条输入(查/荐/比),端到端链路被完整覆盖:能力发现(tools/resources)→ 资源读取(roots/meta)→ 工具调用(lookup/suggest/compare)→ 结构化输出(可解释、可追溯)。单条术语资源(例如 resource://glossary/term/MTM)更适合在 REPL 中按需读取,用于探索与定位细节。
交互

REPL 模式具体实现代码参见源代码中的 repl() 函数实现。

7. 工程化要点与坑位总结(从代码抽象成原则)

企业术语库示例的代码规模很小,但已经覆盖了 MCP 工程化落地时最常遇到的一组“硬问题”:边界如何表达与验证、能力如何自描述、返回结构如何确保稳定以及实现如何在版本差异与噪声输入下保持可用。以下要点从示例代码抽象为可复用原则,便于在更复杂的实际落地的 MCP 服务中直接套用。

7.1 Roots 必须“可移植且可验证”,否则边界形同虚设

问题:Roots 若以随意字符串传递(相对路径、平台相关分隔符、混合 URI/路径),在不同 Host/运行目录/平台上会出现不可预测行为,最终导致“边界声明不可复现”。

示例做法:Host 将 Roots 统一编码为 file://... 形式的 URI;Server 侧做 _normalize_root(),兼容 file:// 与本地路径并标准化为绝对路径。

原则

  • Roots 采用 URI 形式表达,避免隐式依赖工作目录。
  • Server 端必须做规范化与解析容错。
  • 必须提供可读的验证入口(如 roots://list 或 meta 中回显 Roots),使边界可观察。

7.2 数据集选择要“宽容但不暧昧”:兼容布局,避免误命中

问题:数据目录布局在真实项目中常常不统一;若 Server 只支持单一布局,会显著增加接入适配问题;但若探测逻辑过于宽松,也容易误命中错误数据集。

示例做法:对每个 root 仅尝试两种明确布局(根目录或 root/glossary),同时要求 terms.jsonmeta.json 同时存在才算命中;否则回退内置数据集。

原则

  • 兼容应基于“有限候选集合”,而不是递归扫描或模糊匹配。
  • 命中条件要严格(关键文件成对存在、必要字段可解析)。
  • fallback 策略要明确,避免 silent failure。

7.3 state 缓存应视为“服务稳定性组件”,并处理同步/异步差异

问题:高频工具调用若每次都读文件,会导致性能波动与 I/O 噪声;同时不同 MCP/框架版本对 state API 的同步/异步行为可能不一致,容易引入运行时错误。

示例做法_load_dataset() 使用 ctx.get_state()/set_state() 做会话级缓存,并通过 _maybe_await() 兼容同步/异步差异。

原则

  • 将缓存作为服务的默认能力,而不是优化项。
  • 兼容 awaitable 行为,避免版本升级后出现隐蔽故障。
  • 缓存对象应包含版本/来源信息,便于追溯与排障。

7.4 Schema 稳定性优先于“返回更丰富”:字段一旦暴露就要长期可依赖

问题:Host 往往会基于返回字段做渲染与路由;若 Server 随意增删字段或改变语义,会导致上层系统断裂。

示例做法:工具统一返回 versionlookup_term() 明确 matched_bysuggest_terms() 明确 strategy/reason/tokenscompare_terms() 明确 differences/references

原则

  • 输出字段要为“机器可决策”服务(matched_by/strategy)。
  • 返回结构要可预测(数组/对象形态固定)。
  • 变更应采用向后兼容策略(采用新增字段而避免删除/重命名)。

7.5 模糊匹配必须“可控且可解释”,避免噪声进入编排链路

问题:模糊匹配天然具有不确定性,若直接把低质量候选返回给上层编排,容易引发误调用或错误推理。

示例做法lookup_term() 在 fuzzy 模式设置阈值(0.35)并限制 top_k;suggest_terms() 的 context 模式设置更高阈值(0.55)并先精确后模糊。

原则

  • 模糊匹配必须配套阈值与数量上限。
  • 先确定性命中,再进行模糊兜底。
  • 结果应携带可解释信息(score、reason、tokens),便于确认与调试。

7.6 Resources 不应退化为“文档出口”,而要成为可编排资源接口

问题:若资源只提供不可结构化的描述文本,Host 很难基于资源做自动化;同时资源缺少版本与边界信息时,可追溯性不足。

示例做法resource://glossary/meta 返回结构化 meta 并附带 Roots 快照;resource://glossary/terms 返回 count/version/terms;单条 term 资源复用 tool 语义。

原则

  • 资源返回结构化对象,而非仅字符串说明。
  • meta 类资源必须包含版本与字段语义,形成自描述能力。
  • 资源与工具应复用语义底座,避免多处实现导致结果漂移。

7.7 错误处理要“结构化可恢复”,避免异常把 Host 交互打断

问题:在交互式系统中,抛异常会使 Host 侧难以统一处理;更重要的是,异常往往丢失上下文,使排障困难。

示例做法:对未知资源 item 返回 {error: ...};对查不到术语返回空 hits 与 matched_by 标识;对 compare 缺失项返回 summary 与 references。

原则

  • 业务型错误优先用结构化返回表达(error/message + context)。
  • 保持返回结构稳定,即使在错误场景也可被渲染与记录。
  • 将“不可恢复错误”限制在真正的系统级故障(例如缺少 Context)。

以上原则可直接迁移到更复杂的 MCP 服务:边界可声明可验证、能力可发现可自描述、输出可结构化可追溯、实现可兼容可稳定。

8. 从示例走向生产:下一步怎么扩展

企业术语库示例刻意只覆盖“查、荐、比”三类只读能力,以便把 MCP 的基本形态讲清楚即Tools/Resources 的标准化暴露Roots 的边界机制。在真实生产环境中,术语服务通常是知识治理与业务协同的基础设施,需求会迅速从“可查询”走向“可维护、可审计、可联动”。以下扩展方向不改变 MCP 的基本接口哲学,而是在其上逐步补齐企业落地所需的能力。

8.1 增加写入工具:从只读到可治理(upsert + 审批 + 审计)

生产中的术语库首先面临“变化”问题:术语会新增、定义会修订、归属会调整。只读检索无法支撑治理闭环,因此需要引入写入类工具,并将治理过程纳入可控流程。

可扩展的工具形态包括:

  • upsert_term(entry, mode="draft"|"publish")

    新增或更新术语条目。mode 用于区分“草稿态”与“发布态”,避免直接覆盖线上定义。

  • submit_change(term, change_request) / approve_change(id) / reject_change(id)

    用标准化工具提供审批流支持,保证变更可追溯且可回滚。

  • delete_term(term, soft=True)

    默认软删除,保留历史版本以支持审计与追溯。

与之配套的资源接口也应同步增强:

  • resource://glossary/changes:变更请求列表
  • resource://glossary/history/{term}:单术语历史版本与差异

这类扩展的关键不是“多写几个接口”,而是把“可维护性”结构化:写入行为必须能被记录、审核、回溯,从而使智能体具备参与治理的能力而不破坏治理规则。

8.2 引入版本化与一致性策略:让结果在规模化协作中可复现

示例中 version 来自 meta.json,这只是最小的追溯信息。生产环境通常需要更严格的版本与一致性策略:

  • 数据集版本:语义版本号(SemVer)或日期版本号
  • 内容哈希:对 terms 内容生成 hash,用于确认“同版本是否同内容”
  • 会话一致性:同一会话固定使用同一数据快照,避免工具调用时数据漂移
  • 回滚能力:支持回滚到指定版本,或在审批失败时恢复

在 MCP 语义下,版本化不需要改变工具调用模式,而是通过稳定字段与 meta 资源将“版本事实”公开出来,使 Host、日志系统与回归测试都能可靠对齐。

8.3 引入语义检索:从字符串匹配走向语义理解(向量化 + 重排)

当术语量增大、定义更长、表达更自然时,单纯的前缀/模糊匹配会迅速触达上限。此时可以在不破坏既有接口的情况下加入语义检索能力:

  • 新增工具:search_terms(query, top_k=10, filters=...)
    基于向量检索返回候选(definition/aliases/domain 可分别建索引)。
  • 结果重排:对向量召回结果进行 cross-encoder 或轻量重排,提高精度。
  • 领域过滤:domain/owner/source 等字段作为 filters,降低误匹配。

同时应保持“可解释输出”原则:语义检索结果至少包含 score、召回来源(definition/alias)、以及命中过的字段线索,避免把不可解释的相似度直接暴露给上层编排。

8.4 多数据源融合:从单一 JSON 走向系统集成

JSON 文件适合作为演示与最小落地,但生产中的术语通常来自多个系统:

  • 需求/研发系统中的字段字典
  • 数据治理平台中的指标口径
  • 运维平台中的告警术语与分级
  • 行业规范与内部标准文档

扩展策略通常是把“数据访问”隐藏在 Server 内部,而对外仍保持 MCP 工具与资源的统一语义。例如:

  • Server 内部增加适配层:DB/ES/Graph/对象存储
  • Tools/Resources 输出仍保持一致 Schema(尤其是 term/definition/domain/owner/source/updated_at 等核心字段)
  • Roots 的角色从“文件目录”扩展为“数据源范围声明”,例如:db://...s3://...https://...(具体取决于实现与安全策略)

这里的关键点在于外部契约要稳定,内部实现可演进。这也是标准化暴露的工程价值所在。

8.5 多 Server 聚合:从单服务到“可组合工具生态”

真实智能体系统往往需要多个工具域协同:术语只是其中一类。下一步自然是把 Terminology Server 与其他 MCP Server 组合起来,形成可组合生态,例如:

  • Terminology Server:术语查询与治理
  • FileOps Server:文件读取/写入/检索(受 Roots 限制)
  • SQL/BI Server:受控查询与报表生成
  • Ticket/Workflow Server:工单创建、状态查询、审批流触发

Host 侧通过发现机制聚合能力,并以“按能力路由”的方式选择工具执行路径。此时,工具命名、Schema 稳定性、版本字段与可解释返回就变得更关键,因为它们直接决定了跨 Server 编排是否可靠。

8.6 观测与审计:让工具调用成为可治理事件(AgentOps 基础)

生产落地最终绕不开观测与审计。即使工具接口标准化,若缺乏可观测性,排障与合规成本仍会迅速上升。其演进方向包括:

  • 调用日志:记录 tool 名称、参数摘要、返回摘要、耗时、版本、Roots
  • 资源访问日志:记录读取的 URI、返回 size/版本、Roots
  • Trace/Span:将一次任务中的多次 tool/resource 操作串起来
  • 权限审计:将“谁在什么边界下访问了什么资源/调用了什么工具”固化为审计事件

这类能力最好以“旁路方式”接入,即在不破坏 MCP 工具语义前提下在 Host 或 Server 的中间层统一采集,从而保持协议层简洁且可演进。

这条演进链可以概括为:只读检索 → 写入治理 → 版本一致性 → 语义检索 → 多源融合 → 多 Server 生态 → 观测审计。在该过程中核心原则不变:对外接口稳定、语义可解释、边界可验证。

9. 小结

MCP 将 Tools(可调用动作)Resources(可读取资源) 以统一协议标准化暴露出来,Host 通过 发现(list)—读取(read)—调用(call) 的固定链路实现可组合编排,而 Roots 进一步把“能访问什么”显式化为可验证边界,使同一服务既可复用又可控。

本文示例代码仓库:

https://gitcode.com/gtyan/AgentHandBook/tree/main/08

关注我,持续阅读Agent系列文章

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐