Skip to content

Comments

feat: add SubAgent-scoped MCP tool isolation#5246

Closed
whatevertogo wants to merge 2 commits intoAstrBotDevs:masterfrom
whatevertogo:feat/mcp-subagent-scope
Closed

feat: add SubAgent-scoped MCP tool isolation#5246
whatevertogo wants to merge 2 commits intoAstrBotDevs:masterfrom
whatevertogo:feat/mcp-subagent-scope

Conversation

@whatevertogo
Copy link
Contributor

@whatevertogo whatevertogo commented Feb 19, 2026

Summary / 概述

Fixes #5237

Adds SubAgent-level MCP tool isolation to address token consumption explosion and maintain clean SubAgent orchestration architecture.

Problem / 问题

Previously, _plugin_tool_fix in astr_main_agent.py unconditionally injected all MCP tools to the main agent, causing:

  • Token consumption explosion (tools from ~16 to 100+)
  • Breaking SubAgent orchestration design (main agent should only hold handoff tools)

Solution / 解决方案

Implemented two-phase approach:

Phase 1: Global Opt-out Switch

  • Added inject_global_mcp_tools config (default True for backward compatibility)
  • Located in subagent_orchestrator config block

Phase 2: Per-Server Agent Scope

  • Added agent_scope to MCP server configuration
  • Supports filtering MCP tools by SubAgent names
  • UI integration in Dashboard MCP servers section

Files Changed / 修改文件

  • astrbot/core/agent/mcp_client.py - Added scope filtering support
  • astrbot/core/agent/mcp_scope.py - New scope management module
  • astrbot/core/astr_main_agent.py - Updated tool injection logic
  • astrbot/core/config/default.py - Added config options
  • astrbot/core/provider/func_tool_manager.py - Scope integration
  • astrbot/dashboard/routes/tools.py - API endpoints for scope
  • dashboard/src/components/extension/McpServersSection.vue - UI for agent_scope
  • tests/test_plugin_tool_fix.py - Unit tests for tool fix
  • tests/test_mcp_scope.py - Unit tests for scope logic

Usage / 使用方式

Disable global MCP injection:

{
  "subagent_orchestrator": {
    "inject_global_mcp_tools": false
  }
}

Configure per-server scope:

{
  "mcpServers": {
    "search_server": {
      "agent_scope": ["search_agent"]
    }
  }
}

Test Results / 测试结果

All 17 tests passing:

  • 9 tests for _plugin_tool_fix logic
  • 8 tests for MCP scope filtering

  • This is NOT a breaking change / 这不是一个破坏性变更

Summary by Sourcery

引入以 SubAgent 为作用域的 MCP 工具可见性和配置机制,防止全局 MCP 工具注入并减少不必要的工具暴露。

New Features:

  • 为每个服务器新增 MCP agent_scope 配置,用于控制哪些代理(agents)可以访问各 MCP 服务器的工具。
  • 在控制面板(dashboard)UI 中暴露 MCP 服务器的作用域信息和快速选择控件,包括按已启用的子代理过滤以及显示作用域概览。

Enhancements:

  • 在子代理编排器(subagent orchestrator)上,通过可配置的 inject_global_mcp_tools 标志位控制是否启用全局 MCP 工具自动注入。
  • 确保工具分配和交接流程只包含在目标代理可见范围内(基于服务器作用域)的 MCP 工具。
  • 在 MCP 客户端、函数工具管理器以及工具列表 API 中传播 MCP 服务器作用域元数据。
  • 引入共享的 MCP 作用域工具方法,用于在各组件间对作用域字段进行规范化、校验和剥离。

Tests:

  • 为插件工具过滤逻辑添加单元测试,包括 MCP 注入开关以及基于作用域的可见性规则。
  • 为 MCP 作用域规范化、配置提取和可见性辅助方法添加单元测试。
Original summary in English

Summary by Sourcery

Introduce SubAgent-scoped MCP tool visibility and configuration to prevent global MCP tool injection and reduce unnecessary tool exposure.

New Features:

  • Add per-server MCP agent_scope configuration to control which agents can access each MCP server's tools.
  • Expose MCP server scope information and quick-select controls in the dashboard UI, including filtering by enabled subagents and scope summaries.

Enhancements:

  • Gate global MCP tool auto-injection behind a configurable inject_global_mcp_tools flag for the subagent orchestrator.
  • Ensure tool assignment and handoff flows only include MCP tools visible to the target agent based on server scopes.
  • Propagate MCP server scope metadata through MCP client, function tool manager, and tool listing APIs.
  • Introduce shared MCP scope utilities for normalizing, validating, and stripping scope fields across components.

Tests:

  • Add unit tests for plugin tool filtering logic, including MCP injection toggle and scope-based visibility rules.
  • Add unit tests for MCP scope normalization, config extraction, and visibility helpers.

Copilot AI review requested due to automatic review settings February 19, 2026 19:25
@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Feb 19, 2026
@dosubot
Copy link

dosubot bot commented Feb 19, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

@dosubot dosubot bot added area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. area:webui The bug / feature is about webui(dashboard) of astrbot. labels Feb 19, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey——我发现了两个问题,并给出了一些整体性的反馈:

  • 目前作用域(scope)的规范化 / 校验逻辑分别在前端(normalizeAgentScope、正则校验)和后端(normalize_mcp_scope_valueMCP_SCOPE_WILDCARDS)各自实现,两者行为存在轻微差异(例如大小写处理、通配符语义);建议考虑统一实现或至少对齐规则,以避免 UI 接受的内容与服务端实际应用的规则之间出现细微不一致。
  • _plugin_tool_fix 和 handoff 的执行路径中,你已经通过 is_mcp_tool_visible_to_agent 对 MCP 工具做了限制,但其他为子 Agent 构建工具集的可能入口看起来仍然依赖全局的 llm_tools.func_list;建议检查所有子 Agent 工具集构建路径是否都显式传入正确的 agent_name,并统一使用同一个可见性辅助函数,以避免工具在全局范围内被意外暴露。
给 AI Agent 的提示词
Please address the comments from this code review:

## Overall Comments
- The scope normalization/validation logic is now implemented separately in the frontend (`normalizeAgentScope`, regex validation) and backend (`normalize_mcp_scope_value`, `MCP_SCOPE_WILDCARDS`), which slightly diverge in behavior (e.g., case handling, wildcard semantics); consider consolidating or at least aligning the rules to avoid subtle inconsistencies between what the UI accepts and what the server applies.
- In `_plugin_tool_fix` and the handoff execution path you now gate MCP tools via `is_mcp_tool_visible_to_agent`, but other possible entry points that build toolsets for subagents still seem to rely on the global `llm_tools.func_list`; it would be good to double-check that all subagent toolset construction paths explicitly pass the appropriate `agent_name` and use the same visibility helper to avoid accidental global exposure.

## Individual Comments

### Comment 1
<location> `tests/test_mcp_scope.py:36-38` </location>
<code_context>
+    assert get_mcp_scopes_from_config(cfg) == ("agent_x",)
+
+
+def test_get_mcp_scopes_from_agent_scope_string():
+    cfg = {"agent_scope": "main"}
+    assert get_mcp_scopes_from_config(cfg) == ("main",)
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing tests for `get_mcp_scopes_from_config` when config is not a mapping or does not contain any scope fields.

Since the function returns `None` for non-mapping configs and when both `scopes` and `agent_scope` are absent, please add tests for `get_mcp_scopes_from_config(None)`, `get_mcp_scopes_from_config([])`, and a config without those keys (e.g. `{"command": "python"}`) to cover these branches and document the default behavior.
</issue_to_address>

### Comment 2
<location> `astrbot/core/astr_main_agent.py:751` </location>
<code_context>
     req.contexts = sanitized_contexts


-def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
+def _plugin_tool_fix(
+    event: AstrMessageEvent,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring `_plugin_tool_fix` into smaller helpers that separately handle MCP visibility filtering, plugin-based filtering, and MCP injection to simplify the control flow and remove duplication.

You can reduce `_plugin_tool_fix` complexity and avoid duplicated MCP visibility logic by extracting small helpers and composing them linearly.

### 1. Centralize MCP visibility filtering

Right now MCP visibility is checked in two separate passes. You can extract this into a single helper and call it once at the top:

```python
def _filter_visible_mcp_tools(tools: ToolSet | None, agent_name: str) -> ToolSet | None:
    if not tools:
        return tools
    filtered = ToolSet()
    for tool in tools.tools:
        if isinstance(tool, MCPTool) and not is_mcp_tool_visible_to_agent(tool, agent_name):
            continue
        filtered.add_tool(tool)
    return filtered
```

Then at the start of `_plugin_tool_fix`:

```python
req.func_tool = _filter_visible_mcp_tools(req.func_tool, agent_name)
```

This removes the need to repeat `is_mcp_tool_visible_to_agent` in later branches.

### 2. Split plugin filtering from MCP concerns

The plugin filtering logic can be made MCP-agnostic by keeping MCP tools out of its responsibility entirely:

```python
def _filter_tools_by_plugins(tools: ToolSet, plugins_name: set[str]) -> ToolSet:
    filtered = ToolSet()
    for tool in tools.tools:
        if isinstance(tool, MCPTool):
            # MCP already filtered by visibility; always keep here
            filtered.add_tool(tool)
            continue

        mp = tool.handler_module_path
        if not mp:
            continue

        plugin = star_map.get(mp)
        if not plugin:
            continue

        if plugin.name in plugins_name or plugin.reserved:
            filtered.add_tool(tool)

    return filtered
```

Then in `_plugin_tool_fix`:

```python
if event.plugins_name is not None and req.func_tool:
    req.func_tool = _filter_tools_by_plugins(req.func_tool, event.plugins_name)
```

Note MCP visibility has already been applied once via `_filter_visible_mcp_tools`.

### 3. Isolate MCP injection and always return a new ToolSet

The injection branch can be made more consistent by not mutating `req.func_tool` in place and by always working on a new `ToolSet`:

```python
def _inject_global_mcp_tools(
    base_tools: ToolSet | None,
    agent_name: str,
) -> ToolSet:
    result = ToolSet()
    if base_tools:
        for tool in base_tools.tools:
            result.add_tool(tool)

    for tool in llm_tools.func_list:
        if isinstance(tool, MCPTool) and is_mcp_tool_visible_to_agent(tool, agent_name):
            result.add_tool(tool)

    return result
```

Then in `_plugin_tool_fix`:

```python
elif inject_mcp:
    req.func_tool = _inject_global_mcp_tools(req.func_tool, agent_name)
```

### 4. Simplified `_plugin_tool_fix` orchestration

Putting it together, `_plugin_tool_fix` becomes a linear orchestrator of the three concerns:

```python
def _plugin_tool_fix(
    event: AstrMessageEvent,
    req: ProviderRequest,
    inject_mcp: bool = True,
    *,
    agent_name: str = "main",
) -> None:
    # 1. Agent-scoped MCP visibility
    req.func_tool = _filter_visible_mcp_tools(req.func_tool, agent_name)

    # 2. Plugin filtering (MCP passes through)
    if event.plugins_name is not None and req.func_tool:
        req.func_tool = _filter_tools_by_plugins(req.func_tool, event.plugins_name)
    # 3. Optional global MCP injection
    elif inject_mcp:
        req.func_tool = _inject_global_mcp_tools(req.func_tool, agent_name)
```

This preserves all current behavior (including `inject_mcp` and agent scoping) while reducing branching, duplication, and the coupling of concerns inside a single function.
</issue_to_address>

Sourcery 对开源项目是免费的——如果你觉得这次 Review 有帮助,欢迎分享 ✨
请帮我变得更有用!可以对每条评论点 👍 或 👎,我会根据反馈改进后续的 Review。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • The scope normalization/validation logic is now implemented separately in the frontend (normalizeAgentScope, regex validation) and backend (normalize_mcp_scope_value, MCP_SCOPE_WILDCARDS), which slightly diverge in behavior (e.g., case handling, wildcard semantics); consider consolidating or at least aligning the rules to avoid subtle inconsistencies between what the UI accepts and what the server applies.
  • In _plugin_tool_fix and the handoff execution path you now gate MCP tools via is_mcp_tool_visible_to_agent, but other possible entry points that build toolsets for subagents still seem to rely on the global llm_tools.func_list; it would be good to double-check that all subagent toolset construction paths explicitly pass the appropriate agent_name and use the same visibility helper to avoid accidental global exposure.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The scope normalization/validation logic is now implemented separately in the frontend (`normalizeAgentScope`, regex validation) and backend (`normalize_mcp_scope_value`, `MCP_SCOPE_WILDCARDS`), which slightly diverge in behavior (e.g., case handling, wildcard semantics); consider consolidating or at least aligning the rules to avoid subtle inconsistencies between what the UI accepts and what the server applies.
- In `_plugin_tool_fix` and the handoff execution path you now gate MCP tools via `is_mcp_tool_visible_to_agent`, but other possible entry points that build toolsets for subagents still seem to rely on the global `llm_tools.func_list`; it would be good to double-check that all subagent toolset construction paths explicitly pass the appropriate `agent_name` and use the same visibility helper to avoid accidental global exposure.

## Individual Comments

### Comment 1
<location> `tests/test_mcp_scope.py:36-38` </location>
<code_context>
+    assert get_mcp_scopes_from_config(cfg) == ("agent_x",)
+
+
+def test_get_mcp_scopes_from_agent_scope_string():
+    cfg = {"agent_scope": "main"}
+    assert get_mcp_scopes_from_config(cfg) == ("main",)
+
+
</code_context>

<issue_to_address>
**suggestion (testing):** Missing tests for `get_mcp_scopes_from_config` when config is not a mapping or does not contain any scope fields.

Since the function returns `None` for non-mapping configs and when both `scopes` and `agent_scope` are absent, please add tests for `get_mcp_scopes_from_config(None)`, `get_mcp_scopes_from_config([])`, and a config without those keys (e.g. `{"command": "python"}`) to cover these branches and document the default behavior.
</issue_to_address>

### Comment 2
<location> `astrbot/core/astr_main_agent.py:751` </location>
<code_context>
     req.contexts = sanitized_contexts


-def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
+def _plugin_tool_fix(
+    event: AstrMessageEvent,
</code_context>

<issue_to_address>
**issue (complexity):** Consider refactoring `_plugin_tool_fix` into smaller helpers that separately handle MCP visibility filtering, plugin-based filtering, and MCP injection to simplify the control flow and remove duplication.

You can reduce `_plugin_tool_fix` complexity and avoid duplicated MCP visibility logic by extracting small helpers and composing them linearly.

### 1. Centralize MCP visibility filtering

Right now MCP visibility is checked in two separate passes. You can extract this into a single helper and call it once at the top:

```python
def _filter_visible_mcp_tools(tools: ToolSet | None, agent_name: str) -> ToolSet | None:
    if not tools:
        return tools
    filtered = ToolSet()
    for tool in tools.tools:
        if isinstance(tool, MCPTool) and not is_mcp_tool_visible_to_agent(tool, agent_name):
            continue
        filtered.add_tool(tool)
    return filtered
```

Then at the start of `_plugin_tool_fix`:

```python
req.func_tool = _filter_visible_mcp_tools(req.func_tool, agent_name)
```

This removes the need to repeat `is_mcp_tool_visible_to_agent` in later branches.

### 2. Split plugin filtering from MCP concerns

The plugin filtering logic can be made MCP-agnostic by keeping MCP tools out of its responsibility entirely:

```python
def _filter_tools_by_plugins(tools: ToolSet, plugins_name: set[str]) -> ToolSet:
    filtered = ToolSet()
    for tool in tools.tools:
        if isinstance(tool, MCPTool):
            # MCP already filtered by visibility; always keep here
            filtered.add_tool(tool)
            continue

        mp = tool.handler_module_path
        if not mp:
            continue

        plugin = star_map.get(mp)
        if not plugin:
            continue

        if plugin.name in plugins_name or plugin.reserved:
            filtered.add_tool(tool)

    return filtered
```

Then in `_plugin_tool_fix`:

```python
if event.plugins_name is not None and req.func_tool:
    req.func_tool = _filter_tools_by_plugins(req.func_tool, event.plugins_name)
```

Note MCP visibility has already been applied once via `_filter_visible_mcp_tools`.

### 3. Isolate MCP injection and always return a new ToolSet

The injection branch can be made more consistent by not mutating `req.func_tool` in place and by always working on a new `ToolSet`:

```python
def _inject_global_mcp_tools(
    base_tools: ToolSet | None,
    agent_name: str,
) -> ToolSet:
    result = ToolSet()
    if base_tools:
        for tool in base_tools.tools:
            result.add_tool(tool)

    for tool in llm_tools.func_list:
        if isinstance(tool, MCPTool) and is_mcp_tool_visible_to_agent(tool, agent_name):
            result.add_tool(tool)

    return result
```

Then in `_plugin_tool_fix`:

```python
elif inject_mcp:
    req.func_tool = _inject_global_mcp_tools(req.func_tool, agent_name)
```

### 4. Simplified `_plugin_tool_fix` orchestration

Putting it together, `_plugin_tool_fix` becomes a linear orchestrator of the three concerns:

```python
def _plugin_tool_fix(
    event: AstrMessageEvent,
    req: ProviderRequest,
    inject_mcp: bool = True,
    *,
    agent_name: str = "main",
) -> None:
    # 1. Agent-scoped MCP visibility
    req.func_tool = _filter_visible_mcp_tools(req.func_tool, agent_name)

    # 2. Plugin filtering (MCP passes through)
    if event.plugins_name is not None and req.func_tool:
        req.func_tool = _filter_tools_by_plugins(req.func_tool, event.plugins_name)
    # 3. Optional global MCP injection
    elif inject_mcp:
        req.func_tool = _inject_global_mcp_tools(req.func_tool, agent_name)
```

This preserves all current behavior (including `inject_mcp` and agent scoping) while reducing branching, duplication, and the coupling of concerns inside a single function.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +36 to +38
def test_get_mcp_scopes_from_agent_scope_string():
cfg = {"agent_scope": "main"}
assert get_mcp_scopes_from_config(cfg) == ("main",)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): 当前缺少针对 get_mcp_scopes_from_config 的测试,用于覆盖配置不是映射类型,或不包含任何 scope 字段的情况。

由于该函数在配置不是映射类型,以及同时缺少 scopesagent_scope 时会返回 None,请补充以下测试用例:get_mcp_scopes_from_config(None)get_mcp_scopes_from_config([]),以及一个不包含这些键的配置(例如 {"command": "python"}),以覆盖这些分支并记录默认行为。

Original comment in English

suggestion (testing): Missing tests for get_mcp_scopes_from_config when config is not a mapping or does not contain any scope fields.

Since the function returns None for non-mapping configs and when both scopes and agent_scope are absent, please add tests for get_mcp_scopes_from_config(None), get_mcp_scopes_from_config([]), and a config without those keys (e.g. {"command": "python"}) to cover these branches and document the default behavior.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds SubAgent-level MCP tool isolation to address token consumption explosion in multi-agent orchestration scenarios. The implementation introduces a two-phase approach: a global opt-out switch (inject_global_mcp_tools) and per-server agent scope configuration (agent_scope).

Changes:

  • Added MCP tool scope filtering by agent name with case-insensitive matching
  • Added global configuration inject_global_mcp_tools (default True) to control automatic MCP tool injection
  • Added per-server agent_scope configuration to restrict MCP tools to specific agents
  • Updated UI to support agent scope configuration with quick-select chips and validation

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
astrbot/core/agent/mcp_scope.py New module providing scope normalization and visibility checking functions
astrbot/core/agent/mcp_client.py Updated MCPTool to store and strip scope fields
astrbot/core/provider/func_tool_manager.py Integrated scope extraction during MCP client initialization
astrbot/core/astr_main_agent.py Updated _plugin_tool_fix to support inject_mcp flag and scope filtering
astrbot/core/astr_agent_tool_exec.py Added scope filtering in handoff tool execution
astrbot/core/config/default.py Added inject_global_mcp_tools config option
astrbot/dashboard/routes/tools.py Exposed mcp_server_scopes in tool list API
dashboard/src/components/extension/McpServersSection.vue Added agent scope UI with validation and quick-select
dashboard/src/i18n/locales/*/features/tool-use.json Added i18n strings for scope UI
tests/test_plugin_tool_fix.py Added unit tests for plugin tool fix logic
tests/test_mcp_scope.py Added unit tests for scope normalization and filtering

@whatevertogo
Copy link
Contributor Author

whatevertogo commented Feb 19, 2026

为啥不是agent拥有工具而是工具指定可用agent啊,困惑喵

@Soulter Soulter requested a review from advent259141 February 20, 2026 05:44
@Soulter
Copy link
Member

Soulter commented Feb 20, 2026

为啥不是agent拥有工具而是工具指定可用agent啊,困惑喵

没太理解你的意思?现在就是agent拥有工具

理解了,其实更合理的做法应该是在 人格页 页配置人格(其实就是agent)可用的mcp、tools、skills

@whatevertogo
Copy link
Contributor Author

目前是:
在 MCP 页面里指定哪些 subagent 可以使用这个 MCP
也就是「工具 → 指定可用 agent」。
我担心的是,当 subagent 变多以后,这种配置方式会比较难维护。比如:
新增一个 subagent,需要去多个 MCP 页面配置
想知道某个 subagent 能用哪些工具,要去各个 MCP 页面翻
工具和 agent 的关系是分散在不同地方的
我更倾向于:
在 subagent 里声明它使用哪些 MCP

@whatevertogo
Copy link
Contributor Author

但是我不太了解项目第一次做贡献,所以我担心是否是因为历史遗留问题要这么写

@whatevertogo
Copy link
Contributor Author

所以我是按照issue的问题方式写的

@whatevertogo
Copy link
Contributor Author

我能改吗o( ̄ヘ ̄o#)

@Soulter
Copy link
Member

Soulter commented Feb 20, 2026

目前是: 在 MCP 页面里指定哪些 subagent 可以使用这个 MCP 也就是「工具 → 指定可用 agent」。 我担心的是,当 subagent 变多以后,这种配置方式会比较难维护。比如: 新增一个 subagent,需要去多个 MCP 页面配置 想知道某个 subagent 能用哪些工具,要去各个 MCP 页面翻 工具和 agent 的关系是分散在不同地方的 我更倾向于: 在 subagent 里声明它使用哪些 MCP

现在 AstrBot 的方式是,MCP Server 单独添加和管理,人格(Agent)可以选择添加的 MCP Server,我觉得 MCP Server 放在人格(Agent)管理比较好:

image

我刚刚优化了一下这块,可以显示的清晰一些 ⬆️

并且修复了 #5237

不过还是感谢你的 PR ~

@whatevertogo
Copy link
Contributor Author

修好了我就关咯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. area:webui The bug / feature is about webui(dashboard) of astrbot. size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]支持 SubAgent 级别的 MCP 工具隔离(并提供关闭全局注入的选项)

2 participants