让 Agent 替你看 Bug:简单示例读懂 MCP 服务
英文标题:让 Agent 替你看 Bug:简单示例读懂 MCP 服务
Agent 能写代码、能操作 Git,但访问不了禅道。本文用一个完整的 Python 示例,从零演示如何写一个简单的禅道 MCP 服务
17.7 KB
61.4 KB
中文
如何设计一个手写一个 MCP 服务
什么是 MCP 服务
MCP(Model Context Protocol)是一个让 AI Agent 连接外部系统的标准协议。
Agent 本身能读写代码、操作 Git、搜索文件,但它访问不了禅道、Jira、数据库这类外部系统。MCP 服务就是中间那座桥——你用任意语言写一个小服务,声明几个 Tool(函数),Agent 就能调用它们。
工作原理很简单:Agent 通过 stdio(标准输入输出)向你的 MCP 服务发一个 JSON 请求,比如「调用 zentao_get_bug,参数 bugID=1024」,你的服务去调禅道 API,拿到结果,格式化后返回。Agent 拿到结果后继续它的工作。
Agent ──stdio──> 你的 MCP 服务 ──HTTP──> 禅道服务器
Agent <──stdio── 你的 MCP 服务 <──HTTP── 禅道服务器
一个 MCP 服务就是一个独立进程,核心就三件事:定义 Tool(函数签名 + 描述)、实现 Tool(调外部 API)、返回结果(格式化纯文本)。
从最简单的例子开始
先只做一个 Tool:zentao_get_bug——根据 Bug ID 获取详情。跑通这一个,其余都是同样的套路。
安装依赖
pip install mcp httpx
完整代码
import os, httpx
from mcp.server.fastmcp import FastMCP
ZENTAO_BASE = os.environ.get("ZENTAO_BASE_URL", "https://zentao.example.com")
_token = None
def get_token():
global _token
if _token is None:
resp = httpx.post(f"{ZENTAO_BASE}/api.php/v1/tokens", json={
"account": os.environ["ZENTAO_ACCOUNT"],
"password": os.environ["ZENTAO_PASSWORD"]
})
_token = resp.json()["token"]
return _token
def zentao_get(path):
return httpx.get(f"{ZENTAO_BASE}{path}",
headers={"Token": get_token()}).json()
mcp = FastMCP("zentao", description="禅道 MCP 服务")
@mcp.tool()
def zentao_get_bug(bugID: int) -> str:
"""获取禅道中某个 Bug 的完整详情,包括重现步骤和严重程度。
当用户提到 Bug 编号、要求查看 Bug 细节时使用。"""
bug = zentao_get(f"/api.php/v1/bugs/{bugID}")
severity = {1: "致命", 2: "严重", 3: "一般", 4: "轻微"}
return (
f"Bug #{bug['id']}: {bug['title']}\n"
f"严重程度: {severity.get(bug['severity'], '未知')}\n"
f"状态: {bug['status']} | 指派: {bug['assignedTo']}\n"
f"重现步骤:\n{bug.get('steps', '无')}"
)
if __name__ == "__main__":
mcp.run()
不到 40 行。逐块看:
get_token() — 首次调用时用账号密码换 Token,之后复用缓存。禅道 Token 有效期通常 30 分钟,简单场景够用。
@mcp.tool() — 这个装饰器把普通函数注册为 MCP Tool。函数签名就是参数定义(bugID: int),docstring 就是 Agent 看到的 Tool 描述。docstring 不是给人看的文档,是给 Agent 看的触发条件。 所以不只写「获取 Bug 详情」,还写「当用户提到 Bug 编号时使用」——Agent 靠这句话决定要不要调用。
返回值 — 格式化纯文本,不是 JSON。Agent 上下文窗口宝贵,纯文本比 JSON 省 token,信息密度更高。
前提
跑通这个例子之前,需要确认三件事:
第一,禅道版本 15.0+,且管理员在「后台 → 系统 → 参数 → API」中开启了 REST API。
第二,有一个有 API 权限的账号。建议创建专用账号(如 mcp-bot),不要复用个人账号。
第三,了解禅道的数据层级。Bug 挂在产品下(需要 productID),任务挂在迭代下(需要 executionID)。这直接决定 Tool 的参数怎么设计。
部署到 Cursor
打开 Cursor Settings → Customize → MCP Servers,添加配置:
{
"mcpServers": {
"zentao": {
"command": "python",
"args": ["/你的路径/zentao-mcp/server.py"],
"env": {
"ZENTAO_BASE_URL": "https://你的禅道地址",
"ZENTAO_ACCOUNT": "mcp-bot",
"ZENTAO_PASSWORD": "你的密码"
}
}
}
}
Cursor 会自动启动这个 Python 进程。配好后在 Chat 里说「帮我看看 Bug #1024」,Agent 就会调用 zentao_get_bug,拿到详情后直接开始工作。
完整工作流
来看这个 MCP 服务在真实场景中是怎么工作的。你在 Cursor 里说:
「帮我查看 Bug #1024 的详情」
Agent 的执行链路如下:
sequenceDiagram
participant U as 用户(Cursor)
participant A as Agent
participant MCP as 禅道 MCP 服务
participant Z as 禅道服务器
U->>A: 帮我查看 Bug #1024 的详情
A->>MCP: zentao_get_bug(bugID=1024)
MCP->>Z: GET /api.php/v1/bugs/1024
Z-->>MCP: Bug 详情 JSON
MCP-->>A: 格式化后的 Bug 详情文本
A-->>U: 展示 Bug 详情
实际效果如下(在 Cursor 中输入「查看 bug 5620」,Agent 调用 MCP 服务后返回的 Bug 详情):
扩展更多 Tool
跑通第一个后,后面的都是复制粘贴。
查询 Bug 列表:
@mcp.tool()
def zentao_list_bugs(productID: int, status: str = "active", limit: int = 20) -> str:
"""查询禅道中指定产品的 Bug 列表。可按状态筛选。
当用户询问'有哪些未解决的 Bug'、'严重 Bug 有哪些'时使用。"""
data = zentao_get(f"/api.php/v1/products/{productID}/bugs")
bugs = data.get("bugs", [])[:limit]
if not bugs:
return "当前没有符合条件的 Bug。"
return "\n".join(
f"#{b['id']} {b['title']} | 指派: {b['assignedTo']}" for b in bugs
)
注意 status 和 limit 都有默认值,只有 productID 是必填的。如果 status 也必填,Agent 每次都要先问用户「你要查什么状态」,白白多一轮对话。
更新任务状态:
@mcp.tool()
def zentao_update_task(taskID: int, status: str, consumed: float = None) -> str:
"""更新禅道任务的状态或工时。当用户要求'标记任务完成'、'更新进度'时使用。"""
body = {"status": status}
if consumed is not None:
body["consumed"] = consumed
httpx.put(f"{ZENTAO_BASE}/api.php/v1/tasks/{taskID}",
json=body, headers={"Token": get_token()})
return f"任务 #{taskID} 已更新为 {status}"
写操作要更保守——status 只允许 doing/done/pause,不包含 cancel 和 closed,避免 Agent 误操作。
四条设计原则
从一个 Tool 到多个,贯穿的原则就四条:
按意图拆分 Tool。 list_bugs、get_bug、list_tasks 各管各的,不要做成一个万能查询接口。Agent 选 Tool 靠名字和描述,语义越清晰越不容易选错。
docstring 写触发场景。 「获取 Bug 详情」是功能说明,「当用户提到 Bug 编号时使用」才是 Agent 判断调不调用的依据。
只把真正必填的放 required。 可选参数给合理默认值,减少 Agent 追问次数。
返回纯文本,不返回 JSON。 MCP 服务是 Agent 和外部系统之间的翻译层。
最后
一个 MCP 服务就是一个独立进程,核心是定义 Tool、写清描述、调外部 API、返回格式化结果。
而且你完全可以不自己写。告诉 AI:「帮我写一个禅道 MCP 服务,需要查 Bug、看详情、查任务、更新状态,禅道地址是 xxx」,AI 能生成完整代码。你的价值在于:知道团队最痛的场景是什么,定义 Tool 清单划定能力边界,用真实场景验证结果是否好用。
设计 MCP 服务的核心不是写代码,是理解 Agent 怎么工作、外部系统数据长什么样、两者之间怎么对接。代码让 AI 写就好了。
English / 原文
如何设计一个手写一个 MCP 服务
什么是 MCP 服务
MCP(Model Context Protocol)是一个让 AI Agent 连接外部系统的标准协议。
Agent 本身能读写代码、操作 Git、搜索文件,但它访问不了禅道、Jira、数据库这类外部系统。MCP 服务就是中间那座桥——你用任意语言写一个小服务,声明几个 Tool(函数),Agent 就能调用它们。
工作原理很简单:Agent 通过 stdio(标准输入输出)向你的 MCP 服务发一个 JSON 请求,比如「调用 zentao_get_bug,参数 bugID=1024」,你的服务去调禅道 API,拿到结果,格式化后返回。Agent 拿到结果后继续它的工作。
Agent ──stdio──> 你的 MCP 服务 ──HTTP──> 禅道服务器
Agent <──stdio── 你的 MCP 服务 <──HTTP── 禅道服务器
一个 MCP 服务就是一个独立进程,核心就三件事:定义 Tool(函数签名 + 描述)、实现 Tool(调外部 API)、返回结果(格式化纯文本)。
从最简单的例子开始
先只做一个 Tool:zentao_get_bug——根据 Bug ID 获取详情。跑通这一个,其余都是同样的套路。
安装依赖
pip install mcp httpx
完整代码
import os, httpx
from mcp.server.fastmcp import FastMCP
ZENTAO_BASE = os.environ.get("ZENTAO_BASE_URL", "https://zentao.example.com")
_token = None
def get_token():
global _token
if _token is None:
resp = httpx.post(f"{ZENTAO_BASE}/api.php/v1/tokens", json={
"account": os.environ["ZENTAO_ACCOUNT"],
"password": os.environ["ZENTAO_PASSWORD"]
})
_token = resp.json()["token"]
return _token
def zentao_get(path):
return httpx.get(f"{ZENTAO_BASE}{path}",
headers={"Token": get_token()}).json()
mcp = FastMCP("zentao", description="禅道 MCP 服务")
@mcp.tool()
def zentao_get_bug(bugID: int) -> str:
"""获取禅道中某个 Bug 的完整详情,包括重现步骤和严重程度。
当用户提到 Bug 编号、要求查看 Bug 细节时使用。"""
bug = zentao_get(f"/api.php/v1/bugs/{bugID}")
severity = {1: "致命", 2: "严重", 3: "一般", 4: "轻微"}
return (
f"Bug #{bug['id']}: {bug['title']}\n"
f"严重程度: {severity.get(bug['severity'], '未知')}\n"
f"状态: {bug['status']} | 指派: {bug['assignedTo']}\n"
f"重现步骤:\n{bug.get('steps', '无')}"
)
if __name__ == "__main__":
mcp.run()
不到 40 行。逐块看:
get_token() — 首次调用时用账号密码换 Token,之后复用缓存。禅道 Token 有效期通常 30 分钟,简单场景够用。
@mcp.tool() — 这个装饰器把普通函数注册为 MCP Tool。函数签名就是参数定义(bugID: int),docstring 就是 Agent 看到的 Tool 描述。docstring 不是给人看的文档,是给 Agent 看的触发条件。 所以不只写「获取 Bug 详情」,还写「当用户提到 Bug 编号时使用」——Agent 靠这句话决定要不要调用。
返回值 — 格式化纯文本,不是 JSON。Agent 上下文窗口宝贵,纯文本比 JSON 省 token,信息密度更高。
前提
跑通这个例子之前,需要确认三件事:
第一,禅道版本 15.0+,且管理员在「后台 → 系统 → 参数 → API」中开启了 REST API。
第二,有一个有 API 权限的账号。建议创建专用账号(如 mcp-bot),不要复用个人账号。
第三,了解禅道的数据层级。Bug 挂在产品下(需要 productID),任务挂在迭代下(需要 executionID)。这直接决定 Tool 的参数怎么设计。
部署到 Cursor
打开 Cursor Settings → Customize → MCP Servers,添加配置:
{
"mcpServers": {
"zentao": {
"command": "python",
"args": ["/你的路径/zentao-mcp/server.py"],
"env": {
"ZENTAO_BASE_URL": "https://你的禅道地址",
"ZENTAO_ACCOUNT": "mcp-bot",
"ZENTAO_PASSWORD": "你的密码"
}
}
}
}
Cursor 会自动启动这个 Python 进程。配好后在 Chat 里说「帮我看看 Bug #1024」,Agent 就会调用 zentao_get_bug,拿到详情后直接开始工作。
完整工作流
来看这个 MCP 服务在真实场景中是怎么工作的。你在 Cursor 里说:
「帮我查看 Bug #1024 的详情」
Agent 的执行链路如下:
sequenceDiagram
participant U as 用户(Cursor)
participant A as Agent
participant MCP as 禅道 MCP 服务
participant Z as 禅道服务器
U->>A: 帮我查看 Bug #1024 的详情
A->>MCP: zentao_get_bug(bugID=1024)
MCP->>Z: GET /api.php/v1/bugs/1024
Z-->>MCP: Bug 详情 JSON
MCP-->>A: 格式化后的 Bug 详情文本
A-->>U: 展示 Bug 详情
实际效果如下(在 Cursor 中输入「查看 bug 5620」,Agent 调用 MCP 服务后返回的 Bug 详情):
扩展更多 Tool
跑通第一个后,后面的都是复制粘贴。
查询 Bug 列表:
@mcp.tool()
def zentao_list_bugs(productID: int, status: str = "active", limit: int = 20) -> str:
"""查询禅道中指定产品的 Bug 列表。可按状态筛选。
当用户询问'有哪些未解决的 Bug'、'严重 Bug 有哪些'时使用。"""
data = zentao_get(f"/api.php/v1/products/{productID}/bugs")
bugs = data.get("bugs", [])[:limit]
if not bugs:
return "当前没有符合条件的 Bug。"
return "\n".join(
f"#{b['id']} {b['title']} | 指派: {b['assignedTo']}" for b in bugs
)
注意 status 和 limit 都有默认值,只有 productID 是必填的。如果 status 也必填,Agent 每次都要先问用户「你要查什么状态」,白白多一轮对话。
更新任务状态:
@mcp.tool()
def zentao_update_task(taskID: int, status: str, consumed: float = None) -> str:
"""更新禅道任务的状态或工时。当用户要求'标记任务完成'、'更新进度'时使用。"""
body = {"status": status}
if consumed is not None:
body["consumed"] = consumed
httpx.put(f"{ZENTAO_BASE}/api.php/v1/tasks/{taskID}",
json=body, headers={"Token": get_token()})
return f"任务 #{taskID} 已更新为 {status}"
写操作要更保守——status 只允许 doing/done/pause,不包含 cancel 和 closed,避免 Agent 误操作。
四条设计原则
从一个 Tool 到多个,贯穿的原则就四条:
按意图拆分 Tool。 list_bugs、get_bug、list_tasks 各管各的,不要做成一个万能查询接口。Agent 选 Tool 靠名字和描述,语义越清晰越不容易选错。
docstring 写触发场景。 「获取 Bug 详情」是功能说明,「当用户提到 Bug 编号时使用」才是 Agent 判断调不调用的依据。
只把真正必填的放 required。 可选参数给合理默认值,减少 Agent 追问次数。
返回纯文本,不返回 JSON。 MCP 服务是 Agent 和外部系统之间的翻译层。
最后
一个 MCP 服务就是一个独立进程,核心是定义 Tool、写清描述、调外部 API、返回格式化结果。
而且你完全可以不自己写。告诉 AI:「帮我写一个禅道 MCP 服务,需要查 Bug、看详情、查任务、更新状态,禅道地址是 xxx」,AI 能生成完整代码。你的价值在于:知道团队最痛的场景是什么,定义 Tool 清单划定能力边界,用真实场景验证结果是否好用。
设计 MCP 服务的核心不是写代码,是理解 Agent 怎么工作、外部系统数据长什么样、两者之间怎么对接。代码让 AI 写就好了。