fix(server): Claude MCP app card calls honor Claude's permission rules#31131

merged4
t3/claude-mcp-apps
GitHub fork: fix/claude-mcp-apps-permission-rules
opened1d agobySseth-dixon
View on GitHubTwo-way sync enabledPR last imported 1d ago

Folds the fix from https://github.com/pingdotgg/t3code/pull/29803#issuecomment-6638076015 into #29803, as asked.

Problem

Card tool calls go through Claude's mcp_call control request, which skips Claude's permission check: deny rules, ask rules, and canUseTool all go unchecked. claudeMcpToolForApps also passed Claude's readOnly through as readOnlyHint, and T3 doesn't prompt for read-only tools. So a read-only tool that the user denied in Claude, or set to ask, ran from a card with no prompt.

Fix

The adapter reads the session's rules with list_permission_rules. The SDK types that control request but has no method for it, so it goes through the same request path as mcp_call.

  • callTool refuses denied tools: it refuses, without sending mcp_call, when an in-effect deny rule may cover the tool.
  • listTools keeps readOnlyHint only when it is safe: the rules must have been read, and no in-effect ask or deny rule may cover the tool. Otherwise T3 prompts.
  • Missing or incomplete rules count as unreadable: this covers a failed request, a reply that doesn't decode, and a non-empty state.errors (Claude skipped a settings file it couldn't parse, so that file's rules are missing). The call is refused and readOnlyHint is dropped.
  • Switched-off rules are ignored: rules marked notInEffect don't apply.

Trade-off for you to decide

Claude also matches rules against server aliases that T3 can't list, and an alias may contain __. In CLI 2.1.318, the alias registry sits behind a remote flag, and the alias keys come from the server's address. So a rule such as mcp__finance__prod, mcp__finance__prod__refresh_weather, or *notion* can cover a tool whose listed name it doesn't match. To stay safe, any ask or deny rule whose name starts with mcp__ or contains * counts as covering every card tool.

What users see:

  • Broad refusals: someone with an unrelated rule such as deny: mcp__github__delete_issue gets every card call refused.
  • Broad prompts: someone with an unrelated MCP ask rule gets a prompt on every card call.

A narrower option: a rule counts only if one of its __ splits could name this tool, and wildcards still count. That leaves one rare gap open: a server-wide rule for a hidden alias whose name contains __. I went with the strict version; happy to switch if you'd rather.

Gaps already in this branch, not addressed here

  • Hooks are skipped: mcp_call also skips PreToolUse hooks, so a read-only tool that a hook blocks still runs from a card.
  • Org caps aren't visible: org-level per-tool caps on claude.ai connectors (toolPermissions) aren't reported by list_permission_rules.
  • Refusals all read the same: every refusal reaches the card as the generic McpAppRequestError message.
  • Ask-rule changes are caught late: an ask rule added after a card has listed its tools is only caught on the next listing, because callTool doesn't carry whether the user approved.
  • Older Claude CLIs: a CLI that has the MCP-app capabilities but not list_permission_rules would refuse every card call. CLIs 2.1.314 to 2.1.318 have both.

Tests

  • Adapter rules table: a table test in ClaudeAdapterV2.test.ts checks, for each rule set, both the listed annotations and whether mcp_call was sent.
  • Runner decode: a test in ClaudeAgentSdkQueryRunner.test.ts covers decoding the rules, including skipped settings files and a reply without a rule list.
  • Run locally: I ran vp run check:changed locally.

Made with Claude Opus 5.5 in Claude Code (via T3 Code), with reviews from GPT-6.1 Sol and Claude Opus 5.5.

🤖 Generated with Claude Code

Checks

27 successful · 12 skipped · 2 neutral
All checks passed
3341c88

Added 4 commits

  • fix(server): Claude MCP app cards treat skipped settings and alias rules as covering1a109e9
  • fix(server): any wildcard Claude rule counts as covering MCP app card tools311bf52
  • test(server): an undecodable Claude permission-rules reply counts as unreadable3341c88
  • fix(server): Claude MCP app card calls honor Claude's permission rulesbea27bc
Synced from GitHub

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — The change adds fail-closed permission checks for Claude MCP card calls, but an unrelated MCP deny rule will block every card tool. A maintainer needs to resolve whether that broad restriction is acceptable versus narrower matching.

You can add or adjust custom eligibility rules. Learn more.

Folded into #29803 by fast-forwarding t3/claude-mcp-apps to 3341c88 (check:changed green locally). Keeping the strict rule-coverage version: a wrong match costs a refusal or a prompt, never a bypass. We can narrow it later if users hit it. Thanks Seth!