Skip to content

[RFC] Support MCP 2026-07-28 input_required through Runtime Interactions #4364

Description

@me2seeks

Summary

Maka can negotiate MCP 2026-07-28, but it deliberately rejects the
revision's input_required result today. This means an MCP Tool cannot pause
to ask the user for missing information and then continue the same logical
operation.

I propose supporting the first user-facing MRTR slice:

  1. tools/call may return resultType: "input_required";
  2. the initial supported embedded request is form-mode
    elicitation/create;
  3. Runtime Host projects the form through its existing durable Interaction
    authority;
  4. Desktop and TUI render and answer that same Host-owned Interaction; and
  5. McpClientManager retries the original Tool call with the collected bare
    inputResponses, a fresh JSON-RPC request id, and a byte-exact echo of the
    opaque requestState.

This proposal is based on
064c3b299.

The scope is intentionally narrower than “all MRTR.” URL-mode elicitation,
sampling, roots, prompts/get, resources/read, and process-crash resumption
have different authority and security requirements and are explicit non-goals
for the initial rollout.

Is this an MCP 2026-07-28 feature?

Yes. SEP-2322 replaces an in-flight server-to-client request with a stateless
multi-round exchange:

Client -> Server: tools/call (JSON-RPC id 1)
Server -> Client: input_required(inputRequests, requestState)
Client -> User: collect the requested input
Client -> Server: tools/call (JSON-RPC id 2,
original params,
inputResponses,
exact requestState)
Server -> Client: complete Tool result

The second tools/call is a new protocol request, not a held-open continuation
of id 1. Maka should preserve one logical Tool invocation and one Runtime
Interaction lineage across those protocol legs without pretending the first
HTTP/SSE request remains alive.

Current behavior and problem

McpClientManager currently configures SDK v2 with
inputRequired.autoFulfill: false. A modern input_required result is then
normalized to McpToolCallError("server returned an unsupported deferred tool result"). The regression test also asserts that Maka sends exactly one
tools/call and does not retry silently.

This is the correct safe default while Maka has no end-to-end interaction
owner. Enabling SDK auto-fulfilment alone would not provide one:

  • the MCP connection lives in the Desktop or TUI client process;
  • the model Tool executes under Runtime Host;
  • Client Capability currently carries call, progress, cancellation, and final
    result frames, but no nested user-interaction round trip; and
  • Runtime Host is already the canonical owner of pending questions, answers,
    cancellation, restart closure, and surface projection.

The missing feature is therefore not just a result union in @maka/mcp. It is
an end-to-end continuation across these existing owners:

Model Tool call
|
v
ToolRuntime / Runtime Host Interaction authority
|
v
Client Capability invocation
|
v
Desktop or TUI MCP provider -> McpClientManager -> MCP server

The post-V3 roadmap tracker #4329 explicitly leaves input_required handling
out of its scope, so this focused interaction/continuation design does not
duplicate that roadmap.

User scenario

For example, a deployment Tool may discover during execution that the target
environment is missing:

{
"resultType": "input_required",
"inputRequests": {
"target": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Choose the deployment target",
"requestedSchema": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"enum": ["staging", "production"]
},
"confirm": { "type": "boolean" }
},
"required": ["environment", "confirm"]
}
}
}
},
"requestState": "opaque-server-state"
}

Maka shows a Host-owned form that identifies the Client Capability provider,
MCP server, and Tool. If the user accepts with staging and true, the MCP
adapter retries the original call with:

{
"name": "deploy",
"arguments": { "artifact": "build-42" },
"inputResponses": {
"target": {
"action": "accept",
"content": { "environment": "staging", "confirm": true }
}
},
"requestState": "opaque-server-state"
}

The retry receives a fresh JSON-RPC id. requestState never enters the model
context, renderer-owned state, Runtime Interaction payload, logs, or telemetry.

Authority boundaries

The design keeps one owner for each fact or side effect:

ConcernOwner
MCP wire decoding, original params, round count, inputResponses, and exact requestState echoMcpClientManager
Live MCP connection and the actual retry side effectMcpClientManager plus the selected SDK transport
Client Capability invocation identity and provider connectionRuntime Host Client Capability coordinator/broker
Pending user form, canonical answer, stale-answer rejection, and closureRuntime Host Interaction authority
Tool/Turn cancellationToolRuntime and the exact hosted Run binding
Form rendering and user gesturesDesktop/TUI adapters over the Host projection

An MCP server supplies untrusted form copy and schema. A Client Capability
provider supplies untrusted display provenance. Runtime Host binds both to the
already-authorized exact invocation; neither can mint a Tool, grant a
permission, select another Session, or resume another invocation.

Proposed design

1. Add a provider-neutral form Interaction

Do not flatten an MCP form into the current multiple-choice question kind.
The existing question contract permits a small list of string answers, while
MCP form elicitation has typed strings, numbers/integers, booleans,
single-select and multi-select values, required fields, defaults, and bounded
validation constraints.

Extend the existing core Interaction union with a repository-owned form shape,
illustratively:

typeInteractionFormField=|{kind: "string";name: string;label: string;required: boolean;description?: string;default?: string;minLength?: number;maxLength?: number;format?: "email"|"uri"|"date"|"date-time"}|{kind: "number"|"integer";name: string;label: string;required: boolean;description?: string;default?: number;minimum?: number;maximum?: number}|{kind: "boolean";name: string;label: string;required: boolean;description?: string;default?: boolean}|{kind: "single_select";name: string;label: string;required: boolean;options: readonlyInteractionFormOption[];default?: string}|{kind: "multi_select";name: string;label: string;required: boolean;options: readonlyInteractionFormOption[];default?: readonlystring[];minItems?: number;maxItems?: number};typeInteractionFormValue=string|number|boolean|readonlystring[];interfaceInteractionFormRequest{kind: "form";toolUseId: string;message: string;requester: InteractionRequesterProjection;fields: readonlyInteractionFormField[];}typeInteractionFormAnswer=|{kind: "form";action: "accept";values: Readonly<Record<string,InteractionFormValue>>}|{kind: "form";action: "decline"|"cancel"};

These names are illustrative. The implementation should extend the existing
InteractionRequest, InteractionAnswer, canonical outcome, store decoder,
Host coordinator, and session projection rather than create an MCP-specific
store or queue.

Core owns exact-shape decoding and repository caps for request bytes, answer
bytes, field count, enum options, labels, and descriptions. Runtime Host
validates an accepted value against the stored canonical form before committing
the answer. The MCP adapter validates it again against the original MCP schema
before sending it to the server.

MCP exposes one coarse elicitation.form capability rather than separate
claims per primitive or field count. Maka must therefore implement the complete
bounded form subset the revision specifies before advertising it; it must not
ship a one-field or string-only claim under the same capability bit.

The form UI must show the requester provenance and give three distinct actions:
submit (accept), explicitly decline (decline), and dismiss (cancel). It
must allow review and edits before submit.

2. Add one nested Interaction seam to Client Capability

Extend a live Client Capability invocation with one provider-neutral nested
Interaction request/response pair. The exact wire names can follow the existing
protocol naming, for example:

// Client -> Host, only for an accepted live invocation.interfaceClientCapabilityInteractionRequestFrame{kind: "client.capability.interaction_request";invocationId: string;interactionId: string;request: ClientCapabilityFormRequest;}// Host -> the same Client connection and invocation.interfaceClientCapabilityInteractionResultFrame{kind: "client.capability.interaction_result";invocationId: string;interactionId: string;result: ClientCapabilityFormResult;}

ClientCapabilityProvider.call() receives a callback such as
requestInteraction(form). The client channel sends the request frame and
awaits the matching Host result. The Host broker projects it through the same
ToolRuntime form callback that local Tools use.

The broker, not the provider, binds the form to the current Session, Turn,
Tool call, invocation, registration, and connection. It allows at most one
pending nested Interaction per invocation. Duplicate, late, cross-connection,
or post-cancellation frames fail closed.

The frame's interactionId is only provider-local reverse-call correlation.
ToolRuntime creates the canonical Runtime Interaction id; an untrusted
provider cannot choose or collide with a Host identity.

This seam is generic because the ownership problem is generic: a Tool owned by
a remote Client Capability provider may need user input while Runtime Host owns
the execution. MCP is the first adapter using it. The seam is not a generic
arbitrary callback tunnel; it accepts only the closed repository form contract.

3. Drive MRTR manually inside McpClientManager

Keep ClientOptions.inputRequired.autoFulfill disabled. The manager opts an
individual tools/call into SDK manual mode and owns a bounded loop:

  1. send the original call with its validated Tool definition;
  2. accept either a complete result or a validated InputRequiredResult;
  3. if only requestState is present, retry after a small abortable pacing
    delay without opening a user form;
  4. reject unsupported embedded methods or elicitation modes before publishing
    an Interaction;
  5. project every form-mode elicitation/create through the injected form
    handler and collect the bare ElicitResult values under the original keys;
  6. retry the original method and unchanged method-specific params with the current round's
    inputResponses and byte-exact requestState on a fresh request id; and
  7. repeat up to a repository-owned round cap, then fail with one stable
    McpToolCallError.

The manager retains the original validated Tool binding across every leg. A
tool refresh, reconnect, server replacement, provider loss, abort, or deadline
must not silently move the continuation to another binding or connection.

Capability advertisement is per eligible modern call, not a global Client
claim. The dual-era SDK Client must not add elicitation to its base capabilities,
because a legacy negotiation would then falsely advertise support for legacy
push-style elicitation. When and only when a modern tools/call has a complete
form handler, the manager merges { elicitation: { form: {} } } into that
request's self-describing _meta client capabilities; every retry carries the
same claim. A legacy call or a call without the form handler advertises no
elicitation capability and retains the current rejection behavior.

Multiple input requests in one result are fulfilled in deterministic key order
with a bounded count. Responses are sent together on the next retry. An
unsupported sibling request fails the whole round before any partial retry.

4. Compose Desktop and TUI through the same path

Desktop's native capability provider and TUI's MCP capability provider both
adapt the Client Capability requestInteraction callback into the
McpClientManager.callTool() input handler.

Desktop renderer and TUI read the same active Interaction projection and send
the same interaction.answer operation already used for Host-owned questions.
No surface keeps a second authoritative pending-form list. Renderer/TUI reload
that does not retire the exact Client Capability provider can rehydrate the
pending form from the canonical Host snapshot.

Lifecycle and failure semantics

Execution timeout versus user wait

The current Client Capability invocation has a fixed execution timeout. A
human Interaction must not consume that entire provider execution budget.

When an accepted invocation publishes a valid nested Interaction, the broker
enters awaiting_interaction and suspends its provider-execution timer. After a
canonical answer is delivered, it resumes with a fresh bounded provider leg.
Round count, a bounded pending Interaction, and Tool/Turn cancellation prevent
an unbounded loop. Global Stop aborts the Interaction and the MCP call; it does
not synthesize an MCP cancel answer or send another Tool request.

The form's explicit cancel action is different: it is a user response and is
returned to the MCP server as { action: "cancel" }. decline is likewise
returned as { action: "decline" }. The server may then finish, fail, or ask a
later bounded round.

Provider or connection loss

Provider disconnect, registration retirement, server reconnect/replacement,
or invalidation of the frozen Tool binding aborts the pending form and settles
the Tool through the existing Client Capability lost/outcome-unknown rules.
The answer must never be replayed to a replacement provider or MCP connection.
If a provider returns a final result while its nested Interaction is still
pending, the broker closes that Interaction and rejects the inconsistent
invocation rather than leaving an orphan or accepting both outcomes.

Host and surface restart

The initial implementation preserves current Runtime Interaction recovery:

  • a view-only reconnect within the same Host epoch can query and answer the
    pending canonical form (for example, a Desktop renderer reload while the
    main-process provider remains alive);
  • restarting a TUI that also owns the MCP provider retires that provider and
    therefore cannot resume its old invocation;
  • a Runtime Host restart closes a stored pending form as host_restarted;
  • the old in-memory Client Capability invocation and MCP continuation are not
    reconstructed; and
  • Maka does not automatically repeat a Tool call whose external outcome may be
    unknown.

Crash-resuming MRTR would require a durable invocation record containing the
exact Tool binding, original args, provider identity, round state, delivery
acknowledgements, and a safe replay policy. Adding that state only for MRTR
would conflict with the existing Tool recovery boundary, so it is deferred to
a broader durable Tool continuation design.

Security and privacy requirements

  • Advertise MCP form elicitation capability only on a modern tools/call whose
    complete manager/provider/Host/surface path is installed. Never advertise it
    globally to a dual-era Client or on a legacy connection.
  • The UI identifies both the Client Capability provider and the claimed MCP
    server/Tool. Claimed server copy is display-only and never authorization.
  • Form-mode copy and schema are untrusted. Decode exact shapes, enforce flat
    primitive schemas and repository limits, and reject unknown/unsupported
    forms before publication.
  • The form UI warns that passwords, API keys, access tokens, and payment
    credentials must not be entered. MCP requires sensitive flows to use URL
    mode, which is out of scope here.
  • Accepted values do not enter model-visible Tool arguments. They are sent only
    to the exact MCP continuation; a later Tool result may still contain data the
    server chooses to return and follows existing result handling.
  • Form requests and answers are Runtime control facts, not user messages, and
    are not added to model context as conversational content.
  • Canonical form requests and answers follow the existing InteractionStore
    retention boundary. They are not copied into logs or telemetry.
  • requestState is opaque and untrusted. Maka does not parse, mutate, display,
    persist, log, or use it for authorization; it only echoes the exact string to
    the same manager-owned continuation.
  • The manager bounds requestState bytes, input-request count, total embedded
    request bytes, and rounds before retaining state or opening a Host
    Interaction.
  • Stop, stale answer, duplicate answer, cross-Session answer, provider loss,
    and round-limit exhaustion fail closed with no extra MCP retry.
  • An intermediate input_required result does not settle the Runtime Tool
    operation or its durable T1/T2 boundary. Only the final complete result (or
    terminal failure) settles the one logical Tool invocation.

Rollout

This can be implemented as a stack without creating a second authority:

  1. Provider-neutral form Interaction: extend Core, Runtime,
    InteractionStore, Host coordination/projection, Desktop, and TUI; prove
    canonical answer/closure/reload behavior with a synthetic local Tool.
  2. Client Capability nested Interaction: add the bounded reverse frames and
    provider callback; prove exact invocation/connection binding, timer
    suspension, cancellation, and provider-loss behavior with a synthetic
    Client Capability Tool.
  3. MCP form MRTR adapter: add the manual McpClientManager loop, capability
    advertisement, Desktop/TUI composition, modern MCP fixtures, and end-to-end
    acceptance coverage.

Each child PR should be independently reviewable and remain stacked until its
parent lands. None should replace Runtime Host Interaction authority with a
surface-owned modal or an MCP-specific store.

Acceptance criteria

  1. A modern MCP Tool can return one form elicitation, receive an accepted
    typed answer, and complete through a second tools/call.
  2. The retry uses a fresh JSON-RPC id, deep-equal original Tool name/arguments,
    current-round bare inputResponses, and byte-identical requestState.
  3. A two-round Tool preserves one logical Runtime Tool call and produces one
    final Tool result.
  4. accept, decline, and cancel remain distinct end to end.
  5. Multiple bounded form requests in one round are collected under the exact
    server keys; malformed, unknown, or over-limit input fails before retry.
  6. A result containing only requestState can retry, but the round cap still
    terminates a non-progressing server.
  7. Global Stop while a form is pending sends no retry and leaves no pending
    Host Interaction or Client Capability invocation.
  8. Provider disconnect, registration replacement, MCP reconnect, and stale
    Tool binding cannot receive a previously committed answer.
  9. A view-only reconnect in the same Host epoch rehydrates the form while the
    exact provider remains live. TUI/provider restart and Host restart close the
    old continuation and do not replay the Tool; Host restart records
    host_restarted.
  10. The provider execution timeout is suspended only while the exact canonical
    Interaction is pending and is re-armed after settlement.
  11. Unsupported URL elicitation, sampling, roots, prompts/get, and
    resources/read fail with stable local errors and no partial retry.
  12. requestState is absent from Runtime events, Interaction records, IPC/UI
    payloads, logs, telemetry, and model-visible output.
  13. Legacy MCP behavior and a modern Tool that returns a complete result remain
    behaviorally unchanged at the manager boundary.
  14. Desktop and TUI pass the same real-server end-to-end form fixture.
  15. A dual-era auto connection advertises form elicitation on eligible modern
    Tool calls but never advertises legacy push elicitation after falling back.
  16. Provider final-result, failure, cancellation, and release paths close every
    nested Interaction; no invocation can settle while leaving an orphan form.

Non-goals

  • legacy-era push-style server-to-client elicitation;
  • URL-mode elicitation or opening external authorization/payment pages;
  • MCP sampling or roots input requests;
  • MRTR for prompts/get or resources/read;
  • MCP Tasks, Apps, or a generic workflow engine;
  • nested objects, arbitrary JSON Schema, or secret-entry fields;
  • changing Tool authorization, sandbox, or Client Capability trust policy;
  • exposing requestState to the model or user;
  • resuming an accepted Client Capability/MCP side effect after Host or provider
    process restart; or
  • a second surface-local Interaction store.

Alternatives considered

Enable SDK auto-fulfilment and register handlers

The SDK driver correctly handles fresh request ids and retry fields, but its
handlers are registered on the shared Client while Maka's Host continuation is
per Tool invocation. Routing concurrent calls would require hidden ambient
context or serializing an entire MCP connection. The SDK driver also owns the
retry internally, so McpClientManager cannot place its current Tool-binding,
connection-generation, and provider-liveness fences immediately before every
new network side effect. Manual mode still uses the SDK's modern wire decoder;
it keeps the small retry policy explicit at Maka's existing manager seam.

Show a modal directly in Desktop/TUI

This is smaller locally but creates separate pending state per surface, cannot
work consistently with a remote Runtime Host, and can deliver an answer after
the authoritative Tool invocation has disappeared.

Return a Tool error and let the model ask the user/retry

That changes a protocol continuation into a new model decision, cannot safely
preserve opaque requestState, may expose protocol state to the model, and can
repeat side effects or choose different arguments.

Reuse the current multiple-choice question by stringifying fields

This loses numeric/boolean/enum types, validation constraints, defaults, and
the distinct accept/decline/cancel actions. A small closed form contract is
deeper and safer than MCP-specific encoding conventions inside question text.

Persist a complete MRTR continuation immediately

This promises crash recovery before Maka can prove whether an accepted remote
Tool leg is safe to replay. The initial design instead follows the current
host_restarted close/fail-closed boundary and can be deepened with the general
Tool continuation architecture later.

References

Activity

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions