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();
}