Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,50 @@ Top-level schema properties are exposed as exchange headers. Nested values are p

The root schema must be a JSON Schema object with a top-level `properties` map. Camel always allowlists top-level property names when invoking the tool route, even if the schema sets `additionalProperties: true`. Nested fields are not flattened into headers; only top-level properties become exchange headers (Map/List/primitive values).

=== Structured Tool Output (outputSchema)

MCP tools can declare an `outputSchema` and return `structuredContent` (typed JSON) so clients parse tool results reliably instead of re-interpreting free text. Use `outputParameter.*` for flat field definitions or `outputSchema` for raw JSON Schema (mirroring the input-side `parameter.*` / `argSchema` pattern). The two options are mutually exclusive.

When an output schema is declared, the route body must be JSON (a JSON string, `Map`, or `List`). Camel parses it into structured content and forwards it through the MCP bridge as `CallToolResult.structuredContent`. The text representation remains available for LLM adapters (LangChain4j, Spring AI) that consume string tool results.

[tabs]
====
Java::
+
[source,java]
----
from("ai-tool:getWeather?tags=weather&description=Get weather"
+ "&outputSchema=classpath:schemas/weather-result.json")
.process(exchange -> exchange.getMessage().setBody(
"{\"temperature\":21.5,\"unit\":\"celsius\"}"));
----
YAML::
+
[source,yaml]
----
- route:
from:
uri: ai-tool:getWeather
parameters:
tags: weather
description: "Get weather"
outputParameter.temperature: number
outputParameter.unit: string
steps:
- setBody:
constant: '{"temperature":21.5,"unit":"celsius"}'
----
====

Flat `outputParameter.*` options use the same sub-option syntax as input `parameter.*`:

* `outputParameter.NAME=TYPE` — field type (`string`, `integer`, `number`, `boolean`)
* `outputParameter.NAME.description=TEXT` — field description in the generated JSON Schema
* `outputParameter.NAME.required=true` — marks the field as required in the output schema
* `outputParameter.NAME.enum=val1,val2` — restricts allowed values

`outputSchema` supports Camel resource references such as `classpath:schemas/weather-result.json`.

=== MCP Tool Annotation Hints

When exposing `ai-tool` routes through the MCP Server component, you can declare optional behavioral hints aligned with the MCP specification (`ToolAnnotations`). MCP clients may use these hints for per-tool policy (for example auto-approving read-only tools or requiring confirmation before destructive ones).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj
case "idempotentHint": getOrCreateConfiguration(target).setIdempotentHint(property(camelContext, java.lang.Boolean.class, value)); return true;
case "openworldhint":
case "openWorldHint": getOrCreateConfiguration(target).setOpenWorldHint(property(camelContext, java.lang.Boolean.class, value)); return true;
case "outputparameters":
case "outputParameters": getOrCreateConfiguration(target).setOutputParameters(property(camelContext, java.util.Map.class, value)); return true;
case "outputschema":
case "outputSchema": getOrCreateConfiguration(target).setOutputSchema(property(camelContext, java.lang.String.class, value)); return true;
case "parameters": getOrCreateConfiguration(target).setParameters(property(camelContext, java.util.Map.class, value)); return true;
case "readonlyhint":
case "readOnlyHint": getOrCreateConfiguration(target).setReadOnlyHint(property(camelContext, java.lang.Boolean.class, value)); return true;
Expand All @@ -70,6 +74,10 @@ public Class<?> getOptionType(String name, boolean ignoreCase) {
case "idempotentHint": return java.lang.Boolean.class;
case "openworldhint":
case "openWorldHint": return java.lang.Boolean.class;
case "outputparameters":
case "outputParameters": return java.util.Map.class;
case "outputschema":
case "outputSchema": return java.lang.String.class;
case "parameters": return java.util.Map.class;
case "readonlyhint":
case "readOnlyHint": return java.lang.Boolean.class;
Expand Down Expand Up @@ -97,6 +105,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
case "idempotentHint": return getOrCreateConfiguration(target).getIdempotentHint();
case "openworldhint":
case "openWorldHint": return getOrCreateConfiguration(target).getOpenWorldHint();
case "outputparameters":
case "outputParameters": return getOrCreateConfiguration(target).getOutputParameters();
case "outputschema":
case "outputSchema": return getOrCreateConfiguration(target).getOutputSchema();
case "parameters": return getOrCreateConfiguration(target).getParameters();
case "readonlyhint":
case "readOnlyHint": return getOrCreateConfiguration(target).getReadOnlyHint();
Expand All @@ -109,6 +121,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
@Override
public Object getCollectionValueType(Object target, String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
case "outputparameters":
case "outputParameters": return java.lang.String.class;
case "parameters": return java.lang.String.class;
default: return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj
case "idempotentHint": target.setIdempotentHint(property(camelContext, java.lang.Boolean.class, value)); return true;
case "openworldhint":
case "openWorldHint": target.setOpenWorldHint(property(camelContext, java.lang.Boolean.class, value)); return true;
case "outputparameters":
case "outputParameters": target.setOutputParameters(property(camelContext, java.util.Map.class, value)); return true;
case "outputschema":
case "outputSchema": target.setOutputSchema(property(camelContext, java.lang.String.class, value)); return true;
case "parameters": target.setParameters(property(camelContext, java.util.Map.class, value)); return true;
case "readonlyhint":
case "readOnlyHint": target.setReadOnlyHint(property(camelContext, java.lang.Boolean.class, value)); return true;
Expand All @@ -53,6 +57,10 @@ public Class<?> getOptionType(String name, boolean ignoreCase) {
case "idempotentHint": return java.lang.Boolean.class;
case "openworldhint":
case "openWorldHint": return java.lang.Boolean.class;
case "outputparameters":
case "outputParameters": return java.util.Map.class;
case "outputschema":
case "outputSchema": return java.lang.String.class;
case "parameters": return java.util.Map.class;
case "readonlyhint":
case "readOnlyHint": return java.lang.Boolean.class;
Expand All @@ -75,6 +83,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
case "idempotentHint": return target.getIdempotentHint();
case "openworldhint":
case "openWorldHint": return target.getOpenWorldHint();
case "outputparameters":
case "outputParameters": return target.getOutputParameters();
case "outputschema":
case "outputSchema": return target.getOutputSchema();
case "parameters": return target.getParameters();
case "readonlyhint":
case "readOnlyHint": return target.getReadOnlyHint();
Expand All @@ -87,6 +99,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
@Override
public Object getCollectionValueType(Object target, String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
case "outputparameters":
case "outputParameters": return java.lang.String.class;
case "parameters": return java.lang.String.class;
default: return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ public boolean configure(CamelContext camelContext, Object obj, String name, Obj
case "idempotentHint": target.getConfiguration().setIdempotentHint(property(camelContext, java.lang.Boolean.class, value)); return true;
case "openworldhint":
case "openWorldHint": target.getConfiguration().setOpenWorldHint(property(camelContext, java.lang.Boolean.class, value)); return true;
case "outputparameters":
case "outputParameters": target.getConfiguration().setOutputParameters(property(camelContext, java.util.Map.class, value)); return true;
case "outputschema":
case "outputSchema": target.getConfiguration().setOutputSchema(property(camelContext, java.lang.String.class, value)); return true;
case "parameters": target.getConfiguration().setParameters(property(camelContext, java.util.Map.class, value)); return true;
case "readonlyhint":
case "readOnlyHint": target.getConfiguration().setReadOnlyHint(property(camelContext, java.lang.Boolean.class, value)); return true;
Expand Down Expand Up @@ -65,6 +69,10 @@ public Class<?> getOptionType(String name, boolean ignoreCase) {
case "idempotentHint": return java.lang.Boolean.class;
case "openworldhint":
case "openWorldHint": return java.lang.Boolean.class;
case "outputparameters":
case "outputParameters": return java.util.Map.class;
case "outputschema":
case "outputSchema": return java.lang.String.class;
case "parameters": return java.util.Map.class;
case "readonlyhint":
case "readOnlyHint": return java.lang.Boolean.class;
Expand Down Expand Up @@ -93,6 +101,10 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
case "idempotentHint": return target.getConfiguration().getIdempotentHint();
case "openworldhint":
case "openWorldHint": return target.getConfiguration().getOpenWorldHint();
case "outputparameters":
case "outputParameters": return target.getConfiguration().getOutputParameters();
case "outputschema":
case "outputSchema": return target.getConfiguration().getOutputSchema();
case "parameters": return target.getConfiguration().getParameters();
case "readonlyhint":
case "readOnlyHint": return target.getConfiguration().getReadOnlyHint();
Expand All @@ -105,6 +117,8 @@ public Object getOptionValue(Object obj, String name, boolean ignoreCase) {
@Override
public Object getCollectionValueType(Object target, String name, boolean ignoreCase) {
switch (ignoreCase ? name.toLowerCase() : name) {
case "outputparameters":
case "outputParameters": return java.lang.String.class;
case "parameters": return java.lang.String.class;
default: return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class AiToolEndpointUriFactory extends org.apache.camel.support.component
private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
private static final Map<String, String> MULTI_VALUE_PREFIXES;
static {
Set<String> props = new HashSet<>(13);
Set<String> props = new HashSet<>(15);
props.add("argSchema");
props.add("bridgeErrorHandler");
props.add("description");
Expand All @@ -33,6 +33,8 @@ public class AiToolEndpointUriFactory extends org.apache.camel.support.component
props.add("exchangePattern");
props.add("idempotentHint");
props.add("openWorldHint");
props.add("outputParameters");
props.add("outputSchema");
props.add("parameters");
props.add("readOnlyHint");
props.add("tags");
Expand All @@ -41,7 +43,8 @@ public class AiToolEndpointUriFactory extends org.apache.camel.support.component
PROPERTY_NAMES = Collections.unmodifiableSet(props);
SECRET_PROPERTY_NAMES = Collections.emptySet();
ENDPOINT_IDENTITY_PROPERTY_NAMES = Collections.emptySet();
Map<String, String> prefixes = new HashMap<>(1);
Map<String, String> prefixes = new HashMap<>(2);
prefixes.put("outputParameters", "outputParameter.");
prefixes.put("parameters", "parameter.");
MULTI_VALUE_PREFIXES = Collections.unmodifiableMap(prefixes);
}
Expand Down
Loading