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 ef3c6e9d03 docs: MCP example module (juneau-examples-mcp) + v2
adoption conveniences
ef3c6e9d03 is described below
commit ef3c6e9d035d58e101f0f36a6f525dc961189786
Author: James Bognar <[email protected]>
AuthorDate: Thu Aug 6 04:49:23 2026 -0700
docs: MCP example module (juneau-examples-mcp) + v2 adoption conveniences
Co-authored-by: Cursor <[email protected]>
---
artifact-paths.json | 2 ++
pages/release-notes/10.0.0.md | 27 +++++++++++++++++++++++++++
pages/topics/11.03.JuneauMcpRecipes.md | 20 ++++++++++++++++----
pages/topics/11.04.JuneauRestServerMcp.md | 8 +++++++-
pages/topics/11.05.JuneauRestClientMcp.md | 4 ++++
5 files changed, 56 insertions(+), 5 deletions(-)
diff --git a/artifact-paths.json b/artifact-paths.json
index 30f2a7b50b..8a0787658e 100644
--- a/artifact-paths.json
+++ b/artifact-paths.json
@@ -22,6 +22,7 @@
"juneau-distrib": "juneau-distrib",
"juneau-examples": "juneau-examples",
"juneau-examples-core": "juneau-examples/juneau-examples-core/build-overlay",
+ "juneau-examples-mcp": "juneau-examples/juneau-examples-mcp/build-overlay",
"juneau-integration-tests": "juneau-integration-tests",
"juneau-marshall": "juneau-core/juneau-marshall",
"juneau-marshall-rdf": "juneau-core/juneau-marshall-rdf",
@@ -45,6 +46,7 @@
"juneau-rest-client-classic": "juneau-rest/juneau-rest-client-classic",
"juneau-rest-client-jetty": "juneau-rest/juneau-rest-client-jetty",
"juneau-rest-client-mcp": "juneau-rest/juneau-rest-client-mcp",
+ "juneau-rest-client-mcp-auth": "juneau-rest/juneau-rest-client-mcp-auth",
"juneau-rest-client-mcp-v20250618":
"juneau-rest/juneau-rest-client-mcp-v20250618",
"juneau-rest-client-mcp-v20260728":
"juneau-rest/juneau-rest-client-mcp-v20260728",
"juneau-rest-client-okhttp": "juneau-rest/juneau-rest-client-okhttp",
diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index 40f427abfa..32942e6cfd 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -987,6 +987,33 @@ See
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp#replay-protection-
See the [OAuth 2.1
recipe](/docs/topics/JuneauMcpRecipes#securing-an-mcp-client-and-server-with-oauth-21-v2-only)
(end-to-end client + server), the [DCR + step-up
recipes](/docs/topics/JuneauMcpRecipes#dynamic-client-registration-and-step-up-v2-only),
the [juneau-rest-client-mcp auth
reference](/docs/topics/JuneauRestClientMcp#oauth-21-authorization-juneau-rest-client-mcp-auth),
and the [juneau-rest-server-mcp resource-server
reference](/docs/topics/JuneauRestServerMcp#oauth-21-resource- [...]
+### MCP `2026-07-28` — first-party runnable example module
(`juneau-examples-mcp`) (TODO-312)
+
+A new example module, `juneau-examples-mcp` (under `juneau-examples/`,
alongside `juneau-examples-core`), ships the single highest-value adoption
artifact for the now-complete MCP framework above: a **runnable, copy-me
reference implementation** of both the MCP server and client sides, built
against the `2026-07-28` revision. One cohesive in-memory "notes" service
(`NoteStore`, a `title -> body` map) is exercised end to end by every headline
surface:
+
+- **tool** — `publishNote(title, body)`, which stores a note and pushes a
`resources/updated` change to subscribers.
+- **tool + elicitation/MRTR** — `deleteNote(title)`, which pauses with
`input_required` to confirm before resuming to delete.
+- **prompt + completion** — `summarize(title)`, whose `title` argument offers
`completion/complete` suggestions from existing note titles.
+- **resource** — the fixed `note:///index` resource, listing all note titles.
+- **resource template + completion** — `note:///{title}`, reading one note by
title, with the same title-completion behavior on its `{title}` variable.
+- **subscription (SEP-2575)** — `subscriptions/listen` held-open SSE,
delivering the `resources/updated` change `publishNote` fires.
+
+`ExampleMcpServer` (a v2 `McpRestServlet`) registers every surface above;
`ExampleServer` is an embedded-Jetty launcher (`Microservice` +
`JettyConfiguration`, `main(...)` plus a `start(port)` handle); `ExampleClient`
is a guided, numbered `McpClient` walkthrough printing each step's
request/response top-to-bottom like a tutorial; and a Spring Boot variant
(`SpringExampleMcpServer`/`SpringExampleApplication`) uses
`SpringMcpRestServlet` to resolve a Spring-managed `GreetingService` throu [...]
+
+See the [MCP
Recipes](/docs/topics/JuneauMcpRecipes#full-example-juneau-examples-mcp),
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp), and
[juneau-rest-client-mcp](/docs/topics/JuneauRestClientMcp) topics for pointers
to the module, and the module's own `README.md` for run instructions.
+
+### MCP `2026-07-28` — client/server adoption convenience methods
+
+Four small, additive convenience methods land alongside the example module
above, each replacing a small piece of repetitive code the example (and
real-world adopters) would otherwise repeat at every call site. None deprecates
or replaces the lower-level API it sits on top of.
+
+- **`CallToolResult.firstText()`** (`juneau-bean-mcp-v20260728`) — returns the
text of the first `TextContent` block found while scanning the result's
`content` list in order, or `null` if the list is empty/unset or contains no
`TextContent` block. Kills the `((TextContent)
result.getContent().get(i)).getText()` cast-and-scan a caller who only wants
the tool's text result would otherwise repeat everywhere.
+- **`McpClient.callToolText(String name, Map<String,Object> arguments)`**
(`juneau-rest-client-mcp-v20260728`) — convenience for `callTool(...)` followed
by `CallToolResult.firstText()`; null-safe if `callTool(...)` itself returns a
`null` result.
+- **`ServerDiscoverResult.getServerInfo()`** (`juneau-bean-mcp-v20260728`) —
null-safe shortcut for `getMeta() == null ? null : getMeta().getServerInfo()`,
so a caller doesn't have to null-check the intermediate `_meta` envelope just
to reach the server's `Implementation` identity. `@BeanIgnore`d so it does not
add a second, top-level `serverInfo` member to the `server/discover` wire
format.
+- **`McpMrtrResumeContext.continuationAsString()`**
(`juneau-rest-server-mcp-v20260728`) — shorthand for
`continuationAs(String.class)`, for the common case of a plain `String`
continuation.
+- **`ElicitationResponses.getBoolean(ctx, id, field)` / `getString(ctx, id,
field)`** (`juneau-rest-server-mcp-v20260728`) — decline/omission-safe typed
reads of a single field from an elicitation answer: a missing id, a missing
field, or a non-`ACCEPT` action (declined/cancelled) all resolve to a safe
default (`false` for `getBoolean`, `null` for `getString`) instead of requiring
the caller to null-check `get(...)` and its content map by hand.
+
+See the [confirm-delete
recipe](/docs/topics/JuneauMcpRecipes#elicitation--multi-round-trip-requests-v2-only)
and the [client
recipe](/docs/topics/JuneauMcpRecipes#client-call-read-and-handle-errors) in
MCP Recipes, the
[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp#elicitation-mcp-2026-07-28-sep-2322)
elicitation reference, and the
[juneau-rest-client-mcp](/docs/topics/JuneauRestClientMcp) client reference for
these methods in context.
+
### MCP `2025-06-18` — resource-not-found error code corrected to `-32002`
(TODO-332)
`2025-06-18`-only bug fix. A `resources/read` for a URI the server does not
serve now returns the JSON-RPC error code **`-32002`** ("Resource not found") —
the `2025-06-18` spec's dedicated missing-resource code — instead of the
previous generic **`-32601`** ("method not found"). This was one of the four
"not found" kinds that the re-layered `2025-06-18` `McpRevision` initially
collapsed onto `-32601` (see the `juneau-rest-server-mcp-v20250618` module
notes above); resource-not-found is [...]
diff --git a/pages/topics/11.03.JuneauMcpRecipes.md
b/pages/topics/11.03.JuneauMcpRecipes.md
index f03d77e76e..5613f58f42 100644
--- a/pages/topics/11.03.JuneauMcpRecipes.md
+++ b/pages/topics/11.03.JuneauMcpRecipes.md
@@ -5,6 +5,7 @@ slug: JuneauMcpRecipes
Copy-pasteable snippets for common MCP tasks, targeting revision `2026-07-28`
unless noted. Each recipe is self-contained: assume `config` is an
`McpServerConfig` you're building up (as in the
[Quickstart](/docs/topics/JuneauMcpQuickstart)), and paste imports as shown.
+- [Full example: `juneau-examples-mcp`](#full-example-juneau-examples-mcp)
- [Writing a tool](#writing-a-tool)
- [Writing a prompt](#writing-a-prompt)
- [Serving a resource](#serving-a-resource)
@@ -17,6 +18,10 @@ Copy-pasteable snippets for common MCP tasks, targeting
revision `2026-07-28` un
- [Securing an MCP client and server with OAuth 2.1 (v2
only)](#securing-an-mcp-client-and-server-with-oauth-21-v2-only)
- [Dynamic client registration and step-up (v2
only)](#dynamic-client-registration-and-step-up-v2-only)
+## Full example: `juneau-examples-mcp`
+
+Before diving into individual recipes, consider starting from
[`juneau-examples-mcp`](https://github.com/apache/juneau/tree/master/juneau-examples/juneau-examples-mcp)
— a complete, runnable "notes" service exercising every recipe on this page
(and more) in one small, copy-me Maven module targeting revision `2026-07-28`:
a `publishNote` tool, a `deleteNote` tool that walks a full elicitation/MRTR
confirm-then-resume round-trip, a `summarize` prompt with argument completion,
a `note:///in [...]
+
## Writing a tool
**Raw, via `McpToolHandler.of(...)`** — full control over the input schema:
@@ -216,8 +221,7 @@ McpToolHandler confirmDeleteTool = McpToolHandler.of(
var question = new ElicitRequest().setMessage("Proceed with
deletion?").setRequestedSchema(schema);
throw ElicitationRequests.of("confirm", question,
"confirm-delete-continuation");
}
- var answer = ElicitationResponses.get(resume.get(), "confirm");
- if (answer == null || answer.getAction() != ElicitAction.ACCEPT || !
Boolean.TRUE.equals(answer.getContent().get("confirm")))
+ if (! ElicitationResponses.getBoolean(resume.get(), "confirm",
"confirm"))
return McpToolOutcome.text("Deletion cancelled.");
// ... perform the deletion here ...
return McpToolOutcome.text("Deleted.");
@@ -226,8 +230,8 @@ McpToolHandler confirmDeleteTool = McpToolHandler.of(
```
- On the first round, `ctx.getBean(McpMrtrResumeContext.class)` is empty, so
the handler throws `ElicitationRequests.of(id, request, continuation)` — an
`McpInputRequiredSignal` (a `RuntimeException`) that the dispatcher turns into
an `input_required` pause.
-- The client answers, then re-sends the call; on that round,
`McpMrtrResumeContext` is present, and `ElicitationResponses.get(ctx, id)`
decodes the typed `ElicitResult` keyed by the same `id` you posed the question
under.
-- `ElicitResult.getContent()` is populated only when `getAction() ==
ElicitAction.ACCEPT`; a `DECLINE`/`CANCEL` answer carries no content.
+- The client answers, then re-sends the call; on that round,
`McpMrtrResumeContext` is present. `ElicitationResponses.getBoolean(ctx, id,
field)` reads a single boolean field decline/omission-safely in one call — a
missing id, a missing field, or a non-`ACCEPT` action (declined/cancelled) all
resolve to `false`, so there's no need to null-check `get(...)` and its content
map by hand for a plain confirm/deny question.
`ElicitationResponses.getString(ctx, id, field)` is the equivalent for [...]
+- If a handler needs to carry state across rounds (e.g. which record to
delete), stash it in the `continuation` argument to
`ElicitationRequests.of(...)` and recover it on resume via
`resume.get().continuationAsString()` (or `continuationAs(Class)` for a
non-`String` shape) — see [MRTR resume
contexts](/docs/topics/JuneauRestServerMcp#resuming-the-two-beanstore-injected-handler-contexts)
in the server reference.
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).
@@ -415,12 +419,18 @@ import org.apache.juneau.bean.jsonrpc.McpException;
import org.apache.juneau.rest.client.mcp.v20260728.McpClient;
try (var client = McpClient.connect("http://localhost:8080/mcp")) {
+ System.out.println("Connected to: " +
client.discoveredServer().getServerInfo());
+
var tools = client.listTools();
System.out.println(tools);
var callResult = client.callTool("weather", Map.of("city", "Seattle"));
System.out.println(callResult.getContent());
+ // Just want the tool's text? callToolText(...) is callTool(...) +
CallToolResult.firstText() in one
+ // call -- no manual ((TextContent)
callResult.getContent().get(0)).getText() cast-and-scan needed.
+ System.out.println(client.callToolText("weather", Map.of("city",
"Seattle")));
+
var resource = client.readResource("file:///readme.txt");
System.out.println(resource.getContents());
} catch (McpException e) {
@@ -432,6 +442,8 @@ try (var client =
McpClient.connect("http://localhost:8080/mcp")) {
`McpClient` is `Closeable`, so try-with-resources closes the underlying
transport when the block exits, on either the success or error path. Every
typed call (`callTool`, `getPrompt`, `readResource`, `listTools`,
`listPrompts`, `listResources`, `listResourceTemplates`, `complete`) throws
`McpException` for a JSON-RPC error response and `IOException` for a
transport/(de)serialization failure — there's no single unified exception type,
so a caller that needs both should catch both, as shown.
+`ServerDiscoverResult.getServerInfo()` and `McpClient.callToolText(...)` are
additive convenience methods: the former is a null-safe shortcut to the
server's `Implementation` identity off the `server/discover` result that
`connect(...)` (and `serverDiscover()`) already cache, and the latter folds
`callTool(...)` + `CallToolResult.firstText()` into one call for the common
case of wanting just a tool's rendered text. Neither replaces the lower-level
`getContent()`/`getMeta()` access shown [...]
+
## Securing an MCP client and server with OAuth 2.1 (v2 only)
This recipe walks both sides of an OAuth 2.1 / MCP `2026-07-28` deployment as
one story: turn a v2 MCP endpoint into a protected resource server, then
acquire a token on the client and call it. Authorization is **v2-only** and
**off by default** — an endpoint that doesn't opt in behaves exactly as before.
diff --git a/pages/topics/11.04.JuneauRestServerMcp.md
b/pages/topics/11.04.JuneauRestServerMcp.md
index 987735473d..de3c122c73 100644
--- a/pages/topics/11.04.JuneauRestServerMcp.md
+++ b/pages/topics/11.04.JuneauRestServerMcp.md
@@ -6,6 +6,10 @@ slug: JuneauRestServerMcp
New to MCP in Juneau? Start with the [MCP
Quickstart](/docs/topics/JuneauMcpQuickstart) and [MCP
Recipes](/docs/topics/JuneauMcpRecipes) — this page is the deep reference for
lookup once you're past the basics.
+:::tip Want a runnable, end-to-end example instead?
+[`juneau-examples-mcp`](https://github.com/apache/juneau/tree/master/juneau-examples/juneau-examples-mcp)
is a complete, copy-me Maven module exercising every major surface on this
page — tools, elicitation/MRTR, prompts + completion, resources + resource
templates, and subscriptions — against a tiny in-memory "notes" service,
targeting revision `2026-07-28`. It ships as a downloadable project zip in the
Apache release. See [MCP
Recipes](/docs/topics/JuneauMcpRecipes#full-example-juneau- [...]
+:::
+
`juneau-rest-server-mcp` is a **revision-neutral core** for exposing a [Model
Context Protocol (MCP)](https://modelcontextprotocol.io/) JSON-RPC endpoint on
`juneau-rest-server`. On its own it has zero compile-time knowledge of any MCP
protocol revision; a protocol revision is supplied by a separate adapter
module. Today that's **`juneau-rest-server-mcp-v20250618`**, which binds the
core to the `2025-06-18` wire beans in
[`juneau-bean-mcp-v20250618`](/docs/topics/JuneauBeanMcp). This pag [...]
## Overview
@@ -763,7 +767,7 @@ The dispatcher catches the signal, seals the continuation
(plus the method, a 1-
On a resume call the same handler runs again; two immutable beans in the
per-request `BeanStore` let it detect and react to the round:
-- **`McpMrtrResumeContext`** — stashed **only** on a resume call. Carries
`continuation()` (the decoded value, as generic JSON — use
`continuationAs(Class)` to recover a typed view) and `inputResponses()` (the
caller's answers). A handler detects resume via
`ctx.getBean(McpMrtrResumeContext.class)` returning a present `Optional`;
absent means a first-round call.
+- **`McpMrtrResumeContext`** — stashed **only** on a resume call. Carries
`continuation()` (the decoded value, as generic JSON — use
`continuationAs(Class)` to recover a typed view) and `inputResponses()` (the
caller's answers). A handler detects resume via
`ctx.getBean(McpMrtrResumeContext.class)` returning a present `Optional`;
absent means a first-round call. For the common case of a `String`
continuation, `continuationAsString()` is a shorthand for
`continuationAs(String.class)`.
- **`McpMrtrCapabilityContext`** — stashed on **every** in-scope call
(first-round and resume), exposing `elicitationSupported()` so a handler can
pre-check and degrade gracefully (return a normal result or a domain error)
instead of throwing a pause into a client that can't handle it.
Only the sealed `continuation` is tamper-evident across rounds; the per-round
`arguments` are re-sent by the client and fully client-controlled, so anything
a handler must trust across rounds belongs *inside* the continuation, never
re-read from the arguments.
@@ -877,6 +881,8 @@ var answer = ElicitationResponses.get(resume.get(),
"confirm");
Both helpers support the multi-question case identically:
`ElicitationRequests.of(Map<String,ElicitRequest>, continuation)` places
several questions in one round, and `ElicitationResponses.all(resume.get())`
returns every answer keyed the same way — no extra round trip is needed to ask
(or answer) more than one question at once.
+For the common case of reading a single typed field — a boolean confirm/deny
or a string answer — `ElicitationResponses.getBoolean(ctx, id, field)` and
`getString(ctx, id, field)` skip the `get(...)` + null-check + content-map
dance: a missing id, a missing field, or a non-`ACCEPT` action
(declined/cancelled) all resolve to a safe default (`false` for `getBoolean`,
`null` for `getString`) rather than requiring the caller to null-check by hand.
See the [confirm-delete recipe](/docs/topics [...]
+
See
[juneau-bean-mcp-v20260728](/docs/topics/JuneauBeanMcp#mcp-2026-07-28-elicitation)
for `ElicitRequest`/`ElicitResult`/`ElicitAction`/`ElicitSchema`'s wire shapes
and the `ElicitSchema` builder API in full.
## Related Modules
diff --git a/pages/topics/11.05.JuneauRestClientMcp.md
b/pages/topics/11.05.JuneauRestClientMcp.md
index dd73b00d8f..dc2a061dde 100644
--- a/pages/topics/11.05.JuneauRestClientMcp.md
+++ b/pages/topics/11.05.JuneauRestClientMcp.md
@@ -6,6 +6,10 @@ slug: JuneauRestClientMcp
New to MCP in Juneau? Start with the [MCP
Quickstart](/docs/topics/JuneauMcpQuickstart) and [MCP
Recipes](/docs/topics/JuneauMcpRecipes) — this page is the deep reference for
lookup once you're past the basics.
+:::tip Want a runnable, end-to-end example instead?
+[`juneau-examples-mcp`](https://github.com/apache/juneau/tree/master/juneau-examples/juneau-examples-mcp)
is a complete, copy-me Maven module pairing a `2026-07-28` MCP server with a
guided `McpClient` walkthrough (`ExampleClient`) that exercises every client
call covered by this reference — discovery, tool calls, resource/template
reads, completions, prompts, subscriptions, and an elicitation round-trip. It
ships as a downloadable project zip in the Apache release. See [MCP
Recipes](/do [...]
+:::
+
`juneau-rest-client-mcp` is the revision-neutral MCP client core built on
`juneau-rest-client`, with two dated adapters:
`juneau-rest-client-mcp-v20250618` and `juneau-rest-client-mcp-v20260728`.
## Overview