This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 4bee9c98c9 First-party runnable MCP example module 
(juneau-examples-mcp) + v2 client/server ergonomic conveniences
4bee9c98c9 is described below

commit 4bee9c98c980e119dfcd870de7e9ba32cafbf3db
Author: James Bognar <[email protected]>
AuthorDate: Thu Aug 6 04:49:21 2026 -0700

    First-party runnable MCP example module (juneau-examples-mcp) + v2 
client/server ergonomic conveniences
    
    Adds juneau-examples-mcp: a runnable 2026-07-28 reference app (a small 
"notes" service)
    exercising every headline surface — a tool, an elicitation/MRTR 
confirm-then-resume tool,
    a prompt with argument completion, a resource, a resource-template with 
completion, and
    subscriptions/listen held-open-SSE change notifications — plus an 
embedded-Jetty launcher,
    a guided McpClient walkthrough, and a Spring Boot variant 
(SpringMcpRestServlet). Ships in
    the Apache release via juneau-distrib as a standalone, runnable project zip 
alongside
    juneau-examples-core.
    
    Also adds four additive v2 conveniences (used by the example so the 
teaching code stays clean):
    - CallToolResult.firstText() + McpClient.callToolText(...) — null-safe; 
scans for the first
      text content block instead of assuming index 0.
    - ServerDiscoverResult.getServerInfo() — null-safe passthrough; 
@BeanIgnore'd so the
      server/discover wire format is unchanged.
    - McpMrtrResumeContext.continuationAsString() — companion to 
continuationAs(Class).
    - ElicitationResponses.getBoolean(...) / getString(...) — 
decline/missing-safe typed reads.
    
    Co-authored-by: Cursor <[email protected]>
---
 .../juneau/bean/mcp/v20260728/CallToolResult.java  |  22 ++
 .../bean/mcp/v20260728/ServerDiscoverResult.java   |  20 ++
 .../juneau/bean/mcp/v20260728/McpV2Beans_Test.java |  52 ++++
 juneau-distrib/pom.xml                             |  17 ++
 juneau-examples/juneau-examples-mcp/.gitignore     |   6 +
 juneau-examples/juneau-examples-mcp/README.md      | 122 +++++++++
 .../juneau-examples-mcp/build-overlay/README.md    | 116 ++++++++
 .../juneau-examples-mcp/build-overlay/pom.xml      | 134 +++++++++
 juneau-examples/juneau-examples-mcp/pom.xml        | 162 +++++++++++
 .../juneau-examples-mcp/src/assembly/bin.xml       |  50 ++++
 .../apache/juneau/examples/mcp/ExampleClient.java  | 177 ++++++++++++
 .../juneau/examples/mcp/ExampleMcpServer.java      | 301 +++++++++++++++++++++
 .../apache/juneau/examples/mcp/ExampleServer.java  | 138 ++++++++++
 .../org/apache/juneau/examples/mcp/NoteStore.java  |  90 ++++++
 .../examples/mcp/spring/GreetingService.java       |  35 +++
 .../mcp/spring/SpringExampleApplication.java       |  80 ++++++
 .../mcp/spring/SpringExampleMcpServer.java         |  71 +++++
 .../examples/mcp/ExampleMcpEndToEnd_Test.java      | 202 ++++++++++++++
 juneau-examples/pom.xml                            |   1 +
 .../rest/client/mcp/v20260728/McpClient.java       |  20 ++
 .../mcp/v20260728/McpClient_Methods_Test.java      |  26 ++
 .../server/mcp/v20260728/ElicitationResponses.java |  56 ++++
 .../server/mcp/v20260728/McpMrtrResumeContext.java |  22 ++
 .../mcp/v20260728/ElicitationResponses_Test.java   | 113 ++++++++
 .../mcp/v20260728/McpInputRequiredSignal_Test.java |  23 ++
 25 files changed, 2056 insertions(+)

diff --git 
a/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/CallToolResult.java
 
b/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/CallToolResult.java
index b7c4f03213..e0d52415ba 100644
--- 
a/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/CallToolResult.java
+++ 
b/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/CallToolResult.java
@@ -90,6 +90,28 @@ public class CallToolResult extends Result<CallToolResult> {
                return this;
        }
 
+       /**
+        * Returns the text of the first {@link TextContent} block anywhere in 
the content list.
+        *
+        * <p>
+        * Convenience for the common case of a tool that returns text 
(possibly alongside other blocks, e.g. an
+        * image with a caption): scans past any leading non-text blocks 
instead of only ever looking at index 0,
+        * and avoids the {@code 
((TextContent)result.getContent().get(i)).getText()} cast every such caller 
would
+        * otherwise repeat.
+        *
+        * @return The first {@link TextContent} block's text found while 
scanning the content list in order, or
+        *      {@code null} if the content list is empty or unset, or it 
contains no {@link TextContent} block.
+        */
+       public String firstText() {
+               var c = getContent();
+               if (c == null)
+                       return null;
+               for (var b : c)
+                       if (b instanceof TextContent t)
+                               return t.getText();
+               return null;
+       }
+
        /**
         * When {@code true}, the tool reported an application-level error.
         *
diff --git 
a/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/ServerDiscoverResult.java
 
b/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/ServerDiscoverResult.java
index 0ea5987f5e..77d0a679f3 100644
--- 
a/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/ServerDiscoverResult.java
+++ 
b/juneau-bean/juneau-bean-mcp-v20260728/src/main/java/org/apache/juneau/bean/mcp/v20260728/ServerDiscoverResult.java
@@ -21,6 +21,7 @@ import static org.apache.juneau.commons.utils.Shorts.*;
 
 import java.util.*;
 
+import org.apache.juneau.commons.bean.*;
 import org.apache.juneau.marshall.*;
 
 /**
@@ -136,4 +137,23 @@ public class ServerDiscoverResult extends 
CacheableResult<ServerDiscoverResult>
                instructions = value;
                return this;
        }
+
+       /**
+        * Returns this result's server identity, if present.
+        *
+        * <p>
+        * Null-safe shortcut for {@code getMeta() == null ? null : 
getMeta().getServerInfo()}, so a caller does not
+        * have to null-check the intermediate {@link #getMeta()} just to reach 
the server's {@link Implementation}.
+        *
+        * @return The server identity, or {@code null} if {@link #getMeta()} 
is unset, or its
+        *      {@link ResultMeta#getServerInfo()} is unset.
+        */
+       // @BeanIgnore keeps this convenience getter from being (re)discovered 
as its own bean property, which
+       // would add a second, top-level "serverInfo" member to the 
server/discover wire format - identity
+       // belongs solely under the inherited _meta envelope (see the class 
javadoc).
+       @BeanIgnore
+       public Implementation getServerInfo() {
+               var m = getMeta();
+               return m == null ? null : m.getServerInfo();
+       }
 }
diff --git 
a/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java
 
b/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java
index efd95a40d4..a701f1bcf7 100644
--- 
a/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java
+++ 
b/juneau-bean/juneau-bean-mcp-v20260728/src/test/java/org/apache/juneau/bean/mcp/v20260728/McpV2Beans_Test.java
@@ -902,4 +902,56 @@ class McpV2Beans_Test {
                        LoggingCapability.class);
                assertBean(logging, "level", "info");
        }
+
+       @Test void m01_firstText_returnsFirstTextContentText() {
+               var result = new CallToolResult().setContent(new 
TextContent().setText("hi"),
+                       new 
AudioContent().setData("QUJD").setMimeType("audio/wav"));
+               assertEquals("hi", result.firstText());
+       }
+
+       @Test void m02_firstText_emptyContentList_returnsNull() {
+               assertNull(new 
CallToolResult().setContent(List.of()).firstText());
+       }
+
+       @Test void m03_firstText_unsetContent_returnsNull() {
+               assertNull(new CallToolResult().firstText());
+       }
+
+       @Test void m04_firstText_noTextContentAnywhere_returnsNull() {
+               var result = new CallToolResult().setContent(new 
AudioContent().setData("QUJD").setMimeType("audio/wav"));
+               assertNull(result.firstText());
+       }
+
+       @Test void m05_firstText_scansPastLeadingNonTextBlocks() {
+               // M-1: firstText() scans the whole list (not just index 0) - 
the common image+caption ordering.
+               var result = new CallToolResult().setContent(
+                       new 
ImageContent().setData("aW1n").setMimeType("image/png"),
+                       new TextContent().setText("caption"));
+               assertEquals("caption", result.firstText());
+       }
+
+       @Test void n01_getServerInfo_returnsImplementationFromMeta() {
+               var result = new ServerDiscoverResult()
+                       .setMeta(new ResultMeta().setServerInfo(new 
Implementation().setName("s").setVersion("1")));
+               assertBean(result.getServerInfo(), "name,version", "s,1");
+       }
+
+       @Test void n02_getServerInfo_unsetMeta_returnsNull() {
+               assertNull(new ServerDiscoverResult().getServerInfo());
+       }
+
+       @Test void n03_getServerInfo_metaWithoutServerInfo_returnsNull() {
+               assertNull(new ServerDiscoverResult().setMeta(new 
ResultMeta()).getServerInfo());
+       }
+
+       @Test void 
n04_getServerInfo_isBeanIgnored_noTopLevelServerInfoInWireFormat() {
+               // H-4: guards the @BeanIgnore on getServerInfo(). Parse (don't 
substring-match) since _meta itself
+               // legitimately nests a "serverInfo" key one level down - only 
the top level must never carry one.
+               var result = new ServerDiscoverResult()
+                       .setMeta(new ResultMeta().setServerInfo(new 
Implementation().setName("s").setVersion("1")));
+               var json = JsonSerializer.DEFAULT.write(result);
+               var m = JsonParser.DEFAULT.read(json, JsonMap.class);
+               assertFalse(m.containsKey("serverInfo"), () -> "must not add a 
top-level serverInfo member: " + json);
+               
assertTrue(((JsonMap)m.get("_meta")).containsKey(ResultMeta.KEY_SERVER_INFO));
+       }
 }
diff --git a/juneau-distrib/pom.xml b/juneau-distrib/pom.xml
index 83b96a07d9..0c6247ca6c 100644
--- a/juneau-distrib/pom.xml
+++ b/juneau-distrib/pom.xml
@@ -300,6 +300,12 @@
                        <version>${project.version}</version>
                        <scope>provided</scope>
                </dependency>
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-examples-mcp</artifactId>
+                       <version>${project.version}</version>
+                       <scope>provided</scope>
+               </dependency>
                <dependency>
                        <groupId>org.apache.juneau</groupId>
                        <artifactId>juneau-petstore-jetty</artifactId>
@@ -1272,6 +1278,17 @@
                                                                        
<destFileName>apache-juneau-examples-core-${project.version}.zip</destFileName>
                                                                </artifactItem>
 
+                                                               <!-- 
juneau-examples-mcp -->
+                                                               <artifactItem>
+                                                                       
<outputDirectory>${project.build.directory}/bin/projects</outputDirectory>
+                                                                       
<groupId>org.apache.juneau</groupId>
+                                                                       
<artifactId>juneau-examples-mcp</artifactId>
+                                                                       
<classifier>bin</classifier>
+                                                                       
<type>zip</type>
+                                                                       
<version>${project.version}</version>
+                                                                       
<destFileName>apache-juneau-examples-mcp-${project.version}.zip</destFileName>
+                                                               </artifactItem>
+
                                                                <!-- 
juneau-petstore-jetty -->
                                                                <artifactItem>
                                                                        
<outputDirectory>${project.build.directory}/bin/projects</outputDirectory>
diff --git a/juneau-examples/juneau-examples-mcp/.gitignore 
b/juneau-examples/juneau-examples-mcp/.gitignore
new file mode 100644
index 0000000000..34acf885cb
--- /dev/null
+++ b/juneau-examples/juneau-examples-mcp/.gitignore
@@ -0,0 +1,6 @@
+/target/
+**/.DS_Store
+.classpath
+.project
+/.settings/
+/bin/
diff --git a/juneau-examples/juneau-examples-mcp/README.md 
b/juneau-examples/juneau-examples-mcp/README.md
new file mode 100644
index 0000000000..31bfd91cc3
--- /dev/null
+++ b/juneau-examples/juneau-examples-mcp/README.md
@@ -0,0 +1,122 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+# Apache Juneau — MCP Example
+
+A **runnable, copy-me reference implementation** of both the server and client 
sides of the
+Model Context Protocol (MCP), built on Apache Juneau against the 
**`2026-07-28` (v2)** revision.
+
+It is deliberately tiny: one in-memory *notes* service exercised by every 
major MCP surface, so
+you can read the whole thing and run it in a few minutes.
+
+## What it demonstrates
+
+A single domain — an in-memory `title → body` note store (`NoteStore`) — is 
exposed through:
+
+| MCP surface | Where | What to look at |
+|---|---|---|
+| **tool** | `publishNote(title, body)` | stores a note, then *pushes* a 
change to subscribers |
+| **tool + elicitation / MRTR** | `deleteNote(title)` | returns 
`input_required` to confirm, then resumes |
+| **prompt + completion** | `summarize(title)` | renders a prompt; `title` 
argument auto-completes |
+| **resource** | `note:///index` | lists all note titles |
+| **resource template + completion** | `note:///{title}` | reads one note; 
`{title}` auto-completes |
+| **subscription** | `subscriptions/listen` | held-open SSE; `publishNote` 
fires `onResourceUpdated` |
+
+## Key files
+
+| File | Role |
+|---|---|
+| 
[`ExampleMcpServer.java`](src/main/java/org/apache/juneau/examples/mcp/ExampleMcpServer.java)
 | The MCP server — every surface registered in `createMcpConfig()` |
+| 
[`NoteStore.java`](src/main/java/org/apache/juneau/examples/mcp/NoteStore.java) 
| The tiny in-memory domain state |
+| 
[`ExampleServer.java`](src/main/java/org/apache/juneau/examples/mcp/ExampleServer.java)
 | Embedded-Jetty launcher (`main` + `start(port)`) |
+| 
[`ExampleClient.java`](src/main/java/org/apache/juneau/examples/mcp/ExampleClient.java)
 | Guided `McpClient` walkthrough of every surface |
+| 
[`spring/SpringExampleMcpServer.java`](src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleMcpServer.java)
 | Spring Boot variant using `SpringMcpRestServlet` |
+| 
[`spring/SpringExampleApplication.java`](src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleApplication.java)
 | `@SpringBootApplication` launcher |
+| 
[`ExampleMcpEndToEnd_Test.java`](src/test/java/org/apache/juneau/examples/mcp/ExampleMcpEndToEnd_Test.java)
 | In-process end-to-end proof |
+
+## Run it
+
+Build the module (and its dependencies) first:
+
+```bash
+mvn -pl juneau-examples/juneau-examples-mcp -am clean install
+```
+
+> **Note:** these `exec:java` commands use `-f 
juneau-examples/juneau-examples-mcp/pom.xml`, not
+> `-pl juneau-examples/juneau-examples-mcp`. Because `.mvn/maven.config` sets 
`--also-make`
+> globally, `-pl` resolves `exec:java` against the reactor's root project 
(which has no
+> `exec.mainClass` configured) rather than this module. `-f` runs Maven 
directly against this
+> module's POM instead.
+
+### 1. The embedded-Jetty server + client walkthrough
+
+Start the server (defaults to port `5000`; pass a port to override):
+
+```bash
+mvn -f juneau-examples/juneau-examples-mcp/pom.xml exec:java \
+  -Dexec.mainClass=org.apache.juneau.examples.mcp.ExampleServer
+```
+
+In another terminal, run the client walkthrough (defaults to 
`http://localhost:5000/`):
+
+```bash
+mvn -f juneau-examples/juneau-examples-mcp/pom.xml exec:java \
+  -Dexec.mainClass=org.apache.juneau.examples.mcp.ExampleClient
+```
+
+The client prints a numbered, top-to-bottom transcript: discovery → listing → 
tool call →
+templated resource read → completion → prompt → a live subscription 
notification → an
+auto-answered `deleteNote` elicitation.
+
+### 2. The Spring Boot variant
+
+```bash
+mvn -f juneau-examples/juneau-examples-mcp/pom.xml exec:java \
+  
-Dexec.mainClass=org.apache.juneau.examples.mcp.spring.SpringExampleApplication
+```
+
+This serves the MCP endpoint at **`http://localhost:8080/mcp`** on embedded 
Tomcat. Its one
+`greet` tool resolves a Spring-managed `GreetingService` through the 
per-request `BeanStore` —
+the distinguishing feature of `SpringMcpRestServlet` over the plain 
`McpRestServlet`.
+
+The Spring app only registers `greet` — none of the notes surfaces from part 1 
— so
+`ExampleClient`'s full walkthrough is not applicable here (it calls 
`publishNote`, which this
+server doesn't have, and would abort with a `Tool not found` error). To 
exercise it manually,
+call `server/discover` and `tools/call` directly instead:
+
+```bash
+curl -s http://localhost:8080/mcp -H 'Content-Type: application/json' -d \
+  
'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"greet","arguments":{"name":"world"}}}'
+```
+
+### 3. The test
+
+```bash
+mvn -f juneau-examples/juneau-examples-mcp/pom.xml test
+```
+
+`ExampleMcpEndToEnd_Test` boots the server in-process on an ephemeral port and 
drives the real
+client through every surface, asserting each outcome.
+
+## How the wiring works
+
+- **Server:** `ExampleMcpServer extends McpRestServlet` (the v2 base). 
`createMcpConfig()` lists
+  the tools/prompts/resources; `createMcpOptions()` advertises capabilities. 
`SseSerializer` is
+  registered on `@Rest` so the subscription stream can negotiate 
`text/event-stream`.
+- **Launcher:** a Jetty `Server` plus the servlet are placed in a 
`BasicBeanStore` and handed to
+  a `Microservice` with `JettyConfiguration`, which auto-mounts the `@Rest` 
servlet at `/`.
+- **Client:** `McpClient` (v2) — `connect()` does the mandatory 
`server/discover` handshake;
+  
`callTool`/`readResource`/`getPrompt`/`complete`/`listen`/`callToolWithElicitation`
 do the rest.
diff --git a/juneau-examples/juneau-examples-mcp/build-overlay/README.md 
b/juneau-examples/juneau-examples-mcp/build-overlay/README.md
new file mode 100644
index 0000000000..f358b783e9
--- /dev/null
+++ b/juneau-examples/juneau-examples-mcp/build-overlay/README.md
@@ -0,0 +1,116 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+# Apache Juneau — MCP Example
+
+A **runnable, copy-me reference implementation** of both the server and client 
sides of the
+Model Context Protocol (MCP), built on Apache Juneau against the 
**`2026-07-28` (v2)** revision.
+
+It is deliberately tiny: one in-memory *notes* service exercised by every 
major MCP surface, so
+you can read the whole thing and run it in a few minutes.
+
+## What it demonstrates
+
+A single domain — an in-memory `title → body` note store (`NoteStore`) — is 
exposed through:
+
+| MCP surface | Where | What to look at |
+|---|---|---|
+| **tool** | `publishNote(title, body)` | stores a note, then *pushes* a 
change to subscribers |
+| **tool + elicitation / MRTR** | `deleteNote(title)` | returns 
`input_required` to confirm, then resumes |
+| **prompt + completion** | `summarize(title)` | renders a prompt; `title` 
argument auto-completes |
+| **resource** | `note:///index` | lists all note titles |
+| **resource template + completion** | `note:///{title}` | reads one note; 
`{title}` auto-completes |
+| **subscription** | `subscriptions/listen` | held-open SSE; `publishNote` 
fires `onResourceUpdated` |
+
+## Key files
+
+| File | Role |
+|---|---|
+| 
[`ExampleMcpServer.java`](src/main/java/org/apache/juneau/examples/mcp/ExampleMcpServer.java)
 | The MCP server — every surface registered in `createMcpConfig()` |
+| 
[`NoteStore.java`](src/main/java/org/apache/juneau/examples/mcp/NoteStore.java) 
| The tiny in-memory domain state |
+| 
[`ExampleServer.java`](src/main/java/org/apache/juneau/examples/mcp/ExampleServer.java)
 | Embedded-Jetty launcher (`main` + `start(port)`) |
+| 
[`ExampleClient.java`](src/main/java/org/apache/juneau/examples/mcp/ExampleClient.java)
 | Guided `McpClient` walkthrough of every surface |
+| 
[`spring/SpringExampleMcpServer.java`](src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleMcpServer.java)
 | Spring Boot variant using `SpringMcpRestServlet` |
+| 
[`spring/SpringExampleApplication.java`](src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleApplication.java)
 | `@SpringBootApplication` launcher |
+| 
[`ExampleMcpEndToEnd_Test.java`](src/test/java/org/apache/juneau/examples/mcp/ExampleMcpEndToEnd_Test.java)
 | In-process end-to-end proof |
+
+## Run it
+
+This is a standalone Maven project (this `pom.xml` is at the root of the 
unzipped archive), so
+every command below is a plain `mvn ...` invocation — no `-f`/`-pl`/reactor 
flags needed.
+
+Build it first:
+
+```bash
+mvn clean install
+```
+
+### 1. The embedded-Jetty server + client walkthrough
+
+Start the server (defaults to port `5000`; pass a port to override):
+
+```bash
+mvn exec:java -Dexec.mainClass=org.apache.juneau.examples.mcp.ExampleServer
+```
+
+In another terminal, run the client walkthrough (defaults to 
`http://localhost:5000/`):
+
+```bash
+mvn exec:java -Dexec.mainClass=org.apache.juneau.examples.mcp.ExampleClient
+```
+
+The client prints a numbered, top-to-bottom transcript: discovery → listing → 
tool call →
+templated resource read → completion → prompt → a live subscription 
notification → an
+auto-answered `deleteNote` elicitation.
+
+### 2. The Spring Boot variant
+
+```bash
+mvn exec:java 
-Dexec.mainClass=org.apache.juneau.examples.mcp.spring.SpringExampleApplication
+```
+
+This serves the MCP endpoint at **`http://localhost:8080/mcp`** on embedded 
Tomcat. Its one
+`greet` tool resolves a Spring-managed `GreetingService` through the 
per-request `BeanStore` —
+the distinguishing feature of `SpringMcpRestServlet` over the plain 
`McpRestServlet`.
+
+The Spring app only registers `greet` — none of the notes surfaces from part 1 
— so
+`ExampleClient`'s full walkthrough is not applicable here (it calls 
`publishNote`, which this
+server doesn't have, and would abort with a `Tool not found` error). To 
exercise it manually,
+call `server/discover` and `tools/call` directly instead:
+
+```bash
+curl -s http://localhost:8080/mcp -H 'Content-Type: application/json' -d \
+  
'{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"greet","arguments":{"name":"world"}}}'
+```
+
+### 3. The test
+
+```bash
+mvn test
+```
+
+`ExampleMcpEndToEnd_Test` boots the server in-process on an ephemeral port and 
drives the real
+client through every surface, asserting each outcome.
+
+## How the wiring works
+
+- **Server:** `ExampleMcpServer extends McpRestServlet` (the v2 base). 
`createMcpConfig()` lists
+  the tools/prompts/resources; `createMcpOptions()` advertises capabilities. 
`SseSerializer` is
+  registered on `@Rest` so the subscription stream can negotiate 
`text/event-stream`.
+- **Launcher:** a Jetty `Server` plus the servlet are placed in a 
`BasicBeanStore` and handed to
+  a `Microservice` with `JettyConfiguration`, which auto-mounts the `@Rest` 
servlet at `/`.
+- **Client:** `McpClient` (v2) — `connect()` does the mandatory 
`server/discover` handshake;
+  
`callTool`/`readResource`/`getPrompt`/`complete`/`listen`/`callToolWithElicitation`
 do the rest.
diff --git a/juneau-examples/juneau-examples-mcp/build-overlay/pom.xml 
b/juneau-examples/juneau-examples-mcp/build-overlay/pom.xml
new file mode 100644
index 0000000000..242e19c0d7
--- /dev/null
+++ b/juneau-examples/juneau-examples-mcp/build-overlay/pom.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+       <modelVersion>4.0.0</modelVersion>
+
+       <groupId>org.apache.juneau</groupId>
+       <artifactId>juneau-examples-mcp</artifactId>
+       <version>${project.version}</version>
+       <name>Apache Juneau MCP Examples</name>
+       <description>Runnable first-party example of a Juneau MCP (Model 
Context Protocol) server and client (revision 2026-07-28).</description>
+       <packaging>jar</packaging>
+
+       <properties>
+               
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+               <!-- This standalone project has no <parent>, so it inherits 
nothing from the reactor root pom.xml
+                       (which sets this same property) - every example source 
uses `var`, so without this the
+                       maven-compiler-plugin default (1.8) fails the build. -->
+               <maven.compiler.release>17</maven.compiler.release>
+               <!-- Default exec:java target; override with 
-Dexec.mainClass=... for ExampleClient or the Spring launcher. -->
+               
<exec.mainClass>org.apache.juneau.examples.mcp.ExampleServer</exec.mainClass>
+       </properties>
+
+       <dependencies>
+
+               <!--
+                       The v2 (2026-07-28) MCP server adapter.  Transitively 
brings the revision-neutral MCP server core,
+                       the v2 wire beans, JSON-RPC beans, and (importantly) 
juneau-rest-server-reactive, which powers the
+                       held-open-SSE subscriptions/listen response.
+               -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       
<artifactId>juneau-rest-server-mcp-v20260728</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!-- The v2 MCP client facade used by ExampleClient. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       
<artifactId>juneau-rest-client-mcp-v20260728</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!-- Embedded Jetty launcher (Microservice + 
JettyConfiguration).  Brings jetty-server / jetty-ee11-servlet. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-microservice-jetty</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!--
+                       Backs the Spring Boot variant (SpringMcpRestServlet's 
BasicSpringRestServlet base).  Declared
+                       explicitly because it is marked <optional> in 
juneau-rest-server-mcp-v20260728, so it does not
+                       transit to consumers of that module.
+               -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-rest-server-springboot</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!-- Spring Boot itself, for the @SpringBootApplication 
launcher (embedded Tomcat). -->
+               <dependency>
+                       <groupId>org.springframework.boot</groupId>
+                       <artifactId>spring-boot-starter-web</artifactId>
+                       <version>${spring.version}</version>
+                       <exclusions>
+                               <exclusion>
+                                       
<groupId>org.springframework.boot</groupId>
+                                       
<artifactId>spring-boot-starter-logging</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+
+               <!-- Test: JUnit Jupiter, for the in-process end-to-end test. 
-->
+               <dependency>
+                       <groupId>org.junit.jupiter</groupId>
+                       <artifactId>junit-jupiter</artifactId>
+                       <version>${junit.version}</version>
+                       <scope>test</scope>
+               </dependency>
+
+               <!-- Test: TestBase, per repo test conventions. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-test-utils</artifactId>
+                       <version>${project.version}</version>
+                       <scope>test</scope>
+               </dependency>
+
+               <!-- Test: assertBean(...), per repo test conventions. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-test</artifactId>
+                       <version>${project.version}</version>
+                       <scope>test</scope>
+               </dependency>
+
+       </dependencies>
+
+       <build>
+               <plugins>
+                       <!-- Lets the README's `mvn exec:java 
-Dexec.mainClass=...` walkthrough commands run the example's
+                               main classes directly. No <configuration> block 
here (unlike the in-repo pom.xml): this whole
+                               build-overlay/ directory is filtered by the 
assembly plugin (see src/assembly/bin.xml), so a
+                               literal <mainClass> value here would get 
${exec.mainClass} baked in at package time to
+                               whichever class happened to be the reactor's 
default - and an explicit XML <mainClass> always
+                               wins over a same-named -Dexec.mainClass= 
command-line property regardless. With no XML
+                               configuration at all, exec-maven-plugin reads 
exec.mainClass directly from the -D system
+                               property the README's commands pass. -->
+                       <plugin>
+                               <groupId>org.codehaus.mojo</groupId>
+                               <artifactId>exec-maven-plugin</artifactId>
+                               <version>3.3.0</version>
+                       </plugin>
+               </plugins>
+       </build>
+
+</project>
diff --git a/juneau-examples/juneau-examples-mcp/pom.xml 
b/juneau-examples/juneau-examples-mcp/pom.xml
new file mode 100644
index 0000000000..ec222c9e24
--- /dev/null
+++ b/juneau-examples/juneau-examples-mcp/pom.xml
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
https://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+       <modelVersion>4.0.0</modelVersion>
+
+       <parent>
+               <artifactId>juneau-examples</artifactId>
+               <groupId>org.apache.juneau</groupId>
+               <version>10.0.0-SNAPSHOT</version>
+       </parent>
+
+       <artifactId>juneau-examples-mcp</artifactId>
+       <name>Apache Juneau MCP Examples</name>
+       <description>Runnable first-party example of a Juneau MCP (Model 
Context Protocol) server and client (revision 2026-07-28).</description>
+       <packaging>jar</packaging>
+
+       <properties>
+               
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+               <!-- Default exec:java target; override with 
-Dexec.mainClass=... for ExampleClient or the Spring launcher. -->
+               
<exec.mainClass>org.apache.juneau.examples.mcp.ExampleServer</exec.mainClass>
+       </properties>
+
+       <dependencies>
+
+               <!--
+                       The v2 (2026-07-28) MCP server adapter.  Transitively 
brings the revision-neutral MCP server core,
+                       the v2 wire beans, JSON-RPC beans, and (importantly) 
juneau-rest-server-reactive, which powers the
+                       held-open-SSE subscriptions/listen response.
+               -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       
<artifactId>juneau-rest-server-mcp-v20260728</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!-- The v2 MCP client facade used by ExampleClient. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       
<artifactId>juneau-rest-client-mcp-v20260728</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!-- Embedded Jetty launcher (Microservice + 
JettyConfiguration).  Brings jetty-server / jetty-ee11-servlet. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-microservice-jetty</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!--
+                       Backs the Spring Boot variant (SpringMcpRestServlet's 
BasicSpringRestServlet base).  Declared
+                       explicitly because it is marked <optional> in 
juneau-rest-server-mcp-v20260728, so it does not
+                       transit to consumers of that module.
+               -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-rest-server-springboot</artifactId>
+                       <version>${project.version}</version>
+               </dependency>
+
+               <!-- Spring Boot itself, for the @SpringBootApplication 
launcher (embedded Tomcat). -->
+               <dependency>
+                       <groupId>org.springframework.boot</groupId>
+                       <artifactId>spring-boot-starter-web</artifactId>
+                       <version>${spring.version}</version>
+                       <exclusions>
+                               <exclusion>
+                                       
<groupId>org.springframework.boot</groupId>
+                                       
<artifactId>spring-boot-starter-logging</artifactId>
+                               </exclusion>
+                       </exclusions>
+               </dependency>
+
+               <!-- Test: JUnit Jupiter, for the in-process end-to-end test. 
-->
+               <dependency>
+                       <groupId>org.junit.jupiter</groupId>
+                       <artifactId>junit-jupiter</artifactId>
+                       <version>${junit.version}</version>
+                       <scope>test</scope>
+               </dependency>
+
+               <!-- Test: TestBase, per repo test conventions. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-test-utils</artifactId>
+                       <version>${project.version}</version>
+                       <scope>test</scope>
+               </dependency>
+
+               <!-- Test: assertBean(...), per repo test conventions. -->
+               <dependency>
+                       <groupId>org.apache.juneau</groupId>
+                       <artifactId>juneau-test</artifactId>
+                       <version>${project.version}</version>
+                       <scope>test</scope>
+               </dependency>
+
+       </dependencies>
+
+       <build>
+               <plugins>
+                       <!--
+                               Lets the README's `mvn ... exec:java` 
walkthrough commands run the example's main
+                               classes directly.  Per .mvn/maven.config's 
global also-make flag, invoke with `-f
+                               juneau-examples/juneau-examples-mcp/pom.xml` 
rather than `-pl ...` — `-pl` still
+                               resolves the goal against the reactor's root 
project (which has no exec.mainClass),
+                               not this module. See 
juneau-integration-tests/pom.xml's microbench profile for the
+                               same trap.
+                       -->
+                       <plugin>
+                               <groupId>org.codehaus.mojo</groupId>
+                               <artifactId>exec-maven-plugin</artifactId>
+                               <version>3.3.0</version>
+                               <configuration>
+                                       <!-- ${exec.mainClass} (declared in 
<properties> above) so -Dexec.mainClass=... on the
+                                               command line actually overrides 
it; a plugin-level literal value cannot be
+                                               overridden by a same-named -D 
system property. -->
+                                       <mainClass>${exec.mainClass}</mainClass>
+                               </configuration>
+                       </plugin>
+                       <!--
+                               Packages a 'bin' classifier zip (source + a 
standalone build-overlay/pom.xml) so this example
+                               ships in the Apache release alongside 
juneau-examples-core; see juneau-distrib/pom.xml.
+                       -->
+                       <plugin>
+                               <artifactId>maven-assembly-plugin</artifactId>
+                               <executions>
+                                       <execution>
+                                               <id>juneau-assembly</id>
+                                               <phase>package</phase>
+                                               <goals>
+                                                       <goal>single</goal>
+                                               </goals>
+                                               <configuration>
+                                                       
<finalName>juneau-examples-mcp-${project.version}</finalName>
+                                                       <descriptors>
+                                                               
<descriptor>src/assembly/bin.xml</descriptor>
+                                                       </descriptors>
+                                               </configuration>
+                                       </execution>
+                               </executions>
+                       </plugin>
+               </plugins>
+       </build>
+
+</project>
diff --git a/juneau-examples/juneau-examples-mcp/src/assembly/bin.xml 
b/juneau-examples/juneau-examples-mcp/src/assembly/bin.xml
new file mode 100644
index 0000000000..2b3df3853f
--- /dev/null
+++ b/juneau-examples/juneau-examples-mcp/src/assembly/bin.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<!--
+       This assembly is used to create the template zip file that developers 
use to run standalone with Maven
+       to start their own MCP server/client projects.  See 
juneau-examples-core's src/assembly/bin.xml for the
+       sibling pattern this mirrors.
+ -->
+<assembly
+       
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3
 https://maven.apache.org/xsd/assembly-1.1.3.xsd";>
+       <id>bin</id>
+       <formats>
+               <format>zip</format>
+       </formats>
+       <baseDirectory>/</baseDirectory>
+       <includeBaseDirectory>true</includeBaseDirectory>
+       <fileSets>
+               <fileSet>
+                       <useDefaultExcludes>false</useDefaultExcludes>
+                       <directory>${basedir}</directory>
+                       <includes>
+                               <include>src/main/**</include>
+                               <include>src/test/**</include>
+                       </includes>
+               </fileSet>
+               <fileSet>
+                       <directory>${basedir}/build-overlay</directory>
+                       <outputDirectory>/</outputDirectory>
+                       <filtered>true</filtered>
+               </fileSet>
+       </fileSets>
+
+</assembly>
diff --git 
a/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleClient.java
 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleClient.java
new file mode 100644
index 0000000000..a74aba55f9
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleClient.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.marshall.marshaller.Json;
+import org.apache.juneau.rest.client.mcp.v20260728.*;
+
+/**
+ * A guided, end-to-end walkthrough of every {@link ExampleMcpServer} surface 
using the v2
+ * {@link McpClient}.
+ *
+ * <p>
+ * Run {@link #main(String[]) main} (optionally passing the server endpoint; 
defaults to
+ * {@code http://localhost:5000/}) after starting {@link ExampleServer}. Each 
numbered step prints
+ * what it is doing and what it got back, so the transcript reads 
top-to-bottom like a tutorial.
+ *
+ * <p>
+ * The client advertises the {@code elicitation} capability, which the server 
requires before it will
+ * pause a call for input; without it the {@code deleteNote} elicitation step 
would be rejected.
+ *
+ * <p>
+ * <b>Console output caveat:</b> every printed line below is produced by 
re-serializing the already-parsed
+ * response bean with {@code Json.of(...)}, purely for readable console 
output. That re-serialization drops
+ * the polymorphic content-block {@code "type"} discriminator (e.g. {@code 
"text"}/{@code "audio"}) the real
+ * wire format carries, so what you see here is not byte-for-byte what came 
off the wire.
+ */
+public final class ExampleClient {
+
+       private ExampleClient() {}
+
+       /**
+        * Runs the walkthrough against a running {@link ExampleServer}.
+        *
+        * @param args Optional single argument: the server endpoint (defaults 
to {@code http://localhost:5000/}).
+        * @throws Exception If any step fails.
+        */
+       public static void main(String[] args) throws Exception {
+               var endpoint = args.length > 0 ? args[0] : "http://localhost:"; 
+ ExampleServer.DEFAULT_PORT + "/";
+               try (var client = connect(endpoint)) {
+                       run(client);
+               }
+       }
+
+       /**
+        * Builds and connects a client that advertises the elicitation 
capability.
+        *
+        * @param endpoint The MCP server endpoint URL.
+        * @return A connected client (its mandatory {@code server/discover} 
handshake already done).
+        * @throws IOException If the connection or handshake fails.
+        */
+       public static McpClient connect(String endpoint) throws IOException {
+               return McpClient.connect(McpClient.builder()
+                       .endpoint(endpoint)
+                       .clientInfo(new 
Implementation().setName("juneau-notes-example-client").setVersion("1.0.0"))
+                       // Required for the deleteNote elicitation step: the 
server only pauses for input when the
+                       // client says it can answer.
+                       .clientCapabilities(new 
ClientCapabilities().setElicitation(new ElicitationCapability())));
+       }
+
+       /**
+        * Executes each numbered step of the walkthrough against an 
already-connected client.
+        *
+        * @param client The connected MCP client.
+        * @throws Exception If any step fails.
+        */
+       public static void run(McpClient client) throws Exception {
+
+               section("1. server/discover — who are we talking to?");
+               var discovered = client.discoveredServer();
+               System.out.println("   server info:  " + 
Json.of(discovered.getServerInfo()));
+               System.out.println("   versions:     " + 
Json.of(discovered.getSupportedVersions()));
+               System.out.println("   capabilities: " + 
Json.of(discovered.getCapabilities()));
+               System.out.println("   instructions: " + 
discovered.getInstructions());
+
+               section("2. Discovery — list every advertised surface");
+               System.out.println("   tools:             " + 
Json.of(client.listTools().getTools()));
+               System.out.println("   prompts:           " + 
Json.of(client.listPrompts().getPrompts()));
+               System.out.println("   resources:         " + 
Json.of(client.listResources().getResources()));
+               System.out.println("   resourceTemplates: " + 
Json.of(client.listResourceTemplates().getResourceTemplates()));
+
+               section("3. tools/call publishNote — store a note (and notify 
subscribers)");
+               System.out.println("   -> " + 
client.callToolText("publishNote", Map.of("title", "groceries", "body", "Milk, 
eggs, bread")));
+
+               section("4. resources/read note:///groceries — read it back via 
the template");
+               var read = client.readResource(NoteStore.uriFor("groceries"));
+               System.out.println("   " + Json.of(read.getContents()));
+
+               section("5. resources/read note:///index — the fixed index 
resource");
+               System.out.println("   " + 
Json.of(client.readResource(NoteStore.SCHEME + "index").getContents()));
+
+               section("6. completion/complete — complete the template's 
{title} variable for prefix 'gr'");
+               var ref = new 
ResourceTemplateReference().setUri(NoteStore.SCHEME + "{title}");
+               var completion = client.complete(ref, "title", "gr", null);
+               System.out.println("   suggestions: " + 
Json.of(completion.getCompletion()));
+
+               section("7. prompts/get summarize — render a prompt from the 
stored note");
+               var prompt = client.getPrompt("summarize", Map.of("title", 
"groceries"));
+               System.out.println("   " + Json.of(prompt.getMessages()));
+
+               section("8. subscriptions/listen — receive a live change 
notification");
+               runSubscriptionDemo(client);
+
+               section("9. tools/call deleteNote — an elicitation (confirm) 
round-trip, auto-answered");
+               var deleted = client.callToolWithElicitation("deleteNote", 
Map.of("title", "groceries"), requests -> {
+                       // The server asked one or more questions; answer each 
with ACCEPT + confirm=true. A real client
+                       // would present these to a user (the schema in each 
request says how to render the control).
+                       System.out.println("   server asked: " + 
Json.of(requests));
+                       var answers = new LinkedHashMap<String,ElicitResult>();
+                       requests.keySet().forEach(id -> answers.put(id,
+                               new 
ElicitResult().setAction(ElicitAction.ACCEPT).putContent("confirm", true)));
+                       return answers;
+               });
+               System.out.println("   -> " + deleted.firstText());
+
+               section("10. resources/read note:///index — confirm the note is 
gone");
+               System.out.println("   " + 
Json.of(client.readResource(NoteStore.SCHEME + "index").getContents()));
+
+               System.out.println("\nWalkthrough complete.");
+       }
+
+       /**
+        * Subscribes for changes to a note URI, publishes that note, and 
prints the change frame that arrives
+        * over the held-open SSE stream.
+        */
+       private static void runSubscriptionDemo(McpClient client) throws 
Exception {
+               var noteUri = NoteStore.uriFor("todo");
+               var updates = new LinkedBlockingQueue<String>();
+               var acknowledged = new CountDownLatch(1);
+
+               var handle = client.listen(
+                       new 
SubscriptionFilter().setResourceSubscriptions(List.of(noteUri)),
+                       new McpSubscriptionListener() {
+                               @Override public void 
onAcknowledged(SubscriptionFilter honoredFilter) { acknowledged.countDown(); }
+                               @Override public void onResourceUpdated(String 
uri) { updates.add(uri); }
+                               @Override public void onError(Throwable t) { 
System.out.println("   subscription error: " + t); }
+                       });
+               try {
+                       if (! acknowledged.await(10, TimeUnit.SECONDS))
+                               System.out.println("   (warning: subscription 
was not acknowledged in time)");
+                       else
+                               System.out.println("   subscription 
acknowledged; publishing '" + noteUri + "' ...");
+
+                       client.callTool("publishNote", Map.of("title", "todo", 
"body", "Write MCP example"));
+
+                       var updatedUri = updates.poll(10, TimeUnit.SECONDS);
+                       if (updatedUri == null)
+                               System.out.println("   (warning: no 
resources/updated notification arrived in time)");
+                       else
+                               System.out.println("   -> received 
resources/updated for: " + updatedUri);
+               } finally {
+                       handle.close();
+               }
+       }
+
+       private static void section(String title) {
+               System.out.println("\n=== " + title + " ===");
+       }
+}
diff --git 
a/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleMcpServer.java
 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleMcpServer.java
new file mode 100644
index 0000000000..74dd6bb636
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleMcpServer.java
@@ -0,0 +1,301 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp;
+
+import java.util.*;
+
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.sse.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.*;
+
+/**
+ * A complete, runnable MCP <c>2026-07-28</c> server exposing every major 
surface of the protocol
+ * over one tiny, coherent scenario: an in-memory {@link NoteStore notes} 
service.
+ *
+ * <p>
+ * This is the reference implementation to copy. Everything the server exposes 
is registered in
+ * {@link #createMcpConfig()} below, and every handler is a small anonymous 
class that closes over a
+ * single shared {@link NoteStore}. What each surface demonstrates:
+ *
+ * <ul>
+ *     <li><b>tool</b> &mdash; {@code publishNote(title, body)} stores a note 
and then <i>pushes</i> a
+ *             {@code resources/updated} change to any subscribers (see the 
subscription bullet).
+ *     <li><b>tool + elicitation / MRTR</b> &mdash; {@code deleteNote(title)} 
returns an
+ *             {@code input_required} pause asking the caller to confirm, then 
resumes to actually delete.
+ *             This is one of the framework's two headline features.
+ *     <li><b>prompt + completion</b> &mdash; {@code summarize} renders a 
prompt message from a stored
+ *             note; its {@code title} argument offers {@code 
completion/complete} suggestions from existing
+ *             note titles.
+ *     <li><b>resource</b> &mdash; the fixed {@code note:///index} resource 
lists all note titles.
+ *     <li><b>resource template + completion</b> &mdash; {@code 
note:///{title}} reads one note by title;
+ *             its {@code title} variable also offers completions.
+ *     <li><b>subscription</b> &mdash; {@code subscriptions/listen} (SEP-2575 
held-open SSE) delivers the
+ *             {@code resources/updated} change {@code publishNote} fires. The 
other headline feature.
+ * </ul>
+ *
+ * <p>
+ * The two overridable factory hooks are the whole story:
+ * <ul>
+ *     <li>{@link #createMcpConfig()} &mdash; <i>what</i> the server exposes 
(tools/prompts/resources).
+ *     <li>{@link #createMcpOptions()} &mdash; <i>how</i> it behaves 
(advertised capabilities, etc.).
+ * </ul>
+ *
+ * <p>
+ * The {@link Rest @Rest} annotation must register {@link SseSerializer} 
(alongside the JSON
+ * serializer) so a real servlet container can satisfy the {@code Accept: 
text/event-stream}
+ * negotiation the subscription stream requires; the base {@link 
McpRestServlet} wires only JSON.
+ *
+ * @serial exclude
+ */
+@Rest(serializers = {JsonSerializer.class, SseSerializer.class}, parsers = 
JsonParser.class, defaultAccept = "application/json")
+public class ExampleMcpServer extends McpRestServlet {
+
+       private static final long serialVersionUID = 1L;
+
+       // The entire domain state. Handlers below close over this instance, so 
there is no DI ceremony to
+       // follow: the server object owns its notes, and every MCP surface is a 
thin view over them.
+       private final transient NoteStore notes = new NoteStore();
+
+       /**
+        * Declares everything this server exposes. This one method is the 
"table of contents" for the demo.
+        */
+       @Override
+       protected McpServerConfig createMcpConfig() {
+               return new McpServerConfig()
+                       .setName("juneau-notes-example")
+                       .setVersion("1.0.0")
+                       .addTool(publishNoteTool(notes))
+                       .addTool(deleteNoteTool(notes))
+                       .addPrompt(summarizePrompt(notes))
+                       .addResource(noteIndexResource(notes))
+                       .addResourceTemplate(noteTemplate(notes));
+       }
+
+       /**
+        * Advertises capabilities and instructions. Explicit here so the 
{@code server/discover} handshake
+        * truthfully reports everything this server supports &mdash; notably 
{@code resources.subscribe},
+        * which pairs with the {@code subscriptions/listen} demo.
+        *
+        * <p>
+        * On revision {@code 2026-07-28}, {@code server/discover}'s {@code 
instructions} field is sourced from
+        * this {@link McpOptions}, <b>not</b> {@link 
McpServerConfig#setInstructions}, which only feeds the
+        * legacy v1 {@code initialize} handshake.
+        */
+       @Override
+       protected McpOptions createMcpOptions() {
+               return new McpOptions()
+                       .setInstructions("A tiny notes service. Use 
'publishNote' to add notes, read them via the "
+                               + "'note:///{title}' resource template, 
subscribe for change notifications, and try "
+                               + "'deleteNote' to see an elicitation (confirm) 
round-trip.")
+                       .setCapabilities(new ServerCapabilities()
+                               .setTools(new 
ToolCapability().setListChanged(true))
+                               .setPrompts(new 
PromptCapability().setListChanged(true))
+                               .setResources(new 
ResourceCapability().setSubscribe(true).setListChanged(true))
+                               .setCompletions(new CompletionCapability()));
+       }
+
+       // 
=================================================================================================
+       // Tools
+       // 
=================================================================================================
+
+       /**
+        * A plain tool that mutates server state and then notifies subscribers.
+        *
+        * <p>
+        * The {@code resourceUpdated(...)} call is the server-push half of the 
subscription feature: any client
+        * currently {@code listen}ing for changes to this note's URI receives 
an {@code onResourceUpdated} frame.
+        * The {@link McpSubscriptions} SPI is resolved from the per-request 
{@link BeanStore}.
+        */
+       private static McpToolHandler publishNoteTool(NoteStore notes) {
+               return new McpToolHandler() {
+                       @Override public McpToolSpec descriptor() {
+                               return new McpToolSpec().setName("publishNote")
+                                       .setDescription("Stores a note under 
the given title and notifies subscribers of the change.");
+                       }
+                       @Override public McpToolOutcome call(Map<String,Object> 
arguments, BeanStore ctx) {
+                               var title = 
String.valueOf(arguments.getOrDefault("title", ""));
+                               var body = 
String.valueOf(arguments.getOrDefault("body", ""));
+                               notes.put(title, body);
+                               ctx.getBean(McpSubscriptions.class).ifPresent(s 
-> s.resourceUpdated(NoteStore.uriFor(title)));
+                               return McpToolOutcome.text("Stored note '" + 
title + "' (" + body.length() + " chars).");
+                       }
+               };
+       }
+
+       /**
+        * A tool that pauses for confirmation before acting &mdash; the 
elicitation / Multi-Round-Trip-Request
+        * (MRTR, SEP-2322) flow.
+        *
+        * <p>
+        * On the first call there is no resume context, so the handler 
<i>throws</i> an {@code input_required}
+        * signal built by {@link ElicitationRequests#of(String, ElicitRequest, 
Object)}: it carries one question
+        * (id {@code "confirmDelete"}, asking for a boolean {@code confirm} 
field) plus a continuation token
+        * ({@code "delete:<title>"}) that the framework seals and echoes back. 
The id and the field name are
+        * deliberately distinct (rather than both {@code "confirm"}) so the 
two-level {@code (id, field)} lookup
+        * below is legible. When the client answers, the framework re-invokes 
this same handler with a populated
+        * {@link McpMrtrResumeContext}; the handler reads the answer and the 
continuation and finishes.
+        *
+        * <p>
+        * Note the title is recovered from the continuation on resume rather 
than from the arguments: carrying
+        * per-pause state through the continuation is the intended MRTR 
pattern.
+        *
+        * <p>
+        * The framework explicitly allows a resume with zero elicitation 
answers (a client that never asked the
+        * user, or a transport that dropped the answer), so the continuation 
and the {@code confirm} answer are
+        * both treated as possibly missing below &mdash; never dereferenced 
directly. {@link McpMrtrResumeContext#continuationAsString()}
+        * and {@link ElicitationResponses#getBoolean} are the 
decline/omission-safe accessors that make that
+        * possible without hand-rolled null checks.
+        */
+       private static McpToolHandler deleteNoteTool(NoteStore notes) {
+               return new McpToolHandler() {
+                       @Override public McpToolSpec descriptor() {
+                               return new McpToolSpec().setName("deleteNote")
+                                       .setDescription("Deletes a note, after 
asking the caller to confirm (demonstrates elicitation).");
+                       }
+                       @Override public McpToolOutcome call(Map<String,Object> 
arguments, BeanStore ctx) {
+                               var resume = 
ctx.getBean(McpMrtrResumeContext.class);
+                               if (resume.isEmpty()) {
+                                       // PAUSE: ask the caller to confirm. 
Execution stops here and control returns to the client.
+                                       var title = 
String.valueOf(arguments.getOrDefault("title", ""));
+                                       var question = new ElicitRequest()
+                                               .setMessage("Really delete note 
'" + title + "'?")
+                                               
.setRequestedSchema(ElicitSchema.create().booleanField("confirm").title("Confirm
 deletion").build());
+                                       throw 
ElicitationRequests.of("confirmDelete", question, "delete:" + title);
+                               }
+                               // RESUME: the caller answered (or didn't). 
Recover state from the continuation; a missing
+                               // continuation or a missing/declined "confirm" 
answer both cleanly mean "cancelled".
+                               var rc = resume.get();
+                               var continuation = rc.continuationAsString();
+                               var title = continuation != null && 
continuation.startsWith("delete:")
+                                       ? 
continuation.substring("delete:".length()) : "";
+                               if (ElicitationResponses.getBoolean(rc, 
"confirmDelete", "confirm")) {
+                                       var removed = notes.remove(title);
+                                       return McpToolOutcome.text(removed
+                                               ? "Deleted note '" + title + 
"'."
+                                               : "There was no note titled '" 
+ title + "'.");
+                               }
+                               return McpToolOutcome.text("Deletion of '" + 
title + "' was cancelled.");
+                       }
+               };
+       }
+
+       // 
=================================================================================================
+       // Prompt
+       // 
=================================================================================================
+
+       /**
+        * A prompt whose {@code title} argument declares a completer, so a 
client's {@code completion/complete}
+        * request for that argument is answered with matching existing note 
titles.
+        */
+       private static McpPromptHandler summarizePrompt(NoteStore notes) {
+               var spec = new McpPromptSpec()
+                       .setName("summarize")
+                       .setDescription("Builds a prompt asking a model to 
summarize a stored note.")
+                       .setArguments(List.of(new McpPromptArgument()
+                               .setName("title")
+                               .setDescription("The title of the note to 
summarize.")
+                               .setRequired(true)
+                               .setCompleter(noteTitleCompleter(notes))));
+               return new McpPromptHandler() {
+                       @Override public McpPromptSpec descriptor() { return 
spec; }
+                       @Override public McpPromptOutcome 
get(Map<String,Object> arguments, BeanStore ctx) {
+                               var title = 
String.valueOf(arguments.getOrDefault("title", ""));
+                               var body = notes.get(title);
+                               var text = body == null
+                                       ? "There is no note titled '" + title + 
"'."
+                                       : "Summarize the following note titled 
'" + title + "':\n\n" + body;
+                               var message = new 
McpPromptMessage().setRole(McpRole.USER).setContent(McpContentBlock.text(text));
+                               return new McpPromptOutcome()
+                                       .setDescription("Summary prompt for 
note '" + title + "'.")
+                                       .setMessages(List.of(message));
+                       }
+               };
+       }
+
+       // 
=================================================================================================
+       // Resources
+       // 
=================================================================================================
+
+       /**
+        * A fixed (non-templated) resource that lists all current note titles.
+        */
+       private static McpResourceHandler noteIndexResource(NoteStore notes) {
+               var spec = new McpResourceSpec()
+                       .setUri(NoteStore.SCHEME + "index")
+                       .setName("note-index")
+                       .setDescription("A plain-text list of all note titles.")
+                       .setMimeType("text/plain");
+               return new McpResourceHandler() {
+                       @Override public McpResourceSpec descriptor() { return 
spec; }
+                       @Override public McpResourceOutcome read(String uri, 
BeanStore ctx) {
+                               var titles = notes.titles();
+                               var text = titles.isEmpty() ? "(no notes yet)" 
: String.join("\n", titles);
+                               return new 
McpResourceOutcome().setContents(List.of(McpResourceContents.text(uri, 
"text/plain", text)));
+                       }
+               };
+       }
+
+       /**
+        * A resource template ({@code note:///{title}}) that reads one note by 
title, and offers completions
+        * for its {@code title} variable.
+        *
+        * <p>
+        * An exact resource registration (here, {@code note:///index}) always 
beats a matching template, so
+        * {@code note:///index} reads the index above while {@code 
note:///groceries} routes here.
+        */
+       private static McpResourceTemplateHandler noteTemplate(NoteStore notes) 
{
+               var spec = new McpResourceTemplateSpec()
+                       .setUriTemplate(NoteStore.SCHEME + "{title}")
+                       .setName("note")
+                       .setDescription("Reads a single note by title.")
+                       .setMimeType("text/plain");
+               var completer = noteTitleCompleter(notes);
+               return new McpResourceTemplateHandler() {
+                       @Override public McpResourceTemplateSpec descriptor() { 
return spec; }
+                       @Override public McpResourceOutcome read(String uri, 
Map<String,String> variables, BeanStore ctx) {
+                               var title = variables.get("title");
+                               var body = notes.get(title);
+                               var text = body == null ? "(no note titled '" + 
title + "')" : body;
+                               return new 
McpResourceOutcome().setContents(List.of(McpResourceContents.text(uri, 
"text/plain", text)));
+                       }
+                       @Override public McpCompleter completer(String 
variableName) {
+                               return "title".equals(variableName) ? completer 
: null;
+                       }
+               };
+       }
+
+       // 
=================================================================================================
+       // Shared completion logic
+       // 
=================================================================================================
+
+       /**
+        * A completer that suggests existing note titles beginning with the 
caller's partial input. Used by both
+        * the prompt argument and the resource-template variable, so one 
implementation drives every
+        * {@code completion/complete} response in this server.
+        */
+       private static McpCompleter noteTitleCompleter(NoteStore notes) {
+               return (request, ctx) -> {
+                       var prefix = request.getValue();
+                       var matches = notes.titles().stream().filter(t -> 
t.startsWith(prefix)).toList();
+                       return new McpCompletionResult().setValues(matches);
+               };
+       }
+}
diff --git 
a/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleServer.java
 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleServer.java
new file mode 100644
index 0000000000..7eca0011e5
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/ExampleServer.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp;
+
+import java.net.*;
+
+import org.apache.juneau.commons.inject.*;
+import org.apache.juneau.microservice.*;
+import org.apache.juneau.microservice.jetty.*;
+import org.eclipse.jetty.ee11.servlet.*;
+import org.eclipse.jetty.server.*;
+
+import jakarta.servlet.*;
+
+/**
+ * Boots {@link ExampleMcpServer} on an embedded Jetty server.
+ *
+ * <p>
+ * Run {@link #main(String[]) main} to start the server on a fixed port 
(default {@code 5000}, or pass a
+ * port as the first argument) and leave it running so you can point {@link 
ExampleClient} (or any MCP
+ * client) at it. The in-process end-to-end test uses {@link #start(int) 
start(0)} to boot on an
+ * OS-assigned ephemeral port instead.
+ *
+ * <p>
+ * The wiring mirrors Juneau's own test fixture: a Jetty {@link Server} plus 
the {@link ExampleMcpServer}
+ * servlet are placed in a {@link BasicBeanStore}, and {@link 
JettyConfiguration} (added via
+ * {@link Microservice.Builder#configurations(Class...)}) auto-mounts the 
{@link org.apache.juneau.rest.server.Rest @Rest}
+ * servlet at {@code "/"} and drives the server lifecycle. Because the {@code 
Server} is supplied as a bean,
+ * we control the listen port directly &mdash; no {@code jetty.xml} required.
+ */
+public final class ExampleServer implements AutoCloseable {
+
+       /** Default listen port used by {@link #main(String[])} when none is 
supplied. */
+       public static final int DEFAULT_PORT = 5000;
+
+       private final Microservice microservice;
+       private final URI rootUrl;
+
+       private ExampleServer(Microservice microservice, URI rootUrl) {
+               this.microservice = microservice;
+               this.rootUrl = rootUrl;
+       }
+
+       /**
+        * Starts the example MCP server.
+        *
+        * @param port The TCP port to listen on, or {@code 0} to let the OS 
assign an ephemeral port.
+        * @return A running server handle. Close it (or call {@link #close()}) 
to stop.
+        * @throws Exception If the server fails to start.
+        */
+       @SuppressWarnings("resource") // The bean store is handed to (and 
closed by) the Microservice lifecycle.
+       public static ExampleServer start(int port) throws Exception {
+               var jetty = buildServer(port);
+
+               var beanStore = new BasicBeanStore();
+               beanStore.addBean(Server.class, jetty);
+               beanStore.addBean(Servlet.class, new ExampleMcpServer());
+
+               var microservice = Microservice.create()
+                       .beanStore(beanStore)
+                       .configurations(JettyConfiguration.class)
+                       // This example has no interactive commands to offer; 
skip the Java console entirely so
+                       // startup doesn't print "Could not create console 
command" for whatever default console
+                       // commands a discovered config might otherwise 
register.
+                       .consoleEnabled(false)
+                       .build();
+               microservice.start();
+
+               return new ExampleServer(microservice, 
URI.create("http://localhost:"; + boundPort(jetty) + "/"));
+       }
+
+       /**
+        * Returns the root URL the server is listening on (e.g. {@code 
http://localhost:5000/}).
+        *
+        * @return The root URL. Never <jk>null</jk>.
+        */
+       public URI getRootUrl() {
+               return rootUrl;
+       }
+
+       @Override
+       public void close() throws Exception {
+               microservice.stop();
+       }
+
+       /**
+        * Runs the server until the process is killed.
+        *
+        * @param args Optional single argument: the port to listen on 
(defaults to {@link #DEFAULT_PORT}).
+        * @throws Exception If the server fails to start.
+        */
+       public static void main(String[] args) throws Exception {
+               var port = args.length > 0 ? Integer.parseInt(args[0]) : 
DEFAULT_PORT;
+               var server = start(port);
+               System.out.println("Juneau MCP example server is listening at " 
+ server.getRootUrl());
+               System.out.println("Drive it with:  ExampleClient " + 
server.getRootUrl());
+               System.out.println("Press Ctrl-C to stop.");
+               // Microservice.join() defaults to a documented no-op, so it 
won't actually block here; join the
+               // main thread directly to keep the process (and the Jetty 
daemon threads it started) alive.
+               Thread.currentThread().join();
+       }
+
+       /** Builds a Jetty server with a single connector on {@code port} and a 
root servlet context. */
+       private static Server buildServer(int port) {
+               var server = new Server();
+               var connector = new ServerConnector(server);
+               connector.setPort(port);
+               server.addConnector(connector);
+               var handler = new ServletContextHandler();
+               handler.setContextPath("/");
+               // JettyServerComponent discovers the handler via this 
attribute (same convention as jetty.xml).
+               server.setAttribute("ServletContextHandler", handler);
+               server.setHandler(handler);
+               return server;
+       }
+
+       /** Reads the actual bound port (meaningful even when the server was 
started on port 0). */
+       private static int boundPort(Server server) {
+               for (var connector : server.getConnectors())
+                       if (connector instanceof ServerConnector sc)
+                               return sc.getLocalPort();
+               throw new IllegalStateException("Could not determine the bound 
port of the Jetty server.");
+       }
+}
diff --git 
a/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/NoteStore.java
 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/NoteStore.java
new file mode 100644
index 0000000000..d28dd97a09
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/NoteStore.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp;
+
+import java.util.*;
+
+/**
+ * The example's entire "domain": a tiny in-memory map of note title &rarr; 
note body.
+ *
+ * <p>
+ * Every MCP surface the {@link ExampleMcpServer} exposes (tools, prompt, 
resources, resource
+ * template, completions) is just a thin wrapper over this one object, so a 
reader can see the
+ * whole scenario at a glance without wading through business logic. It is 
deliberately trivial;
+ * the interesting part is the MCP wiring, not the storage.
+ *
+ * <p>
+ * Access is synchronized because subscription/tool handlers can run on 
different threads
+ * (the held-open SSE pump, concurrent HTTP requests). A {@link LinkedHashMap} 
keeps listing
+ * order stable so the demo output is deterministic.
+ */
+public final class NoteStore {
+
+       /** URI scheme used to address a single note, e.g. {@code 
note:///groceries}. */
+       public static final String SCHEME = "note:///";
+
+       private final Map<String,String> notes = new LinkedHashMap<>();
+
+       /**
+        * Stores (or replaces) a note.
+        *
+        * @param title The note title (its key). Must not be <jk>null</jk>.
+        * @param body The note body. Must not be <jk>null</jk>.
+        */
+       public synchronized void put(String title, String body) {
+               notes.put(title, body);
+       }
+
+       /**
+        * Returns the body of a note.
+        *
+        * @param title The note title. Can be <jk>null</jk>.
+        * @return The body, or <jk>null</jk> if no such note exists.
+        */
+       public synchronized String get(String title) {
+               return notes.get(title);
+       }
+
+       /**
+        * Removes a note.
+        *
+        * @param title The note title. Can be <jk>null</jk>.
+        * @return <jk>true</jk> if a note was actually removed.
+        */
+       public synchronized boolean remove(String title) {
+               return notes.remove(title) != null;
+       }
+
+       /**
+        * Returns a snapshot of all note titles, in insertion order.
+        *
+        * @return An immutable, point-in-time copy of the titles. Never 
<jk>null</jk>.
+        */
+       public synchronized List<String> titles() {
+               return List.copyOf(notes.keySet());
+       }
+
+       /**
+        * Builds the canonical resource URI for a note title (e.g. {@code 
note:///groceries}).
+        *
+        * @param title The note title.
+        * @return The resource URI.
+        */
+       public static String uriFor(String title) {
+               return SCHEME + title;
+       }
+}
diff --git 
a/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/GreetingService.java
 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/GreetingService.java
new file mode 100644
index 0000000000..17a7505ac9
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/GreetingService.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp.spring;
+
+/**
+ * An ordinary Spring-managed service, injected into an MCP tool handler by 
{@link SpringExampleMcpServer}
+ * to prove that Spring dependency injection flows into MCP handlers when the 
server extends
+ * {@code SpringMcpRestServlet}.
+ */
+public class GreetingService {
+
+       /**
+        * Builds a greeting.
+        *
+        * @param name The name to greet.
+        * @return The greeting text.
+        */
+       public String greet(String name) {
+               return "Hello, " + name + "! (this greeting came from a 
Spring-managed bean)";
+       }
+}
diff --git 
a/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleApplication.java
 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleApplication.java
new file mode 100644
index 0000000000..62a54b1e5c
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleApplication.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp.spring;
+
+import org.springframework.boot.*;
+import org.springframework.boot.autoconfigure.*;
+import org.springframework.boot.web.servlet.*;
+import org.springframework.context.annotation.*;
+
+/**
+ * A minimal Spring Boot application that serves {@link 
SpringExampleMcpServer} on embedded Tomcat.
+ *
+ * <p>
+ * Run {@link #main(String[]) main} and the MCP endpoint is available at 
{@code http://localhost:8080/mcp}
+ * (default Spring Boot port). Point {@link 
org.apache.juneau.examples.mcp.ExampleClient ExampleClient} at
+ * that URL, or drive it with any MCP client. The three {@code @Bean} methods 
are the entire integration:
+ *
+ * <ul>
+ *     <li>the {@link GreetingService} the MCP tool resolves via Spring DI;
+ *     <li>the {@link SpringExampleMcpServer} servlet itself; and
+ *     <li>a {@link ServletRegistrationBean} mounting it at {@code /*} (so the 
MCP operation lands at {@code /mcp}).
+ * </ul>
+ */
+@SpringBootApplication
+public class SpringExampleApplication {
+
+       /**
+        * The Spring-managed service injected into the MCP {@code greet} tool.
+        *
+        * @return A new {@link GreetingService}.
+        */
+       @Bean
+       public GreetingService greetingService() {
+               return new GreetingService();
+       }
+
+       /**
+        * The MCP servlet bean (constructed by Spring so it participates in 
the application context).
+        *
+        * @return A new {@link SpringExampleMcpServer}.
+        */
+       @Bean
+       public SpringExampleMcpServer springExampleMcpServer() {
+               return new SpringExampleMcpServer();
+       }
+
+       /**
+        * Mounts the MCP servlet on the embedded container.
+        *
+        * @param servlet The MCP servlet bean.
+        * @return The registration mapping the servlet at {@code /*}.
+        */
+       @Bean
+       public ServletRegistrationBean<SpringExampleMcpServer> 
mcpServletRegistration(SpringExampleMcpServer servlet) {
+               return new ServletRegistrationBean<>(servlet, "/*");
+       }
+
+       /**
+        * Boots the application.
+        *
+        * @param args Standard Spring Boot arguments.
+        */
+       public static void main(String[] args) {
+               SpringApplication.run(SpringExampleApplication.class, args);
+       }
+}
diff --git 
a/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleMcpServer.java
 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleMcpServer.java
new file mode 100644
index 0000000000..816042a6d9
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/main/java/org/apache/juneau/examples/mcp/spring/SpringExampleMcpServer.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp.spring;
+
+import org.apache.juneau.marshall.json.*;
+import org.apache.juneau.marshall.sse.*;
+import org.apache.juneau.rest.server.*;
+import org.apache.juneau.rest.server.mcp.*;
+import org.apache.juneau.rest.server.mcp.v20260728.*;
+
+/**
+ * The Spring Boot flavor of the MCP server.
+ *
+ * <p>
+ * Extending {@link SpringMcpRestServlet} (rather than the plain {@code 
McpRestServlet}) is the whole point:
+ * it bridges the Spring {@code ApplicationContext} into each MCP handler's 
per-request {@link org.apache.juneau.commons.inject.BeanStore},
+ * so {@code ctx.getBean(GreetingService.class)} resolves the real 
Spring-managed {@link GreetingService}
+ * singleton. The same handler on a plain {@code McpRestServlet} would not see 
Spring beans.
+ *
+ * <p>
+ * When registered at servlet mapping {@code /*} (see {@link 
SpringExampleApplication}), the MCP endpoint is
+ * served at {@code /mcp} (the Spring convenience servlet mounts its operation 
at {@code /mcp} relative to the
+ * servlet root).
+ *
+ * @serial exclude
+ */
+@Rest(serializers = {JsonSerializer.class, SseSerializer.class}, parsers = 
JsonParser.class, defaultAccept = "application/json")
+public class SpringExampleMcpServer extends SpringMcpRestServlet {
+
+       private static final long serialVersionUID = 1L;
+
+       @Override
+       public McpServerConfig getMcpConfig() {
+               return new McpServerConfig()
+                       .setName("juneau-spring-greeting-example")
+                       .setVersion("1.0.0")
+                       .addTool(McpToolHandler.of(
+                               new 
McpToolSpec().setName("greet").setDescription("Greets a name using a 
Spring-injected GreetingService."),
+                               (arguments, ctx) -> {
+                                       var name = 
String.valueOf(arguments.getOrDefault("name", "world"));
+                                       return 
McpToolOutcome.text(ctx.getBean(GreetingService.class)
+                                               .map(service -> 
service.greet(name))
+                                               .orElse("(GreetingService was 
not resolvable from the BeanStore)"));
+                               }));
+       }
+
+       /**
+        * On revision {@code 2026-07-28}, {@code server/discover}'s {@code 
instructions} field is sourced from
+        * {@link McpOptions}, <b>not</b> {@link 
McpServerConfig#setInstructions}, which only feeds the legacy v1
+        * {@code initialize} handshake.
+        */
+       @Override
+       public McpOptions getMcpOptions() {
+               return new McpOptions()
+                       .setInstructions("Call the 'greet' tool; its greeting 
is produced by a Spring-managed bean.");
+       }
+}
diff --git 
a/juneau-examples/juneau-examples-mcp/src/test/java/org/apache/juneau/examples/mcp/ExampleMcpEndToEnd_Test.java
 
b/juneau-examples/juneau-examples-mcp/src/test/java/org/apache/juneau/examples/mcp/ExampleMcpEndToEnd_Test.java
new file mode 100644
index 0000000000..e98885948c
--- /dev/null
+++ 
b/juneau-examples/juneau-examples-mcp/src/test/java/org/apache/juneau/examples/mcp/ExampleMcpEndToEnd_Test.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.juneau.examples.mcp;
+
+import static org.apache.juneau.test.bct.BctAssertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.TestBase;
+import org.apache.juneau.bean.mcp.v20260728.*;
+import org.apache.juneau.rest.client.mcp.v20260728.*;
+import org.junit.jupiter.api.*;
+
+/**
+ * Proves the example actually works: boots {@link ExampleServer} in-process 
on an ephemeral port and drives
+ * the real {@link McpClient} through every surface the example demonstrates 
&mdash; discovery, listing, a
+ * tool call, a templated resource read, a completion, a prompt, a 
subscription change notification, and an
+ * elicitation (MRTR) round-trip &mdash; asserting the observable outcome of 
each.
+ *
+ * <p>
+ * It reuses {@link ExampleClient#connect(String)} so the exact client wiring 
the walkthrough uses is what is
+ * being tested. Each test publishes its own distinctly-titled note (rather 
than relying on notes published by
+ * other tests), so tests remain independent even though the server/client 
pair is shared across the class.
+ */
+class ExampleMcpEndToEnd_Test extends TestBase {
+
+       private static ExampleServer server;
+       private static McpClient client;
+
+       @BeforeAll
+       static void setUp() throws Exception {
+               server = ExampleServer.start(0);
+               client = ExampleClient.connect(server.getRootUrl().toString());
+       }
+
+       @AfterAll
+       static void tearDown() throws Exception {
+               if (client != null)
+                       client.close();
+               if (server != null)
+                       server.close();
+       }
+
+       // -------- server/discover --------
+
+       @Test
+       void a01_discover_reportsServerIdentityAndCapabilities() {
+               var discovered = client.discoveredServer();
+               assertBean(discovered, 
"serverInfo{name,version},capabilities{resources{subscribe}}",
+                       "{juneau-notes-example,1.0.0},{{true}}");
+       }
+
+       @Test
+       void a02_discover_reportsInstructions() {
+               // HIGH-1 regression guard: instructions are configured via 
McpOptions (createMcpOptions()), not
+               // McpServerConfig; on revision 2026-07-28 the latter is 
silently ignored for server/discover.
+               var discovered = client.discoveredServer();
+               assertEquals("A tiny notes service. Use 'publishNote' to add 
notes, read them via the "
+                       + "'note:///{title}' resource template, subscribe for 
change notifications, and try "
+                       + "'deleteNote' to see an elicitation (confirm) 
round-trip.", discovered.getInstructions());
+       }
+
+       // -------- listing --------
+
+       @Test
+       void b01_listTools_exposesRegisteredTools() throws Exception {
+               assertBean(client.listTools(), "tools{#{name}}", 
"{[{publishNote},{deleteNote}]}");
+       }
+
+       @Test
+       void b02_listPrompts_exposesRegisteredPrompts() throws Exception {
+               assertBean(client.listPrompts(), "prompts{#{name}}", 
"{[{summarize}]}");
+       }
+
+       @Test
+       void b03_listResources_exposesRegisteredResources() throws Exception {
+               assertBean(client.listResources(), "resources{#{name}}", 
"{[{note-index}]}");
+       }
+
+       @Test
+       void b04_listResourceTemplates_exposesRegisteredTemplate() throws 
Exception {
+               var templates = 
client.listResourceTemplates().getResourceTemplates();
+               assertEquals(1, templates.size());
+               assertBean(templates.get(0), "name,uriTemplate", 
"note,note:///{title}");
+       }
+
+       // -------- tool call + resource read --------
+
+       @Test
+       void c01_publishThenReadViaTemplate_roundTrips() throws Exception {
+               var stored = client.callTool("publishNote", Map.of("title", 
"groceries", "body", "Milk, eggs, bread"));
+               assertEquals("Stored note 'groceries' (17 chars).", 
stored.firstText());
+
+               var read = client.readResource(NoteStore.uriFor("groceries"));
+               var contents = read.getContents();
+               assertEquals(1, contents.size());
+               assertEquals("Milk, eggs, bread", 
((TextResourceContents)contents.get(0)).getText());
+       }
+
+       @Test
+       void c02_readIndex_listsPublishedNoteTitle() throws Exception {
+               client.callTool("publishNote", Map.of("title", "index-check", 
"body", "x"));
+               var contents = client.readResource(NoteStore.SCHEME + 
"index").getContents();
+               assertEquals(1, contents.size());
+               
assertTrue(((TextResourceContents)contents.get(0)).getText().contains("index-check"));
+       }
+
+       // -------- completion --------
+
+       @Test
+       void d01_completion_suggestsMatchingNoteTitles() throws Exception {
+               client.callTool("publishNote", Map.of("title", 
"groceries-for-completion", "body", "x"));
+               var ref = new 
ResourceTemplateReference().setUri(NoteStore.SCHEME + "{title}");
+               var completion = client.complete(ref, "title", 
"groceries-for-completion", null);
+               assertEquals(List.of("groceries-for-completion"), 
completion.getCompletion().getValues());
+       }
+
+       // -------- prompt --------
+
+       @Test
+       void e01_prompt_rendersMessageFromStoredNote() throws Exception {
+               client.callTool("publishNote", Map.of("title", "prompt-note", 
"body", "Milk, eggs, bread"));
+               var prompt = client.getPrompt("summarize", Map.of("title", 
"prompt-note"));
+               var messages = prompt.getMessages();
+               assertEquals(1, messages.size());
+               assertEquals(Role.USER, messages.get(0).getRole());
+               
assertTrue(((TextContent)messages.get(0).getContent()).getText().contains("Milk,
 eggs, bread"));
+       }
+
+       // -------- subscription --------
+
+       @Test
+       void f01_subscription_deliversResourceUpdatedOnPublish() throws 
Exception {
+               var noteUri = NoteStore.uriFor("subscribed");
+               var updates = new LinkedBlockingQueue<String>();
+               var acknowledged = new CountDownLatch(1);
+
+               var handle = client.listen(
+                       new 
SubscriptionFilter().setResourceSubscriptions(List.of(noteUri)),
+                       new McpSubscriptionListener() {
+                               @Override public void 
onAcknowledged(SubscriptionFilter honoredFilter) { acknowledged.countDown(); }
+                               @Override public void onResourceUpdated(String 
uri) { updates.add(uri); }
+                       });
+               try {
+                       assertTrue(acknowledged.await(15, TimeUnit.SECONDS), 
"subscription was not acknowledged");
+                       client.callTool("publishNote", Map.of("title", 
"subscribed", "body", "hi"));
+                       assertEquals(noteUri, updates.poll(15, 
TimeUnit.SECONDS));
+               } finally {
+                       handle.close();
+               }
+       }
+
+       // -------- deleteNote elicitation (MRTR) --------
+
+       @Test
+       void g01_deleteWithElicitation_confirmsThenDeletes() throws Exception {
+               client.callTool("publishNote", Map.of("title", "trash", "body", 
"delete me"));
+
+               var result = client.callToolWithElicitation("deleteNote", 
Map.of("title", "trash"), requests -> {
+                       var answers = new LinkedHashMap<String,ElicitResult>();
+                       requests.keySet().forEach(id -> answers.put(id,
+                               new 
ElicitResult().setAction(ElicitAction.ACCEPT).putContent("confirm", true)));
+                       return answers;
+               });
+               assertEquals("Deleted note 'trash'.", result.firstText());
+
+               // And it is really gone from the index.
+               var contents = client.readResource(NoteStore.SCHEME + 
"index").getContents();
+               
assertFalse(((TextResourceContents)contents.get(0)).getText().contains("trash"));
+       }
+
+       @Test
+       void g02_deleteWithElicitation_declineKeepsNote() throws Exception {
+               client.callTool("publishNote", Map.of("title", "keep", "body", 
"keep me"));
+
+               var result = client.callToolWithElicitation("deleteNote", 
Map.of("title", "keep"), requests -> {
+                       var answers = new LinkedHashMap<String,ElicitResult>();
+                       requests.keySet().forEach(id -> answers.put(id, new 
ElicitResult().setAction(ElicitAction.DECLINE)));
+                       return answers;
+               });
+               assertEquals("Deletion of 'keep' was cancelled.", 
result.firstText());
+
+               var contents = client.readResource(NoteStore.SCHEME + 
"index").getContents();
+               
assertTrue(((TextResourceContents)contents.get(0)).getText().contains("keep"));
+       }
+}
diff --git a/juneau-examples/pom.xml b/juneau-examples/pom.xml
index 44e4b9176e..9d92f4fb01 100644
--- a/juneau-examples/pom.xml
+++ b/juneau-examples/pom.xml
@@ -32,6 +32,7 @@
 
        <modules>
                <module>juneau-examples-core</module>
+               <module>juneau-examples-mcp</module>
        </modules>
 
        <build>
diff --git 
a/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
index 40ba44b98b..9722ac7902 100644
--- 
a/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
+++ 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/main/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient.java
@@ -234,6 +234,26 @@ public final class McpClient extends AbstractMcpClient {
                return call(McpMethods.TOOLS_CALL, params, 
CallToolResult.class);
        }
 
+       /**
+        * Convenience for {@link #callTool} followed by {@link 
CallToolResult#firstText()}.
+        *
+        * <p>
+        * Kills the {@code 
((TextContent)result.getContent().get(i)).getText()} cast-and-scan that a 
caller who
+        * only wants the tool's text result would otherwise repeat at every 
call site.
+        *
+        * @param name The tool name to invoke.
+        * @param arguments The tool arguments. Can be <jk>null</jk> (sent as 
an empty object).
+        * @return The first {@link TextContent} block's text found while 
scanning the result's content list in
+        *      order, or <jk>null</jk> if the content list is empty or unset, 
it contains no {@link TextContent}
+        *      block, or {@link #callTool} itself returned a <jk>null</jk> 
result.
+        * @throws IOException If a transport-level or (de)serialization error 
occurs.
+        * @throws McpException If the server returned a JSON-RPC error.
+        */
+       public String callToolText(String name, Map<String,Object> arguments) 
throws IOException {
+               var r = callTool(name, arguments);
+               return r == null ? null : r.firstText();
+       }
+
        /**
         * Sends {@value McpMethods#PROMPTS_LIST}.
         *
diff --git 
a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient_Methods_Test.java
 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient_Methods_Test.java
index 23899fd916..4025b09a34 100644
--- 
a/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient_Methods_Test.java
+++ 
b/juneau-rest/juneau-rest-client-mcp-v20260728/src/test/java/org/apache/juneau/rest/client/mcp/v20260728/McpClient_Methods_Test.java
@@ -75,6 +75,32 @@ class McpClient_Methods_Test {
                }
        }
 
+       @Test
+       void c02_callToolText_returnsFirstTextContentText() throws Exception {
+               var wire = 
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"hello\"}]}}";
+               try (var c = 
McpClient.builder().endpoint("http://x/mcp";).transport(ok(wire)).build()) {
+                       assertEquals("hello", c.callToolText("echo", 
Map.of("text", "hello")));
+               }
+       }
+
+       @Test
+       void c03_callToolText_nonTextFirstBlock_returnsNull() throws Exception {
+               var wire = 
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"content\":[{\"type\":\"audio\",\"data\":\"QUJD\",\"mimeType\":\"audio/wav\"}]}}";
+               try (var c = 
McpClient.builder().endpoint("http://x/mcp";).transport(ok(wire)).build()) {
+                       assertNull(c.callToolText("echo", Map.of()));
+               }
+       }
+
+       @Test
+       void c04_callToolText_nullResult_returnsNullNotNpe() throws Exception {
+               // The server returning "result":null is a documented, legal 
callTool() outcome; callToolText must
+               // not NPE dereferencing it.
+               var wire = "{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":null}";
+               try (var c = 
McpClient.builder().endpoint("http://x/mcp";).transport(ok(wire)).build()) {
+                       assertNull(c.callToolText("echo", Map.of()));
+               }
+       }
+
        @Test
        void d01_callTool_traceEchoEnabled_readsMetaPath() throws Exception {
                var wire = 
"{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"_meta\":{\"traceparent\":\"00-parent-01\",\"tracestate\":\"v=x\",\"baggage\":\"u=42\"},\"content\":[{\"type\":\"text\",\"text\":\"ok\"}]}}";
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses.java
index 6ad90d3659..defbdd6038 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses.java
@@ -72,4 +72,60 @@ public final class ElicitationResponses {
                ctx.inputResponses().keySet().forEach(id -> out.put(id, 
get(ctx, id)));
                return out;
        }
+
+       /**
+        * Returns a boolean field's value from a single question's answer, 
decline-safe.
+        *
+        * <p>
+        * Treats a missing id, a missing field, or a non-{@link 
ElicitAction#ACCEPT} action (declined, cancelled)
+        * as <jk>false</jk> rather than requiring the caller to null-check 
{@link #get} and its content map by
+        * hand &mdash; the safe default for a confirmation question is to 
never proceed on ambiguity.
+        *
+        * @param ctx The resume context.  Must not be <jk>null</jk>.
+        * @param id The server-assigned id this question was posed under.  
Must not be <jk>null</jk>.
+        * @param field The field name within the answer's content map.  Must 
not be <jk>null</jk>.
+        * @return <jk>true</jk> only if {@code id} is present, the action is 
{@link ElicitAction#ACCEPT}, and
+        *      {@code field} is present in the content map with value {@link 
Boolean#TRUE}. <jk>false</jk> in every
+        *      other case.
+        * @throws IllegalArgumentException If {@code ctx}, {@code id}, or 
{@code field} is <jk>null</jk>.
+        * @throws RuntimeException If the answer's decoded shape cannot be 
converted to {@link ElicitResult}.
+        */
+       public static boolean getBoolean(McpMrtrResumeContext ctx, String id, 
String field) {
+               assertArgNotNull("ctx", ctx);
+               assertArgNotNull("id", id);
+               assertArgNotNull("field", field);
+               var answer = get(ctx, id);
+               if (answer == null || answer.getAction() != ElicitAction.ACCEPT)
+                       return false;
+               var content = answer.getContent();
+               return content != null && 
Boolean.TRUE.equals(content.get(field));
+       }
+
+       /**
+        * Returns a string field's value from a single question's answer, 
decline-safe.
+        *
+        * <p>
+        * Treats a missing id, a missing field, or a non-{@link 
ElicitAction#ACCEPT} action (declined, cancelled)
+        * as <jk>null</jk> rather than requiring the caller to null-check 
{@link #get} and its content map by hand.
+        *
+        * @param ctx The resume context.  Must not be <jk>null</jk>.
+        * @param id The server-assigned id this question was posed under.  
Must not be <jk>null</jk>.
+        * @param field The field name within the answer's content map.  Must 
not be <jk>null</jk>.
+        * @return The field's value as a {@link String}, or <jk>null</jk> if 
{@code id} is absent, the action is
+        *      not {@link ElicitAction#ACCEPT}, {@code field} is absent, or 
its value is not a {@link String}.
+        * @throws IllegalArgumentException If {@code ctx}, {@code id}, or 
{@code field} is <jk>null</jk>.
+        * @throws RuntimeException If the answer's decoded shape cannot be 
converted to {@link ElicitResult}.
+        */
+       public static String getString(McpMrtrResumeContext ctx, String id, 
String field) {
+               assertArgNotNull("ctx", ctx);
+               assertArgNotNull("id", id);
+               assertArgNotNull("field", field);
+               var answer = get(ctx, id);
+               if (answer == null || answer.getAction() != ElicitAction.ACCEPT)
+                       return null;
+               var content = answer.getContent();
+               if (content == null)
+                       return null;
+               return content.get(field) instanceof String s ? s : null;
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrResumeContext.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrResumeContext.java
index ab09064110..69b68aeae6 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrResumeContext.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/main/java/org/apache/juneau/rest/server/mcp/v20260728/McpMrtrResumeContext.java
@@ -90,4 +90,26 @@ public record McpMrtrResumeContext(Object continuation, 
Map<String,Object> input
                        return null;
                return Json.to(Json.of(continuation), type);
        }
+
+       /**
+        * Returns the continuation as a {@link String}.
+        *
+        * <p>
+        * Convenience for the common case documented on {@link 
#continuationAs(Class)}: a handler that pauses with
+        * a plain {@code String} continuation can call this instead of 
spelling out {@code continuationAs(String.class)}
+        * at the headline pause/resume call site.
+        *
+        * <p>
+        * <b>Conversion is not lenient.</b> {@code 
continuationAs(String.class)} routes through the JSON marshaller,
+        * which parses {@code String} the same as any other target type 
&mdash; it requires a quoted JSON string
+        * literal, so a continuation that decoded to anything else (a bare 
number, a {@code JsonMap}, a
+        * {@code JsonList}, a boolean) throws below rather than being 
stringified. Only a continuation whose
+        * decoded shape is already a {@link String} converts successfully.
+        *
+        * @return The continuation as a {@link String}, or <jk>null</jk> if 
the continuation is <jk>null</jk>.
+        * @throws RuntimeException If the continuation's decoded shape is not 
already a {@link String}.
+        */
+       public String continuationAsString() {
+               return continuationAs(String.class);
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses_Test.java
index e0981f57f7..eb786e2dda 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses_Test.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/ElicitationResponses_Test.java
@@ -92,4 +92,117 @@ class ElicitationResponses_Test {
                var ctx = new McpMrtrResumeContext(null, responses);
                assertThrows(RuntimeException.class, () -> 
ElicitationResponses.get(ctx, "q1"));
        }
+
+       @Test void a10_getBoolean_acceptedTrueContent_returnsTrue() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "accept", "content", JsonMap.of("confirm", true)));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertTrue(ElicitationResponses.getBoolean(ctx, "q1", 
"confirm"));
+       }
+
+       @Test void a11_getBoolean_acceptedFalseContent_returnsFalse() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "accept", "content", JsonMap.of("confirm", false)));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertFalse(ElicitationResponses.getBoolean(ctx, "q1", 
"confirm"));
+       }
+
+       @Test void a12_getBoolean_declinedAction_returnsFalse() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "decline"));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertFalse(ElicitationResponses.getBoolean(ctx, "q1", 
"confirm"));
+       }
+
+       @Test void a13_getBoolean_missingId_returnsFalse() {
+               var ctx = new McpMrtrResumeContext(null, Map.of());
+               assertFalse(ElicitationResponses.getBoolean(ctx, "missing", 
"confirm"));
+       }
+
+       @Test void a14_getBoolean_missingField_returnsFalse() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "accept", "content", JsonMap.of()));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertFalse(ElicitationResponses.getBoolean(ctx, "q1", 
"confirm"));
+       }
+
+       @Test void a15_getBoolean_nullFieldThrows() {
+               var ctx = new McpMrtrResumeContext(null, Map.of());
+               var e = assertThrows(IllegalArgumentException.class, () -> 
ElicitationResponses.getBoolean(ctx, "q1", null));
+               assertEquals("Argument 'field' cannot be null.", 
e.getMessage());
+       }
+
+       @Test void a16_getString_acceptedContent_returnsValue() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "accept", "content", JsonMap.of("name", "al")));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertEquals("al", ElicitationResponses.getString(ctx, "q1", 
"name"));
+       }
+
+       @Test void a17_getString_declinedAction_returnsNull() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "decline"));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertNull(ElicitationResponses.getString(ctx, "q1", "name"));
+       }
+
+       @Test void a18_getString_missingId_returnsNull() {
+               var ctx = new McpMrtrResumeContext(null, Map.of());
+               assertNull(ElicitationResponses.getString(ctx, "missing", 
"name"));
+       }
+
+       @Test void a19_getString_nonStringValue_returnsNull() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "accept", "content", JsonMap.of("name", 42)));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertNull(ElicitationResponses.getString(ctx, "q1", "name"));
+       }
+
+       @Test void a20_getString_nullFieldThrows() {
+               var ctx = new McpMrtrResumeContext(null, Map.of());
+               var e = assertThrows(IllegalArgumentException.class, () -> 
ElicitationResponses.getString(ctx, "q1", null));
+               assertEquals("Argument 'field' cannot be null.", 
e.getMessage());
+       }
+
+       @Test void a21_getString_acceptedMissingField_returnsNull() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "accept", "content", JsonMap.of()));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertNull(ElicitationResponses.getString(ctx, "q1", "name"));
+       }
+
+       @Test void a22_getBoolean_nonBooleanStringValue_returnsFalseSafely() {
+               // L-3: a string "true" is not Boolean.TRUE - getBoolean must 
not do a lenient/truthy coercion.
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "accept", "content", JsonMap.of("confirm", "true")));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertFalse(ElicitationResponses.getBoolean(ctx, "q1", 
"confirm"));
+       }
+
+       @Test void a23_getBoolean_cancelledAction_returnsFalse() {
+               // L-3: cancel (not just decline) is also a non-ACCEPT action 
that must be treated as false.
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "cancel"));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertFalse(ElicitationResponses.getBoolean(ctx, "q1", 
"confirm"));
+       }
+
+       @Test void a24_getString_cancelledAction_returnsNull() {
+               var responses = Map.<String,Object>of("q1", 
JsonMap.of("action", "cancel"));
+               var ctx = new McpMrtrResumeContext(null, responses);
+               assertNull(ElicitationResponses.getString(ctx, "q1", "name"));
+       }
+
+       @Test void a25_getBoolean_nullCtxBlamesCtxBeforeField() {
+               // L-2: ctx/id must be validated before field, so an all-null 
call blames "ctx", not "field".
+               var e = assertThrows(IllegalArgumentException.class, () -> 
ElicitationResponses.getBoolean(null, null, null));
+               assertEquals("Argument 'ctx' cannot be null.", e.getMessage());
+       }
+
+       @Test void a26_getBoolean_nullIdBlamesIdBeforeField() {
+               var ctx = new McpMrtrResumeContext(null, Map.of());
+               var e = assertThrows(IllegalArgumentException.class, () -> 
ElicitationResponses.getBoolean(ctx, null, null));
+               assertEquals("Argument 'id' cannot be null.", e.getMessage());
+       }
+
+       @Test void a27_getString_nullCtxBlamesCtxBeforeField() {
+               var e = assertThrows(IllegalArgumentException.class, () -> 
ElicitationResponses.getString(null, null, null));
+               assertEquals("Argument 'ctx' cannot be null.", e.getMessage());
+       }
+
+       @Test void a28_getString_nullIdBlamesIdBeforeField() {
+               var ctx = new McpMrtrResumeContext(null, Map.of());
+               var e = assertThrows(IllegalArgumentException.class, () -> 
ElicitationResponses.getString(ctx, null, null));
+               assertEquals("Argument 'id' cannot be null.", e.getMessage());
+       }
 }
diff --git 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpInputRequiredSignal_Test.java
 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpInputRequiredSignal_Test.java
index f310ad62a7..9749f14c43 100644
--- 
a/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpInputRequiredSignal_Test.java
+++ 
b/juneau-rest/juneau-rest-server-mcp-v20260728/src/test/java/org/apache/juneau/rest/server/mcp/v20260728/McpInputRequiredSignal_Test.java
@@ -112,6 +112,29 @@ class McpInputRequiredSignal_Test {
                assertThrows(RuntimeException.class, () -> 
a.continuationAs(B04_Continuation.class));
        }
 
+       @Test void b08_continuationAsStringReturnsStringContinuation() {
+               var a = new McpMrtrResumeContext("hello", Map.of());
+               assertEquals("hello", a.continuationAsString());
+       }
+
+       @Test void b09_continuationAsStringNullContinuationReturnsNull() {
+               var a = new McpMrtrResumeContext(null, Map.of());
+               assertNull(a.continuationAsString());
+       }
+
+       @Test void b10_continuationAsStringNumericContinuationThrows() {
+               // L-4: empirically, Json.to(Json.of(42), String.class) throws 
rather than stringifying - String
+               // conversion requires an already-quoted JSON string literal, 
so a bare number does not convert.
+               var a = new McpMrtrResumeContext(42, Map.of());
+               assertThrows(RuntimeException.class, a::continuationAsString);
+       }
+
+       @Test void b11_continuationAsStringJsonMapContinuationThrows() {
+               // L-4: a JsonMap continuation is likewise structurally 
incompatible with String.
+               var a = new McpMrtrResumeContext(JsonMap.of("step", 1), 
Map.of());
+               assertThrows(RuntimeException.class, a::continuationAsString);
+       }
+
        // -------- McpMrtrCapabilityContext ---------
 
        @Test void c01_recordAccessorReturnsConstructedValue() {

Reply via email to