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 2ac3576c0b docs: MCP subscriptions/listen + adoption-first
landing/quickstart/recipes (TODO-312c8)
2ac3576c0b is described below
commit 2ac3576c0b1eb73dac540ab2d5edd9f14063a117
Author: James Bognar <[email protected]>
AuthorDate: Tue Aug 4 13:17:46 2026 -0700
docs: MCP subscriptions/listen + adoption-first landing/quickstart/recipes
(TODO-312c8)
Adds an adoption-focused MCP documentation set and documents the 2026-07-28
subscriptions/listen server->client push surface.
- New 11.00 landing, 11.01 quickstart, 11.02 choosing-setup guide, 11.03
recipes.
- Relocated the server/client reference pages to 11.04/11.05.
- Examples verified against the real API.
Co-authored-by: Cursor <[email protected]>
---
pages/topics/11.00.JuneauMcp.md | 21 ++
pages/topics/11.01.JuneauMcpQuickstart.md | 123 ++++++++
pages/topics/11.02.JuneauMcpChoosingSetup.md | 115 +++++++
pages/topics/11.03.JuneauMcpRecipes.md | 335 +++++++++++++++++++++
...stServerMcp.md => 11.04.JuneauRestServerMcp.md} | 12 +-
...stClientMcp.md => 11.05.JuneauRestClientMcp.md} | 4 +-
sidebars.ts | 42 ++-
7 files changed, 639 insertions(+), 13 deletions(-)
diff --git a/pages/topics/11.00.JuneauMcp.md b/pages/topics/11.00.JuneauMcp.md
new file mode 100644
index 0000000000..711cefb34f
--- /dev/null
+++ b/pages/topics/11.00.JuneauMcp.md
@@ -0,0 +1,21 @@
+---
+id: 11.juneau-mcp
+title: "MCP (Model Context Protocol)"
+slug: JuneauMcp
+---
+
+Juneau provides both server-side and client-side support for the [Model
Context Protocol (MCP)](https://modelcontextprotocol.io/), a JSON-RPC 2.0
protocol that lets AI assistants and agents discover and invoke external
**tools**, **prompts**, and **resources**.
+
+Two protocol revisions are supported side-by-side as separate, revision-bound
Maven modules: `2025-06-18` and `2026-07-28` (the current revision). A
client/server pair picks one revision at compile time — see [Choosing your MCP
setup](/docs/topics/JuneauMcpChoosingSetup) for the full comparison.
+
+## Where to start
+
+- **[MCP Quickstart](/docs/topics/JuneauMcpQuickstart)** — a working server
and client talking to each other in about 5 minutes.
+- **[Choosing your MCP setup](/docs/topics/JuneauMcpChoosingSetup)** —
protocol revision, dedicated servlet vs mixin, plain vs Spring Boot.
+- **[MCP Recipes](/docs/topics/JuneauMcpRecipes)** — copy-pasteable snippets
for tools, prompts, resources, resource templates, completions, error handling,
elicitation, and subscriptions.
+
+## Deep reference
+
+- **[juneau-rest-server-mcp](/docs/topics/JuneauRestServerMcp)** — full
server-side API reference: capabilities, pagination, notifications, cache
hints, tracing, Multi-Round-Trip Requests, elicitation, and subscriptions.
+- **[juneau-rest-client-mcp](/docs/topics/JuneauRestClientMcp)** — full
client-side API reference: the duplex server-request channel, auth, and
response caching.
+- **[juneau-bean-mcp-v20250618](/docs/topics/JuneauBeanMcp)** — the MCP
wire-bean catalog underlying both the server and client modules.
diff --git a/pages/topics/11.01.JuneauMcpQuickstart.md
b/pages/topics/11.01.JuneauMcpQuickstart.md
new file mode 100644
index 0000000000..fe95b6f893
--- /dev/null
+++ b/pages/topics/11.01.JuneauMcpQuickstart.md
@@ -0,0 +1,123 @@
+---
+title: "MCP Quickstart"
+slug: JuneauMcpQuickstart
+---
+
+Get a working [Model Context Protocol (MCP)](https://modelcontextprotocol.io/)
server and client talking to each other in about 5 minutes, using MCP revision
`2026-07-28` (the current revision — see [Choosing your MCP
setup](/docs/topics/JuneauMcpChoosingSetup) if you need `2025-06-18` instead).
+
+## 1. Add the dependency
+
+```xml
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-server-mcp-v20260728</artifactId>
+ <version>${juneau.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-client-mcp-v20260728</artifactId>
+ <version>${juneau.version}</version>
+</dependency>
+```
+
+The server artifact transitively pulls in the revision-neutral
`juneau-rest-server-mcp` core plus the `2026-07-28` wire beans
(`juneau-bean-mcp-v20260728`). The client artifact does the same on the client
side.
+
+## 2. Write a minimal server
+
+Extend the concrete, revision-bound `McpRestServlet` and register one tool.
`McpToolHandler.of(...)` wires a descriptor and a call body together in one
expression — `McpToolHandler` has two abstract methods, so it can't be
satisfied by a bare lambda:
+
+```java
+package com.example;
+
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet;
+
+@Rest(path="/mcp")
+public class EchoMcpServlet extends McpRestServlet {
+
+ @Override
+ protected McpServerConfig createMcpConfig() {
+ return new McpServerConfig()
+ .setName("echo-server")
+ .setVersion("1.0.0")
+ .addTool(McpToolHandler.of(
+ new McpToolSpec().setName("echo").setDescription("Echoes the
supplied text back."),
+ (arguments, ctx) ->
McpToolOutcome.text(String.valueOf(arguments.getOrDefault("text", "")))
+ ));
+ }
+}
+```
+
+Mount `EchoMcpServlet` like any other `@Rest` resource (Jetty microservice,
`web.xml`, or Spring Boot — see [Choosing your MCP
setup](/docs/topics/JuneauMcpChoosingSetup)).
+
+## 3. Write a minimal client
+
+`McpClient.connect(endpoint)` builds a client and performs its one mandatory
`server/discover` handshake in a single call — `2026-07-28` has no `initialize`
method, so this is the call that replaces it:
+
+```java
+package com.example;
+
+import java.util.*;
+
+import org.apache.juneau.rest.client.mcp.v20260728.McpClient;
+
+public class EchoMcpClientDemo {
+ public static void main(String[] args) throws Exception {
+ try (var client = McpClient.connect("http://localhost:8080/mcp")) {
+ System.out.println(client.listTools());
+ System.out.println(client.callTool("echo", Map.of("text",
"hello")));
+ }
+ }
+}
+```
+
+`McpClient` implements `Closeable`, so try-with-resources closes the
underlying transport for you.
+
+## 4. Talk to it with raw curl / JSON-RPC
+
+MCP `2026-07-28` (SEP-2243) requires every request to carry `Mcp-Method` and
`Mcp-Name` HTTP headers alongside the JSON-RPC body — `Mcp-Method` echoes the
JSON-RPC `method`, and `Mcp-Name` is the routed tool/prompt name (for
`tools/call`/`prompts/get`) or resource URI (for `resources/read`); it's empty
for every other method:
+
+```bash
+curl -s http://localhost:8080/mcp \
+ -H "Content-Type: application/json" \
+ -H "Mcp-Method: tools/call" \
+ -H "Mcp-Name: echo" \
+ -d '{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "method": "tools/call",
+ "params": {
+ "name": "echo",
+ "arguments": {"text": "hello"},
+ "_meta": {
+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
+ "io.modelcontextprotocol/clientCapabilities": {}
+ }
+ }
+ }'
+```
+
+Response:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "id": 1,
+ "result": {
+ "resultType": "complete",
+ "_meta": {
+ "io.modelcontextprotocol/serverInfo": {"name": "echo-server", "version":
"1.0.0"}
+ },
+ "content": [{"type": "text", "text": "hello"}]
+ }
+}
+```
+
+A request missing the required `_meta.protocolVersion`/`clientCapabilities`,
or missing the `Mcp-Method`/`Mcp-Name` headers, is rejected with a JSON-RPC
`-32600` error before it ever reaches your tool handler.
+
+## Next steps
+
+- **[Choosing your MCP setup](/docs/topics/JuneauMcpChoosingSetup)** —
`2025-06-18` vs `2026-07-28`, dedicated servlet vs mixin, plain vs Spring Boot.
+- **[MCP Recipes](/docs/topics/JuneauMcpRecipes)** — copy-pasteable snippets
for typed tools, prompts, resources, resource templates, completions, error
handling, elicitation, subscriptions, and the client side.
+- **[juneau-rest-server-mcp reference](/docs/topics/JuneauRestServerMcp)** and
**[juneau-rest-client-mcp reference](/docs/topics/JuneauRestClientMcp)** — full
API reference for both sides.
diff --git a/pages/topics/11.02.JuneauMcpChoosingSetup.md
b/pages/topics/11.02.JuneauMcpChoosingSetup.md
new file mode 100644
index 0000000000..79e82caf27
--- /dev/null
+++ b/pages/topics/11.02.JuneauMcpChoosingSetup.md
@@ -0,0 +1,115 @@
+---
+title: "Choosing your MCP setup"
+slug: JuneauMcpChoosingSetup
+---
+
+Three independent decisions determine which classes you extend/implement and
which Maven artifacts you depend on. They can be mixed freely — e.g.
`2026-07-28` + mixin + Spring Boot is a perfectly normal combination.
+
+## 1. Which protocol revision: `2025-06-18` vs `2026-07-28`
+
+A client/server pair is revision-bound at compile time — pick one adapter
module per side. There is no automatic runtime revision switching.
+
+| | `2025-06-18` (v1) | `2026-07-28` (v2) |
+|---|---|---|
+| Server artifact | `juneau-rest-server-mcp-v20250618` |
`juneau-rest-server-mcp-v20260728` |
+| Client artifact | `juneau-rest-client-mcp-v20250618` |
`juneau-rest-client-mcp-v20260728` |
+| Server package | `org.apache.juneau.rest.server.mcp.v20250618` |
`org.apache.juneau.rest.server.mcp.v20260728` |
+| Client package | `org.apache.juneau.rest.client.mcp.v20250618` |
`org.apache.juneau.rest.client.mcp.v20260728` |
+| Client handshake | `McpClient.initialize()` | `McpClient.serverDiscover()`
(also via `McpClient.connect(...)`) |
+| Structured tool output | Object-rooted schemas only; `structuredContent`
must be a JSON object | Arbitrary JSON Schema Draft 2020-12 shapes;
`structuredContent` may be any JSON value |
+| Resource templates, template reads, completions | Yes (cross-revision core
feature) | Yes (cross-revision core feature) |
+| Cache hints (SEP-2549) | No | Yes |
+| Multi-Round-Trip Requests / elicitation (SEP-2322) | No | Yes |
+| Subscriptions (SEP-2575) | No | Yes |
+| Trace-context propagation via `params._meta` | No | Yes |
+| Spring Boot convenience (`SpringMcpRestServlet`) | Yes | Yes |
+
+If you don't have a specific reason to target `2025-06-18` (an existing client
that only speaks that revision), use `2026-07-28` — it's a strict superset of
adopter-facing capability. See the [Recipes](/docs/topics/JuneauMcpRecipes)
page for the v2-only features and the [juneau-rest-server-mcp
reference](/docs/topics/JuneauRestServerMcp) for the full breakdown.
+
+## 2. Dedicated servlet vs mixin
+
+Both paths bind the same revision and expose the same hooks (`capabilities()`,
cache/MRTR/subscriptions config); they differ only in how the endpoint attaches
to your REST resource.
+
+**Dedicated servlet** — use when MCP is the *only* thing this resource does,
or you're starting fresh:
+
+```java
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet;
+
+@Rest(path="/mcp")
+public class MyMcpServlet extends McpRestServlet {
+ @Override
+ protected McpServerConfig createMcpConfig() {
+ return new
McpServerConfig().setName("my-server").setVersion("1.0.0").addTool(new
MyEchoTool());
+ }
+}
+```
+
+**Mixin** (`McpEndpoint`) — use when you already have a `@Rest` resource (with
its own unrelated endpoints) and want to bolt an MCP endpoint onto it at `POST
/mcp`:
+
+```java
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.McpEndpoint;
+
+@Rest(path="/api")
+public class MyResource extends BasicRestServlet implements McpEndpoint {
+ @Override
+ public McpServerConfig getMcpConfig() {
+ return new McpServerConfig().addTool(new MyEchoTool());
+ }
+}
+```
+
+Both `org.apache.juneau.rest.server.mcp.v20260728.McpRestServlet` and
`...v20260728.McpEndpoint` are the concrete, revision-bound classes you
actually extend/implement. They in turn extend the revision-neutral core types
`org.apache.juneau.rest.server.mcp.AbstractMcpRestServlet` and
`org.apache.juneau.rest.server.mcp.McpEndpointMixin` — you don't touch those
core types directly; they exist so a future protocol revision can be added as a
second adapter without changing the JSON-RPC envelo [...]
+
+## 3. Plain vs Spring Boot
+
+**Plain** (Jetty microservice, `web.xml`, or any non-Spring container) — use
the dedicated servlet or mixin exactly as shown above; there is no second
bean-resolution path to worry about.
+
+**Spring Boot** — extend `SpringMcpRestServlet` instead of composing
`BasicRestServlet implements McpEndpoint` yourself. It combines the revision's
`McpEndpoint` mixin with `BasicSpringRestServlet`, giving your
tool/prompt/resource handlers access to Spring-managed beans via
`BeanStore.getBean(...)`:
+
+```java
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.SpringMcpRestServlet;
+
+@Rest(path="/mcp")
+public class MyMcpResource extends SpringMcpRestServlet {
+ @Override
+ public McpServerConfig getMcpConfig() {
+ return new McpServerConfig().addTool(new MySpringAwareTool());
+ }
+}
+```
+
+```xml
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-server-springboot</artifactId>
+ <version>${juneau.version}</version>
+</dependency>
+<dependency>
+ <groupId>org.apache.juneau</groupId>
+ <artifactId>juneau-rest-server-mcp-v20260728</artifactId>
+ <version>${juneau.version}</version>
+</dependency>
+```
+
+`SpringMcpRestServlet` ships for both revisions
(`org.apache.juneau.rest.server.mcp.v20250618.SpringMcpRestServlet` and
`...v20260728.SpringMcpRestServlet`).
+
+:::caution Don't extend the plain `McpRestServlet` under Spring Boot
+`McpRestServlet extends BasicRestServlet` — a *sibling* of
`SpringRestServlet`, not a subclass of it. A servlet that extends it still
answers MCP requests under Spring Boot, but every `BeanStore.getBean(...)` call
inside a handler silently comes back empty, because that servlet was never
wired to Spring's `ApplicationContext`. There's no compile-time signal for this
— always use `SpringMcpRestServlet` (or the mixin on `BasicSpringRestServlet`)
under Spring Boot instead.
+:::
+
+See [juneau-rest-server-mcp reference](/docs/topics/JuneauRestServerMcp) for
servlet registration details (self-registration, `web.xml`, Spring Boot
auto-registration) and [Spring Boot Overview](/docs/topics/SpringBootOverview)
for the Spring bean-store bridge these classes build on.
+
+## Which client class
+
+Same shape as the server side — pick the dated `McpClient` matching your
target revision:
+
+- `org.apache.juneau.rest.client.mcp.v20260728.McpClient` — use
`McpClient.connect(endpoint)` for the common case (see
[Quickstart](/docs/topics/JuneauMcpQuickstart)).
+- `org.apache.juneau.rest.client.mcp.v20250618.McpClient` — use
`builder().endpoint(...).build()` then call `initialize()` before any other
method.
+
+See [juneau-rest-client-mcp reference](/docs/topics/JuneauRestClientMcp) for
the full client API.
diff --git a/pages/topics/11.03.JuneauMcpRecipes.md
b/pages/topics/11.03.JuneauMcpRecipes.md
new file mode 100644
index 0000000000..1ee82c7bc3
--- /dev/null
+++ b/pages/topics/11.03.JuneauMcpRecipes.md
@@ -0,0 +1,335 @@
+---
+title: "MCP Recipes"
+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.
+
+- [Writing a tool](#writing-a-tool)
+- [Writing a prompt](#writing-a-prompt)
+- [Serving a resource](#serving-a-resource)
+- [Serving a resource template with
completions](#serving-a-resource-template-with-completions)
+- [Error handling](#error-handling)
+- [Elicitation / Multi-Round-Trip Requests (v2
only)](#elicitation--multi-round-trip-requests-v2-only)
+- [Subscriptions (v2 only)](#subscriptions-v2-only)
+- [Client: call, read, and handle errors](#client-call-read-and-handle-errors)
+
+## Writing a tool
+
+**Raw, via `McpToolHandler.of(...)`** — full control over the input schema:
+
+```java
+import java.util.*;
+
+import org.apache.juneau.marshall.collections.JsonMap;
+import org.apache.juneau.rest.server.mcp.*;
+
+McpToolHandler weatherTool = McpToolHandler.of(
+ new McpToolSpec()
+ .setName("weather")
+ .setDescription("Looks up the current weather for a city.")
+ .setInputSchema(McpSchema.of(JsonMap.of(
+ "type", "object",
+ "properties", JsonMap.of("city", JsonMap.of("type", "string")),
+ "required", List.of("city")
+ ))),
+ (arguments, ctx) -> {
+ String city = String.valueOf(arguments.get("city"));
+ return McpToolOutcome.text("Sunny in " + city);
+ }
+);
+
+config.addTool(weatherTool);
+```
+
+**Typed, via `McpTypedHandlers.tool(...)`** — derives the JSON Schema from
your argument/result classes and binds/canonicalizes automatically:
+
+```java
+import org.apache.juneau.commons.inject.BeanStore;
+import org.apache.juneau.rest.server.mcp.*;
+
+public class WeatherArgs {
+ public String city;
+}
+
+public class WeatherResult {
+ public String summary;
+ public double tempC;
+}
+
+McpToolHandler weatherTool = McpTypedHandlers.tool(
+ "weather", "Looks up the current weather for a city.",
+ WeatherArgs.class, WeatherResult.class,
+ (args, ctx) -> {
+ WeatherResult r = new WeatherResult();
+ r.summary = "Sunny";
+ r.tempC = 23.5;
+ return r;
+ }
+);
+
+config.addTool(weatherTool);
+```
+
+The typed path populates `structuredContent` on the `CallToolResult` (and
mirrors it as a text block by default); the raw path leaves `structuredContent`
unset unless you call `McpToolOutcome.setStructuredContent(...)` yourself.
+
+## Writing a prompt
+
+```java
+import java.util.*;
+
+import org.apache.juneau.rest.server.mcp.*;
+
+McpPromptHandler greetPrompt = McpPromptHandler.of(
+ new McpPromptSpec()
+ .setName("greet")
+ .setDescription("Greets a named person.")
+ .setArguments(List.of(new
McpPromptArgument().setName("name").setRequired(true))),
+ (arguments, ctx) -> new McpPromptOutcome()
+ .setMessages(List.of(new McpPromptMessage()
+ .setRole(McpRole.USER)
+ .setContent(McpContentBlock.text("Hello, " + arguments.get("name")
+ "!"))))
+);
+
+config.addPrompt(greetPrompt);
+```
+
+## Serving a resource
+
+```java
+import java.util.*;
+
+import org.apache.juneau.rest.server.mcp.*;
+
+McpResourceHandler readmeResource = McpResourceHandler.of(
+ new
McpResourceSpec().setUri("file:///readme.txt").setName("readme").setMimeType("text/plain"),
+ (uri, ctx) -> new McpResourceOutcome()
+ .setContents(List.of(McpResourceContents.text(uri, "text/plain",
"Welcome!")))
+);
+
+config.addResource(readmeResource);
+```
+
+## Serving a resource template with completions
+
+A template needs a named class (or anonymous class) rather than `of(...)` once
you want a per-variable completer, since `of(...)` only wires `descriptor()` +
`read(...)`:
+
+```java
+import java.util.*;
+
+import org.apache.juneau.commons.inject.BeanStore;
+import org.apache.juneau.rest.server.mcp.*;
+
+public class FileResourceTemplate implements McpResourceTemplateHandler {
+
+ @Override
+ public McpResourceTemplateSpec descriptor() {
+ return new McpResourceTemplateSpec()
+ .setUriTemplate("file:///{name}")
+ .setName("files")
+ .setMimeType("text/plain");
+ }
+
+ @Override
+ public McpResourceOutcome read(String uri, Map<String,String> variables,
BeanStore ctx) {
+ String name = variables.get("name");
+ String content = loadFile(name);
+ if (content == null)
+ return null; // reported to the client as resource-not-found
+ return new
McpResourceOutcome().setContents(List.of(McpResourceContents.text(uri,
"text/plain", content)));
+ }
+
+ @Override
+ public McpCompleter completer(String variableName) {
+ if (! "name".equals(variableName))
+ return null;
+ return (request, ctx) ->
McpCompletionResult.empty().setValues(matchingFileNames(request.getValue()));
+ }
+
+ private String loadFile(String name) {
+ // your lookup logic
+ return null;
+ }
+
+ private List<String> matchingFileNames(String partial) {
+ // your completion logic
+ return List.of();
+ }
+}
+
+config.addResourceTemplate(new FileResourceTemplate());
+```
+
+A registration-only template (listable but with no read/complete behavior of
its own) can skip the interface entirely: `config.addResourceTemplate(new
McpResourceTemplateSpec()...)`.
+
+## Error handling
+
+Throw `McpException` with a JSON-RPC 2.0 standard error code to fail a call
explicitly:
+
+```java
+import org.apache.juneau.bean.jsonrpc.McpException;
+
+// Standard JSON-RPC 2.0 codes (no dedicated constants class in Juneau — use
the literals):
+// -32700 Parse error
+// -32600 Invalid Request
+// -32601 Method not found
+// -32602 Invalid params
+// -32603 Internal error
+// -32000..-32099 Server-defined error (reserved range for application use)
+
+McpToolHandler.of(
+ new McpToolSpec().setName("weather").setDescription("Looks up the current
weather for a city."),
+ (arguments, ctx) -> {
+ if (! arguments.containsKey("city"))
+ throw new McpException(-32602, "Missing required argument 'city'");
+ return McpToolOutcome.text("Sunny in " + arguments.get("city"));
+ }
+);
+```
+
+Any other unchecked exception a handler lets escape is automatically converted
to a `-32603` ("Internal error") JSON-RPC response by the dispatcher — you
don't need a catch-all yourself, but throwing `McpException` directly gets you
a more specific code and message.
+
+## Elicitation / Multi-Round-Trip Requests (v2 only)
+
+Elicitation (SEP-2322) lets a tool pause mid-call to ask the end user a
question, then resume once the client relays an answer. It's built on
Multi-Round-Trip Requests (MRTR), a `2026-07-28`-only capability.
+
+```java
+import java.util.*;
+
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.commons.inject.BeanStore;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.*;
+
+McpToolHandler confirmDeleteTool = McpToolHandler.of(
+ new McpToolSpec().setName("confirm-delete").setDescription("Deletes a
record after confirming with the user."),
+ (arguments, ctx) -> {
+ var resume = ctx.getBean(McpMrtrResumeContext.class);
+ if (resume.isEmpty()) {
+ var schema = ElicitSchema.create()
+ .booleanField("confirm").title("Confirm deletion?")
+ .required("confirm")
+ .build();
+ 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")))
+ return McpToolOutcome.text("Deletion cancelled.");
+ // ... perform the deletion here ...
+ return McpToolOutcome.text("Deleted.");
+ }
+);
+```
+
+- 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.
+
+This requires no special server config beyond a v2 servlet/mixin — MRTR
support is on by default (see
`AbstractMcpRestServlet.createMrtrConfig()`/`McpRestServlet.getMrtrConfig()` in
the [server reference](/docs/topics/JuneauRestServerMcp) to customize the
codec, TTL, or round cap).
+
+## Subscriptions (v2 only)
+
+Subscriptions (SEP-2575) let a server push resource/list-changed notifications
to a client over a long-held `subscriptions/listen` stream. This is v2-only and
requires `juneau-rest-server-reactive` on the server's classpath for the async
held-open SSE transport.
+
+**Server: emit a change.** Inject `McpSubscriptions` via the handler's
`BeanStore` and call it from any tool/resource/prompt handler:
+
+```java
+import org.apache.juneau.commons.inject.BeanStore;
+import org.apache.juneau.rest.server.mcp.*;
+
+McpToolHandler touchFileTool = McpToolHandler.of(
+ new McpToolSpec().setName("touch-file").setDescription("Marks a resource
as updated."),
+ (arguments, ctx) -> {
+ String uri = String.valueOf(arguments.get("uri"));
+ ctx.getBean(McpSubscriptions.class)
+ .orElseThrow(() -> new IllegalStateException("McpSubscriptions
bean missing — is this endpoint bound to v20260728?"))
+ .resourceUpdated(uri);
+ return McpToolOutcome.text("published");
+ }
+);
+```
+
+`McpSubscriptions` also exposes `toolsListChanged()`, `promptsListChanged()`,
and `resourcesListChanged()` for list-level notifications.
+
+Optionally tune the broker (queue size, heartbeat, idle timeout) by overriding
a hook on your servlet/endpoint:
+
+```java
+@Override
+protected McpSubscriptionsConfig createSubscriptionsConfig() {
+ return new McpSubscriptionsConfig().setIdleTimeoutMs(5 * 60_000L);
+}
+```
+
+:::caution `idleTimeoutMs` defaults to disabled
+`McpSubscriptionsConfig.getIdleTimeoutMs()` defaults to `0`, which disables
idle-timeout reaping — a subscription stream stays open indefinitely as long as
the connection itself stays up. If you set it to a positive value, the server
auto-closes a stream that has delivered no notifications or heartbeats for that
many milliseconds. A long-lived client listener should treat
`onComplete()`/`onError(...)` as a signal to call `listen(...)` again, not
assume the stream stays open forever once [...]
+:::
+
+**Client: listen for changes:**
+
+```java
+import java.util.*;
+
+import org.apache.juneau.bean.mcp.v20260728.SubscriptionFilter;
+import org.apache.juneau.rest.client.mcp.v20260728.*;
+
+var client = McpClient.connect("http://localhost:8080/mcp");
+
+var filter = new SubscriptionFilter()
+ .setResourceSubscriptions(List.of("file:///a.txt"))
+ .setResourcesListChanged(true);
+
+McpSubscriptionHandle handle = client.listen(filter, new
McpSubscriptionListener() {
+ @Override public void onAcknowledged(SubscriptionFilter honoredFilter) {
+ System.out.println("Subscribed: " + honoredFilter);
+ }
+ @Override public void onResourceUpdated(String uri) {
+ System.out.println("Resource updated: " + uri);
+ }
+ @Override public void onListChanged(McpListChangedKind kind) {
+ System.out.println("List changed: " + kind);
+ }
+ @Override public void onComplete() {
+ System.out.println("Server closed the subscription gracefully.");
+ }
+ @Override public void onError(Throwable t) {
+ t.printStackTrace();
+ }
+});
+
+// ... later, when you're done listening:
+handle.close();
+```
+
+`listen(...)` opens the stream synchronously (a transport failure surfaces as
a thrown `IOException` right there) then dispatches every subsequent frame to
`listener` on a background thread — `onAcknowledged(...)` always arrives first,
echoing the server-honored (capability-gated) subset of your requested filter.
+
+## Client: call, read, and handle errors
+
+```java
+import java.util.*;
+
+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")) {
+ var tools = client.listTools();
+ System.out.println(tools);
+
+ var callResult = client.callTool("weather", Map.of("city", "Seattle"));
+ System.out.println(callResult.getContent());
+
+ var resource = client.readResource("file:///readme.txt");
+ System.out.println(resource.getContents());
+} catch (McpException e) {
+ System.err.println("Server returned JSON-RPC error " + e.getCode() + ": "
+ e.getMessage());
+} catch (java.io.IOException e) {
+ System.err.println("Transport failure: " + e.getMessage());
+}
+```
+
+`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.
+
+## Next steps
+
+- **[juneau-rest-server-mcp reference](/docs/topics/JuneauRestServerMcp)** —
capabilities, pagination, cache hints, tracing, MRTR internals.
+- **[juneau-rest-client-mcp reference](/docs/topics/JuneauRestClientMcp)** —
full client API, including the duplex server-request channel and response
caching.
diff --git a/pages/topics/11.JuneauRestServerMcp.md
b/pages/topics/11.04.JuneauRestServerMcp.md
similarity index 98%
rename from pages/topics/11.JuneauRestServerMcp.md
rename to pages/topics/11.04.JuneauRestServerMcp.md
index af8a7dac2e..4ab2d2e024 100644
--- a/pages/topics/11.JuneauRestServerMcp.md
+++ b/pages/topics/11.04.JuneauRestServerMcp.md
@@ -1,9 +1,10 @@
---
-id: 11.juneau-rest-server-mcp
+id: 11.04.juneau-rest-server-mcp
title: "juneau-rest-server-mcp"
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.
`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 [...]
@@ -56,7 +57,7 @@ This module transitively pulls in `juneau-rest-server-mcp`
(the core) and `junea
### Drop-in servlet
-Subclass `org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet` (not the
abstract core `McpRestServlet` directly) and supply your config in
`createMcpConfig()`. The base class wires up `@Rest`,
`@SerializerConfig(addBeanTypes="true")`, and a `POST /` handler:
+Subclass `org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet` (not the
abstract core `AbstractMcpRestServlet` directly) and supply your config in
`createMcpConfig()`. The base class wires up `@Rest`,
`@SerializerConfig(addBeanTypes="true")`, and a `POST /` handler:
```java
import org.apache.juneau.rest.server.*;
@@ -93,7 +94,7 @@ public class MyResource extends BasicRestServlet implements
McpEndpoint {
}
```
-The default `handleMcpRequest(...)` method on `McpEndpoint` (annotated
`@RestPost("/mcp")`) takes care of dispatch, via
`org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint`'s `revision()`
override.
+The default `handleMcpRequest(...)` method on `McpEndpoint` (inherited from
the revision-neutral `McpEndpointMixin`, annotated `@RestPost("/mcp")`) takes
care of dispatch, via
`org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint`'s `revision()`
override.
## Running Under Spring Boot
@@ -409,8 +410,11 @@ public class FileTemplate implements
McpResourceTemplateHandler {
@Override
public McpResourceOutcome read(String uri, Map<String,String> variables,
BeanStore ctx) {
String name = variables.get("name");
+ String content = loadFile(name);
// return null if this template does not actually serve `uri`
(reported as resource-not-found)
- return McpResourceOutcome.text(uri, loadFile(name));
+ if (content == null)
+ return null;
+ return new
McpResourceOutcome().setContents(List.of(McpResourceContents.text(uri,
"text/plain", content)));
}
}
diff --git a/pages/topics/11.01.JuneauRestClientMcp.md
b/pages/topics/11.05.JuneauRestClientMcp.md
similarity index 94%
rename from pages/topics/11.01.JuneauRestClientMcp.md
rename to pages/topics/11.05.JuneauRestClientMcp.md
index cbf206afd7..5f7cb606b7 100644
--- a/pages/topics/11.01.JuneauRestClientMcp.md
+++ b/pages/topics/11.05.JuneauRestClientMcp.md
@@ -1,9 +1,11 @@
---
-id: 11.01.juneau-rest-client-mcp
+id: 11.05.juneau-rest-client-mcp
title: "juneau-rest-client-mcp"
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.
+
`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
diff --git a/sidebars.ts b/sidebars.ts
index 21f910f30d..4cf73a8b27 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -1774,14 +1774,40 @@ const sidebars: SidebarsConfig = {
},
},
{
- type: 'doc',
- id:
'topics/11.01.juneau-rest-client-mcp',
- label: '11.1. juneau-rest-client-mcp',
- },
- {
- type: 'doc',
- id: 'topics/11.juneau-rest-server-mcp',
- label: '11. juneau-rest-server-mcp',
+ type: 'category',
+ label: '11. MCP (Model Context
Protocol)',
+ collapsed: true,
+ items: [
+ {
+ type: 'doc',
+ id:
'topics/11.01.JuneauMcpQuickstart',
+ label: '11.1.
Quickstart',
+ },
+ {
+ type: 'doc',
+ id:
'topics/11.02.JuneauMcpChoosingSetup',
+ label: '11.2. Choosing
your setup',
+ },
+ {
+ type: 'doc',
+ id:
'topics/11.03.JuneauMcpRecipes',
+ label: '11.3. Recipes',
+ },
+ {
+ type: 'doc',
+ id:
'topics/11.04.juneau-rest-server-mcp',
+ label: '11.4.
juneau-rest-server-mcp (reference)',
+ },
+ {
+ type: 'doc',
+ id:
'topics/11.05.juneau-rest-client-mcp',
+ label: '11.5.
juneau-rest-client-mcp (reference)',
+ },
+ ],
+ link: {
+ type: 'doc',
+ id: 'topics/11.juneau-mcp',
+ },
},
{
type: 'category',