Skip to content

fix: check underlying tool permissions for hub invoke/exec#13282

Merged
kangfenmao merged 1 commit intomainfrom
DeJeune/fix-hub-tool-permission
Mar 9, 2026
Merged

fix: check underlying tool permissions for hub invoke/exec#13282
kangfenmao merged 1 commit intomainfrom
DeJeune/fix-hub-tool-permission

Conversation

@DeJeune
Copy link
Copy Markdown
Collaborator

@DeJeune DeJeune commented Mar 7, 2026

What this PR does

Before this PR:

  • In MCP hub/auto-discovery mode, all tools were auto-approved, bypassing the permission system entirely. Hub tools get serverId: 'hub', and isToolAutoApproved() always returned true because the hub server object has no disabledAutoApproveTools field.

After this PR:

  • Hub meta-tools list and inspect remain auto-approved (read-only, safe)
  • Hub meta-tools invoke and exec require approval by default
  • For invoke/exec, the underlying tool's server auto-approve configuration is checked via IPC, so tools that are individually auto-approved on their original server still auto-execute through the hub

Why we need it and why it was done in this way

The following tradeoffs were made:

  • Added a new IPC channel (Mcp_ResolveHubTool) to resolve hub JS tool names back to their original serverId/toolName pairs, since the tool name mapping only exists in the main process
  • isToolAutoApproved() returns false for hub invoke/exec by default; the execute callback in mcp.ts performs the IPC resolution to check the underlying tool's auto-approve setting

The following alternatives were considered:

  • Embedding original server metadata in the hub tool response — rejected because the MCP protocol doesn't carry custom metadata
  • Simply blocking all hub tools — rejected because list/inspect are safe read-only operations and should remain fast

Breaking changes

None. This is a security fix — tools that previously bypassed approval will now correctly require it (unless their underlying server has auto-approve enabled).

Special notes for your reviewer

  • mcp-bridge.ts: Added resolveHubToolName() to resolve JS tool names to original {serverId, toolName}
  • IpcChannel.ts / ipc.ts / preload/index.ts: New IPC channel for the resolution
  • mcp-tools.ts: Hub-specific check in isToolAutoApprovedlist/inspect auto-approve, others don't
  • mcp.ts: For hub invoke/exec, resolves underlying tool and checks its server's auto-approve config
  • Includes 31 unit tests across 3 test files (toolname, mcp-bridge, mcp-tools-approval)

Checklist

Release note

Fix hub/auto-discovery mode bypassing tool permission checks. Hub tools now correctly respect per-tool auto-approve settings from their original MCP server configuration.

In MCP hub/auto-discovery mode, all tools were auto-approved because
hubMCPServer has no disabledAutoApproveTools field. This bypassed
the permission system entirely.

- Add hub-specific check in isToolAutoApproved: list/inspect are
  auto-approved (read-only), invoke/exec require approval by default
- Add Mcp_ResolveHubTool IPC to resolve hub JS tool names to their
  original serverId and toolName via the main process tool mapping
- In execute callback, resolve hub invoke/exec underlying tool and
  check the original server's disabledAutoApproveTools config
- Add tests for toolname, mcp-bridge resolver, and isToolAutoApproved

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: suyao <[email protected]>
@DeJeune DeJeune requested a review from 0xfullex as a code owner March 7, 2026 10:24
Copy link
Copy Markdown
Collaborator

@EurFelux EurFelux left a comment

Choose a reason for hiding this comment

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

Review Summary

Excellent security fix! 🔒 This PR correctly addresses a real permission bypass where all hub/auto-discovery tools were unconditionally auto-approved.

What I reviewed

  • mcp-tools.ts: Hub-specific isToolAutoApproved logic — list/inspect (read-only) stay auto-approved, invoke/exec (execution) require approval. Clean and correct.
  • mcp-bridge.ts: resolveHubToolName() — straightforward reverse-lookup from JS name → {serverId, toolName}. Uses indexOf('__') for the separator which correctly handles serverId with single underscores.
  • mcp.ts: The execute callback now resolves the underlying tool via IPC and checks its original server's disabledAutoApproveTools. The fail-closed design (all null/error paths → require approval) is the right security posture.
  • IPC layer (IpcChannel.ts, ipc.ts, preload/index.ts): Clean additions, consistent with existing patterns.
  • Tests: 31 tests across 3 files with good edge case coverage (unknown tools, uninitialized mapping, collisions, built-in overrides, agent allowed_tools).

Design tradeoffs — well justified

  • IPC round-trip for resolution is acceptable since it only happens for invoke/exec (not the frequent list/inspect).
  • Not embedding metadata in MCP protocol is the right call — keeps protocol clean.

Minor

  • One nit on missing beforeAll import in toolname.test.ts (see inline comment).

LGTM — well-structured fix with solid test coverage. 👍

let mapping: ToolNameMapping

beforeAll(() => {
mapping = buildToolNameMapping([
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: beforeAll is used here but not imported from vitest at the top of the file (line 1 only imports describe, expect, it). It works because vitest exposes lifecycle hooks as globals, but it's inconsistent with the other test files in this PR (mcp-bridge.test.ts explicitly imports beforeEach/afterEach).

-import { describe, expect, it } from 'vitest'
+import { beforeAll, describe, expect, it } from 'vitest'

Comment on lines +101 to +113
(mcpTool.name === 'invoke' || mcpTool.name === 'exec')
) {
const underlyingToolName = (params as Record<string, unknown>)?.name as string | undefined
if (underlyingToolName) {
try {
const resolved = await window.api.mcp.resolveHubTool(underlyingToolName)
if (resolved) {
const underlyingServer = store.getState().mcp.servers.find((s) => s.id === resolved.serverId)
if (underlyingServer) {
isAutoApproveEnabled = !underlyingServer.disabledAutoApproveTools?.includes(resolved.toolName)
}
}
} catch (err) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice defensive pattern here — when resolveHubTool returns null or the underlying server isn't found, the code safely falls through to isAutoApproveEnabled = false, which means the tool requires manual approval. This is the correct security posture (fail-closed).

One minor thought: resolveHubToolName in mcp-bridge.ts returns null if toolNameMapping hasn't been initialized yet (unlike callMcpTool which auto-refreshes). In practice this shouldn't happen since the hub server populates the mapping on startup before any tool execution, but it's worth noting the fail-closed behavior here guarantees safety even in that edge case.

Copy link
Copy Markdown
Collaborator

@GeorgeDong32 GeorgeDong32 left a comment

Choose a reason for hiding this comment

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

Code Review Conclusion

✅ Approval Reasons

This is a high-quality security fix PR:

Dimension Assessment
Security ⭐⭐⭐⭐⭐ Critical security vulnerability correctly fixed
Code Quality ⭐⭐⭐⭐⭐ Clear structure, correct logic
API Compatibility ✅ Consistent with main branch
Test Coverage ⭐⭐⭐⭐ 31 unit tests

Best Practices

  1. Secure by Default: Defaults to no auto-approval when parsing fails
  2. Principle of Least Privilege: invoke/exec require approval, list/inspect auto-approved
  3. Good Test Coverage: 31 test cases covering core logic
  4. Clear PR Description: Detailed explanation of fix rationale and trade-offs

Minor Suggestions (Optional)

// mcp-bridge.ts:51-55
// Consider adding empty string check
if (separatorIndex <= 0 || separatorIndex === toolId.length - 2) return null

Does not affect functional correctness, can be optimized later.


Approve

GeorgeDong32

This comment was marked as duplicate.

@DeJeune DeJeune requested a review from zhibisora March 9, 2026 10:08
@kangfenmao kangfenmao merged commit 5a2a609 into main Mar 9, 2026
20 checks passed
@kangfenmao kangfenmao deleted the DeJeune/fix-hub-tool-permission branch March 9, 2026 12:14
@kangfenmao kangfenmao mentioned this pull request Mar 13, 2026
6 tasks
kangfenmao added a commit that referenced this pull request Mar 13, 2026
### What this PR does

Before this PR:
- Version is 1.7.24

After this PR:
- Version is 1.7.25
- Release notes updated in `electron-builder.yml`

### Why we need it and why it was done in this way

Standard release workflow: collect commits since v1.7.24, generate
bilingual release notes, bump version.

The following tradeoffs were made:
N/A

The following alternatives were considered:
N/A

### Breaking changes

**Action Required**: The built-in filesystem MCP server now requires
manual approval for write/edit/delete operations by default.

### Special notes for your reviewer

**Included commits since v1.7.24:**

✨ New Features:
- feat(websearch): add Querit search provider (#13050)
- feat(minapp,provider): add MiniMax Agent, IMA mini apps and MiniMax
Global, Z.ai providers (#13099)
- feat(provider): add agent support filter for provider list (#11932)
- feat(agent): add custom environment variables to agent configuration
(#13357)
- feat: add GPT-5.4 support (#13293)
- feat(gemini): add thought signature persistence (#13100)

🐛 Bug Fixes:
- fix: secure built-in filesystem MCP root handling (#13294)
- fix: check underlying tool permissions for hub invoke/exec (#13282)
- fix: remove approval countdown timers and add system notifications
(#13281)
- fix: agent tool status not stopping on abort (#13111)
- fix: resolve spawn ENOENT on Windows for Code Tools (#13405)
- fix: Use default assistant for topic auto-renaming (#13387)
- fix(backup): defer auto backup during streaming response (#13307)
- fix(ui): fix Move To submenu overflow (#13399)
- fix: render xml fenced svg blocks as svg previews (#13431)
- fix: correct Gemini reasoning params (#13388)
- fix: improve Qwen 3.5 reasoning model detection (#13235)
- fix: upgrade xAI web search to Responses API (#12812)
- fix(api-server): relax chat completion validation (#13279)
- fix: install or uninstall button state issues (#13114)
- fix: improve update dialog behavior (#13363)
- fix: auto-convert reasoning_effort to reasoningEffort (#12831)

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: You have left the code cleaner than you found it (Boy
Scout Rule)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A user-guide update was considered and is present
(link) or not required.
- [x] Self-review: I have reviewed my own code before requesting review
from others

### Release note

```release-note
See release notes in electron-builder.yml
```

---------

Signed-off-by: kangfenmao <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants