Repository navigation
MRTR: inputRequests is a closed enum of three methods — add an extensible / reverse-DNS-namespaced member type #2919
Description
Activity
hippoley commented
on Sep 26, 2026 More actionsThis external-approval use case exposes one more requirement beyond extensibility of the member method: the approval round trip needs stable resume semantics, not just a custom payload.
I would separate these fields conceptually:
origin_request_id // the suspended tools/call / MRTR owner approval_request_id // this human boundary resolver // who/what is allowed to answer outcome // approved | rejected | expired | cancelled resume // continue the original suspended operationThe important invariant is that an approval result should complete the same suspended operation; it should not require the server/client/model to synthesize a second tool call and hope it is equivalent.
That also makes timeout/cancellation explicit. “No human answered” should not be representable as a successful human answer.
A reverse-DNS method looks like a reasonable extension mechanism, but I think clients will eventually need at least a common lifecycle contract even if the domain-specific approval payload stays namespaced. Otherwise every external approval type will reinvent pending-state, expiry, resolver identity, cancellation and request correlation.
I’ve been testing this exact contract in a small protocol experiment called
human://:
https://github.com/hippoley/HumanQueue/blob/main/docs/protocol.mdThe thing I’m trying to falsify is whether a generic boundary lifecycle is actually useful across runtimes, or whether MCP-native MRTR semantics make it unnecessary. This issue is a strong test case for that distinction.
+1 to a namespaced MRTR member. Beyond the use case of human approval, we are trying to enable fetching of host owned data.
We run an MCP host (NimbleBrain). User upload files to isolated workspaces. During use, an MCP server may need one of these files while handling
tools/call. Without a host-to-server read path the host would be required to dump the whole file into the model context. This burns loads of context and works really poorly with binary files.We implemented a capability-negotiated extension,
ai.nimblebrain/host-resources, with server-to-client methodsai.nimblebrain/resources/readandai.nimblebrain/resources/list. Their payloads reuse the MCP types/shapes,. So, a tool receives a URI such asfiles://fl_abc123, and the server asks the host for its contents.Today this fetch happens as in-flight server-to-client request during
tools/call. My understanding, SEP-2322 would replace this flow, yeah? If we were to use this via MRTR, we need both a namespacedinputRequestsmember and a way to carry its corresponding result ininputResponses:"inputRequests": { "seed": { "method": "ai.nimblebrain/resources/read", "params": { "uri": "files://fl_abc123" } } }
Two things worth bringing up:
- A fetch doesn’t need an approval lifecycle. Let the extension define the request and response shapes, with the server sending them only when the client advertises support.
- An inline read result would come back in inputResponses on the retried tools/call. That’s fine for a small file, but awkward for a large one. We may need a way to return a reference instead, which also overlaps with the Files WG and SEP-2631.
Happy to share more if helpful.
+1 to namespaced members. One data point for the human-approval case, and one gap on the response side.
My coding agents (mostly Claude Code) push questions that need me into a personal decision inbox. Small sample, one user, so directional only: 55 inbox items, plus ~640 agent-question / my-answer pairs pulled from a month of local session logs and classified with a heuristic script.
- About half of the inbox items carried business risk (money, legal, brand, external commitments). That risk lives in the decision, not in any single tool call, so a tool-permission prompt never sees it.
- When the agent offered buttons, 36 of 39 answers were just a button. In free chat, about a third of decision answers were "option B, but only if X", roughly one in nine was a follow-up question instead of an answer, and a few set a standing rule for similar future cases.
Form-mode elicitation can approximate the request (an enum with a default, a message, an optional text field). The answer is where it breaks:
ElicitResultisaccept | decline | cancelplus flat content, so an approval with conditions, an answer given by a standing rule rather than a person, and "expired, nobody answered" all look like an ordinary submit or cancel. The client also can't tell this form from any other, so it can't route it to an out-of-band approver.Which points to a gap:
InputResponseis a closed union too (CreateMessageResult | ListRootsResult | ElicitResult). As @mgoldsborough noted, a namespaced request needs a matching namespaced entry ininputResponses, so the proposal should probably open both, with the same capability rule.On layering with @hippoley's lifecycle: MRTR's retry with
requestStatealready resumes the same operation. What would still benefit from a shared contract is outcome / expiry / resolver, and that could be its own proposal that payloads opt into, since (as @mgoldsborough says) a host file read needs none of it. In my payload draft an expiry is recorded asdecided_by: "expiry", never as a human answer, which is the invariant @hippoley describes.Trimmed example (the full draft schema and 30 example records are at https://github.com/f46msgd/decision-object; corrections welcome):
"inputRequests": { "launch_price": { "method": "com.example/decision", "params": { "kind": "decision", "title": "Launch price?", "why": "Pricing is the owner's call.", "options": [ { "key": "paid", "label": "Paid upfront" }, { "key": "sub", "label": "Subscription" } ], "rec": "paid", "rec_reason": "No recurring server cost to cover.", "due": "2026-10-10T00:00:00Z", "on_expiry": "cancel" } } }
and on retry:
"inputResponses": { "launch_price": { "status": "decided", "choice": "paid", "conditions": "Only if the price stays under $5.", "decided_by": "human", "decided_at": "2026-10-03T09:31:00Z" } }
Disclosure: Claude Code wrote the log-analysis script and drafted this comment; I reviewed it and stand behind it.
Reacted by hippo
Summary
Under the 2026-07-28 MRTR (Multi Round-Trip Request) model, the members of
InputRequiredResult.inputRequestsare restricted to a closed set of three JSON-RPC methods:elicitation/create,sampling/createMessage,roots/list. There is no extension hook.Problem
Application-defined "input required" interactions cannot be modeled natively. The motivating case is an external human-approval step: a server wants to pause a
tools/calluntil an out-of-band approver (a different human, a ticketing/approval system) acts, then resume. Conceptually this is exactly an MRTR round trip — "I need input before I can finish, here's the request, retry with the result" — but becauseinputRequestsis closed, it can only be tunneled throughurl-modeelicitation/create, which:Request
Allow a reverse-DNS-namespaced member method in
inputRequests(e.g.com.example/approval), mirroring how_metakeys are already namespaced in MCP. Semantics:paramsshape is defined by the namespace owner.This keeps the closed standard set while giving the ecosystem a forward-compatible extension path — the same trade-off
_metanamespacing already makes elsewhere in the spec.Use case
External/asynchronous approval gating ("AARM"-style) before a sensitive tool proceeds — a common enterprise pattern that today has no first-class MRTR representation.