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