← 返回文章列表

Dify MCP 集成实验(02):工具进阶与协议原语——MCP 三原语如何落地?

1. 业务场景

先讲一个我们实际遇到的场景。

一家做客服工单 SaaS 的公司,支持团队每天处理大量工单查询:「退款相关的工单有哪些?」「T1002 现在什么状态?」这些查询如果能直接做成 MCP 工具,客服门户的 AI 助手就能自己查。同时还有排障手册(可读资源)和工单分析模板(提示词)——在 MCP 协议里,工具、资源、提示词是三种原语,一个 server 都能表达。

我们第一次接这类需求时,第一反应是「把查询做成工具就完事了」。真正动手才发现——客户要的不只是工具:排障手册、分析模板也是交付的一部分,三原语都得能表达;而且工具返回裸 dict 看着能用,下游解析一碰就碎。协议能表达什么是上限,平台消费什么是边界,两头都要摸清。

这不是个例。任何「外部系统数据进 Dify」的集成都是这个模式:先搞清楚协议能表达什么(tools / resources / prompts),才知道哪些能力 Dify 用得上、哪些要换种方式包装——「Dify 只消费 tools」的源码结论,要靠本实验的 server + 107-03 接入实证。

2. 场景痛点

这个流程的痛点,在协议落地时体现得最直接:

本质上,协议能表达什么是上限,平台消费什么是边界——两头都清楚,交付才不会返工

3. 方案:为什么是 MCP 三原语完整实现

在 107-01 地基上,把 MCP 协议三原语(tools / resources / prompts)在 server 侧完整实现——多工具、结构化输出、参数校验、资源与提示词模板。

选它的理由:

这篇文章我们就用它扩展 107-01 的 server,把三原语完整落地,为客户「资源读取」类诉求的包装方式提供依据。

4. 整体架构

graph TD dev["本地开发机"] server["dify107_02_support_server(在 107-01 环境上扩展,uvicorn :8902/mcp)"] tools["tools:search_tickets / get_ticket_status(多工具 + 参数校验 + 结构化输出)"] res["resources:support://troubleshooting(list/read 处理器)"] prompts["prompts:ticket_analysis(list/get 处理器)"] dify["Dify 服务器(107-03 接入:预期只见 tools,resources/prompts 不可用)"] dev --> server server --> tools server --> res server --> prompts server -- "HTTP" --> dify

链路很清晰:本地 server(tools + resources + prompts 三原语)→ HTTP → Dify 服务器(107-03 接入)。关键设计是三原语同 server 共存,为「Dify 只见 tools」的对照结论提供运行级实证基础。

5. 模块设计

5.1 结构化输出工具(返回类型必须 Pydantic 模型)

from pydantic import BaseModel

class TicketStatus(BaseModel):

    ticket_id: str

    status: str

    updated_at: str

    title: str = ""

@server.tool(structured_output=True)

def get_ticket_status(ticket_id: str) -> TicketStatus:

    """按工单号查状态;格式错/不存在 → raise ValueError("not_found: ...")"""

    ...

坑点预埋structured_output=True 时返回类型必须是 Pydantic BaseModel,裸 dict 报 InvalidSignature

5.2 资源与提示词(三原语补齐)

# 资源:静态 + 模板(模板可读但不进 list,SDK 2.0 观察点)

@server.resource("support://troubleshooting")

@server.resource("support://troubleshooting/{topic}")

def troubleshooting(topic: str | None = None) -> str: ...

# 提示词:SDK 2.0 PromptMessage 只认 user/assistant,无 system 角色

@server.prompt()

def ticket_analysis(ticket_id: str) -> list[dict]:

    return [{"role": "user", "content": f"请分析工单 {ticket_id} 的处理情况…"}]

5.3 多工具注册

一个 server 暴露多个工具:@server.tool() 重复装饰即可(1:N 关系实证);工具名冲突时 SDK 自动告警(warn_on_duplicate_tools)。

6. 运行验证

输入 预期 结果
search_tickets(退款+pending) 返回 T1003 通过
search_tickets(登录+open) 空列表 structured={'result': []}(空结果 ≠ 错误) 通过
search_tickets(非法状态 BAD) isError=True + 中文错误 通过
get_ticket_status(T1002) structured_content 完整返回 通过
get_ticket_status(t1004 小写) 归一化 T1004 正常返回 通过
get_ticket_status(T9999 不存在) status: not_found(显式空结果,非静默) 通过
resources/list + read 列出并读取 support://troubleshooting 条目 通过
prompts/list + get 返回 ticket_analysis 模板(user 消息) 通过

7. 实战坑

现象 修复
结构化输出要求 Pydantic 模型 structured_output=True 返回裸 dict 报 InvalidSignature: return type dict is not serializable for structured output 返回类型声明为 BaseModel 子类(实测)
prompt 无 system 角色 role: system 报 ValidationError SDK 2.0 PromptMessage 只接受 user/assistant(实测)
模板资源不进 list resources/list 只列静态 Resource,{topic} 模板可读但不在列表 静态 + 模板双装饰;read(login) 成功证明注册有效(实测)
模板资源错误 read 未知主题 → server 端 raise 客户端收到 "Error creating resource from template"(错误透传,server 打堆栈日志)(实测)
空结果语义 空列表返回 structured={'result': []} 按「空结果 ≠ 错误」纪律处理,下游不误判失败(实测)
多工具命名冲突 工具重名注册不报错 SDK 自动告警(warn_on_duplicate_tools),命名规范避免(实测)

8. 实验文档及源码获取

文章聚焦核心配置与采坑点;实验的完整分步操作(节点搭建/参数表/调试指引)见实验文档原文。

联系我

15088711270

手机端点击号码可直接拨打 · 桌面端可复制

微信二维码

扫码加微信 · 备注「门户」更快通过