MCP trust prompts: Allow for this session / Always now stick per tool; one-shot prompts offer only Once / Deny (salvage #109832)

1 hour ago

On an untrusted MCP server, choosing Allow for this session, or Always, now actually stops the next prompt for that tool. Prompts that can only cover one request now offer just Allow Once and Deny. It's a salvage of an earlier fix, rebuilt on current main, and it closes two issues.

Ask about this presentation

Chapters

  1. 0:00Allow for this session now sticks
  2. 0:19The next call asked again
  3. 0:39Why every yes covered one call
  4. 0:59An oversight, not a design choice
  5. 1:19One key per server and tool
  6. 1:39Session and Always are stored
  7. 1:59One-shot prompts offer Once and Deny
  8. 2:17Over the limit, every call asks
  9. 2:30Revoking Always
  10. 2:50Live repro on a real gateway
  11. 3:12Tests fail on main, pass here
  12. 3:34What it leaves alone
Show transcript

Allow for this session now sticks

Hermes
Agent
MCP trust approvals

On a trust: untrusted MCP server

Session and Always stop the next prompt for that tool. One-shot prompts offer only Once and Deny.

Cover art reading Allow for this session, with Before, Now, One-shot prompts and Every surface panels

On an untrusted MCP server, choosing Allow for this session, or Always, now actually stops the next prompt for that tool. Prompts that can only cover one request now offer just Allow Once and Deny. It's a salvage of an earlier fix, rebuilt on current main, and it closes two issues.

The next call asked again

Hermes
Agent
What was wrong

Pressing Session allowed one call, and the next call asked again

Turn 1

notes · write_note

Allow OnceSessionAlwaysDeny

Runs

→

Turn 2, same tool

notes · write_note

Allow OnceSessionAlwaysDeny

Asks again

Every write-capable call to an untrusted MCP server asks first, with Once, Session, Always and Deny. You press Session on the first call, and the tool runs. Then the next call to the same tool asks again. Session and Always behaved exactly like Once, on the gateway, the CLI and the TUI.

Why every yes covered one call

Hermes
Agent
Root cause

The consent helper turned every yes into a one-call accept

Recorded nothing

tools/approval_prompt.py

request_elicitation_consent mapped Once, Session and Always to the same single accept.

return _consent(choice, "decline")

Offered everything

gateway/run_turn_runner.py · tui_gateway/server.py

The gateway request left out allow_session and allow_permanent, so both scopes defaulted to on.

The trust gate asks through request elicitation consent, in the approval prompt module. That helper turned every yes into a one-call accept, and recorded nothing. Its gateway request also left out both scope flags, so the gateway runner and the TUI server defaulted them to on, and showed buttons the helper never honoured.

An oversight, not a design choice

Hermes
Agent
Intent check

The helper's comment, written for server-initiated elicitations

# allow_permanent=False: elicitation is a per-call confirmation — no pattern to remember.

A trust-gate call does have something to remember: one (server, tool) pair.

The trust gate reused a helper written for server-initiated elicitations, before the gate existed. Its comment says per-call confirmation, no pattern to remember. A trust-gate call does have something to remember: one server and tool pair. So the broken Session and Always buttons were an oversight.

One key per server and tool

Hermes
Agent
tools/mcp_tool_handlers.py

The gate keys each grant on one server and one tool

Key for the notes server's write tool

mcp:notes/write_note
def _trust_approval_key(server_name, tool_name):
    return f"mcp:{server_name}/{tool_name}"

def _trust_granted(server_name, tool_name):
    return is_approved(
        get_current_session_key(),
        _trust_approval_key(server_name, tool_name))

gated = (
    trust == _core._TRUST_UNTRUSTED
    and not _tool_is_read_only(server_name, tool_name)
    and not _trust_granted(server_name, tool_name))

In the MCP tool handlers, each answer is kept under a key made of the server name and the tool name, so allowing one write tool never covers the server's others. Before it asks, the gate checks whether that key is already approved in this session. A granted tool skips the prompt, and the spend limit check still runs.

Session and Always are stored

Hermes
Agent
tools/approval_prompt.py

A key turns Session and Always into stored answers

def _remembered_consent(session_key: str, choice,
                        remember_key: str | None,
                        unresolved: str) -> str:
    if remember_key is not None:
        from tools import approval as _a
        _a._persist_choice(session_key, choice,
                           [remember_key])
    return _consent(choice, unresolved)
Session
That tool, for the rest of this chat
Always
Written to the active profile's command_allowlist

request elicitation consent gains one keyword argument, remember key. With a key, the answer goes through the existing persist choice, the same path command approvals use. Session covers that tool for the rest of this chat. Always writes the key into the active profile's command allowlist, so other chats skip the prompt too.

One-shot prompts offer Once and Deny

Hermes
Agent
Consent without a key

Prompts with nothing to remember offer only Once and Deny

Allow OnceSessionAlwaysDeny
allow_session: false allow_permanent: false
MCP elicitation
Sampling
Payment-card fill
Spend-limit prompts

Without a key, the helper sends both scope flags as false. Every surface then shows only Allow Once and Deny. That covers MCP elicitation, sampling, payment card fill, and spend limit prompts. A stale or typed broader answer still covers just that one call.

Over the limit, every call asks

Hermes
Agent
_ask_for_call

An over-limit call asks every time, even after Always

# Over the limit the answer covers this call only:
# a remembered trust grant must not pre-approve
# the next over-limit call.
remember_key = (
    None if over
    else _trust_approval_key(server_name, tool_name))
scope = (
    "once" if over
    else "once, for this session, or always")

A call that would take the chat past its spend limit carries no key. So it offers only Once and Deny, and the description says so. An earlier Session or Always answer for that tool doesn't silence it.

Revoking Always

Hermes
Agent
Review thread

Review found a gap in revoking Always, and the docs now cover it

“Deleting the command_allowlist entry does not revoke an Always answer in the chat that granted it.”

greptile-apps · mcp-config-reference.md

“Deleting the entry revokes it for every other chat, and the chat that answered Always keeps it until it ends.”

teknium1 · fixed in f3414ff1dd6

The automated reviewer raised a real concern about revocation. Persist choice also keeps an Always answer in the granting chat's session, so deleting the allowlist entry leaves that chat approved. The author agreed, since command approvals work the same way, and fixed the docs to say the granting chat keeps it until it ends.

Live repro on a real gateway

Hermes
Agent
Live repro · Telegram stand-in

On a real gateway, Session and Always stopped the second prompt

A hermes gateway run child, real inline-button presses, a stdio server notes with trust: untrusted

Pressed on turn 1Base · turn 2Head · turn 2command_allowlist on head
SessionNew promptNo prompt, note two writtennone
AlwaysNew promptNo prompt, note two written['mcp:notes/write_note']
OncePromptPromptnone

The live repro ran a real gateway process, a Telegram stand-in pressing real inline buttons, and an untrusted notes server. On the base commit, Session or Always on turn one still prompted on turn two. On this branch, turn two wrote the second note without a prompt, and Always left the key in the allowlist. Once, the control, asks both times.

Tests fail on main, pass here

Hermes
Agent
tests/tools/test_mcp_trust_approval_scope.py

Three new test cases fail on main and pass here

On origin/main

assert 3 == 2 # prompts KeyError: 'allow_session'

Driven through the real gateway approval queue

143

existing MCP and approval tests passed

Same fix seen live on

Classic CLITUI / Desktop backend

The new tests drive the real gateway approval queue. On main, all three cases fail, with three prompts where two were expected, and a missing allow session flag. All three pass here. More than 140 existing MCP and approval tests still pass, and the classic CLI and the TUI backend showed the same fix with real processes.

What it leaves alone

Hermes
Agent
Scope and next steps

Still one-shot

Remembering elicitation answers per server request is a separate feature, in #142803.

No adapter changes

Telegram, Discord, Slack, Matrix, Teams and Feishu buttons, /approve, TUI and Desktop, the API server and ACP already honour the flags.

Revoke an Always answer

command_allowlist: - mcp:<server>/<tool> # delete

then end the chat that answered Always.

Allow for this session now sticks.

Elicitation answers still cover one request each. Remembering them per server request is a separate feature. The adapters stay as they were, because the messaging buttons, the TUI, Desktop, the API server and ACP already honour these flags. To take back an Always answer, delete its key from the command allowlist, then end the chat that granted it. Allow for this session now sticks, one tool at a time.

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️