This is an automated email from the ASF dual-hosted git repository.
jamesbognar pushed a commit to branch docs
in repository https://gitbox.apache.org/repos/asf/juneau.git
The following commit(s) were added to refs/heads/docs by this push:
new 36b38d776b docs: MCP client-side MRTR auto-resume ergonomics (TODO-326)
36b38d776b is described below
commit 36b38d776b6c5c683db1a4071c178a3a5cb9d2d7
Author: James Bognar <[email protected]>
AuthorDate: Wed Aug 5 18:05:35 2026 -0700
docs: MCP client-side MRTR auto-resume ergonomics (TODO-326)
Co-authored-by: Cursor <[email protected]>
---
pages/topics/11.03.JuneauMcpRecipes.md | 21 +++++++++++++++++++++
pages/topics/11.05.JuneauRestClientMcp.md | 30 ++++++++++++++++++++++++++++--
2 files changed, 49 insertions(+), 2 deletions(-)
diff --git a/pages/topics/11.03.JuneauMcpRecipes.md
b/pages/topics/11.03.JuneauMcpRecipes.md
index 4942e7f7c6..f03d77e76e 100644
--- a/pages/topics/11.03.JuneauMcpRecipes.md
+++ b/pages/topics/11.03.JuneauMcpRecipes.md
@@ -231,6 +231,27 @@ McpToolHandler confirmDeleteTool = McpToolHandler.of(
This requires no special server config beyond a v2 servlet/mixin — MRTR
support is on by default (see `McpOptions.mrtr(Consumer<McpMrtrConfig>)` in the
[server reference](/docs/topics/JuneauRestServerMcp) to customize the codec,
TTL, or round cap).
+### Client side: answering the elicitation
+
+On the client, `McpClient.callToolWithElicitation(...)` (and its
`getPromptWithElicitation`/`readResourceWithElicitation` siblings) drives the
whole pause/resume loop for you — supply an `McpElicitationHandler` and get
back the terminal typed result. No hand-managed `requestState`/`inputResponses`:
+
+```java
+import java.util.*;
+
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.rest.client.mcp.v20260728.*;
+
+var result = client.callToolWithElicitation("confirm-delete", Map.of(),
requests -> {
+ var answers = new LinkedHashMap<String,ElicitResult>();
+ requests.forEach((id, req) -> // req.getMessage() → "Proceed with
deletion?"
+ answers.put(id, new
ElicitResult().setAction(ElicitAction.ACCEPT).putContent("confirm", true)));
+ return answers;
+});
+// result is a CallToolResult; the input_required round(s) were answered and
resumed transparently.
+```
+
+The handler is invoked once per `input_required` pause and may answer several
requests in one round. Return an `ElicitResult` with
`ElicitAction.DECLINE`/`CANCEL` to refuse (the server decides the outcome). The
loop is bounded by `McpClient.DEFAULT_MAX_ELICITATION_ROUNDS` (override via a
`maxRounds` argument); exceeding it throws `McpElicitationLimitException`. For
full manual control, `callRaw(...)` +
`ElicitationRequests`/`ElicitationResponses` remain available — see the [client
refere [...]
+
## MRTR key management (v2 only)
By default, MRTR `requestState` tokens are sealed under a random key generated
once per process (`EphemeralKeyProvider`, the zero-config default beneath
`AeadRequestStateCodec`) — deliberately **not shareable across instances and
not durable across a restart** (see the [server
reference](/docs/topics/JuneauRestServerMcp#key-management-keyprovider)). A
load-balanced deployment, where RESUME can land on a different node than the
PAUSE, needs a shared sealing key instead.
diff --git a/pages/topics/11.05.JuneauRestClientMcp.md
b/pages/topics/11.05.JuneauRestClientMcp.md
index a6ef98a67c..dd73b00d8f 100644
--- a/pages/topics/11.05.JuneauRestClientMcp.md
+++ b/pages/topics/11.05.JuneauRestClientMcp.md
@@ -31,7 +31,33 @@ The illustrative server-initiated method name used elsewhere
in this codebase's
## Elicitation (MCP `2026-07-28`, SEP-2322)
-**v2-only.** Two static helper classes in
`org.apache.juneau.rest.client.mcp.v20260728` drive elicitation over
`McpClient.callRaw(...)`'s raw `Map<String,Object>` result — the typed
`callTool`/`getPrompt`/`readResource` methods cannot represent a paused
`input_required` result or set `requestState`/`inputResponses` on a resume
call, so a caller must go through `callRaw(...)` for the resumable leg of the
loop:
+**v2-only.** A `tools/call`, `prompts/get`, or `resources/read` handler can
pause with an `input_required` result to elicit more input (Multi-Round-Trip
Requests). The client offers two ways to consume that loop: an ergonomic
**auto-resume** helper for the common case, and a raw **manual** path as the
escape hatch.
+
+### Auto-resume (recommended)
+
+`McpClient.callToolWithElicitation(...)`, `getPromptWithElicitation(...)`, and
`readResourceWithElicitation(...)` take an `McpElicitationHandler` and drive
the whole loop for you: they detect each `input_required` pause, decode that
round's requests, invoke your handler for the answers, echo them back with the
carried `requestState`, and repeat until a terminal result — which they return
already decoded into the same typed result bean
(`CallToolResult`/`GetPromptResult`/`ReadResourceResu [...]
+
+```java
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.rest.client.mcp.v20260728.*;
+
+// One call. The handler is invoked once per input_required pause and answers
that round's
+// requests (there may be more than one), keyed by the same server-assigned
ids.
+var result = client.callToolWithElicitation("confirm", Map.of(), requests -> {
+ var answers = new LinkedHashMap<String,ElicitResult>();
+ requests.forEach((id, req) -> // req.getMessage() is the prompt to
show the end user
+ answers.put(id, new
ElicitResult().setAction(ElicitAction.ACCEPT).putContent("confirm", true)));
+ return answers; // Map<String,ElicitResult>
+});
+```
+
+- **Multiple requests per round** — a round's `requests` map may hold more
than one entry; the handler answers all of them and returns one keyed map.
+- **Decline / cancel** — return an `ElicitResult` with `ElicitAction.DECLINE`
or `ElicitAction.CANCEL` for a request. Refusals are echoed back like any other
answer (the *server* decides the terminal outcome of a refused elicitation);
the client does not short-circuit locally.
+- **Bounded** — the loop is capped at
`McpClient.DEFAULT_MAX_ELICITATION_ROUNDS` (8) rounds; a `maxRounds` overload
sets your own bound. A server that keeps pausing past the cap throws
`McpElicitationLimitException` (with `getMaxRounds()`) rather than looping
forever.
+
+### Manual resume (escape hatch)
+
+For full control — or to resume a call you issued through `callRaw(...)`
yourself — two static helper classes in
`org.apache.juneau.rest.client.mcp.v20260728` work directly on `callRaw`'s raw
`Map<String,Object>` result:
| Helper | Purpose |
|---|---|
@@ -55,7 +81,7 @@ if (ElicitationRequests.isInputRequired(paused)) {
}
```
-Both helpers work at the raw map/JSON level rather than exposing typed
overloads per concrete request bean, because
`CallToolRequest`/`GetPromptRequest`/`ReadResourceRequest` share no common
"has-`inputResponses`-and-`requestState`" interface — a caller resuming a
specific one of the three makes its own final
`.setInputResponses(...).setRequestState(...)` call on the concrete bean it
already knows it holds.
`ElicitationResponses.toInputResponses(Map<String,ElicitResult>)` encodes
several [...]
+Both helpers work at the raw map/JSON level rather than exposing typed
overloads per concrete request bean, because
`CallToolRequest`/`GetPromptRequest`/`ReadResourceRequest` share no common
"has-`inputResponses`-and-`requestState`" interface — a caller resuming a
specific one of the three makes its own final
`.setInputResponses(...).setRequestState(...)` call on the concrete bean it
already knows it holds.
`ElicitationResponses.toInputResponses(Map<String,ElicitResult>)` encodes
several [...]
See
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp#elicitation-mcp-2026-07-28-sep-2322)
for the server-side helper half of this loop.