fix: 解决 MCP 工具与内置工具重名时的连坐问题#5925
Conversation
- 修改 get_func 方法:优先返回已激活的工具 - 修改 get_full_tool_set 方法:使用 add_tool 防止同名冲突 - 修改 add_tool 方法:优先保留已激活的工具 Fixes AstrBotDevs#5821 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 此拉取请求旨在解决当 MCP 外部工具与内置工具重名时,内置工具被禁用会导致 MCP 工具也一并被错误移除的问题。通过优化工具管理逻辑,特别是 Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Hey - 我给出了一些整体性的反馈:
get_full_tool_set的文档字符串目前仍然描述的是简单的「最后一个获胜」行为,但在新的add_tool逻辑下,实际规则变成了「优先使用激活的工具,否则采用最后一个」;建议更新该文档字符串,使其准确反映现在的行为,避免让后续维护者产生困惑。FuncToolManager.func_list的顺序、get_func和ToolSet.add_tool之间的交互,现在隐含了比较微妙的优先级规则(激活 vs 未激活、内置 vs MCP、较早 vs 较晚);可以考虑把这一决策逻辑集中到一个小的辅助函数中,或者在注释中更明确地说明优先级规则,以降低将来修改add_tool时出现回归问题的风险。
面向 AI 代理的提示
Please address the comments from this code review:
## Overall Comments
- The docstring for `get_full_tool_set` still describes a simple "last one wins" behavior, but with the new `add_tool` logic the actual rule is "prefer active tools, otherwise last one wins"; consider updating the docstring to accurately reflect this to avoid confusion for future maintainers.
- The interaction between `FuncToolManager.func_list` ordering, `get_func`, and `ToolSet.add_tool` now encodes fairly subtle precedence rules (active vs inactive, built-in vs MCP, earlier vs later); it may help to centralize this resolution logic in a small helper or make the precedence rules more explicit in comments to reduce the risk of regressions if `add_tool` is changed later.帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English
Hey - I've left some high level feedback:
- The docstring for
get_full_tool_setstill describes a simple "last one wins" behavior, but with the newadd_toollogic the actual rule is "prefer active tools, otherwise last one wins"; consider updating the docstring to accurately reflect this to avoid confusion for future maintainers. - The interaction between
FuncToolManager.func_listordering,get_func, andToolSet.add_toolnow encodes fairly subtle precedence rules (active vs inactive, built-in vs MCP, earlier vs later); it may help to centralize this resolution logic in a small helper or make the precedence rules more explicit in comments to reduce the risk of regressions ifadd_toolis changed later.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The docstring for `get_full_tool_set` still describes a simple "last one wins" behavior, but with the new `add_tool` logic the actual rule is "prefer active tools, otherwise last one wins"; consider updating the docstring to accurately reflect this to avoid confusion for future maintainers.
- The interaction between `FuncToolManager.func_list` ordering, `get_func`, and `ToolSet.add_tool` now encodes fairly subtle precedence rules (active vs inactive, built-in vs MCP, earlier vs later); it may help to centralize this resolution logic in a small helper or make the precedence rules more explicit in comments to reduce the risk of regressions if `add_tool` is changed later.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
此 PR 旨在解决当 MCP 工具与禁用的内置工具重名时,MCP 工具被意外移除的问题。通过修改 add_tool 的逻辑以优先保留激活的工具,以及调整 get_full_tool_set 使用 add_tool 来处理同名工具,从根源上解决了该问题。同时,get_func 也被优化,以优先返回激活的工具。
然而,本次修改引入了一个重要的安全隐患:工具选择逻辑存在不一致性。具体来说,用于执行工具的 get_func 方法和用于 LLM 展示工具的 get_full_tool_set 方法在处理同名且激活的工具时,采用了不同的冲突解决策略。这种不一致可能导致 LLM 看到的工具定义与实际执行的工具不符,从而引发意外行为或潜在的“工具劫持”问题。建议将 get_func 的逻辑与 ToolSet.add_tool 中使用的“后激活者胜出”策略对齐,以确保一致性。此外,我还提出了一些关于代码可读性和效率的改进建议。
There was a problem hiding this comment.
Pull request overview
该 PR 旨在修复 MCP 外接工具与内置工具重名时,由于工具集合构建与清洗(remove)逻辑导致 同名工具被“连坐”全部移除、进而使 MCP 工具彻底失效的问题,确保工具集合在输出给 LLM 前能正确保留可用工具。
Changes:
- 调整
FunctionToolManager.get_func():优先返回已激活工具,并在退化场景下选择后加载的同名工具。 - 调整
FunctionToolManager.get_full_tool_set():通过ToolSet.add_tool()构建工具集以去重同名工具。 - 调整
ToolSet.add_tool():同名冲突时优先保留active=True的工具,避免禁用工具覆盖启用工具。
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
astrbot/core/provider/func_tool_manager.py |
调整工具检索与完整工具集构建方式,尝试避免同名工具在清洗时被整体移除 |
astrbot/core/agent/tool.py |
更新 ToolSet.add_tool() 的同名覆盖策略,使其优先保留激活工具 |
- Fix inconsistency: get_func now uses reversed() to match ToolSet.add_tool's "last-active-wins" logic, preventing potential "tool hijacking" issues - Improve readability: replace double negative condition with clearer logic - Add compatibility: use getattr with default for tools without 'active' attribute - Remove unnecessary deepcopy: MCPTool runtime objects should not be deep copied - Update docstring: accurately describe the actual tool resolution behavior Addresses review comments from sourcery-ai, gemini-code-assist, and Copilot. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
已根据评审意见修复感谢 @sourcery-ai、@gemini-code-assist 和 @Copilot 的评审意见。已修复以下问题: 安全问题
性能问题
代码质量
所有测试通过 ✅ |
|
感谢您的更新,
这些改进使得工具管理逻辑更加健壮、高效且易于理解。做得非常棒! |
Add comprehensive tests for ToolSet.add_tool, get_func, and get_full_tool_set to verify the conflict resolution behavior when MCP tools share names with built-in tools. Test cases: - ToolSet.add_tool: active/inactive priority, last-one-wins for same state - get_func: returns last active tool, fallback to last matching tool - get_full_tool_set: deduplication logic, no deepcopy, MCP overrides disabled builtin Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
如果要更好的修复方式的话要引入命名空间,所以我未采用 |
|
@sourcery-ai review |
There was a problem hiding this comment.
Hey - 我发现了 2 个问题
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/provider/func_tool_manager.py" line_range="314-315" />
<code_context>
def get_func(self, name) -> FuncTool | None:
- for f in self.func_list:
+ # 优先返回已激活的工具(后加载的覆盖前面的,与 ToolSet.add_tool 保持一致)
+ for f in reversed(self.func_list):
+ if f.name == name and getattr(f, "active", False):
+ return f
</code_context>
<issue_to_address>
**issue (bug_risk):** 将 `get_func` 与 `ToolSet.add_tool` 中对 `active` 的默认处理方式对齐,以避免产生不一致的行为。
这里 `getattr(f, "active", False)` 会将没有 `active` 属性的工具视为未激活,而 `ToolSet.add_tool` 使用的是 `getattr(..., True)`,在冲突处理时会将它们视为已激活。这种不一致意味着,当存在一个同名且 `active=False` 的工具时,一个工具在集合中可能被视为已激活,但在 `get_func` 中却会被跳过。请在这两个位置对齐默认值,并相应调整注释/行为,以保证冲突处理与查找逻辑保持一致。
</issue_to_address>
### Comment 2
<location path="tests/unit/test_tool_conflict_resolution.py" line_range="44-52" />
<code_context>
+ assert len(toolset.tools) == 1
+ assert toolset.tools[0].active is True
+
+ def test_both_active_last_one_wins(self):
+ """When both tools are active, the new one should overwrite."""
+ toolset = ToolSet()
+ toolset.add_tool(make_tool("web_search", active=True))
+ toolset.add_tool(make_tool("web_search", active=True))
+
+ assert len(toolset.tools) == 1
+ # The second tool should be the one kept
+ assert toolset.tools[0].description == "Test tool web_search"
+
+ def test_both_inactive_last_one_wins(self):
</code_context>
<issue_to_address>
**issue (testing):** 该测试实际上并不能证明被保留的是第二个处于激活状态的工具。
由于这两个工具的创建方式完全相同,即使保留的是第一个工具,这里的断言依然会通过。为了真正验证覆盖行为,需要将两个实例区分开,并断言第二个实例被保留下来,例如:
```python
first = make_tool("web_search", active=True)
second = make_tool("web_search", active=True)
second.description = "Second web search"
toolset = ToolSet()
toolset.add_tool(first)
toolset.add_tool(second)
assert len(toolset.tools) == 1
assert toolset.tools[0] is second
assert toolset.tools[0].description == "Second web search"
```
如果实现实际上保留的是第一个工具而不是最后一个,这个测试就会失败。
</issue_to_address>Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Original comment in English
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="astrbot/core/provider/func_tool_manager.py" line_range="314-315" />
<code_context>
def get_func(self, name) -> FuncTool | None:
- for f in self.func_list:
+ # 优先返回已激活的工具(后加载的覆盖前面的,与 ToolSet.add_tool 保持一致)
+ for f in reversed(self.func_list):
+ if f.name == name and getattr(f, "active", False):
+ return f
</code_context>
<issue_to_address>
**issue (bug_risk):** Align default `active` handling between `get_func` and `ToolSet.add_tool` to avoid inconsistent behavior.
Here `getattr(f, "active", False)` treats tools without an `active` attribute as inactive, while `ToolSet.add_tool` uses `getattr(..., True)`, treating them as active in conflict resolution. This mismatch means a tool can be active in the set but skipped by `get_func` when a same-name tool has `active=False`. Please align the default in both places and adjust comments/behavior so conflict resolution and lookup remain consistent.
</issue_to_address>
### Comment 2
<location path="tests/unit/test_tool_conflict_resolution.py" line_range="44-52" />
<code_context>
+ assert len(toolset.tools) == 1
+ assert toolset.tools[0].active is True
+
+ def test_both_active_last_one_wins(self):
+ """When both tools are active, the new one should overwrite."""
+ toolset = ToolSet()
+ toolset.add_tool(make_tool("web_search", active=True))
+ toolset.add_tool(make_tool("web_search", active=True))
+
+ assert len(toolset.tools) == 1
+ # The second tool should be the one kept
+ assert toolset.tools[0].description == "Test tool web_search"
+
+ def test_both_inactive_last_one_wins(self):
</code_context>
<issue_to_address>
**issue (testing):** This test does not actually prove that the second active tool is the one kept.
Because both tools are created identically, this assertion would still pass if the first tool were kept. To really validate the overwrite behavior, differentiate the two instances and assert that the second is the one retained, for example:
```python
first = make_tool("web_search", active=True)
second = make_tool("web_search", active=True)
second.description = "Second web search"
toolset = ToolSet()
toolset.add_tool(first)
toolset.add_tool(second)
assert len(toolset.tools) == 1
assert toolset.tools[0] is second
assert toolset.tools[0].description == "Second web search"
```
This will fail if the implementation keeps the first tool instead of the last.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
@sourcery-ai review |
|
你可以自己跑一下,如果只有重构建议就没什么问题了 |
There was a problem hiding this comment.
Hey - 我在这里给出一些整体性的反馈:
- 在
FunctionToolManager.get_func中,你可以通过一次自后向前遍历来避免对func_list进行两次迭代:在反向遍历时记录最后一次遇到的工具,并在找到第一个处于激活状态的匹配项时立即返回;如果没有处于激活状态的匹配项,则回退到最后一次遇到的匹配项。这样可以在保持相同语义的同时,让逻辑更简单,也稍微更高效一些。
给 AI Agent 的提示词
Please address the comments from this code review:
## Overall Comments
- In `FunctionToolManager.get_func`, you can avoid iterating `func_list` twice by doing a single reverse pass that tracks the last-seen tool and immediately returns on the first active match, falling back to the last-seen match if none are active, which keeps the same semantics with simpler and slightly more efficient logic.帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English
Hey - I've left some high level feedback:
- In
FunctionToolManager.get_func, you can avoid iteratingfunc_listtwice by doing a single reverse pass that tracks the last-seen tool and immediately returns on the first active match, falling back to the last-seen match if none are active, which keeps the same semantics with simpler and slightly more efficient logic.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `FunctionToolManager.get_func`, you can avoid iterating `func_list` twice by doing a single reverse pass that tracks the last-seen tool and immediately returns on the first active match, falling back to the last-seen match if none are active, which keeps the same semantics with simpler and slightly more efficient logic.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Fixes #5821
Modifications / 改动点
问题根因:
当 MCP 外接工具(如 web_search)与内置工具重名时,如果内置工具被禁用(active=False),
get_full_tool_set()会将所有同名工具都加入 ToolSet,随后remove_tool()会把所有同名工具全部移除,导致 MCP 工具也被"连坐"清除。修复内容:
func_tool_manager.py-get_func方法func_tool_manager.py-get_full_tool_set方法add_tool()进行填充,而非直接复制列表tool.py-add_tool方法改动文件:
astrbot/core/agent/tool.py- 修改 add_tool 逻辑astrbot/core/provider/func_tool_manager.py- 修改 get_func 和 get_full_tool_set 逻辑This is NOT a breaking change. / 这不是一个破坏性变更。
Screenshots or Test Results / 运行截图或测试结果
场景验证:
Checklist / 检查清单
requirements.txt和pyproject.toml文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations inrequirements.txtandpyproject.toml.由 Sourcery 提供的摘要
通过调整工具选择和聚合逻辑,防止当 MCP 外部工具与被禁用的内置工具同名时被意外移除。
错误修复:
增强内容:
Original summary in English
Summary by Sourcery
解决 MCP 外部工具与同名内置工具之间的冲突,确保活跃工具能够被正确保留。
Bug 修复:
增强:
ToolSet.add_tool构建完整的函数工具集,以便在所有工具上统一应用去重和冲突处理规则。测试:
ToolSet.add_tool和FunctionToolManager在各种名称与激活状态冲突场景下的行为,包括 MCP 工具与内置工具之间的交互。Original summary in English
Summary by Sourcery
Resolve conflicts between MCP external tools and built-in tools with the same name to ensure active tools are retained correctly.
Bug Fixes:
Enhancements:
Tests:
Bug 修复:
增强:
ToolSet.add_tool构建完整工具集,从而在所有工具上统一应用去重规则。测试:
ToolSet.add_tool和 FunctionToolManager 在各种名称和激活状态冲突场景下的行为,包括 MCP 工具与内置工具的交互。Original summary in English
Summary by Sourcery
解决 MCP 外部工具与同名内置工具之间的冲突,确保活跃工具能够被正确保留。
Bug 修复:
增强:
ToolSet.add_tool构建完整的函数工具集,以便在所有工具上统一应用去重和冲突处理规则。测试:
ToolSet.add_tool和FunctionToolManager在各种名称与激活状态冲突场景下的行为,包括 MCP 工具与内置工具之间的交互。Original summary in English
Summary by Sourcery
Resolve conflicts between MCP external tools and built-in tools with the same name to ensure active tools are retained correctly.
Bug Fixes:
Enhancements:
Tests: