[RFC] Support MCP 2026-07-28 input_required through Runtime Interactions
- Dominant language
- TypeScript
- Stars
- 5.4k
- Forks
- 502
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 715
Description
## 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`](https://github.com/apache/maka/commit/064c3b299f5853c08a0af85c5f798dbce3e8b922).
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:
```text
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:
```text
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:
```json
{
"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:
```json
{
"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:
| Concern | Owner |
| --- | --- |
| MCP wire decoding, original params, round count, `inputResponses`, and exact `requestState` echo | `McpClientManager` |
| Live MCP connection and the actual retry side effect | `McpClientManager` plus the selected SDK transport |
| Client Capability invocation identity and provider connection | Runtime Host Client Capability coordinator/broker |
| Pending user form, canonical answer, stale-answer rejection, and closure | Runtime Host Interaction authority |
| Tool/Turn cancellation | `ToolRuntime` and the exact hosted Run binding |
| Form rendering and user gestures | Desktop/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:
```ts
type InteractionFormField =
| { 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: readonly InteractionFormOption[];
default?: string }
| { kind: "multi_select"; name: string; label: string;
required: boolean; options: readonly InteractionFormOption[];
default?: readonly string[]; minItems?: number; maxItems?: number };
type InteractionFormValue = string | number | boolean | readonly string[];
interface InteractionFormRequest {
kind: "form";
toolUseId: string;
message: string;
requester: InteractionRequesterProjection;
fields: readonly InteractionFormField[];
}
type InteractionFormAnswer =
| { kind: "form"; action: "accept";
values: Readonly> }
| { 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:
```ts
// Client -> Host, only for an accepted live invocation.
interface ClientCapabilityInteractionRequestFrame {
kind: "client.capability.interaction_request";
invocationId: string;
interactionId: string;
request: ClientCapabilityFormRequest;
}
// Host -> the same Client connection and invocation.
interface ClientCapabilityInteractionResultFrame {
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
- [MCP `2026-07-28` release announcement](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/blog/content/posts/2026-07-28-spec-ga/index.md)
- [SEP-2322: Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2322-MRTR.md)
- [MCP MRTR pattern](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/basic/patterns/mrtr.mdx)
- [MCP `2026-07-28` elicitation](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2026-07-28/client/elicitation.mdx)
- [TypeScript SDK `2026-07-28` migration notes](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md)
- [Current `McpClientManager` input-required policy](https://github.com/apache/maka/blob/064c3b299f5853c08a0af85c5f798dbce3e8b922/packages/mcp/src/index.ts#L830-L846)
- [Current modern input-required regression](https://github.com/apache/maka/blob/064c3b299f5853c08a0af85c5f798dbce3e8b922/packages/mcp/src/__tests__/modern-manager.test.ts#L124-L141)
- [Current Client Capability protocol](https://github.com/apache/maka/blob/064c3b299f5853c08a0af85c5f798dbce3e8b922/packages/runtime-host/src/protocol/client-capability.ts#L69-L221)
- [Current Runtime Interaction contract](https://github.com/apache/maka/blob/064c3b299f5853c08a0af85c5f798dbce3e8b922/packages/core/src/interaction.ts#L40-L128)
- [Current MCP Client Capability provider](https://github.com/apache/maka/blob/064c3b299f5853c08a0af85c5f798dbce3e8b922/packages/cli/src/mcp-capability-provider.ts#L35-L113)
- [Post-V3 MCP roadmap tracker #4329](https://github.com/apache/maka/issues/4329)
Contributor guide
Assessment
This issue has not been assessed yet.