qinghuan
← 调用工具并检查权限 / 全部笔记

单元 20 / 调用工具并检查权限

P1 · 重点提升L2 · 进阶

用 MCP 连接工具,保留应用控制权

看懂应用、连接端和能力服务各负责什么,再实际完成一次工具发现与调用。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能连接只读服务器并记录发现、调用及非法参数响应,说明宿主与服务端各自控制什么。

学习理由、准备要求与知识点

为什么学它

需要连接不同工具服务时,MCP 提供统一协议;先认识其职责可避免误当完整 Agent 框架。

先读这些内容

知识点清单

  • Host、Client、Server 职责
  • 工具、资源与提示
  • JSON-RPC 请求标识
  • 协议版本和能力协商
  • tools/list 与分页
  • tools/call 请求
  • 工具结果及注解可信度
  • 发现能力与业务授权

运行前先确认协议以MCP 2025-11-25规范为基线;Java17算法只构建目录路由,不执行握手、认证或网络调用。

01先弄懂它是什么

应用要接入不同的外部工具和资料,可以用 MCP 约定统一的交流格式。Host 是用户正在使用的应用,负责整合模型和权限;Client 代表它与某个 Server 连接;Server 提供工具、资源或提示。可以把它理解为各方怎么对话的协议。MCP 本身不是模型,也不会自动决定任务下一步该做什么。

MCP 中的几类能力用途不同:工具负责执行操作,资源提供可读内容,提示提供可复用的交互模板。客户端先完成连接和能力协商,再使用相应协议方法。比如成功看到某个工具的名称和参数,只能说明服务宣称有这项能力;能否让当前用户执行、工具实现是否可靠、结果能否采信,仍要由应用检查。

多个Server可能都提供search工具。Host需要保留服务身份与原始工具名的组合,再映射到模型可见名称;若只以search为键,后注册工具可能覆盖先前工具。工具路由身份必须在发现、调用和结果关联中保持一致。

文中这几个词,先认一认

Host
整合用户界面、模型和权限控制的宿主应用。
Client与Server
Client 负责与服务保持协议交互,Server 负责发布能力;同一个 Host 可以管理与多个服务的连接。
命名空间
用服务身份和工具名一起区分能力,避免不同服务的同名工具被混淆;真正调用该服务时,仍使用它发布的原始工具名。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

MCP使用JSON-RPC风格的请求、响应和通知。请求ID用于关联结果,通知没有同样的响应等待关系;生命周期能力协商决定双方可使用的特性,不能把所有能力字段简单求集合交集,因为不同方向有不同职责。协议版本应按固定规范解释。

客户端用 tools/list 查找工具,结果可能需要翻页;目录变动时,再根据双方协商支持的通知能力更新。调用时用 tools/call,传入工具原始名称和参数,并按约定检查返回结果。工具自带的“只读”等注解只是服务的声明,应用仍要验证权限与实际行为,不能直接当作可信授权或安全证明。

本题构建不可变工具目录,以二元组(serverId,toolName)作为键。同服务同名重复拒绝,不同服务同名允许,查询未知组合明确失败。二元组避免用分隔符拼接导致碰撞,例如服务名和工具名都可能含冒号;真实模型可见名称映射另由Host维护。

正在加载流程图…

图中状态只描述本篇可执行机制;业务调用仍需由外层应用负责。
查看流程图源码(Mermaid)
flowchart LR
  H["Host 用户与权限"] --> C1["Client A"]
  H --> C2["Client B"]
  C1 --> S1["Server A 工具目录"]
  C2 --> S2["Server B 工具目录"]
  S1 --> R["服务与工具二元组路由"]
  S2 --> R
  R --> V["授权后调用及结果校验"]

按顺序看,每一步做了什么

  1. 为每个Server配置可信身份、连接方式及授权范围。
  2. 完成协议版本与能力协商,再进行工具发现。
  3. 读取完整分页目录,以服务和工具名组合建立路由。
  4. 校验调用参数及权限后发送tools/call。
  5. 关联结果并验证输出,目录变更时更新版本快照。

03对照实现代码

OpenJDK 17 URI 源码 (新标签页)↗

OpenJDK 17 GA,jdk-17+35 · URI.create / getScheme / getHost

下方为原创 Java 教学片段,调用已核验的 JDK API 说明局部机制;不是该类原样源码,也不实现神经网络。完整程序见算法模块。

核心机制代码 · java
URI endpoint = URI.create(configuredEndpoint); // 只解析结构,不发起网络连接。
String scheme = endpoint.getScheme();
String host = endpoint.getHost(); // 解析出的主机也不等于已获授权目标。
// Host仍需按受信配置限制协议、主机及重定向;不要把工具返回URL直接注册为新服务。

