mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(agent): scope MCP tools to explicit per-agent selection (#117)
This commit is contained in:
parent
e97433cfc5
commit
18df97a8e6
@ -312,13 +312,17 @@ public class AgentBindingService {
|
||||
* tools and skill-expanded tools:
|
||||
* <ul>
|
||||
* <li>{@link #SYSTEM_LEVEL_TOOLS} — agent-wide primitives.</li>
|
||||
* <li>Every currently-bindable MCP tool (any tool with
|
||||
* {@code source="mcp"} and {@code available=true} in the picker).
|
||||
* MCP servers are administrator-level capabilities; once enabled
|
||||
* globally they should not be silently hidden from an agent that
|
||||
* happens to have any other binding. To deny a specific MCP tool
|
||||
* to a specific agent, use the tool-guard deny path applied
|
||||
* upstream in {@code AgentGraphBuilder}.</li>
|
||||
* <li>Every currently-bindable MCP tool ({@code source="mcp"},
|
||||
* {@code available=true} in the picker) — but only when the agent
|
||||
* has not ticked any MCP tool itself. MCP servers are
|
||||
* administrator-level capabilities, so an agent that bound merely
|
||||
* a skill or a built-in tool keeps full MCP access. Once the
|
||||
* operator ticks specific MCP rows, that is read as a deliberate
|
||||
* per-agent scope: only the ticked MCP tools stay and the rest
|
||||
* are not auto-joined, so a role can be limited to a fixed MCP
|
||||
* tool set. To hide a single MCP tool from an agent that ticked
|
||||
* no MCP row, use the tool-guard deny path applied upstream in
|
||||
* {@code AgentGraphBuilder}.</li>
|
||||
* </ul>
|
||||
*/
|
||||
public Set<String> getEffectiveToolNames(Long agentId) {
|
||||
@ -361,16 +365,22 @@ public class AgentBindingService {
|
||||
// (the LLM stops being able to write to LESSONS.md / MEMORY.md).
|
||||
merged.addAll(SYSTEM_LEVEL_TOOLS);
|
||||
|
||||
// Enabled MCP server tools auto-join the allowlist for the same
|
||||
// reason SYSTEM_LEVEL_TOOLS does: MCP servers are an
|
||||
// administrator-enabled capability, not a per-agent opt-in. Without
|
||||
// this union, an agent with any skill or built-in tool bound would
|
||||
// silently lose every MCP tool — users hit this when they bound one
|
||||
// built-in tool, didn't tick the MCP rows, and observed "only
|
||||
// built-in tools work". Operators who need to hide a specific MCP
|
||||
// tool from a specific agent still have the tool-guard deny path
|
||||
// (AgentGraphBuilder applies withDeniedToolsFiltered before this).
|
||||
merged.addAll(getEnabledMcpToolNames());
|
||||
// MCP tools. An agent that bound only a skill or a built-in tool
|
||||
// and ticked no MCP row keeps full access to every enabled MCP
|
||||
// tool: MCP servers are an administrator-enabled capability and
|
||||
// must not silently vanish just because some unrelated binding
|
||||
// exists. But once the operator ticks specific MCP rows, that is a
|
||||
// deliberate per-agent scope — only those MCP tools (already merged
|
||||
// via directTools above) stay, and the rest are not auto-joined, so
|
||||
// a role can be limited to a fixed MCP tool set. To instead hide a
|
||||
// single MCP tool from an agent that ticked no MCP row, use the
|
||||
// tool-guard deny path applied upstream in AgentGraphBuilder.
|
||||
Set<String> enabledMcpTools = getEnabledMcpToolNames();
|
||||
boolean agentScopedMcpExplicitly =
|
||||
directTools != null && !Collections.disjoint(directTools, enabledMcpTools);
|
||||
if (!agentScopedMcpExplicitly) {
|
||||
merged.addAll(enabledMcpTools);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ import vip.mate.MateClawApplication;
|
||||
import vip.mate.agent.binding.model.AgentToolBinding;
|
||||
import vip.mate.agent.binding.service.AgentBindingService;
|
||||
import vip.mate.exception.MateClawException;
|
||||
import vip.mate.tool.model.AvailableToolDTO;
|
||||
import vip.mate.tool.service.AvailableToolService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@ -41,6 +43,9 @@ class AgentBindingServiceTest {
|
||||
@Autowired
|
||||
private AgentBindingService bindingService;
|
||||
|
||||
@Autowired
|
||||
private AvailableToolService availableToolService;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@ -357,6 +362,41 @@ class AgentBindingServiceTest {
|
||||
+ "否则 AgentToolSet.withAllowedToolsOnly 会变成空集禁掉所有工具");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Issue #117: agent 显式勾选某个 MCP 工具后,只有该工具进入 allowlist,其它 MCP 工具不再自动并入")
|
||||
void mcpToolsScopedWhenAgentPicksSpecificMcpTool() {
|
||||
// Enterprise scenario: a role should be limited to a fixed subset
|
||||
// of MCP tools. Two enabled MCP servers exist; the operator ticks
|
||||
// only server A's tool. Server B's tool must NOT leak into the
|
||||
// allowlist just because its server is enabled at the system level.
|
||||
seedMcpServerWithOneTool(8_888_101L, "issue117-server-a", "alpha_probe");
|
||||
seedMcpServerWithOneTool(8_888_102L, "issue117-server-b", "beta_probe");
|
||||
|
||||
String mcpA = mcpToolNameForServer(8_888_101L);
|
||||
String mcpB = mcpToolNameForServer(8_888_102L);
|
||||
assertNotNull(mcpA, "server A 的 MCP 工具应出现在 picker 中");
|
||||
assertNotNull(mcpB, "server B 的 MCP 工具应出现在 picker 中");
|
||||
|
||||
bindingService.setToolBindings(agentId, List.of(mcpA));
|
||||
|
||||
Set<String> effective = bindingService.getEffectiveToolNames(agentId);
|
||||
assertNotNull(effective, "binding 非空时应返回 allowlist(非 null)");
|
||||
assertTrue(effective.contains(mcpA), "显式勾选的 MCP 工具必须在 allowlist 中");
|
||||
assertFalse(effective.contains(mcpB),
|
||||
"未勾选的其它 MCP 工具不得自动并入 —— 这正是 issue #117 要求的按岗位限定 MCP 范围。"
|
||||
+ "实际 allowlist: " + effective);
|
||||
}
|
||||
|
||||
/** Picker name the UI would save for the (only) MCP tool of {@code serverId}. */
|
||||
private String mcpToolNameForServer(long serverId) {
|
||||
return availableToolService.listAvailable().stream()
|
||||
.filter(t -> "mcp".equals(t.getSource()))
|
||||
.filter(t -> t.getProviderId() != null && serverId == t.getProviderId())
|
||||
.map(AvailableToolDTO::getName)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Issue #143: 绑定任意工具后,wiki 知识库工具仍留在 effective allowlist(可读写知识库)")
|
||||
void wikiToolsSurviveSkillBindingAllowlist() {
|
||||
|
||||
@ -395,18 +395,22 @@
|
||||
<template v-for="group in filteredAvailableToolGroups" :key="group.groupId">
|
||||
<div class="binding-group-header">
|
||||
<span>{{ group.label }}</span>
|
||||
<!-- MCP groups: ticking is now record-only — enabled MCP
|
||||
tools auto-join the agent's effective allowlist
|
||||
(server is admin-enabled at the system level). Tell
|
||||
the user that here so they don't think unchecked
|
||||
MCP rows are disabled. To deny a specific MCP tool,
|
||||
users still have Security → Tool Guard. -->
|
||||
<!-- MCP group badge reflects the agent's MCP scope:
|
||||
with no MCP tool ticked, every enabled MCP tool is
|
||||
available by default; once any MCP tool is ticked,
|
||||
the agent is restricted to the ticked set. To deny
|
||||
an MCP tool when none are ticked, users still have
|
||||
Security → Tool Guard. -->
|
||||
<span
|
||||
v-if="group.groupId && group.groupId.startsWith('mcp:')"
|
||||
class="binding-group-note"
|
||||
:title="t('agents.binding.mcpAutoIncludedTooltip')"
|
||||
:title="anyMcpToolSelected
|
||||
? t('agents.binding.mcpScopedTooltip')
|
||||
: t('agents.binding.mcpAutoIncludedTooltip')"
|
||||
>
|
||||
{{ t('agents.binding.mcpAutoIncludedBadge') }}
|
||||
{{ anyMcpToolSelected
|
||||
? t('agents.binding.mcpScopedBadge')
|
||||
: t('agents.binding.mcpAutoIncludedBadge') }}
|
||||
</span>
|
||||
</div>
|
||||
<label
|
||||
@ -638,6 +642,16 @@ const availableToolGroups = computed(() => {
|
||||
|
||||
const filteredAvailableToolGroups = computed(() => filterAgentToolGroups(availableToolGroups.value, toolBindingSearch.value))
|
||||
|
||||
/**
|
||||
* True when at least one ticked tool is an MCP tool. The backend reads
|
||||
* this as a deliberate per-agent MCP scope: only ticked MCP tools stay
|
||||
* in the effective allowlist. When false, every enabled MCP tool is
|
||||
* auto-included instead. Drives the MCP group badge in the picker.
|
||||
*/
|
||||
const anyMcpToolSelected = computed(() =>
|
||||
availableTools.value.some((tool) => tool.source === 'mcp' && selectedToolNames.value.includes(tool.name)),
|
||||
)
|
||||
|
||||
/**
|
||||
* Manual checkbox handler — replaces v-model on the picker row so that
|
||||
* two rows sharing a tool name (collision/duplicate twins) don't drag
|
||||
|
||||
Loading…
Reference in New Issue
Block a user