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

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


The following commit(s) were added to refs/heads/docs by this push:
     new 04ddb14e7b feat: MCP structured tool output across both revisions 
(2025-06-18 + 2026-07-28)
04ddb14e7b is described below

commit 04ddb14e7b046fec2220eda83252110084caaa63
Author: James Bognar <[email protected]>
AuthorDate: Thu Jul 30 19:33:59 2026 -0400

    feat: MCP structured tool output across both revisions (2025-06-18 + 
2026-07-28)
    
    Neutral outputSchema/structuredContent, neutral typed-tool bridge with 
schema
    auto-derivation, v1 wire completion + schema narrowing, shared bounded-JSON
    safety, and serializer parity. TODO-312c1.
---
 pages/release-notes/10.0.0.md          | 26 +++++++++++++--------
 pages/topics/05.07.JuneauBeanMcp.md    | 26 ++++++++++++++++++---
 pages/topics/11.JuneauRestServerMcp.md | 41 +++++++++++++++++++++-------------
 3 files changed, 66 insertions(+), 27 deletions(-)

diff --git a/pages/release-notes/10.0.0.md b/pages/release-notes/10.0.0.md
index c58929b894..e31457859b 100644
--- a/pages/release-notes/10.0.0.md
+++ b/pages/release-notes/10.0.0.md
@@ -753,7 +753,7 @@ The bean module has been renamed from `juneau-bean-mcp` to 
`juneau-bean-mcp-2025
 ### Coverage
 
 - **Initialization** — `InitializeRequest`, `InitializeResult`, 
`Implementation`, `ClientCapabilities`, `ServerCapabilities`, and the 
per-feature capability beans (`ToolCapability`, `PromptCapability`, 
`ResourceCapability`, `LoggingCapability`, `RootsCapability`).
-- **Tools** — `Tool`, `CallToolRequest`, `CallToolResult`, `ListToolsResult`, 
plus the `JsonSchema` argument-schema bean.
+- **Tools** — `Tool`, `CallToolRequest`, `CallToolResult`, `ListToolsResult`, 
plus the `JsonSchema` argument-schema bean. `Tool.outputSchema` and 
`CallToolResult.structuredContent` — part of MCP structured tool output since 
`2025-06-18` but previously missing from this module — are now present, closing 
a prior gap in this revision's own bean coverage; see below.
 - **Prompts** — `Prompt`, `PromptArgument`, `GetPromptRequest`, 
`GetPromptResult`, `ListPromptsResult`, `PromptMessage`, `Role`.
 - **Resources** — `Resource`, `ReadResourceRequest`, `ReadResourceResult`, 
`ListResourcesResult`, plus polymorphic `ResourceContents` 
(`TextResourceContents`, `BlobResourceContents`).
 - **Polymorphic content blocks** — `Content` interface with `TextContent`, 
`ImageContent`, `EmbeddedResourceContent` discriminated by a `type` property.
@@ -798,9 +798,13 @@ A new adapter module, `juneau-rest-server-mcp-2025-06-18` 
(package `org.apache.j
 - **`org.apache.juneau.rest.server.mcp.v20250618.McpRevision`** — the 
`McpRevision` implementation for protocol revision `2025-06-18`. Replaces the 
old `McpDispatcher`/`Mcp` façade: owns the JSON-RPC method table (`initialize`, 
`ping`, `tools/list|call`, `prompts/list|get`, `resources/list|read`) and the 
error-code table. The error-code table intentionally preserves a known-wrong 
mapping carried over unmodified from the pre-re-layering dispatcher — 
unknown-method, tool-not-found, prompt- [...]
 - **`org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet`** — concrete 
abstract servlet for this revision; subclass it (instead of the core 
`McpRestServlet`) and implement `createMcpConfig()`. Exposes a `protected 
ServerCapabilities capabilities()` hook (returns `null` by default) for 
explicitly overriding the `initialize` capabilities advertisement instead of 
relying on auto-derivation from the registered tool/prompt/resource lists.
 - **`org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint`** — mixin 
interface for this revision, at parity with 
`org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet`: implement 
`getMcpConfig()`, and optionally override its `default ServerCapabilities 
capabilities()` hook.
-- **`McpTypedToolHandler<A,R>`**, **`McpTypedPromptHandler<A>`**, 
**`McpTypedHandlers`** — the typed sugar layer, moved here (package-renamed 
only) since it's revision-specific by construction: it binds arguments into 
this revision's wire-bean argument types and adapts the result to the neutral 
`McpToolHandler`/`McpPromptHandler` raw interfaces the core registry holds.
+- **`McpTypedPromptHandler<A>`**, **`McpTypedHandlers`** (prompt-only) — the 
typed *prompt* sugar layer stays here, since it's genuinely revision-specific 
by construction: it binds arguments into this revision's wire-bean argument 
types and adapts the result to the neutral `McpPromptHandler` raw interface the 
core registry holds.
 
-**New behavior:** on a given `McpServerConfig`'s first dispatch, this revision 
validates that every registered tool's `McpSchema` uses only the 6 JSON Schema 
keywords its wire `JsonSchema` bean can represent (`type`, `properties`, 
`required`, `additionalProperties`, `items`, `$defs`) — throwing 
`IllegalArgumentException` naming the offending tool and keyword if not, rather 
than silently dropping an unsupported keyword on the wire.
+**Typed *tool* sugar is revision-neutral, not revision-specific.** 
`McpTypedToolHandler<A,R>` and its adaptation into a raw `McpToolHandler` now 
live in the core (`org.apache.juneau.rest.server.mcp`), not this adapter — one 
typed tool handler serves either dated adapter unchanged, deriving 
`inputSchema`/`outputSchema` from its declared argument/result `Type`s and 
producing `structuredContent` automatically. This corrects an earlier plan-time 
assumption that all typed sugar was dated by c [...]
+
+**Gap closed: structured tool output, present since `2025-06-18`, is now fully 
supported end to end.** `Tool.outputSchema` and 
`CallToolResult.structuredContent` were part of the MCP `2025-06-18` 
specification from the start (constrained to object-shaped schemas and a 
JSON-object `structuredContent` value) but were missing from this module's 
`Tool`/`CallToolResult` beans and unreachable from the neutral core. 10.0.0 
adds both fields to the `2025-06-18` wire beans, adds matching `outputSc [...]
+
+**New behavior:** on a given `McpServerConfig`'s first dispatch, this revision 
validates that every registered tool's `McpSchema` uses only the 6 JSON Schema 
keywords its wire `JsonSchema` bean can represent (`type`, `properties`, 
`required`, `additionalProperties`, `items`, `$defs`) — throwing 
`IllegalArgumentException` naming the offending tool and keyword if not, rather 
than silently dropping an unsupported keyword on the wire. This same 
first-dispatch check now also covers `outputSch [...]
 
 ### Drop-in servlet
 
@@ -828,19 +832,23 @@ public class MyResource extends BasicRestServlet 
implements McpEndpoint {
 
 ### Typed handlers
 
+Typed *tools* use the revision-neutral `McpTypedToolHandler<A,R>` (core), so 
the same handler works unchanged against this adapter or the `2026-07-28` 
adapter:
+
 ```java
 public class WeatherArgs { String city; String unit; /* getters/setters */ }
+public class WeatherResult { String summary; /* getters/setters */ }
 
-McpTypedToolHandler<WeatherArgs, String> typed = new McpTypedToolHandler<>() {
-    @Override public Tool descriptor() { return new Tool().setName("weather"); 
}
-    @Override public Class<WeatherArgs> argumentType() { return 
WeatherArgs.class; }
-    @Override public String call(WeatherArgs a, BeanStore ctx) { return 
"sunny"; }
+McpTypedToolHandler<WeatherArgs, WeatherResult> typed = new 
McpTypedToolHandler<>() {
+    @Override public McpToolSpec descriptor() { return new 
McpToolSpec().setName("weather"); }
+    @Override public Type argumentType() { return WeatherArgs.class; }
+    @Override public Type resultType() { return WeatherResult.class; }
+    @Override public WeatherResult call(WeatherArgs a, BeanStore ctx) { return 
new WeatherResult(); }
 };
 
-config.addTool(McpTypedHandlers.adaptTool(typed));
+config.addTool(typed);
 ```
 
-A non-`CallToolResult` return value (like the `String` above) is 
JSON-serialized and wrapped in a single-`TextContent` `CallToolResult`; 
returning a `CallToolResult` directly passes it through unchanged.
+`inputSchema`/`outputSchema` are derived from `argumentType()`/`resultType()`; 
the returned `R` is canonicalized once into `structuredContent` and mirrored as 
compact JSON text by default (override `mirrorStructuredContentAsText()` to opt 
out). Typed *prompts* still use the dated `McpTypedPromptHandler<A>` (see "New 
Classes" above) and adapt via `McpTypedHandlers.adaptPrompt(...)`.
 
 ### Pagination
 
diff --git a/pages/topics/05.07.JuneauBeanMcp.md 
b/pages/topics/05.07.JuneauBeanMcp.md
index 0c3b1917df..7ab9a7d5aa 100644
--- a/pages/topics/05.07.JuneauBeanMcp.md
+++ b/pages/topics/05.07.JuneauBeanMcp.md
@@ -43,10 +43,10 @@ The JSON-RPC 2.0 envelope beans (`JsonRpcRequest`, 
`JsonRpcResponse`, `JsonRpcEr
 
 | Bean | Purpose |
 |---|---|
-| `Tool` | Tool descriptor (`name`, `description`, `inputSchema`). |
+| `Tool` | Tool descriptor: `name`, `description`, `inputSchema`, 
`outputSchema`. |
 | `ListToolsResult` | Result of `tools/list`. |
-| `CallToolRequest` / `CallToolResult` | `tools/call` request/response. |
-| `JsonSchema` | Lightweight JSON Schema bean for tool argument schemas (six 
keywords: `type`, `properties`, `required`, `additionalProperties`, `items`, 
`$defs`). |
+| `CallToolRequest` / `CallToolResult` | `tools/call` request/response. 
`CallToolResult` carries `content`, `isError`, `structuredContent` (JSON object 
in this revision). |
+| `JsonSchema` | Six-keyword schema bean (`type`, `properties`, `required`, 
`additionalProperties`, `items`, `$defs`) used by both `Tool.inputSchema` and 
`Tool.outputSchema`. |
 
 ### Prompts
 
@@ -149,6 +149,26 @@ Tool echo = new Tool()
 ListToolsResult listed = new ListToolsResult().setTools(List.of(echo));
 ```
 
+### Building a tool with structured output
+
+MCP `2025-06-18` introduced structured tool output (`Tool.outputSchema`, 
`CallToolResult.structuredContent`), constrained to object-shaped schemas and a 
JSON-object `structuredContent` value; MCP `2026-07-28` broadens this to 
arbitrary JSON Schema Draft 2020-12 shapes and any JSON value.
+
+```java
+Tool weather = new Tool()
+    .setName("weather")
+    .setDescription("Looks up the current weather for a city.")
+    .setOutputSchema(new JsonSchema()
+        .setType("object")
+        .setProperties(Map.of(
+            "summary", new JsonSchema().setType("string"),
+            "temp", new JsonSchema().setType("number")
+        ))
+    );
+
+CallToolResult result = new CallToolResult()
+    .setStructuredContent(JsonMap.of("summary", "Sunny", "temp", 23.5));
+```
+
 ### Building a tool result with mixed content
 
 ```java
diff --git a/pages/topics/11.JuneauRestServerMcp.md 
b/pages/topics/11.JuneauRestServerMcp.md
index eb605e216c..64e9edd7e9 100644
--- a/pages/topics/11.JuneauRestServerMcp.md
+++ b/pages/topics/11.JuneauRestServerMcp.md
@@ -14,7 +14,7 @@ MCP is a JSON-RPC 2.0 protocol that lets AI assistants 
discover and invoke exter
 - A **revision SPI** (`McpRevision`) that a protocol revision implements — the 
neutral core dispatches through it without knowing what's on the other side.
 - A drop-in **servlet** (`McpRestServlet`, concretely 
`org.apache.juneau.rest.server.mcp.v20250618.McpRestServlet`) for the common 
case of "expose an MCP endpoint at `POST /mcp`".
 - An **interface mixin** (`McpEndpoint`, concretely 
`org.apache.juneau.rest.server.mcp.v20250618.McpEndpoint`) for embedding an MCP 
endpoint inside an existing `@Rest` resource.
-- Functional **handler interfaces** for tools/prompts/resources, typed against 
a revision-neutral model, plus a typed sugar layer (in the adapter) that does 
argument binding and result wrapping for you.
+- Functional **handler interfaces** for tools/prompts/resources, typed against 
a revision-neutral model, plus a revision-neutral **typed tool bridge** 
(`McpTypedToolHandler`, in the core) that derives input/output schemas and does 
argument binding and result canonicalization for you. Typed prompts remain an 
adapter-specific sugar layer.
 - A pluggable **pagination** seam for `*/list` methods.
 
 The split exists so that a future MCP protocol revision can be added as a 
second adapter module without touching the JSON-RPC envelope or this neutral 
core — enforced by a `maven-enforcer-plugin` banned-dependency rule on the core 
module's `pom.xml` that fails the build if `juneau-rest-server-mcp` ever 
depends on `juneau-bean-mcp-*` or `juneau-rest-server-mcp-*`. There's 
deliberately no `ServiceLoader` auto-discovery (unlike the transport-provider 
pattern in `juneau-rest-client-apache-ht [...]
@@ -143,12 +143,13 @@ The `2025-06-18` adapter's wire `JsonSchema` bean only 
supports six keywords (`t
 
 ### Typed tool handler (sugar)
 
-When you'd rather receive a Juneau bean for arguments and let the framework 
wrap your result, use `McpTypedToolHandler` and adapt it via 
`McpTypedHandlers.adaptTool(...)`. These two types are revision-specific (they 
bind to this revision's wire-bean argument/return types), so they live in the 
`v20250618` adapter package, not the core:
+When you'd rather receive a Juneau bean for arguments and return one for the 
result — with both an input and output JSON Schema derived for you — implement 
the revision-neutral `McpTypedToolHandler<A,R>` and register it directly. 
Unlike typed prompts, typed tools are **not** adapter-specific: one handler 
serves either dated adapter unchanged.
 
 ```java
-import org.apache.juneau.bean.mcp.v20250618.*;
+import java.lang.reflect.*;
+
 import org.apache.juneau.commons.inject.*;
-import org.apache.juneau.rest.server.mcp.v20250618.*;
+import org.apache.juneau.rest.server.mcp.*;
 
 public class WeatherArgs {
     public String city;
@@ -162,12 +163,17 @@ public class WeatherResult {
 
 public class WeatherTool implements McpTypedToolHandler<WeatherArgs, 
WeatherResult> {
     @Override
-    public Tool descriptor() {
-        return new Tool().setName("weather");
+    public McpToolSpec descriptor() {
+        return new McpToolSpec()
+            .setName("weather")
+            .setDescription("Looks up the current weather for a city.");
     }
 
     @Override
-    public Class<WeatherArgs> argumentType() { return WeatherArgs.class; }
+    public Type argumentType() { return WeatherArgs.class; }
+
+    @Override
+    public Type resultType() { return WeatherResult.class; }
 
     @Override
     public WeatherResult call(WeatherArgs args, BeanStore ctx) {
@@ -178,18 +184,23 @@ public class WeatherTool implements 
McpTypedToolHandler<WeatherArgs, WeatherResu
     }
 }
 
-config.addTool(McpTypedHandlers.adaptTool(new WeatherTool()));
+config.addTool(new WeatherTool());
 ```
 
-The adapter:
+The core bridge (`McpTypedHandlers.adaptTool(...)`, invoked implicitly by the 
singular `McpServerConfig.addTool(McpTypedToolHandler)` overload above):
+
+1. Derives `inputSchema` from `argumentType()` and `outputSchema` from 
`resultType()` with `juneau-marshall`'s `JsonSchemaGenerator`, including 
`$defs` for referenced bean types. Schemas you set manually on `descriptor()` 
are replaced by the derived ones, so the declared types and the advertised 
schema can never drift.
+2. JSON-binds the inbound argument map into `argumentType()`; a binding 
failure is a JSON-RPC `-32602 Invalid params` error naming the argument type.
+3. Invokes your typed `call(...)`.
+4. Canonicalizes the returned `R` to a JSON tree in a single serialization 
pass — with `addBeanTypes(false)` so no `_type` discriminator leaks into 
structured data — bounds the tree, and assigns it to 
`McpToolOutcome.structuredContent`.
+5. By default, mirrors that same canonical tree as one compact-JSON 
`TextContent` block for backward compatibility with clients that only read 
`content`. Override `mirrorStructuredContentAsText()` to return `false` and 
suppress the mirrored text block.
+
+Both dated adapters map `outputSchema`/`structuredContent` on the wire, but 
with different constraints, matching MCP's own history: **structured tool 
output was introduced in `2025-06-18`** (`Tool.outputSchema`, 
`CallToolResult.structuredContent`), constrained to object-shaped schemas and a 
JSON-object `structuredContent` value, and **broadened in `2026-07-28`** to 
arbitrary JSON Schema Draft 2020-12 shapes and any JSON value. Concretely:
 
-1. JSON-roundtrips the inbound argument map into `argumentType()`.
-2. Invokes your typed `call(...)`.
-3. If the return value is a `CallToolResult`, passes it through unchanged.
-4. Otherwise, JSON-serializes the return and wraps it in a 
single-`TextContent` `CallToolResult`.
-5. Converts the whole thing (descriptor and outcome) to the neutral 
`McpToolSpec`/`McpToolOutcome` types the core registry holds — this 
wire→neutral mapping is the mirror image of the neutral→wire mapping the 
revision performs when handling a raw `McpToolHandler`.
+- The `2025-06-18` adapter validates both schemas against its six-keyword, 
object-rooted `JsonSchema` bean at first dispatch (see "Tool schema validation" 
below) and requires `structuredContent` to be a JSON object; a typed tool 
returning a bare scalar/array works under `2026-07-28` but is rejected at 
`2025-06-18`'s first dispatch.
+- The `2026-07-28` adapter maps both fields through its unrestricted Draft 
2020-12 `JsonSchema<?>` bean and accepts any JSON value for `structuredContent`.
 
-If binding fails, the adapter emits a JSON-RPC `-32602 Invalid params` error.
+Both dated servlets and mixins apply identical serializer policy centrally 
(`@SerializerConfig(addBeanTypes="true", uriResolution="NONE")` on the neutral 
operations), so a typed tool produces byte-identical structured output and 
mirrored text whether it's registered behind a servlet or an endpoint mixin.
 
 ### Prompts and resources
 

Reply via email to