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}.
+ *
*/
public Set getEffectiveToolNames(Long agentId) {
Set boundSkillIds = getBoundSkillIds(agentId);
@@ -342,9 +355,41 @@ 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());
+
return merged;
}
+ /**
+ * Names of every currently-bindable MCP tool, sourced from the same
+ * picker that the agent edit screen reads. Failures (picker outage,
+ * cache parse error) yield an empty set so the caller's allowlist is
+ * strictly narrower, never wider, than the picker — never throws.
+ */
+ private Set getEnabledMcpToolNames() {
+ try {
+ return availableToolService.listAvailable().stream()
+ .filter(t -> "mcp".equals(t.getSource()))
+ .filter(AvailableToolDTO::isAvailable)
+ .map(AvailableToolDTO::getName)
+ .filter(n -> n != null && !n.isBlank())
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ } catch (Exception e) {
+ log.warn("AvailableToolService unavailable while computing effective tool allowlist; "
+ + "MCP tools will be excluded for this resolve cycle: {}", e.getMessage());
+ return Collections.emptySet();
+ }
+ }
+
/**
* RFC-090 §11 — tools that exist outside the skill scope and must
* survive any agent-level skill binding restriction.
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 2a47afb5..d5136b1f 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
@@ -286,6 +286,54 @@ class AgentBindingServiceTest {
"validation 必须在 delete 旧绑定之前完成,否则会留下空绑定状态");
}
+ /**
+ * Seed a connected MCP server with one cached tool so
+ * {@link vip.mate.tool.service.AvailableToolService#listAvailable()}
+ * returns at least one bindable MCP row.
+ */
+ private void seedMcpServerWithOneTool(long id, String serverName, String rawToolName) {
+ String toolsCacheJson = "[{\"name\":\"" + rawToolName + "\",\"description\":\"fixture\"}]";
+ jdbcTemplate.update(
+ "MERGE INTO mate_mcp_server (id, name, description, transport, enabled, " +
+ "connect_timeout_seconds, read_timeout_seconds, last_status, tool_count, " +
+ "builtin, tools_cache_json, create_time, update_time, deleted) " +
+ "KEY(id) VALUES (?, ?, '', 'stdio', TRUE, 30, 30, 'connected', 1, FALSE, ?, " +
+ "CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
+ id, serverName, toolsCacheJson);
+ }
+
+ @Test
+ @DisplayName("Issue #108: 绑定任意 builtin tool 后,enabled MCP 工具仍自动出现在 effective allowlist")
+ void mcpToolsAutoIncludedWhenAnyBindingExists() {
+ // Reproduce the user-reported scenario: agent has one built-in tool
+ // bound (e.g. by template), no MCP tools ticked. Before the fix this
+ // returned a whitelist that excluded every MCP tool; after the fix
+ // MCP tools auto-join the allowlist.
+ seedBuiltinTool("builtin_probe");
+ seedMcpServerWithOneTool(8_888_001L, "issue108-server", "search_web");
+ bindingService.setToolBindings(agentId, List.of("builtin_probe"));
+
+ Set effective = bindingService.getEffectiveToolNames(agentId);
+ assertNotNull(effective, "binding 非空时应返回 allowlist(非 null)");
+ assertTrue(effective.contains("builtin_probe"), "用户显式勾选的工具必须在 allowlist 中");
+ boolean hasMcpEntry = effective.stream().anyMatch(n -> n != null && n.startsWith("mcp_"));
+ assertTrue(hasMcpEntry,
+ "enabled MCP server 的工具必须自动并入 allowlist;缺失会让用户在 chat 时只见到 built-in 工具,"
+ + "即 issue #108 描述的现象。实际 allowlist: " + effective);
+ }
+
+ @Test
+ @DisplayName("Issue #108: agent 完全没绑定时 effective allowlist 返回 null(不要意外改成 strict)")
+ void noBindingsStillReturnsNull() {
+ // The auto-union must not flip the three-state contract: an agent
+ // with zero bindings still means "no agent-level restriction".
+ seedMcpServerWithOneTool(8_888_002L, "issue108-no-binding-server", "search_web");
+
+ Set effective = bindingService.getEffectiveToolNames(agentId);
+ assertNull(effective, "完全没有 skill / tool 绑定时必须返回 null(= 不过滤),"
+ + "否则 AgentToolSet.withAllowedToolsOnly 会变成空集禁掉所有工具");
+ }
+
@Test
@DisplayName("unbindTool 后 DB 里真的没行(物理 delete,不是软删留 deleted=1)")
void unbindPhysicallyRemovesRow() {
diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts
index 4a20e2ed..46082e1a 100644
--- a/mateclaw-ui/src/i18n/locales/en-US.ts
+++ b/mateclaw-ui/src/i18n/locales/en-US.ts
@@ -1131,7 +1131,9 @@ export default {
searchTools: 'Search tool name, description, source, or group',
advancedToolsTitle: 'Advanced: Hand-picked atomic tools',
advancedToolsHint: 'Skill bindings already auto-expand allowed tools. Use this only for built-in micro-utilities not packaged as a skill (e.g. datetime, delegate_agent).',
- toolUnionHint: 'Tools selected here are unioned with tools from any bound skills. To restrict an employee to a subset of an MCP server\'s tools, leave the MCP skill unchecked and select only the tools you want here.',
+ toolUnionHint: 'Tools selected here are unioned with tools from any bound skills. MCP servers enabled at the system level are available to this agent by default — ticking MCP rows here only records an explicit binding, leaving them unticked does not block the agent. To deny a specific MCP tool, configure it under Security → Tool Guard.',
+ mcpAutoIncludedBadge: 'Auto-available',
+ mcpAutoIncludedTooltip: 'MCP servers enabled at the system level are available to every agent by default. Ticking only records an explicit binding; leaving rows unticked does not restrict this agent. To deny a specific MCP tool, configure it under Security → Tool Guard.',
toolStaleBadge: 'Cached (offline)',
toolStaleTooltip: 'The owning MCP server is not currently connected. This tool entry comes from the last successful snapshot; the LLM will not invoke it until the connection is restored.',
toolUnavailableBadge: 'Unavailable',
diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts
index b641c788..80266b66 100644
--- a/mateclaw-ui/src/i18n/locales/zh-CN.ts
+++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts
@@ -1029,7 +1029,9 @@ export default {
searchTools: '搜索工具名称、描述、来源或分组',
advancedToolsTitle: '高级:手选原子工具',
advancedToolsHint: 'Skill 绑定已自动展开 allowed-tools。此处仅用于未打包成 Skill 的内置微工具(如 datetime、delegate_agent)。',
- toolUnionHint: '直选工具会与已绑定技能提供的工具合并生效。如果只想让员工使用某 MCP 服务的部分工具,请不要勾选对应的 MCP 技能,只在这里勾选具体工具。',
+ toolUnionHint: '直选工具会与已绑定技能提供的工具合并生效。MCP 服务在系统层启用后默认对此 Agent 可用,勾选 MCP 行只是显式记录绑定,不勾也能调用。需要禁用某个 MCP 工具请到「安全 → 工具防护」配置黑名单。',
+ mcpAutoIncludedBadge: '自动可用',
+ mcpAutoIncludedTooltip: 'MCP 服务在系统中启用后,其工具对所有 Agent 默认可用。勾选只用于记录显式绑定,不勾不会限制此 Agent 调用。如需禁用,请在「安全 → 工具防护」配置。',
toolStaleBadge: '离线缓存',
toolStaleTooltip: '该 MCP 服务当前未连接,工具列表来自上次连接时的快照;连接恢复前 LLM 不会调用这个工具。',
toolUnavailableBadge: '不可用',
diff --git a/mateclaw-ui/src/views/Agents.vue b/mateclaw-ui/src/views/Agents.vue
index 92320515..6c24c0bf 100644
--- a/mateclaw-ui/src/views/Agents.vue
+++ b/mateclaw-ui/src/views/Agents.vue
@@ -382,7 +382,22 @@