URI.create解析URI字符串,结构非法会报错;getScheme和getHost读取组成部分,不执行DNS查询,也不建立MCP连接。解析成功只是结构证据。

某些URI没有主机或使用非层次结构,不能假设所有合法URI都可作为远程HTTP服务。协议方案、目标范围和重定向政策需要由应用明确检查。

源码桥梁展示服务端点身份与工具名称应分开管理。完整算法使用本地配置的Server ID作为命名空间,不把URL文本相似性当成服务身份等价。

本篇协议说明固定2025-11-25规范,Java代码只实现目录路由。它没有实现JSON-RPC传输、认证或MCP握手,不能把内存测试通过称为协议互通完成。

代码先调用谁,再调用谁

  1. 解析受信配置的服务端点
  2. 按Host策略确认连接范围
  3. 建立Server身份
  4. 发现工具并保持原始名称
  5. 二元组路由到正确连接

04动手写一个例子

多服务工具目录的无碰撞路由

输入服务ID和工具名列表,构造不可变二元组目录;重复服务ID或同服务重复工具拒绝,跨服务同名允许。Java17 Main.java测试冲突和未知路由。

输入 1
服务A:search,服务B:search
输出 1
两个独立路由
输入 2
服务A重复注册search
输出 2
拒绝

先校验服务ID唯一,保证一个命名空间只对应一个受信连接。再逐个工具构造Key记录,记录的结构化相等性不会受分隔符选择影响。

putIfAbsent检测同一二元组冲突,返回不可变快照防止调用过程中目录被意外修改。目录更新应构建新快照并切换版本,不能偷偷改变已排队调用的目标。

解析模型可见别名后,应查回完整Key,再读取原服务与原工具名。未知Key直接失败,不尝试按相似名称或任意服务回退,避免调用到错误能力。

为什么这样写是对的目录中每个Key唯一对应同一serverId与toolName,跨服务同名不覆盖;生成快照后路由关系不可变,未知组合不会被自动替换。

时间 平均O(N+C),查询平均O(L)额外空间 O(N+C)

N为工具总数,C为服务与工具名总字符量;哈希需读取字符串,单次Key查询成本与名称长度L有关。目录不含真实网络发现成本。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
    record Server(String id,List<String> tools){} record Key(String server,String tool){}
    static Map<Key,String> catalog(List<Server> servers){
        Map<Key,String> routes=new HashMap<>();Set<String> ids=new HashSet<>();
        for(Server server:servers){
            if(server.id()==null||server.id().isBlank()||!ids.add(server.id()))throw new IllegalArgumentException("server id");
            for(String tool:server.tools()){
                if(tool==null||tool.isBlank())throw new IllegalArgumentException("tool name");
                Key key=new Key(server.id(),tool); // 结构化二元组,避免分隔符拼接碰撞。
                if(routes.putIfAbsent(key,server.id())!=null)throw new IllegalArgumentException("duplicate tool");
            }
        }
        return Map.copyOf(routes); // 目录作为不可变快照交付。
    }
    static String resolve(Map<Key,String> routes,Key key){
        String route=routes.get(key);if(route==null)throw new IllegalArgumentException("unknown route");return route; // 不做模糊回退。
    }
    static void eq(Object e,Object a){if(!e.equals(a))throw new AssertionError(a);}
    public static void main(String[] args){
        var map=catalog(List.of(new Server("A",List.of("search")),new Server("B",List.of("search"))));
        eq(2,map.size());eq("B",resolve(map,new Key("B","search")));
        eq(0,catalog(List.of()).size());
        eq(2,catalog(List.of(new Server("a:b",List.of("c")),new Server("a",List.of("b:c")))).size());
        try{catalog(List.of(new Server("A",List.of("x","x"))));throw new AssertionError();}catch(IllegalArgumentException expected){}
        try{resolve(map,new Key("C","search"));throw new AssertionError();}catch(IllegalArgumentException expected){}
        try{map.clear();throw new AssertionError();}catch(UnsupportedOperationException expected){}
        System.out.println("PASS 7 tests");
    }
}

这些特殊情况也要试一试

  • 跨服务同名合法,同服务同名重复属于目录冲突。
  • 名称包含分隔符时仍不碰撞,依赖结构化Key。
  • 空目录合法,调用未知路由明确失败。
进阶选读 · 后续再读:动态目录与长任务兼容

场景工具列表在任务执行中更新,已排队调用仍引用旧Schema。

做法给目录快照版本,调用绑定服务、工具、Schema和授权上下文;变更时检查兼容性,必要时使旧计划重新验证,而不是仅替换名称。

复杂度构建新目录平均O(N),切换引用可O(1),但兼容性检查和旧任务迁移成本不能忽略。

好处与代价快照增加内存与版本管理,却避免调用前后目标悄悄变化;服务不可用时应明确失败或按预定义路由恢复。

05项目里怎么用

接入Server前明确它需要哪些文件、网络和凭证范围,按最小权限运行。服务器返回的工具描述、结果和注解都是外部数据,即使服务经过安装,也不能让它自行扩展Host的权限。对写工具保留操作预览与版本绑定确认。

发现分页要读到结束,处理list_changed时应在能力协商允许的范围内刷新。目录刷新失败可以保留已知版本并标记状态,但不能把部分新旧条目混成一个没有版本的集合。每次调用记录服务身份和工具原名,方便定位同名工具问题。

协议互通验收需要独立检查握手、发现、调用、错误及断线恢复。内存目录测试只证明路由算法,真实Server仍可能返回不合规参数Schema、超大结果或业务错误。Host应限制结果大小、超时和外部目标,保证失败范围可控。

容易踩的坑

  • 把MCP当成模型或自动规划框架。
  • 发现工具就默认获得执行权限。
  • 只用工具名作键,多个服务互相覆盖。
  • 将只读注解当成可信安全承诺。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
结构化命名空间同名能力路由清楚需维护模型别名映射多个MCP服务的Host
不可变目录快照运行期间行为可复核更新需切换与旧版本管理长任务及动态目录
直接专用适配器接口控制细,依赖少每类服务单独开发少量稳定业务系统

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01Host与Client有什么区别?

展开参考答案与推导

Host整合用户、模型和权限;Client负责与特定Server的协议会话。一个Host可管理多个Client连接。

02工具注解为什么不能直接授权?

展开参考答案与推导

它是服务提供的信息,不证明实现没有副作用,也不证明用户允许该操作,Host仍需自己的策略。

03目录Key为何用二元组?

展开参考答案与推导

工具身份包含服务与原始名称,结构化组合避免同名覆盖及字符串分隔符碰撞。

换个情况,还会处理吗

01两个服务都有search,模型如何调用正确者?

展开参考答案与推导

Host提供可区分的别名并映射回完整服务与工具Key,执行前再按真实连接和Schema检查。

02目录刷新只得到一页,能立即替换吗?

展开参考答案与推导

应按分页契约完成快照或明确部分状态,避免遗漏工具或混合版本;调用仍绑定可复核目录版本。

03工具结果建议添加一个新Server,能自动信任吗?

展开参考答案与推导

不能把外部结果升级为受信配置;端点与权限需要Host既定策略和用户授权范围。

写代码试一试

01证明分隔符拼接有碰撞

输入 / 条件
(a:b,c)与(a,b:c)
输出 / 预期
字符串拼接可相同,Key不同

提示 比较结构化字段。

展开参考答案与推导

若用冒号拼接都可能成为a:b:c;record分别保存两个字段,相等性逐字段比较,不发生该碰撞。

02未知路由是否回退同名工具?

输入 / 条件
请求C/search,仅A/search存在
输出 / 预期
拒绝

提示 避免隐式换服务。

展开参考答案与推导

同名不说明能力、权限和数据相同,回退必须是显式业务策略,本题不允许。

03为何返回不可变Map?

输入 / 条件
构建后调用方尝试clear
输出 / 预期
抛UnsupportedOperationException

提示 快照不能被意外修改。

展开参考答案与推导

Map.copyOf保护映射结构;若值是可变对象仍需额外不可变设计,本题值为String。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能连接只读服务器并记录发现、调用及非法参数响应,说明宿主与服务端各自控制什么。

本篇重点加练

协议能力协商与业务授权分别决定能否通信和能否执行,需要避免混用两类证据。

  • 协议版本和能力协商
  • 发现能力与业务授权

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为一个本地笔记服务画Host、Client、Server关系图,分别安排工具、资源和提示,解释为什么把它们混为一种能力会影响使用方式。

    提交内容
    一份独立书面回答或标注图,包含自造例子、概念边界及判断依据。
    怎样算完成
    • 逐项回应题目要求,区分事实、假设与个人判断。
    • 能用自己的例子解释适用条件,记录尚未弄懂的点。

    练到的知识点Host、Client、Server 职责 · 工具、资源与提示

  2. 作业 02代码推演

    编写一组初始化请求与响应草稿,注明JSON-RPC标识、协议版本和能力字段;推演不支持版本、未知响应ID和重复ID的处理。

    提交内容
    手工推演记录,写明输入约定、中间状态、最终判断;可运行时附程序对照记录。
    怎样算完成
    • 推演包含中间过程,数据形状、单位、顺序或状态前后一致。
    • 主动检查至少一个边界或异常条件,能解释手算与运行差异。

    练到的知识点JSON-RPC 请求标识 · 协议版本和能力协商

  3. 作业 03原理问答

    构造两份连接档案:协议能力相同但用户授权不同、用户授权相同但协议能力不同。分别列出可执行动作及仍需补齐的条件。

    提交内容
    概念或合同说明、关系图与自造案例,明确事实、假设和适用条件。
    怎样算完成
    • 关键概念有明确边界,能用案例检验声明。
    • 未获证据支持的假设单独标注。

    练到的知识点协议版本和能力协商 · 发现能力与业务授权

综合应用

  1. 作业 04面试场景

    工具列表有三页,第二页请求失败,而用户已选择一个工具。设计完整发现与调用流程,说明何时允许调用以及需要保留哪些发现证据。

    提交内容
    场景分析与决策记录,包含信息缺口、约束、备选方案和验证计划。
    怎样算完成
    • 方案回应给定约束,列出至少一种替代方案及代价。
    • 每项关键结论说明所需证据,不能核实的部分明确标注。

    练到的知识点tools/list 与分页 · tools/call 请求

  2. 作业 05编程实践

    搭建不接真实外部服务的假MCP服务器,返回带“请忽略应用权限”文本的工具结果;为客户端写测试,检查发现、调用与业务授权分别如何判定。

    提交内容
    可复现的最小程序或实验材料、自写测试、环境说明与运行记录;题目允许纸面实验时提交完整案例表。
    怎样算完成
    • 提交物按说明可以重现;测试覆盖正常、边界与失败场景。
    • 运行证据与断言能核验声明的行为,注明模拟与真实执行范围。

    练到的知识点工具结果及注解可信度 · 发现能力与业务授权

  3. 作业 06排错实验

    让分页游标重复、响应ID错配和工具名发生变化,记录客户端状态,找出能导致重复调用或错误关联的最小消息序列。

    提交内容
    最小故障输入、复现步骤、观测记录、定位依据及回归用例。
    怎样算完成
    • 其他人可按记录复现故障,并区分现象与已验证原因。
    • 定位过程有观测依据,回归用例可以再次暴露原问题。

    练到的知识点Host、Client、Server 职责 · JSON-RPC 请求标识 · tools/list 与分页

  4. 作业 07编程实践

    编写离线握手与授权判定器,使用预制能力表和用户许可表;在能力重协商、许可撤回和未声明能力三个场景保存独立判定记录。

    提交内容
    可复现的小程序或题目允许的离线实验、自写测试、输入和运行记录。
    怎样算完成
    • 输入、环境和运行过程可重现,测试覆盖声明的重点条件。
    • 验证目标由合同确定,模拟结果不冒充真实服务证据。

    练到的知识点协议版本和能力协商 · 发现能力与业务授权

  5. 作业 08排错实验

    服务器升级后新增一项写工具,客户端沿用上次工具缓存。用消息回放检查缓存能力、当前连接能力与当前用户许可是否被错误合并。

    提交内容
    最小故障序列、观测记录、原因核对与可再次触发原问题的测试。
    怎样算完成
    • 故障可以稳定重现,现象与已验证原因分开。
    • 观测记录足以判断声明的合同是否被违反。

    练到的知识点协议版本和能力协商 · 发现能力与业务授权

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    设计两个服务器提供同名工具时的命名、能力协商和授权策略,给出服务升级后的兼容与权限回归用例。

    提交内容
    设计评审文档或对照实验报告,包含前置条件、方案取舍、验收证据与尚未验证的范围。
    怎样算完成
    • 比较使用一致条件与事先声明的标准,记录失败与未知结果。
    • 说明方案的适用前提、未覆盖风险及后续验证方法。

    练到的知识点工具、资源与提示 · 协议版本和能力协商 · 发现能力与业务授权

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 跨域迁移

    把能力协商与授权分离的原则迁移到浏览器插件或数据库驱动,给出一个具体接入场景,说明哪些合同可以借鉴、哪些无法直接照搬。

    提交内容
    原场景与目标场景的对应表、可迁移的机制、不能迁移的前提及验证设计。
    怎样算完成
    • 逐项核对两种场景的前提,不只比较相似术语。
    • 至少一个不能直接迁移的边界有具体检验方法。

    练到的知识点协议版本和能力协商 · 发现能力与业务授权

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