Visitar URL original
MRTR: `inputRequests` is a closed enum of three methods — add an extensible / reverse-DNS-namespaced member type · Issue #2919 · modelcontextprotocol/modelcontextprotocol · GitHub
Skip to content

MRTR: inputRequests is a closed enum of three methods — add an extensible / reverse-DNS-namespaced member type #2919

Description

@ferentinai

Summary

Under the 2026-07-28 MRTR (Multi Round-Trip Request) model, the members of InputRequiredResult.inputRequests are 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/call until 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 because inputRequests is closed, it can only be tunneled through url-mode elicitation/create, which:

  • loses the semantics (the client renders it as "open this URL", not "an approval is pending"),
  • forces the approval payload through the elicitation shape rather than an application-appropriate one,
  • prevents clients from offering type-specific UX (e.g. "approval pending" status, approver identity, deadline).

Request

Allow a reverse-DNS-namespaced member method in inputRequests (e.g. com.example/approval), mirroring how _meta keys are already namespaced in MCP. Semantics:

  • Servers MAY emit a namespaced member; the params shape is defined by the namespace owner.
  • Clients that don't recognize a namespaced member MUST ignore it (and, per existing rules, the server MUST NOT require a capability the client didn't declare).
  • The existing three methods remain the reserved/standard set.

This keeps the closed standard set while giving the ecosystem a forward-compatible extension path — the same trade-off _meta namespacing 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.

Activity

  1. hippoley commented on Sep 26, 2026

    @hippoley

    This 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 operation
    

    The 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.md

    The 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.

  2. mgoldsborough commented on Sep 26, 2026

    @mgoldsborough

    +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 handlingtools/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 methods ai.nimblebrain/resources/read and ai.nimblebrain/resources/list. Their payloads reuse the MCP types/shapes,. So, a tool receives a URI such as files://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 namespaced inputRequests member and a way to carry its corresponding result in inputResponses:

    "inputRequests": {
      "seed": {
        "method": "ai.nimblebrain/resources/read",
        "params": { "uri": "files://fl_abc123" }
      }
    }

    Two things worth bringing up:

    1. 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.
    2. 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.

  3. f46msgd commented on Oct 3, 2026

    @f46msgd

    +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: ElicitResult is accept | decline | cancel plus 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: InputResponse is a closed union too (CreateMessageResult | ListRootsResult | ElicitResult). As @mgoldsborough noted, a namespaced request needs a matching namespaced entry in inputResponses, so the proposal should probably open both, with the same capability rule.

    On layering with @hippoley's lifecycle: MRTR's retry with requestState already 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 as decided_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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions