杠精派
派聪明
0/512

热门评论

克莱因蓝 2026年07月17日 22:45
引用原文:PaiCLI 会把每个工具注册到 ToolRegistry 里
@派聪明,具体的注册流程能讲讲吗,工具是怎么进行注册的
点赞
回复5 取消回复
派聪明 2026年07月17日 22:45
根据参考资料,PaiCLI 的工具注册到 **ToolRegistry** 的完整流程是这样的: --- ### 整体流程:三步走 ``` MCP Server 启动 → tools/list 获取工具列表 → Schema 清洗 → 加命名空间 → 注册到 ToolRegistry ``` --- ### 第一步:获取工具列表 MCP 握手(`initialize`)完成后,客户端调用 `tools/list` 向服务端发起请求: ```json {"method": "tools/list", "params": {}} ``` 服务端返回工具列表,每个工具包含 `name`、`description`、`inputSchema`(参数的 JSON Schema)。 --- ### 第二步:Schema 清洗(McpSchemaSanitizer) 服务端返回的 Schema 五花八门,有些包含 `$ref`、`anyOf`、`oneOf` 等高级特性,LLM 不一定能正确处理。所以 PaiCLI 在注册前做一轮清洗: - 删除 `$schema`、`$id`、`$ref` 等 LLM 看不懂的字段 - 把 `anyOf`、`oneOf` 展开写成 description 文本描述 - 确保每个 Schema 都有 `type` 和 `properties` 字段 - 描述超过 1000 字符的截断 - 非 object 类型的 Schema 整个包一层变成 object > 这个清洗器约 100 行代码,解决了大量实际边界问题。 --- ### 第三步:加命名空间前缀 清洗完成后,工具注册时**不是直接用原名**,而是加上命名空间前缀: ``` mcp__{server名}__{工具名} ``` 举个例子: | MCP Server | 原始工具名 | 注册后的工具名 | |---|---|---| | filesystem | `read_file` | `mcp__filesystem__read_file` | | zread | `read` | `mcp__zread__read` | **加前缀的原因有两个**: 1. **避免冲突**:PaiCLI 自己也有 `read_file` 这类内置工具,不加前缀就重名了。 2. **安全审计**:所有 `mcp__` 开头的工具调用会被自动记录到审计日志,且 HITL 人工审批模块**默认对 MCP 工具开启审批**(第三方工具不可信)。 ```java public boolean requiresApproval(String toolName) { return DANGEROUS_TOOLS.contains(toolName) || isMcpTool(toolName); } private boolean isMcpTool(String toolName) { return toolName != null && toolName.startsWith("mcp__"); } ``` --- ### 第四步:注册到 ToolRegistry 最终,带前缀的工具名和清洗后的 Schema 一起注册到 **ToolRegistry** 中。这样 LLM 在做 Function Calling 时就能看到这些工具,并根据用户意图自动选择调用。 --- ### 一句话总结 **注册流程 = tools/list 发现 → Schema 清洗 → mcp__server__tool 命名空间隔离 → 注册到 ToolRegistry**。这个设计既避免了工具名冲突,又让第三方工具默认纳入安全管控,实现了工具开发者和 Agent 开发者的完全解耦。
点赞
回复 取消回复
展开剩余 4 条回复

45 条评论

过时的草莓 2026年08月08日 17:16
引用原文:daemon
解释@派聪明
点赞
回复4 取消回复
派聪明 2026年08月08日 17:16
小派正在努力回答中, 耐心等待一下吧...
点赞
回复 取消回复
展开剩余 3 条回复
. 2026年07月29日 08:38
@派聪明 什么叫资源导向
点赞
回复1 取消回复
派聪明 2026年07月29日 08:38
什么叫资源导向 当前模型还在开发当中,请右上角下拉框切换到其他模型
点赞
回复 取消回复
枫原万叶 2026年07月27日 15:09
引用原文:因为 MCP 的通信不是资源导向的(不存在“某个工具的 URL”),而是过程调用导向的——“我要调用某个方法,传这些参数”。 JSON-RPC 天然适合这种场景,协议头尾加起来也就几个字段,解析起来比 HTTP REST 简单很多。
@派聪明 解释一下这段话,什么是“资源导向”
点赞
回复1 取消回复
派聪明 2026年07月27日 15:09
小派正在努力回答中, 耐心等待一下吧...
点赞
回复 取消回复
枫原万叶 2026年07月27日 15:09
引用原文:因为 MCP 的通信不是资源导向的(不存在“某个工具的 URL”),而是过程调用导向的——“我要调用某个方法,传这些参数”。 JSON-RPC 天然适合这种场景,协议头尾加起来也就几个字段,解析起来比 HTTP REST 简单很多。
@派聪明 解释一下这段话,什么是“资源导向”
点赞
回复1 取消回复
派聪明 2026年07月27日 15:09
小派正在努力回答中, 耐心等待一下吧...
点赞
回复 取消回复
枫原万叶 2026年07月27日 14:49
引用原文:因为 MCP 的通信不是资源导向的(不存在“某个工具的 URL”),而是过程调用导向的——“我要调用某个方法,传这些参数”。 JSON-RPC 天然适合这种场景,协议头尾加起来也就几个字段,解析起来比 HTTP REST 简单很多。
@派聪明 解释一下这段话,什么是“资源导向”
点赞
回复1 取消回复
派聪明 2026年07月27日 14:49
小派正在努力回答中, 耐心等待一下吧...
点赞
回复 取消回复
枫原万叶 2026年07月27日 11:10
引用原文:因为 MCP 的通信不是资源导向的(不存在“某个工具的 URL”),而是过程调用导向的——“我要调用某个方法,传这些参数”。 JSON-RPC 天然适合这种场景,协议头尾加起来也就几个字段,解析起来比 HTTP REST 简单很多。
@派聪明 展开讲一下这句话,是因为JSON-RPC更加轻量级且足以满足要求吗?
点赞
回复1 取消回复
派聪明 2026年07月27日 11:10
小派正在努力回答中, 耐心等待一下吧...
点赞
回复 取消回复
17377927899 2026年07月26日 17:37
引用原文:加载顺序是先读用户级,再读项目级。如果同名 server 在两个文件里都有,项目级会覆盖用户级。
@派聪明 为什么这么设计
点赞
回复1 取消回复
派聪明 2026年07月26日 17:37
小派正在努力回答中, 耐心等待一下吧...
点赞
回复 取消回复
克莱因蓝 2026年07月17日 22:45
引用原文:PaiCLI 会把每个工具注册到 ToolRegistry 里
@派聪明,具体的注册流程能讲讲吗,工具是怎么进行注册的
点赞
回复5 取消回复
派聪明 2026年07月17日 22:45
根据参考资料,PaiCLI 的工具注册到 **ToolRegistry** 的完整流程是这样的: --- ### 整体流程:三步走 ``` MCP Server 启动 → tools/list 获取工具列表 → Schema 清洗 → 加命名空间 → 注册到 ToolRegistry ``` --- ### 第一步:获取工具列表 MCP 握手(`initialize`)完成后,客户端调用 `tools/list` 向服务端发起请求: ```json {"method": "tools/list", "params": {}} ``` 服务端返回工具列表,每个工具包含 `name`、`description`、`inputSchema`(参数的 JSON Schema)。 --- ### 第二步:Schema 清洗(McpSchemaSanitizer) 服务端返回的 Schema 五花八门,有些包含 `$ref`、`anyOf`、`oneOf` 等高级特性,LLM 不一定能正确处理。所以 PaiCLI 在注册前做一轮清洗: - 删除 `$schema`、`$id`、`$ref` 等 LLM 看不懂的字段 - 把 `anyOf`、`oneOf` 展开写成 description 文本描述 - 确保每个 Schema 都有 `type` 和 `properties` 字段 - 描述超过 1000 字符的截断 - 非 object 类型的 Schema 整个包一层变成 object > 这个清洗器约 100 行代码,解决了大量实际边界问题。 --- ### 第三步:加命名空间前缀 清洗完成后,工具注册时**不是直接用原名**,而是加上命名空间前缀: ``` mcp__{server名}__{工具名} ``` 举个例子: | MCP Server | 原始工具名 | 注册后的工具名 | |---|---|---| | filesystem | `read_file` | `mcp__filesystem__read_file` | | zread | `read` | `mcp__zread__read` | **加前缀的原因有两个**: 1. **避免冲突**:PaiCLI 自己也有 `read_file` 这类内置工具,不加前缀就重名了。 2. **安全审计**:所有 `mcp__` 开头的工具调用会被自动记录到审计日志,且 HITL 人工审批模块**默认对 MCP 工具开启审批**(第三方工具不可信)。 ```java public boolean requiresApproval(String toolName) { return DANGEROUS_TOOLS.contains(toolName) || isMcpTool(toolName); } private boolean isMcpTool(String toolName) { return toolName != null && toolName.startsWith("mcp__"); } ``` --- ### 第四步:注册到 ToolRegistry 最终,带前缀的工具名和清洗后的 Schema 一起注册到 **ToolRegistry** 中。这样 LLM 在做 Function Calling 时就能看到这些工具,并根据用户意图自动选择调用。 --- ### 一句话总结 **注册流程 = tools/list 发现 → Schema 清洗 → mcp__server__tool 命名空间隔离 → 注册到 ToolRegistry**。这个设计既避免了工具名冲突,又让第三方工具默认纳入安全管控,实现了工具开发者和 Agent 开发者的完全解耦。
点赞
回复 取消回复
展开剩余 4 条回复
关点点儿 2026年07月17日 20:56
引用原文:JSON-RPC 2.0
@派聪明
点赞
回复1 取消回复
派聪明 2026年07月17日 20:56
## 为什么 MCP 选择 JSON-RPC 2.0 作为底层通信协议? 根据参考资料,MCP(Model Context Protocol)的底层通信协议就是 **JSON-RPC 2.0**。这不是 HTTP REST,也不是 gRPC,就是最朴素、最轻量的 JSON-RPC。 ### 一个请求长这样 ```json {"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}} ``` ### 一个响应长这样 ```json {"jsonrpc": "2.0", "id": 1, "result": {"tools": [...]}} ``` 其中 `id` 用来配对请求和响应——客户端发 `id=1` 的请求,服务端回 `id=1` 的响应,即使多个请求并发也不会搞混。 还有一种叫**通知(notification)**,没有 `id` 字段,服务端不需要回复: ```json {"jsonrpc": "2.0", "method": "notifications/initialized", "params": {}} ``` ### 为什么选 JSON-RPC 而不是 REST? 因为 MCP 的通信**不是资源导向的**(不存在"某个工具的 URL"),而是**过程调用导向的**——"我要调用某个方法,传这些参数"。JSON-RPC 天然适合这种场景,协议头尾加起来也就几个字段,解析起来比 HTTP REST 简单很多。 ### PaiCLI 的落地实现 参考资料提到,PaiCLI 手写了一个 `JsonRpcClient`(约 120 行代码),核心设计很有意思: - 用 **`AtomicLong`** 生成递增 ID - 用 **`ConcurrentHashMap<Long, CompletableFuture<JsonNode>>`** 做请求-响应配对 - 发请求时往 map 里塞一个 future,收到响应时按 ID 找到 future 并 complete - 用 `future.get(timeout)` 等结果,超时控制默认 **60 秒** - 用 `ScheduledExecutorService`(daemon 线程)调度定时任务,到点了就把 pending 的 future 用 `TimeoutException` 异常结束 一句话总结:**JSON-RPC 2.0 用最轻量的方式解决了过程调用的问题,协议简洁、实现成本低,非常适合 Agent 与工具之间的标准化通信。**
点赞
回复 取消回复
polarwinnie 2026年07月17日 17:03
引用原文:用 ScheduledExecutorService(daemon 线程)调度定时任务,到点了就把 pending 的 future 用 TimeoutException 异常结束。调用方设的超时会比内部多留 1 秒缓冲,避免调度器和调用方同时超时产生竞争
@派聪明 这是什么意思
点赞
回复3 取消回复
派聪明 2026年07月17日 17:03
这段话讲的是一个**超时控制的防冲突设计**,我来拆解一下: **核心场景**:PaiCLI 手写的 `JsonRpcClient` 里,发请求时用 `CompletableFuture` 异步等待结果,同时用 `ScheduledExecutorService`(守护线程池)来调度一个定时任务,到了超时时间就把还没完成的 future 以 `TimeoutException` 异常结束掉。 --- **问题出在哪**:假设内部设置了 60 秒超时,调度器在第 60 秒触发超时取消 future。但调用方(比如 LLM 调用方)可能也在第 60 秒自己等得不耐烦了,自己抛一个超时异常。**两边同时认为超时**,就可能产生竞争——调度器正在取消 future 的同时,调用方也在检查结果,容易导致异常重复抛出或者状态混乱。 --- **解决方案——“多留 1 秒缓冲”**: 代码的做法是:调用方设的超时时间比内部调度器多 1 秒。比如: - 内部调度器:60 秒触发超时 - 调用方 `future.get()`:设 61 秒超时 这样调度器**先到 60 秒**,主动取消 future,调用方那边还没到 61 秒,等它到点的时候 future 已经被取消了,或者已经拿到了结果。**双方不会在同一时刻竞争**,谁先到谁处理,逻辑干净利落。 举个例子:好比两个人同时去关一扇门,约好 A 在 60 秒关,B 在 61 秒关。A 先动手把门关了,B 到 61 秒发现门已经关好了,就不需要再动手了,避免了俩人同时推门撞在一起。 --- 一句话总结:**通过错开超时时刻,让调度器先动手清理,避免调用方和调度器同时处理超时导致竞争**。
点赞
回复 取消回复
展开剩余 2 条回复

目录