fix(agents): raise OAuth consent interrupt for skill-folded external MCP tools#477
Merged
Merged
Conversation
…MCP tools In skills mode a skill-bound external MCP tool executes through the skill_executor meta-tool, so OAuthConsentHook's provider_lookup (keyed on the selected tool being an MCPAgentTool) resolved nothing and the consent gate silently never fired: the tool ran tokenless, returned an auth error as text, and the model apologized instead of the turn pausing with an oauth_required interrupt. - OAuthConsentHook: optional tool_use_provider_lookup second-chance resolver (gate + 401-retry handler) consulted when provider_lookup returns None - mcp_binding: make_folded_tool_provider_lookup maps the executor's tool_use input (skill_name/tool_name) -> bound FoldedMCPTool -> owning client -> provider id; gateway clients resolve to None (SigV4, not user OAuth) - FoldedMCPTool.invoke now returns a ToolResult-shaped error on failure so the error status survives the fold and the consent hook's 401-retry heuristic (gated on status == "error") can fire for folded tools - SkillAgent wires the resolver over its registry via the new BaseAgent._build_tool_use_provider_lookup hook point (None for chat) The resume path is unchanged: the interrupt is raised on the skill_executor call itself, so consent -> interrupt_responses -> cache warm -> the client's lazy token provider picks up the token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
In skills mode (now the server default,
agent_type="skill"), invoking a skill whose bound external MCP tool has no active OAuth authorization (e.g. "Gmail for employees" with no Gmail token in the vault) produced a text apology from the model instead of pausing the turn with the in-conversation consent prompt.Root cause:
OAuthConsentHook'sprovider_lookuponly resolves whenevent.selected_toolis anMCPAgentTool. In skills mode the model calls theskill_executormeta-tool (a decorated function tool) — the real target only appears in the tool-use input ({skill_name, tool_name, ...}) — so the gate concluded "not OAuth-gated", theFoldedMCPToolrancall_tool_synctokenless, and the auth error surfaced as tool text. No Strands interrupt → nopending_interrupt→ nooauth_requiredSSE event.A compounding bug:
FoldedMCPTool.invokeswallowed all failures into success-status strings, so the hook's 401-retry heuristic (gated onstatus == "error") was also dead for folded tools — even the expired-token refresh path.Fix
oauth_consent.py— optionaltool_use_provider_lookup: a second-chance resolver receiving the rawtool_usedict, consulted by both the consent gate and the 401-retry handler whenprovider_lookupreturnsNone. Plain chat wiring passesNone→ behavior unchanged.skills/mcp_binding.py—make_folded_tool_provider_lookup(registry, provider_for_client): executor tool_use input → boundFoldedMCPTool→ owning client → provider id. Gateway clients resolve toNone(SigV4, not user OAuth).FoldedMCPTool.invokenow returns a ToolResult-shaped{"status": "error", ...}on failure (Strands'@tooldecorator passes status+content dicts through), so error status survives the fold.base_agent.py/skill_agent.py— new_build_tool_use_provider_lookup()hook point (base returnsNone);SkillAgentoverrides it with its registry + the external MCP integration.Resume path is unchanged: the interrupt is raised on the
skill_executorcall itself, so consent →interrupt_responses→ token-cache warm → the client's lazy token provider injects the bearer on the actual MCP request.Tests
TestOAuthConsentHookSkillFoldedTools— skill-bound tool with no token →InterruptExceptionwith the exactoauth_requiredreason; cached token proceeds; 401-through-the-fold clears cache and retries; no-lookup preserves old behavior.TestFoldedToolProviderLookup— resolver against a realSkillRegistry(unknown skill/tool, local non-folded tools, unmapped clients, malformed input).TestToolUseProviderLookupWiring—SkillAgenthands the hook a working resolver.FoldedMCPToolerror-status preservation (exception + error-status MCP result).Full backend suite: 3820 passed, 3 skipped (pytest is not run in CI; local suite is the gate).
Out of scope (flagged separately)
MCPExternalApprovalHookhas the identical hole —needs_approval=Truetools bound to a skill bypass the approval prompt. Tracked as a follow-up task using the sametool_usefallback pattern.🤖 Generated with Claude Code