[codex] add MCP tools visibility management (#1170)

* feat(mcp): add tools visibility management

## Features
- Tools visibility modal with 3 modes: All, Include, Exclude
- 'Manage Tools' button on McpServerCard (enabled only when connected)
- 'Fetch Tools List' button to refresh available tools (raw mode)
- Responsive design for mobile (480px), tablet (768px), desktop (1280px)
- i18n translations for 9 languages (zh/en/zh-TW/ja/ko/de/es/fr/pt)

## Technical Details
- Add raw parameter to fetchMcpTools API for unfiltered tools
- Pass raw parameter through controller → bridgeMcpAction → client
- Backend _mcp_tools_list supports raw_mode to skip include/exclude filter
- 28 MCP unit tests pass (23 controller + 5 bridge action)

## Files Changed
- McpManagerView.vue: Tools visibility modal with mode selector
- McpServerCard.vue: Add manage tools button
- mcp.ts (client): Add raw parameter to fetchMcpTools
- mcp.ts (controller): Pass raw parameter to bridge
- mcp.ts (services): Pass raw parameter to client.mcpTools
- client.ts: Add raw parameter to mcpTools
- hermes_bridge.py: Support raw_mode in _mcp_tools_list
- 9 locale files: Add 14 translation keys each
- mcp-controller.test.ts: Add 3 new test cases
- bridge-mcp-action.test.ts: New test file for parameter passing

* Delete projects directory

chore: remove accidentally committed projects/ directory

* fix MCP tools visibility edge cases

* remove MCP docs screenshots

---------

Co-authored-by: Crafter-feng <succeed_happu@163.com>
Co-authored-by: Crafter-feng <37255449+Crafter-feng@users.noreply.github.com>
This commit is contained in:
ekko
2026-05-31 09:00:38 +08:00
committed by GitHub
parent 9df79c33be
commit c998a53566
29 changed files with 703 additions and 189 deletions
@@ -97,7 +97,10 @@ export async function testServer(ctx: Context) {
export async function listTools(ctx: Context) {
try {
const server = ctx.query.server as string | undefined
const payload = server ? { server } : {}
const raw = ctx.query.raw === '1' || ctx.query.raw === 'true'
const payload: Record<string, any> = {}
if (server) payload.server = server
if (raw) payload.raw = true
ctx.body = await bridgeMcpAction('mcp_tools_list', payload, getProfile(ctx))
} catch (err: any) {
ctx.status = 503
@@ -609,8 +609,8 @@ export class AgentBridgeClient {
return this.request({ action: 'mcp_server_test', name, ...(profile ? { profile } : {}) }, { timeoutMs: 180_000 })
}
mcpTools(server?: string, profile?: string): Promise<McpActionResponse> {
return this.request({ action: 'mcp_tools_list', ...(server ? { server } : {}), ...(profile ? { profile } : {}) })
mcpTools(server?: string, profile?: string, raw?: boolean): Promise<McpActionResponse> {
return this.request({ action: 'mcp_tools_list', ...(server ? { server } : {}), ...(profile ? { profile } : {}), ...(raw ? { raw } : {}) })
}
mcpReload(server?: string, profile?: string): Promise<McpActionResponse> {
@@ -2427,16 +2427,18 @@ class BridgeServer:
cfg = getattr(task, "_config", {})
# Build filtered tool_details (name + description) for card display
srv_cfg = mcp_configs.get(name, {}) if isinstance(mcp_configs.get(name), dict) else {}
tools_filter = srv_cfg.get("tools") or {}
tools_filter = srv_cfg.get("tools") if isinstance(srv_cfg.get("tools"), dict) else {}
has_include_filter = "include" in tools_filter
has_exclude_filter = "exclude" in tools_filter
include_set = set(tools_filter.get("include") or [])
exclude_set = set(tools_filter.get("exclude") or [])
tool_details = []
try:
for mcp_tool in getattr(task, "_tools", []):
tname = getattr(mcp_tool, "name", "?")
if include_set and tname not in include_set:
if has_include_filter and tname not in include_set:
continue
if exclude_set and tname in exclude_set:
if has_exclude_filter and tname in exclude_set:
continue
tool_details.append({
"name": tname,
@@ -2576,6 +2578,7 @@ class BridgeServer:
def _mcp_tools_list(self, req: dict, profile: str, _servers, _lock) -> dict[str, Any]:
server_filter = str(req.get("server") or "").strip() or None
raw_mode = bool(req.get("raw")) # Return unfiltered tools for visibility management
results = []
config = self._read_mcp_config(profile)
@@ -2592,13 +2595,17 @@ class BridgeServer:
registered = set(getattr(task, "_registered_tool_names", None) or [])
tools = []
srv_cfg = mcp_configs.get(sname, {}) if isinstance(mcp_configs.get(sname), dict) else {}
tools_filter = srv_cfg.get("tools") or {}
tools_filter = srv_cfg.get("tools") if isinstance(srv_cfg.get("tools"), dict) else {}
has_include_filter = "include" in tools_filter
has_exclude_filter = "exclude" in tools_filter
include_set = set(tools_filter.get("include") or [])
exclude_set = set(tools_filter.get("exclude") or [])
def _should_include(tn):
if include_set:
if raw_mode:
return True # Skip filter in raw mode
if has_include_filter:
return tn in include_set
if exclude_set:
if has_exclude_filter:
return tn not in exclude_set
return True
try:
+1 -1
View File
@@ -54,7 +54,7 @@ export async function bridgeMcpAction(
break
}
case 'mcp_tools_list':
raw = await client.mcpTools(payload.server as string | undefined, profile)
raw = await client.mcpTools(payload.server as string | undefined, profile, payload.raw as boolean | undefined)
break
case 'mcp_reload':
raw = await client.mcpReload(payload.server as string | undefined, profile)