feat(tools): MCP elicitation handler with gateway-aware approval routing - #49203
Merged
Conversation
Wires support for the MCP `elicitation/create` request (Python SDK 1.11+) so MCP servers can ask the user to confirm sensitive operations mid-tool-call (payment authorization, OAuth confirmation, etc.) instead of failing closed or requiring out-of-band biometrics. Behavior: - `tools/mcp_tool.py` adds `ElicitationHandler`, attached per server task and passed to `ClientSession` as `elicitation_callback`. Form-mode requests route through the existing approval system; URL-mode requests decline cleanly (out of scope for this pass). - `tools/approval.py` adds `request_elicitation_consent()`, which dispatches to whichever surface owns the active session — `_await_gateway_decision` for Telegram / Slack / etc. (so the approval prompt lands on the right platform), `prompt_dangerous_approval` for CLI / TUI. Fails closed on timeout, missing notify_cb, or exception. - The MCP tool wrapper snapshots `contextvars.copy_context()` into `MCPServerTask._pending_call_context` before each `session.call_tool` and clears it after. The recv-loop task that dispatches incoming `elicitation/create` requests does not inherit the agent task's contextvars (HERMES_SESSION_PLATFORM and friends), so without the bridge `_is_gateway_approval_context()` returns False on every gateway session and the elicitation falls through to a CLI prompt that has no TTY → fail-closed decline. The handler now reads the snapshot via its `owner` back-reference and replays it through `Context.copy().run(...)` so attribution survives the task hop. Tests (`tests/tools/test_mcp_elicitation.py`): - form-mode accept / decline / cancel - URL-mode declined without prompting - exception in approval system → decline - timeout in approval → cancel - context-bridge regression tests (replay observed in consent call, missing-context fallback, multiple-replay safety, owner with cleared `_pending_call_context`) Verified end-to-end against pay's MCP server on macOS: agent message arrives via Telegram, agent calls `mcp_pay_curl` against a paid endpoint, pay returns 402, ElicitationHandler routes the approval prompt back to the originating Telegram chat, user replies in TG, the curl tool signs and completes. Platforms tested: macOS 14 (darwin/arm64). No Unix-only syscalls introduced; Windows footgun checker passes on the touched files.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
2 |
unresolved-attribute |
2 |
invalid-argument-type |
1 |
First entries
tests/tools/test_mcp_elicitation.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `mcp.types`
tests/tools/test_mcp_elicitation.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/run_agent/test_credits_notices_toggle.py:76: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_credits_session_start_micros` on type `AIAgent`
run_agent.py:2971: [unresolved-attribute] unresolved-attribute: Object of type `Self@get_credits_spent_micros` has no attribute `_credits_session_start_micros`
tests/tools/test_mcp_elicitation.py:290: [invalid-argument-type] invalid-argument-type: Argument to `ElicitationHandler.__init__` is incorrect: Expected `MCPServerTask | None`, found `SimpleNamespace`
✅ Fixed issues (1):
| Rule | Count |
|---|---|
invalid-assignment |
1 |
First entries
tests/run_agent/test_credits_notices_toggle.py:76: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to attribute `_credits_session_start_micros` of type `int`
Unchanged: 5849 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
13 tasks
Collaborator
1 task
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.
Summary
MCP servers can now ask the user to confirm a sensitive operation mid-tool-call (payment auth, OAuth) and have that prompt surface on whichever surface owns the active session — CLI, TUI, Telegram, Slack, etc. Previously elicitation on a gateway/VM session had no TTY, hit
prompt_dangerous_approvalin a worker thread, and fail-closed declined everything.Salvage of #43112 by @lgalabru, cherry-picked onto current
mainwith authorship preserved.Changes
tools/mcp_tool.py:ElicitationHandler(mirrors the existingSamplingHandlerpattern —session_kwargs()→ClientSession(elicitation_callback=...)),_format_elicitation_schema_summaryhelper,_pending_call_contextslot + contextvars snapshot aroundsession.call_toolto bridge the recv-loop task hop, SDK-gated on_MCP_ELICITATION_TYPES(mcp ≥ 1.11) and per-serverelicitation.enabled(default on).tools/approval.py:request_elicitation_consent()— routes gateway sessions through_await_gateway_decision, CLI/TUI throughprompt_dangerous_approval; fails closed (timeout→cancel, exception / missing notify_cb→decline).tests/tools/test_mcp_elicitation.py: 15 unit tests (form accept/decline/cancel, URL-mode decline, timeout, exception fail-closed, 4 context-bridge regressions).scripts/release.py: AUTHOR_MAP entry for the contributor.Validation
tests/tools/test_mcp_elicitation.py+ siblingtests/tools/test_mcp_tool.py) viascripts/run_tests.sh.call_toolgets the context bridge;list_resources/read_resource/list_prompts/get_promptcan't fire elicitations.No new core tool, no tool-schema change, no env var.
Closes #43112.
Infographic