diff --git a/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java b/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java index 35a66135..c74d5264 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/binding/service/AgentBindingService.java @@ -312,13 +312,17 @@ public class AgentBindingService { * tools and skill-expanded tools: * */ public Set 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 enabledMcpTools = getEnabledMcpToolNames(); + boolean agentScopedMcpExplicitly = + directTools != null && !Collections.disjoint(directTools, enabledMcpTools); + if (!agentScopedMcpExplicitly) { + merged.addAll(enabledMcpTools); + } return merged; } diff --git a/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java b/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java index 70d8f7ad..73f1d5aa 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/binding/AgentBindingServiceTest.java @@ -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 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() { diff --git a/mateclaw-ui/src/views/Agents.vue b/mateclaw-ui/src/views/Agents.vue index 467ff6e6..a705a639 100644 --- a/mateclaw-ui/src/views/Agents.vue +++ b/mateclaw-ui/src/views/Agents.vue @@ -395,18 +395,22 @@