工具发现:分页收集目录,再选择本次允许的工具
正在读取学习记录…
1. 工具目录包含什么
工具发现是向服务询问可用工具,并取得名称、说明与输入Schema。模型与应用需要这些信息,才能选择正确工具并构造参数。服务工具多时,目录会分页返回。
分页游标cursor是服务给出的下一页位置。客户端收到nextCursor后继续请求,直到没有下一页。像看分卷目录,需要将各卷条目收齐再选择。
发现目录和批准操作是两项工作:知道服务有create_tag,接下来仍要决定是否向当前任务开放。MCP工具规范定义工具列表、分页和目录变化通知。
2. 收集两页固定目录
新建discovery_lab.py:
先在 PowerShell 创建本篇的独立目录并进入;路径可换成你自己的非系统盘位置:
mkdir -Force "F:\学习\agentdepth-l2-discovery"
cd "F:\学习\agentdepth-l2-discovery"将本篇的 discovery_lab.py、test_discovery_lab.py 保存到这个目录。保存全部文件后运行 dir *.py,核对上述文件名,再执行下文命令;新开窗口时先回到该目录,并按 Python 环境篇核对 python 的解释器路径。
"""分页收集工具目录,并比较目录变化。"""
from hashlib import sha256
from typing import Dict, List, Tuple
import json
PAGES = {
None: ([{"name": "read_note", "inputSchema": {"type": "object"}}], "page-2"),
"page-2": ([{"name": "create_tag", "inputSchema": {"type": "object", "required": ["note_id"]}}], None),
}
APPROVED = {"read_note"}
# 遍历分页游标,检查重复工具名和重复页。
def collect_pages(pages: Dict) -> List[dict]:
"""沿固定目录的游标翻页,拒绝循环游标和重复工具名。"""
cursor = None
seen_cursors = set()
tools: List[dict] = []
names = set()
while True:
if cursor in seen_cursors or cursor not in pages:
raise ValueError("bad_cursor")
seen_cursors.add(cursor)
batch, next_cursor = pages[cursor]
for tool in batch:
name = tool["name"]
if name in names:
raise ValueError("duplicate_tool")
names.add(name)
tools.append(tool)
if next_cursor is None:
return tools
cursor = next_cursor
# 名称排序后再序列化,让目录顺序变化不影响摘要。
def fingerprint(tools: List[dict]) -> str:
"""供应用核对目录变化的本地摘要,协议未定义这个字段。"""
ordered = sorted(tools, key=lambda item: item["name"])
payload = json.dumps(ordered, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return sha256(payload.encode("utf-8")).hexdigest()[:12]
def review(tools: List[dict], approved: set) -> Tuple[List[str], List[str]]:
discovered = {tool["name"] for tool in tools}
return sorted(discovered & approved), sorted(discovered - approved)
if __name__ == "__main__":
catalog = collect_pages(PAGES)
allowed, pending = review(catalog, APPROVED)
print("discovered", [tool["name"] for tool in catalog])
print("allowed", allowed, "pending", pending)
print("fingerprint", fingerprint(catalog))python discovery_lab.py输出:
discovered ['read_note', 'create_tag']
allowed ['read_note'] pending ['create_tag']
fingerprint ccdebd050af3discovered列出两项。allowed只有read_note,pending是create_tag。fingerprint是目录内容计算出的短摘要,用来比较目录是否变化。
collect_pages跟随游标,seen检查重复页,工具名称集合检查重复工具。fingerprint先按名称排序,再对规范序列化内容计算摘要;目录顺序变化时,内容摘要保持稳定。Schema发生变化,摘要也会变化。
把APPROVED增加create_tag,pending变空,目录摘要不变,因为批准策略与工具内容分别保存。再把一个Schema字段改变,摘要会变化。这分别展示批准列表更新与服务目录更新。
4. 目录更新怎样影响应用
| 变化 | 处理 |
|---|---|
| 出现新工具 | 加入待核对列表 |
| 已有工具Schema改变 | 重新检查参数和调用代码 |
| 工具被移除 | 更新可选工具,处理尚未发送的动作 |
| 工具说明改变 | 更新模型可见说明 |
| 收到目录变化通知 | 重新拉取并比较目录 |
工具名在单服务内唯一,多个服务可用“服务名+工具名”组织,避免碰巧同名。缓存要记录服务身份、协议版本和内容;刷新失败时保留明确状态,执行前再次检查当前工具是否可用。
工具说明来自服务,需要考虑服务可信程度。把目录内容与应用策略分别保存,便于查清“服务新增了什么”和“应用批准了什么”。
总结:分页收齐,检查重复,比较变化,按任务选择。继续读 MCP认证与授权。
5. 验证消息与边界
测试文件保存到同一目录:
下面的测试检查多页工具目录能否合并,并比较工具新增、删除和字段规则变化。
测试代码:test_discovery_lab.py
# 核对工具发现的正常结果和错误分支;断言失败时会显示对应的测试。
import unittest
from discovery_lab import APPROVED, PAGES, collect_pages, fingerprint, review
class DiscoveryTests(unittest.TestCase):
def test_follow_all_pages_and_keep_review_gate(self):
tools = collect_pages(PAGES)
self.assertEqual([item["name"] for item in tools], ["read_note", "create_tag"])
self.assertEqual(review(tools, APPROVED), (["read_note"], ["create_tag"]))
def test_catalog_order_does_not_change_local_fingerprint(self):
tools = collect_pages(PAGES)
self.assertEqual(fingerprint(tools), fingerprint(list(reversed(tools))))
def test_schema_change_changes_fingerprint(self):
tools = collect_pages(PAGES)
changed = [dict(item) for item in tools]
changed[0]["inputSchema"] = {"type": "object", "required": ["note_id"]}
self.assertNotEqual(fingerprint(tools), fingerprint(changed))
def test_cursor_loop_is_rejected(self):
with self.assertRaisesRegex(ValueError, "bad_cursor"):
collect_pages({None: ([], "page-2"), "page-2": ([], "page-2")})
def test_duplicate_name_is_rejected(self):
with self.assertRaisesRegex(ValueError, "duplicate_tool"):
collect_pages({None: ([{"name": "same"}], "next"),
"next": ([{"name": "same"}], None)})
if __name__ == "__main__":
unittest.main()python -m unittest test_discovery_lab -v恢复实验中的改动后运行测试。失败时先查看对应参数、编号或权限条件,再核对程序分支。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“工具发现与能力协商”,哪种做法更稳妥?
对应知识点:L2-discovery
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L2-discovery
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L2-discovery
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L2-discovery
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。