Skip to content

feat(tools): MCP elicitation handler with gateway-aware approval routing - #49203

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-12dda8a3
Jun 19, 2026
Merged

feat(tools): MCP elicitation handler with gateway-aware approval routing#49203
teknium1 merged 2 commits into
mainfrom
hermes/hermes-12dda8a3

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

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_approval in a worker thread, and fail-closed declined everything.

Salvage of #43112 by @lgalabru, cherry-picked onto current main with authorship preserved.

Changes

  • tools/mcp_tool.py: ElicitationHandler (mirrors the existing SamplingHandler pattern — session_kwargs()ClientSession(elicitation_callback=...)), _format_elicitation_schema_summary helper, _pending_call_context slot + contextvars snapshot around session.call_tool to bridge the recv-loop task hop, SDK-gated on _MCP_ELICITATION_TYPES (mcp ≥ 1.11) and per-server elicitation.enabled (default on).
  • tools/approval.py: request_elicitation_consent() — routes gateway sessions through _await_gateway_decision, CLI/TUI through prompt_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

  • 215/215 pass (tests/tools/test_mcp_elicitation.py + sibling tests/tools/test_mcp_tool.py) via scripts/run_tests.sh.
  • E2E against real imports on current main: CLI deny→decline, CLI once→accept, gateway-no-notify_cb→decline (fail-closed), URL-mode→decline without prompting.
  • Scope correct: only call_tool gets the context bridge; list_resources/read_resource/list_prompts/get_prompt can't fire elicitations.

No new core tool, no tool-schema change, no env var.

Closes #43112.

Infographic

mcp-elicitation-handler

lgalabru and others added 2 commits June 19, 2026 11:36
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.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-12dda8a3 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 11168 on HEAD, 11160 on base (🆕 +8)

🆕 New issues (5):

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.

@alt-glitch alt-glitch added type/feature New feature or request tool/mcp MCP client and OAuth comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have labels Jun 19, 2026
@teknium1
teknium1 merged commit 746c46d into main Jun 19, 2026
34 checks passed
@teknium1
teknium1 deleted the hermes/hermes-12dda8a3 branch June 19, 2026 18:46
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related: #43112 (original by @lgalabru, this is the salvage cherry-picked onto current main with authorship preserved), #23021 (MCP elicitation/create -> platform inline-keyboard buttons — overlapping elicitation surface).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have tool/mcp MCP client and OAuth type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants