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

jamesnetherton pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git


The following commit(s) were added to refs/heads/main by this push:
     new 9b4853c1d59 Fixes #9299. Publish outputSchema and structuredContent 
for ai-tool routes over MCP
9b4853c1d59 is described below

commit 9b4853c1d59888a2efd5b22bca9104db58683574
Author: Jiří Ondrušek <[email protected]>
AuthorDate: Tue Oct 20 08:08:24 2026 +0200

    Fixes #9299. Publish outputSchema and structuredContent for ai-tool routes 
over MCP
    
    QuarkusMcpServerEngine now publishes the ai-tool outputSchema in tools/list
    and returns structuredContent in tools/call, like the Vert.x engine.
    
    Co-authored-by: jiriOndrusek-agent <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../pages/reference/extensions/mcp-server.adoc     | 13 ++++++
 .../mcp-server/runtime/src/main/doc/usage.adoc     | 12 +++++
 .../mcp/server/QuarkusMcpServerEngine.java         |  8 +++-
 .../component/mcp/server/it/McpServerRoutes.java   |  9 ++++
 .../src/main/resources/application.properties      |  4 +-
 .../src/main/resources/schema/order-status.json    | 13 ++++++
 .../component/mcp/server/it/McpServerTest.java     | 51 ++++++++++++++++++++++
 7 files changed, 107 insertions(+), 3 deletions(-)

diff --git a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc 
b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc
index 21935ece3e3..6790c70ad1d 100644
--- a/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc
+++ b/docs/modules/ROOT/pages/reference/extensions/mcp-server.adoc
@@ -94,6 +94,19 @@ the JSON type (`string`, `integer`, `number`, `boolean`), 
and the
 `parameter.NAME.enum` options refine the generated input schema. Arguments
 arrive as message headers in the route (`${header.customerId}`).
 
+[id="extensions-mcp-server-usage-structured-tool-output"]
+=== Structured tool output
+
+Tools declaring their output with the `outputSchema` or `outputParameter.NAME`
+options publish it as the MCP `outputSchema`. The route body must then be
+JSON: MCP clients receive it as `structuredContent`, next to the same JSON as
+text content. See
+xref:{cq-camel-components}::ai-tool-component.adoc#_structured_tool_output_outputschema[Structured
 Tool Output]
+in the AI Tool component documentation.
+
+In native mode, an `outputSchema` loaded from `classpath:` must be
+xref:user-guide/native-mode.adoc#embedding-resource-in-native-executable[embedded
 in the native executable].
+
 
[id="extensions-mcp-server-usage-combining-the-camel-and-quarkus-mcp-server-configuration"]
 === Combining the Camel and quarkus-mcp-server configuration
 
diff --git a/extensions/mcp-server/runtime/src/main/doc/usage.adoc 
b/extensions/mcp-server/runtime/src/main/doc/usage.adoc
index 595f81e6ed5..64b4c5b9028 100644
--- a/extensions/mcp-server/runtime/src/main/doc/usage.adoc
+++ b/extensions/mcp-server/runtime/src/main/doc/usage.adoc
@@ -33,6 +33,18 @@ the JSON type (`string`, `integer`, `number`, `boolean`), 
and the
 `parameter.NAME.enum` options refine the generated input schema. Arguments
 arrive as message headers in the route (`${header.customerId}`).
 
+=== Structured tool output
+
+Tools declaring their output with the `outputSchema` or `outputParameter.NAME`
+options publish it as the MCP `outputSchema`. The route body must then be
+JSON: MCP clients receive it as `structuredContent`, next to the same JSON as
+text content. See
+xref:{cq-camel-components}::ai-tool-component.adoc#_structured_tool_output_outputschema[Structured
 Tool Output]
+in the AI Tool component documentation.
+
+In native mode, an `outputSchema` loaded from `classpath:` must be
+xref:user-guide/native-mode.adoc#embedding-resource-in-native-executable[embedded
 in the native executable].
+
 === Combining the Camel and quarkus-mcp-server configuration
 
 The configuration is split by ownership: `quarkus.camel.mcp-server.*`
diff --git 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
index 8c953f5a2ae..9b096423259 100644
--- 
a/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
+++ 
b/extensions/mcp-server/runtime/src/main/java/org/apache/camel/quarkus/component/mcp/server/QuarkusMcpServerEngine.java
@@ -17,6 +17,7 @@
 package org.apache.camel.quarkus.component.mcp.server;
 
 import java.lang.reflect.Type;
+import java.util.List;
 import java.util.Map;
 
 import io.quarkiverse.mcp.server.TextContent;
@@ -80,12 +81,17 @@ public class QuarkusMcpServerEngine extends ServiceSupport 
implements McpServerE
         if (tool.inputSchemaJson() != null) {
             definition.setInputSchema(new JsonObject(tool.inputSchemaJson()));
         }
+        if (tool.outputSchemaJson() != null) {
+            definition.setOutputSchema(new 
JsonObject(tool.outputSchemaJson()));
+        }
         applyAnnotations(definition, tool.annotations());
         definition.setHandler(arguments -> {
             Map<String, Object> args = arguments.args() != null ? 
arguments.args() : Map.of();
             McpToolCallResult result = tool.handler().call(args);
+            // the text content is kept next to structuredContent for clients 
without structured output support
             return result.isError()
-                    ? ToolResponse.error(result.text()) : 
ToolResponse.success(new TextContent(result.text()));
+                    ? ToolResponse.error(result.text())
+                    : new ToolResponse(false, List.of(new 
TextContent(result.text())), result.structuredContent(), null);
         });
         definition.register();
         LOG.debug("MCP tool added: {}", tool.name());
diff --git 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
index a350864e75a..b3aa7354e7c 100644
--- 
a/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
+++ 
b/integration-tests/mcp-server/src/main/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerRoutes.java
@@ -60,5 +60,14 @@ public class McpServerRoutes extends RouteBuilder {
                     List<?> items = e.getMessage().getHeader("items", 
List.class);
                     e.getMessage().setBody("order for customer " + 
customer.get("id") + " with " + items.size() + " item(s)");
                 });
+
+        from("ai-tool:order_status?tags=conformance&description=Get the status 
of an order"
+                + "&outputSchema=classpath:schema/order-status.json")
+                
.setBody(constant("{\"orderId\":\"O-1\",\"status\":\"shipped\",\"items\":[\"BOOK\",\"PEN\"]}"));
+
+        from("ai-tool:get_weather?tags=conformance&description=Get the weather"
+                + 
"&outputParameter.temperature=number&outputParameter.temperature.required=true"
+                + 
"&outputParameter.unit=string&outputParameter.unit.enum=celsius,fahrenheit")
+                
.setBody(constant("{\"temperature\":21.5,\"unit\":\"celsius\"}"));
     }
 }
diff --git 
a/integration-tests/mcp-server/src/main/resources/application.properties 
b/integration-tests/mcp-server/src/main/resources/application.properties
index 92abd5f8cbe..11d2cf00164 100644
--- a/integration-tests/mcp-server/src/main/resources/application.properties
+++ b/integration-tests/mcp-server/src/main/resources/application.properties
@@ -16,5 +16,5 @@
 ## ---------------------------------------------------------------------------
 quarkus.camel.mcp-server.tags=conformance
 quarkus.camel.mcp-server.tool-timeout=2000
-# the argSchema resource must be reachable in native mode
-quarkus.native.resources.includes=schema/create-order.json
+# the argSchema and outputSchema resources must be reachable in native mode
+quarkus.native.resources.includes=schema/create-order.json,schema/order-status.json
diff --git 
a/integration-tests/mcp-server/src/main/resources/schema/order-status.json 
b/integration-tests/mcp-server/src/main/resources/schema/order-status.json
new file mode 100644
index 00000000000..15d6f551795
--- /dev/null
+++ b/integration-tests/mcp-server/src/main/resources/schema/order-status.json
@@ -0,0 +1,13 @@
+{
+  "type": "object",
+  "properties": {
+    "orderId": { "type": "string", "description": "The order id" },
+    "status": { "type": "string", "enum": ["pending", "shipped", "delivered"] 
},
+    "items": {
+      "type": "array",
+      "description": "The ordered SKUs",
+      "items": { "type": "string" }
+    }
+  },
+  "required": ["orderId", "status", "items"]
+}
diff --git 
a/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
 
b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
index f629eec8949..a74e680068a 100644
--- 
a/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
+++ 
b/integration-tests/mcp-server/src/test/java/org/apache/camel/quarkus/component/mcp/server/it/McpServerTest.java
@@ -30,6 +30,8 @@ import io.quarkiverse.mcp.server.test.McpAssured.ToolInfo;
 import io.quarkiverse.mcp.server.test.McpAssured.ToolsPage;
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
 import org.eclipse.microprofile.config.ConfigProvider;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Test;
@@ -115,6 +117,54 @@ class McpServerTest {
                 .thenAssertResults();
     }
 
+    @Test
+    void testOutputSchemaIsPublished() {
+        client().when()
+                .toolsList(page -> {
+                    // outputSchema resource, published as-is with its nested 
structure
+                    JsonObject orderStatus = toolByName(page, 
"order_status").outputSchema();
+                    assertThat(orderStatus).isNotNull();
+                    
assertThat(orderStatus.getJsonObject("properties").getJsonObject("items")
+                            
.getJsonObject("items").getString("type")).isEqualTo("string");
+                    
assertThat(orderStatus.getJsonArray("required")).containsExactly("orderId", 
"status", "items");
+
+                    // flat outputParameter.* options, published as the 
generated schema
+                    JsonObject weather = toolByName(page, 
"get_weather").outputSchema();
+                    assertThat(weather).isNotNull();
+                    
assertThat(weather.getJsonObject("properties").getJsonObject("temperature")
+                            .getString("type")).isEqualTo("number");
+                    
assertThat(weather.getJsonObject("properties").getJsonObject("unit").getJsonArray("enum"))
+                            .containsExactly("celsius", "fahrenheit");
+                    
assertThat(weather.getJsonArray("required")).containsExactly("temperature");
+
+                    // tools without an output declaration are unchanged
+                    assertThat(toolByName(page, 
"say_hello").outputSchema()).isNull();
+                })
+                .thenAssertResults();
+    }
+
+    @Test
+    void testStructuredContentIsReturned() {
+        client().when()
+                .toolsCall("order_status", response -> {
+                    assertThat(response.isError()).isFalse();
+                    assertThat(response.structuredContent()).isEqualTo(new 
JsonObject()
+                            .put("orderId", "O-1")
+                            .put("status", "shipped")
+                            .put("items", new 
JsonArray().add("BOOK").add("PEN")));
+                    // the JSON text is still returned for clients without 
structured output support
+                    assertThat(textOf(response))
+                            
.isEqualTo("{\"orderId\":\"O-1\",\"status\":\"shipped\",\"items\":[\"BOOK\",\"PEN\"]}");
+                })
+                .toolsCall("get_weather", response -> {
+                    assertThat(response.isError()).isFalse();
+                    assertThat(response.structuredContent())
+                            .isEqualTo(new JsonObject().put("temperature", 
21.5).put("unit", "celsius"));
+                    
assertThat(textOf(response)).isEqualTo("{\"temperature\":21.5,\"unit\":\"celsius\"}");
+                })
+                .thenAssertResults();
+    }
+
     @Test
     void testQuarkusAnnotatedToolsCoexistWithCamelTools() {
         // both tool sources are served by the same MCP server
@@ -133,6 +183,7 @@ class McpServerTest {
                 .toolsCall("say_hello", Map.of("name", "World"), response -> {
                     assertThat(response.isError()).isNotEqualTo(Boolean.TRUE);
                     assertThat(textOf(response)).isEqualTo("Hello World");
+                    assertThat(response.structuredContent()).isNull();
                 })
                 .thenAssertResults();
     }

Reply via email to