CLI 与 MCP 接入 IDE
昨天(D12)把 agent 变成了 HTTP 接口。但 HTTP 只是"正门"——今天(D13)我们钻进另外几扇门的真实源码:命令行 agentctl(给 CI/运维用)和 MCP(让 IDE 里的 AI"说人话就调")。你会看到它们如何共用同一个 PlatformClient、如何把 HTTP 状态码翻译成退出码和 MCP 错误、tool 的 schema 又是怎么从 Pydantic 模型自动长出来的。核心共识:这些门都不含业务,最后都通到 D12 那个 HTTP server。
PlatformClient)。最后还有两把"长得像"的钥匙(两个 MCP)要分清,别拿错。四种接入面
同一个 agent 能力,平台通过四种"面(surface)"暴露给不同用户:
| 接入面 | 给谁用 | 怎么用 |
|---|---|---|
| HTTP API | 程序 / 前端 / 集成 | POST /v1/agent/…(Day 12) |
| CLI (agentctl) | CI/CD / 运维 / 脚本 | 命令行调用 |
| MCP | IDE 里的 AI(Cursor/Claude Code) | 自然语言触发 |
| Skills | IDE 里的人 | 一句话 SOP(Day 19) |
关键点:CLI 和 MCP 本身都不含业务逻辑,它们最终都是把请求转成对 Day 12 那个 HTTP server 的调用。二者甚至共用同一个 HTTP 客户端 PlatformClient(就在 gov_agents_mcp/client.py 里,CLI 直接 import 它)。今天就是把这三个"门"的真实源码逐段读一遍。
agentctl 入口:7 个子命令 + 退出码翻译
curl 打 HTTP,脚本只能自己解析 JSON、自己判断成败,各人写各人的。agentctl 把这件事统一了:它按结果返回标准退出码,CI 一句 if agentctl invoke ...; then 就能判成败。packages/gov-agents-cli 提供命令 agentctl(基于 Typer)。先看它的顶部——写死的 agent 名单 + Typer app 装配:
# 跟 platform AGENT_REGISTRY 对齐(避免运行时 import platform 包 · 写死即可)
AGENT_NAMES = ["biz-link-gov", "rca", "risk-review", "alert-triage", "doc-check"] # main.py:31
app = typer.Typer(
name="agentctl",
help="调 gov-agents-platform v1 endpoint · CI/CD 脚本入口。",
no_args_is_help=True, # 不带参数就打印 help,不报错
)
app.add_typer(skill_app) # 挂子命令组:agentctl skill ...(管 L4 skills)
app.add_typer(new_app) # 挂子命令组:agentctl new agent(脚手架,Day 19)
register_validate_agent_id(app) # agentctl validate-agent-id
AGENT_NAMES只认 5 个 canonical agent 名。注意这是路由名(rca 而非注册表名 sre-rca),跟 D12 三层命名对上。no_args_is_help=True裸跑 agentctl 打印帮助而不是报错——CLI 的友好默认。add_typer(...)Typer 用"子 app 挂主 app"来做命令组,于是有了 agentctl skill ... / agentctl new agent 这种两级命令。再看两个"守门函数"——校验 agent 名、把所有异常翻成退出码:
def _validate_agent(name: str) -> str: # main.py:43
if name not in AGENT_NAMES:
stderr().print(f"[red]未知 agent {name!r} · 合法值: {AGENT_NAMES}[/red]")
raise typer.Exit(code=2) # ← 输入错 = 退出码 2
return name
def _run(coro): # main.py:58 · 所有命令都用它跑
"""跑 async coro · 统一 exception 处理 · 翻成退出码。"""
try:
return asyncio.run(coro)
except McpProxyError as e: # ← 复用 MCP 那套错误类型!
if e.code in ("INVALID_INPUT",):
stderr().print(f"[red][{e.code}] {e.message}[/red]")
raise typer.Exit(code=1) from e
if e.code == "UPSTREAM_ERROR" and "无法连接" in e.message:
stderr().print(f"[red]{e.message}[/red]")
raise typer.Exit(code=3) from e # ← 网络错 = 退出码 3
if e.code == "NOT_FOUND":
stderr().print(f"[yellow]{e.message}[/yellow]")
raise typer.Exit(code=1) from e
stderr().print(f"[red][{e.code}] {e.message}[/red]")
raise typer.Exit(code=1) from e # ← 其它 HTTP 错 = 退出码 1
except KeyboardInterrupt:
stderr().print("\n[yellow]Ctrl+C · 中断[/yellow]")
raise typer.Exit(code=130) from None # ← Ctrl+C = 退出码 130(Unix 惯例)
_run(coro)Typer 的命令是同步函数,但底层调用是 async。_run 用 asyncio.run 桥接,顺手把所有异常集中翻译成退出码——每个命令只要 _run(go()),错误处理不重复写。except McpProxyErrorCLI 没有自己的错误类型,直接复用 MCP 客户端的 McpProxyError(因为它们共用 PlatformClient)。一处定义,两处用。code 130Ctrl+C 用 130 是 Unix 约定(128 + SIGINT 的信号号 2)。写脚本的人一看退出码就知道是被手动中断。| 退出码 | 含义 | 来自 |
|---|---|---|
0 | 成功 | 正常返回 |
1 | HTTP 4xx/5xx / agent 错 / NOT_FOUND | _run 兜底分支 |
2 | 输入校验错(agent 名非法 / JSON 不合法) | _validate_agent / _parse_data |
3 | 网络错(连不上平台) | UPSTREAM_ERROR + "无法连接" |
130 | Ctrl+C 中断 | KeyboardInterrupt |
main.py:31)。原因是——CLI 要保持极轻,绝不 import 任何平台业务包。一旦 import,就会连带拉进 langgraph、大模型 SDK 一堆重依赖,agentctl --help 启动都要好几秒。所以它宁可"抄一小份名单"忍受重复。这是"务实的重复"胜过"优雅的耦合"的典型权衡。一条 invoke 命令的完整一生
拿最常用的 agentctl invoke 走一遍,看参数怎么变成一次 HTTP 调用(main.py:99-122):
@app.command()
def invoke(
agent: Annotated[str, typer.Argument(help="5 个之一:biz-link-gov / rca / ...")],
data: Annotated[str | None, typer.Option(help="JSON body string")] = None,
data_file: Annotated[Path | None, typer.Option(help="JSON body 文件路径")] = None,
table: Annotated[bool, typer.Option("--table", help="人类格式 · 默认 JSON")] = False,
no_color: Annotated[bool, typer.Option("--no-color")] = False,
) -> None:
"""sync invoke · POST /v1/agent/{name}/invoke。"""
agent = _validate_agent(agent) # ① 先卡 agent 名(不合法退出码 2)
body = _parse_data(data, data_file) # ② 解析 JSON body
async def go():
client = _make_client() # ③ 造 PlatformClient
try:
return await client.invoke(agent, body) # ④ 真打 HTTP
finally:
await _close(client) # ⑤ 关连接池(无论成败)
env = _run(go()) # ⑥ _run 统一跑 + 翻退出码
if table:
render_envelope(env, no_color=no_color) # 人类友好表格
else:
stdout_console(no_color=no_color).print_json(dump_json(env)) # 默认吐 JSON
其中 _parse_data(main.py:79)负责把 --data 字符串或 --data-file 文件读成 dict,任何一步失败都退出码 2:
def _parse_data(data, data_file) -> dict: # main.py:79
if data_file:
try:
return json.loads(data_file.read_text())
except (OSError, json.JSONDecodeError) as e:
stderr().print(f"[red]--data-file 解析失败: {e}[/red]")
raise typer.Exit(code=2) from e # 文件读不了/JSON 坏 = 输入错
if data:
try:
return json.loads(data)
except json.JSONDecodeError as e:
raise typer.Exit(code=2) from e
stderr().print("[red]必须传 --data 或 --data-file 之一[/red]")
raise typer.Exit(code=2) # 两个都没给也是输入错
--table?因为 CLI 的第一用户是脚本,不是人。脚本要 | jq 接着处理,裸 JSON 最省事;人偶尔看才加 --table。"默认服务主要场景,附加需求用开关"——和 D07 的"约定优于配置"一个味道。stream 命令同理默认解析 SSE 漂亮打印,--raw 才吐原始字节。PlatformClient:CLI 与 MCP 共用的"电话机"
CLI 和 MCP 都靠同一个 PlatformClient(gov-agents-mcp/src/gov_agents_mcp/client.py:49)打平台。先看它怎么派生请求身份 X-User-Id——这个函数很能体现"一份代码两处复用":
def _resolve_user_id(env=None, *, prefix: str = "mcp") -> str: # client.py:28
"""派生 X-User-Id:{PREFIX}_USER_ID → MCP_USER_ID → {prefix}:$USER → {prefix}:anonymous。"""
e = env if env is not None else dict(os.environ)
explicit_env = f"{prefix.upper()}_USER_ID" # MCP→MCP_USER_ID / CLI→AGENTCTL_USER_ID
explicit = (e.get(explicit_env) or "").strip()
if explicit:
return explicit
# 兼容:CLI 没设 AGENTCTL_USER_ID 时也认 MCP_USER_ID
if prefix != "mcp":
mcp_explicit = (e.get("MCP_USER_ID") or "").strip()
if mcp_explicit:
return mcp_explicit
user = (e.get("USER") or "").strip()
if user:
return f"{prefix}:{user}" # 例 mcp:alice / agentctl:alice
return f"{prefix}:anonymous"
prefix 参数MCP 传 prefix="mcp",CLI 造 client 时传 user_id_prefix="agentctl"(见 main.py:50 _make_client)。同一函数,用参数分身份来源。四级回退显式 env → 兼容 MCP_USER_ID → 系统 $USER → anonymous。总能拿到一个非空 user_id,绝不让请求"没身份"。prefix != "mcp"专门给 CLI 开的兼容口子:你之前为 MCP 设过 MCP_USER_ID,用 CLI 时不用再设一遍。再看 client 构造 + 请求头,理解它怎么把 env 变成一次带鉴权的 HTTP:
class PlatformClient: # client.py:49
def __init__(self, *, base_url=None, timeout=120.0, env=None, user_id_prefix="mcp"):
e = env if env is not None else dict(os.environ)
self.base_url = (base_url or e.get("PLATFORM_BASE_URL") or "http://localhost:8080").rstrip("/")
self.user_id = _resolve_user_id(e, prefix=user_id_prefix)
self.anthropic_api_key = (e.get("ANTHROPIC_API_KEY") or "").strip() or None
self._client = httpx.AsyncClient(timeout=timeout) # 复用连接池
def _headers(self) -> dict[str, str]: # client.py:66
h = {"X-User-Id": self.user_id, "Content-Type": "application/json"}
if self.anthropic_api_key:
h["X-Anthropic-Api-Key"] = self.anthropic_api_key # BYOK:自带 key 透传
return h
base_url 默认PLATFORM_BASE_URL 环境变量指定连哪个平台,缺省 http://localhost:8080。.rstrip("/") 防用户多写斜杠拼出 //v1。timeout=120.0默认 2 分钟——因为 agent 跑一次可能要几十秒(多专家 + LLM),HTTP 默认超时太短会误杀。X-Anthropic-Api-KeyBYOK(自带 key):本地设了 ANTHROPIC_API_KEY 就透传给平台,让平台用你的 key 计费。没设就走平台自己的。gov_agents_mcp,CLI 直接 from gov_agents_mcp.client import PlatformClient(见 main.py:15)。代价是 CLI 依赖了 MCP 包,但换来"错误分类逻辑只维护一处"。main.py:15 那行 import 就是这个决定的落地。MCP 是什么:让 IDE 的 AI 能调你的 agent
MCP(Model Context Protocol)是一个标准协议,让 IDE 里的 AI 助手(Cursor / Claude Code / Cline / Windsurf)能"调用外部工具"。你在 IDE 里说一句人话,AI 就能触发一个 MCP tool 帮你干活。IDE 通过 stdio(标准输入输出)启动 MCP server 子进程,用 JSON-RPC 通信。
本框架把 agent 暴露成 MCP tool,于是你在 IDE 里说"帮我分析下 service-A 的超时根因",AI 就会调 invoke_sre_rca 这个 MCP tool,背后其实是打了平台的 HTTP 接口。
帮我看看 service-A 为啥大量超时→ IDE 的 AI 识别该调
invoke_sre_rca 工具,自动填参 {"trace_id":"t-9f2","user_question":"service-A 大量超时"}→ gov-agents-mcp 通过对讲机把它转成
POST http://localhost:8080/v1/agent/rca/invoke(就是 D02 那个 curl!)→ 平台跑完把 envelope 结果回给 AI,AI 用大白话讲给你听。你全程没碰命令行、没写 JSON。
server.py:89 的 logging.basicConfig(stream=sys.stderr) 就是干这个的。tool 的 schema 从 Pydantic 模型自动长出来
MCP tool 要告诉 IDE"我接受什么参数"(inputSchema)。本框架不手写 JSON Schema,而是定义 Pydantic 模型 → 自动生成。先看数据结构层(gov-agents-mcp/src/gov_agents_mcp/schemas.py):
class SreRcaInput(BaseModel): # schemas.py:27
"""sre-rca · 微服务根因分析。"""
trace_id: str = Field(..., description="SkyWalking trace ID")
service_name: str | None = Field(None, description="可选服务名 · 提示重点看哪个服务")
user_question: str = Field(..., description="用户问题描述 · 例 'service-A 慢,DB 连接池打满'")
thread_id: str | None = Field(None, description="可选 thread_id · 中断恢复用")
# tool_name → (输入模型, platform agent 名) 的总映射表 # schemas.py:68
AGENT_INPUTS: dict[str, tuple[type[BaseModel], str]] = {
"invoke_biz_link_gov": (BizLinkGovInput, "biz-link-gov"),
"invoke_sre_rca": (SreRcaInput, "rca"), # ← MCP tool 名 → 路由名
"invoke_risk_review": (RiskReviewInput, "risk-review"),
"invoke_alert_triage": (AlertTriageInput, "alert-triage"),
"invoke_doc_check": (DocCheckInput, "doc-check"),
}
| 字段 | 类型 | 作用 |
|---|---|---|
Field(..., ...) | 必填 | ...(Ellipsis)= 必填。trace_id/user_question 必填 |
Field(None, ...) | 可选 | 默认 None = 可选。service_name/thread_id 可不填 |
description= | str | 会进 JSON Schema,IDE 的 AI 靠它理解每个参数怎么填 |
AGENT_INPUTS | dict | 三合一映射:MCP tool 名 → (Pydantic 模型, HTTP 路由名) |
再看控制层怎么用这张表:list_tools 生成 schema、call_tool 校验后转发(gov-agents-mcp/src/gov_agents_mcp/tools.py):
def list_tools() -> list[Tool]: # tools.py:14
tools = []
for tool_name, (model_cls, _agent_name) in AGENT_INPUTS.items():
schema = model_cls.model_json_schema() # ← Pydantic 自动出 JSON Schema!
tools.append(Tool(name=tool_name,
description=TOOL_DESCRIPTIONS[tool_name],
inputSchema=schema))
return tools
async def call_tool(name, arguments, *, client) -> list[TextContent]: # tools.py:29
if name not in AGENT_INPUTS:
raise McpProxyError("INVALID_INPUT", f"unknown tool {name!r}")
model_cls, agent_name = AGENT_INPUTS[name]
try:
validated = model_cls.model_validate(arguments) # ← 二道校验(MCP SDK 已校一次)
except Exception as e:
raise McpProxyError("INVALID_INPUT", f"参数校验失败: {e}") from e
body = validated.model_dump(exclude_none=True) # 去掉没填的可选字段
envelope = await client.invoke(agent_name, body) # ← 转成 HTTP!用 AGENT_INPUTS 里的路由名
return [TextContent(type="text", text=json.dumps(envelope, ensure_ascii=False, indent=2))]
model_json_schema()Pydantic 一行把模型变成标准 JSON Schema(含字段类型、必填、description)。改字段只改模型,schema 自动跟着变,永远不会对不上。model_validate(arguments)IDE 传来的参数再校验一遍。MCP SDK 本身也会校,这里是"二道保险"——不信任外部输入是安全默认。exclude_none=True没填的可选字段直接不进 body,避免给平台传一堆 null 造成歧义。client.invoke(agent_name, ...)这里的 agent_name 是 AGENT_INPUTS 里那个路由名(如 rca),最终拼成 /v1/agent/rca/invoke。MCP 的活到此为止——纯转发。schemas.py:1-5):"手抄一份 · 不 import agent 包(避免拉 LLM/langgraph deps)"。跟 CLI 硬编码名单同一个理由——MCP server 要能被 uvx 秒起,不能因为 import 一个 Input 模型就连带拉进整个 agent 的重依赖。代价是模型定义有两份、需手动对齐(注释里留了 version: 2026-05-14 和"未来用 OpenAPI 自动派生"的 TODO)。轻量启动 > 消除重复,在"边缘转发进程"这个场景是对的取舍。错误归四类:把 HTTP 状态码翻成人能懂的原因
平台可能返回各种 HTTP 错误码。PlatformClient.invoke 把它们归成 4 类友好错误(client.py:75-115)——CLI 和 MCP 都靠这套分类:
async def invoke(self, agent_name, body) -> dict: # client.py:75
url = f"{self.base_url}/v1/agent/{agent_name}/invoke"
try:
r = await self._client.post(url, json=body, headers=self._headers())
except httpx.HTTPError as e:
raise McpProxyError("UPSTREAM_ERROR", f"无法连接 platform ...") # 连不上=网络错
if r.status_code == 200:
return r.json() # 正常:返 envelope
# ── 错误归类 ──
try: err_body = r.json()
except Exception: err_body = {"detail": r.text[:500]}
if r.status_code == 422: # 输入不合法
raise McpProxyError("INVALID_INPUT", str(err_body.get("detail", err_body)))
if r.status_code in (401, 403): # 鉴权失败
raise McpProxyError("UNAUTHORIZED", f"platform 拒绝(HTTP {r.status_code})...")
if r.status_code == 500: # agent 内部异常
detail = err_body.get("detail") or {}
case_id = detail.get("case_id") if isinstance(detail, dict) else None
error_msg = "agent 内部异常"
if isinstance(detail, dict):
result = detail.get("result") or {}
error_msg = result.get("error") or result.get("error_type") or error_msg
raise McpProxyError("AGENT_ERROR", error_msg, case_id=case_id) # ← 带上 case_id!
raise McpProxyError("UPSTREAM_ERROR", f"HTTP {r.status_code}: {err_body}")
| HTTP | 归类 code | 含义 → 用户该怎么办 |
|---|---|---|
| 连不上 | UPSTREAM_ERROR | 平台没起 / 网络断 → CLI 退出码 3 |
| 422 | INVALID_INPUT | 参数不对 → 改输入 |
| 401/403 | UNAUTHORIZED | 身份不对 → 检查 MCP_USER_ID / 平台鉴权模式 |
| 500 | AGENT_ERROR | agent 跑挂了 → 附带 case_id 可去平台回放 |
| 其它 | UPSTREAM_ERROR | 上游异常 |
detail 字段里(见 D12 router 的异常处理)。所以这里要 err_body["detail"]["case_id"] 层层往里挖,还得处处 isinstance(detail, dict) 防它不是字典时 .get() 崩掉。挖出 case_id 后,IDE 的 AI 就能告诉你"这次失败的回放在平台第 xxx 条"——出错也能追溯,是可信框架的细节。server 装配 + resources:tool 跑 agent、resource 读历史
MCP server 主入口 build_server(gov-agents-mcp/src/gov_agents_mcp/server.py:36)把上面几块用装饰器挂到一个 Server 上:
def build_server(client=None) -> tuple[Server, PlatformClient]: # server.py:36
pc = client if client is not None else PlatformClient() # 单例复用连接池
server = Server("gov-agents-mcp")
@server.list_tools() # server.py:48 · IDE 问"你有啥工具"
async def handle_list_tools():
return _list_tools() # → tools.py 那 5 个 invoke_*
@server.call_tool() # server.py:52 · IDE 说"调这个工具"
async def handle_call_tool(name, arguments):
try:
return await _call_tool(name, arguments, client=pc)
except McpProxyError as e: # ← 错误不外抛,而是当文本返给 IDE 的 AI
text = f"[{e.code}] {e.message}"
if e.case_id: text += f"\n(case_id: {e.case_id})"
return [TextContent(type="text", text=text)]
@server.list_resources() # server.py:62 · 5 个 cases://
async def handle_list_resources():
return _list_static_resources()
@server.read_resource() # server.py:70 · 读某条历史 case
async def handle_read_resource(uri):
try:
return await _read_resource(str(uri), client=pc)
except McpProxyError as e:
return [TextResourceContents(uri=uri, mimeType="text/plain",
text=f"[{e.code}] {e.message}")]
return server, pc
pc = ... PlatformClient()整个 server 共用一个 client(连接池复用),生命周期由 main()/测试管(server.py:100-109 的 try/finally 里 await pc.close())。except → 返 TextContent关键设计:MCP handler 不把异常抛给 IDE,而是把错误信息当成"工具的文本输出"返回。这样 IDE 的 AI 能读到 [AGENT_ERROR] ... case_id: xxx 并转告你,而不是 IDE 直接崩一个红色协议错。tools vs resources两组能力:tools 用来"跑 agent"(有副作用、要花钱),resources 用来"读历史"(只读、免费)。刻意分开。resources 侧的 URI 解析(gov-agents-mcp/src/gov_agents_mcp/resources.py:56)展示了怎么把 cases://rca 这种 URI 变成一次 HTTP GET:
def _parse_uri(uri: str) -> tuple[str, str, str | None]: # resources.py:56
if uri.startswith("cases://"): # 列表:cases://
agent = uri.removeprefix("cases://").rstrip("/")
if not agent or agent not in AGENT_NAMES:
raise McpProxyError("NOT_FOUND", f"未知 agent {agent!r}...")
return "cases", agent, None
if uri.startswith("case://"): # 单条:case:///
rest = uri.removeprefix("case://")
parts = rest.split("/", 1)
if len(parts) != 2 or not parts[0] or not parts[1]:
raise McpProxyError("NOT_FOUND", f"case:// URI 格式应为 case:///...")
agent, case_id = parts[0], parts[1]
...
return "case", agent, case_id
raise McpProxyError("NOT_FOUND", f"未识别的 URI scheme: {uri!r}")
cases://rca(复数)= 最近 50 条概览,case://rca/(单数)= 单条详情。IDE 的 AI 拿到某次调用的 case_id 后,可以直接 read case://rca/01HXXX 把详情读出来给你看,无需再发 tool call。
两个 MCP 别混淆 + 一个 stdio 真实坑
本框架有两个都叫"MCP"、架构却完全不同的东西,一次讲清避免踩坑:
gov-agents-mcp
- HTTP 瘦代理(本节 L05-L08)
- 暴露平台 5 个 agent
- 不含业务,转发 HTTP
uvx gov-agents-mcp
bmc-agent-mcp
- 独立进程内嵌 agent
- 专做 Java BMC 升版
- 直接 import bmc_agent 的图
- curl 一键装
apps/bmc-agent-mcp 用 FastMCP 暴露 4 个 tool,且直接 import 复用 bmc_agent 的图(不走 HTTP!)。看它的 tool 定义和那个著名的 stdio 坑(apps/bmc-agent-mcp/bmc_agent_mcp/server.py):
mcp = FastMCP("bmc-agent") # server.py:37
@mcp.tool() # server.py:71
def bmc_scan(project_path: str, bmc_parent_path: str = "") -> dict:
"""只扫描,returns 项目现状(便宜,几百 ms)。"""
state = _initial_state(project_path, bmc_parent_path=bmc_parent_path)
with redirect_stdout(sys.stderr): # server.py:90 ← 关键 trick!
result = scan_node(state) # 直接调 bmc_agent 的节点(内嵌,不走 HTTP)
return {...}
server.py:16-27):MCP 的 stdio transport 用 stdin/stdout 走 JSON-RPC,任何 print 到 stdout 都会破协议。但 bmc-agent 的节点全在用 rich.Console().print(...) 打进度(走 stdout)。解法就是 with redirect_stdout(sys.stderr): 把节点里的所有打印临时导到 stderr——对节点完全透明,IDE 那边看 stderr 日志即可。做任何 stdio MCP server 都要留意这条;gov-agents-mcp 则是从头就规定日志走 stderr(L05 提过的 server.py:89)来避开它。👶 小白:都做 MCP,为啥不做成一个?两把长得像的钥匙不是很容易拿错吗?
👨🏫 老师:因为它俩服务的场景不同。gov-agents-mcp 是给已经部署好的平台开一扇 IDE 门,所以它当"对讲机"转发 HTTP 最轻。而 bmc-agent-mcp 要能被开发者 curl|bash 装到自己机器上独立跑(不依赖你先起平台),所以它把 agent 的图直接内嵌进进程。一个"连总台",一个"自带小厨房"——需求不同,才有两把钥匙。
顺带另外两组"名字像、职责不同"的东西(前几天讲过,这里汇总):
- 两个 supervisor(Day 04):图内
supervisor_node(选专家、真跑)vs 跨 agentrouter(选 agent、只建议)。 - 三层命名(Day 12/本日 L06):注册表名(
sre-rca) ≠ 路由名(rca) ≠ MCP tool 名(invoke_sre_rca)。
agent_name= 传的什么。名字相近但职责不同,是大型项目的常态。今日小结 + 动手
🧠 今天你应该能回答
- 四种接入面各给谁用?CLI/MCP 为什么都不含业务?(都转发到 D12 的 HTTP server,还共用
PlatformClient) - agentctl 的 5 个退出码分别代表什么?为什么用
_run集中翻译? - CLI 为什么硬编码 5 个 agent 名、MCP schema 为什么手抄不 import?(保持轻量启动,不拉业务重依赖)
- MCP tool 的 inputSchema 从哪来?(Pydantic
model_json_schema()自动生成) PlatformClient把 HTTP 错误归成哪 4 类?500 的 case_id 藏在哪?- tool 和 resource 有什么区别?为什么"读历史"是 resource?
- 两个 MCP 的架构区别?stdio MCP 为什么必须把 stdout 重定向到 stderr?
✋ 动手
# 1. CLI 入口 + 退出码翻译(本日核心)
sed -n '30,93p' packages/gov-agents-cli/src/gov_agents_cli/main.py
# 2. MCP schema 自动生成 + tool 转发
sed -n '27,75p' packages/gov-agents-mcp/src/gov_agents_mcp/schemas.py
cat packages/gov-agents-mcp/src/gov_agents_mcp/tools.py
# 3. PlatformClient 的 user_id 派生 + 错误归 4 类
sed -n '28,115p' packages/gov-agents-mcp/src/gov_agents_mcp/client.py
# 4. server 装配 + resources URI 解析
sed -n '36,85p' packages/gov-agents-mcp/src/gov_agents_mcp/server.py
sed -n '56,102p' packages/gov-agents-mcp/src/gov_agents_mcp/resources.py
# 5. 另一个 MCP:内嵌 agent + stdout 重定向坑
sed -n '1,95p' apps/bmc-agent-mcp/bmc_agent_mcp/server.py