Cameleer

AI Tool component

The ai-tool component turns a route into a tool an AI agent can call. Every call goes through a reliability envelope: the arguments are checked against a JSON Schema, a retried call runs once, destructive calls wait for approval, a slow call is cancelled, and each call is written to a tamper-evident audit log.

Scheme: ai-tool: · Crate: cameleer-ai · Cargo feature: ai · Consumer: yes · Producer: yes (Cameleer extension) · Apache Camel: AI Tool

When to use it

  • Give an agent an action it can take in your systems (issue a refund, create a ticket, book a slot) and enforce the rules in the route, not in the prompt.
  • Make an existing integration route callable by a model, with the same error handling, retries and connectors it already has.
  • Keep a record of what an agent did, for whom, and with which approval.

URI format

text
ai-tool:name[?options]

name is the tool's name as the model sees it. A route that consumes from ai-tool:name publishes the tool when it starts and withdraws it when it stops; two routes can't publish the same name. A producer (to: ai-tool:name) calls the tool through the envelope, which is how routes and tests call tools.

Example

A refund tool. The agent sends {"orderId": "EU-4411", "amount": 25.5}. The arguments arrive as headers, and the route's body becomes the result.

yaml example: ai-tool-refund
- route:
    from:
      uri: ai-tool:refund
      parameters:
        description: Refund part or all of an order
        parameter.orderId: string
        parameter.orderId.description: The order to refund
        parameter.orderId.required: true
        parameter.amount: number
        parameter.amount.required: true
        destructiveHint: true                      # waits for approval (approval=auto)
        approvalEndpoint: direct:ask-a-human       # who is asked
        compensation: direct:reverse-refund        # undo it if the conversation fails later
        timeout: 10s
      steps:
        - toD: "https://payments.internal.example.com/orders/${header.orderId}/refund?amount=${header.amount}"
        - setBody:
            simple: "Refunded ${header.amount} on order ${header.orderId}"
- route:
    from:
      uri: direct:ask-a-human
      steps:
        - to: log:approvals   # or slack, e-mail, a ticket queue

The same tool in the Rust DSL, called with a retry:

rust
use cameleer_ai::{AiRegistry, ToolCall};
use cameleer_core::prelude::*;

let ctx = CamelContext::new();
cameleer_ai::register(&ctx);
ctx.add_routes(|b| {
    b.from("ai-tool:weather?description=Weather for a city&parameter.city=string&parameter.city.required=true&readOnlyHint=true")
        .set_body(simple("${header.city}: 21 degrees"));
})?;
ctx.start()?;
let tools = AiRegistry::of(&ctx)?;
let result = tools.call(ToolCall::new("weather", cameleer_core::json::parse(r#"{"city":"Oslo"}"#)?).with_call_id("call-1"));
assert_eq!(result.text, "Oslo: 21 degrees");

Options

Definition (Camel names)

optiondefaultdescription
descriptionthe tool's nameWhat the tool does; the model reads it to decide when to call the tool. Without one the name is used, as in Camel
titleA human-readable name
tagsComma-separated tags; an agent or server can offer only the tools with given tags
parameter.NAMEstringDeclares an argument and its type: string, integer, number, boolean, object, array
parameter.NAME.descriptionThe argument's description
parameter.NAME.requiredfalseWhether the argument is required
parameter.NAME.enumComma-separated allowed values
argSchemaA full JSON Schema for the arguments, inline or file:PATH, instead of parameter.* (never fetched over the network)
outputSchemaA JSON Schema for the structured result, inline or file:PATH
outputParameter.NAMEDeclares a field of the result, like parameter.NAME
argumentsAsBodyfalseAlso pass the arguments as the body, as a JSON string
readOnlyHintThe tool changes nothing
destructiveHintThe tool may destroy or overwrite something
idempotentHintCalling it twice with the same arguments has the same effect as once
openWorldHintThe tool reaches outside systems
searchabletrueWhether tool search may return it
returnDirectfalseThe result goes straight to the user, not back to the model
authorizationPolicyA bean (#name) implementing ToolAuthorizationPolicy that decides per call and principal

Reliability envelope (Cameleer extensions)

In Camel the hints are advisory. Here they set the defaults of an envelope that every caller goes through.

optiondefaultdescription
idempotencyautoauto deduplicates tools that are neither readOnlyHint nor idempotentHint; true or false forces it
idempotencyWindow1hHow long a call's result is remembered
idempotentRepositoryin memoryA bean (#name, an IdempotentRepository) that records keys across processes
approvalautoauto asks for approval when destructiveHint=true; always; never; or a Simple predicate over the arguments, such as ${header.amount} > 500
approvalEndpointAn endpoint told about each call that waits (arguments as headers, CamelAiToolApprovalHandle)
approvalTimeout1hHow long a call waits for approval
timeout20sHow long a caller waits; then the call's route is cancelled. 0 runs on the caller's thread with no limit
rateLimitCalls allowed per principal in rateLimitPeriod
rateLimitPeriod1mThe rate limit's period
circuitBreakerThreshold0 (off)Consecutive failures that open the breaker
circuitBreakerDelay30sHow long the breaker stays open before one trial call
circuitBreakerFallbackThe text the model gets while the breaker is open
compensationAn endpoint (usually direct:) that undoes the call; see Compensation
authorizeA Simple predicate over the arguments and the caller; a call it doesn't match is refused. See Guardrails
redactKinds of data replaced in results: secret, email, creditCard, iban, ssn, ipv4, phone, all, or regex:NAME=PATTERN, comma separated. See Guardrails
cacheTtl0 (off)Reuse a successful result for the same arguments and principal for this long
audittrueWrite this tool's calls to the audit log
version1The definition's version, for pinning
pintrueRefuse a changed definition that keeps its version
throwOnErrorfalseProducer: raise ToolCallException instead of returning an error result

Message headers

headerdirectiondescription
each argument, by namein (consumer)The call's arguments; camel* and undeclared arguments are dropped
CamelAiToolCallIdinThe model's or protocol's id for the call
CamelAiToolConversationIdinThe conversation or saga the call belongs to
CamelAiToolNameinThe tool's name
CamelAiToolPrincipalinWho the call is made for
CamelAiToolApprovedByin (producer)Marks the call as approved by this approver
CamelAiToolStatusout (producer)completed, error, approval-required or denied
CamelAiToolIsErrorout (producer); in (tool route)Producer: true when the call failed or was refused. In a tool route: set it to true to refuse the call yourself; the body becomes the error text the model reads (MCP isError), shown word for word, unlike an exception, whose cause is hidden from MCP clients. Use it for business refusals the model should act on, such as "order 1007 has shipped, so its address can no longer be changed"
CamelAiToolDuplicateout (producer)true when the recorded result of an earlier call was returned
CamelAiToolApprovalHandleout (producer)The handle of a call that waits for approval
CamelAiToolReturnDirectout (producer)The tool's returnDirect flag
CamelAiToolStructuredContentout (producer)The structured result (an object or array)
CamelAiToolContentout (tool route)Set by the route: MCP content blocks for the result (images, audio, embedded resources)
CamelMcpSecurityPrincipal, CamelMcpSecurityPrincipalName, CamelMcpSecurityPrincipalRoles, CamelMcpSecurityPrincipalScopes, CamelMcpSecurityMethodin (over MCP)The authenticated caller: its name, its scopes as a list (Roles) and as one space-separated string (Scopes), and how it authenticated
CamelAiToolProgress, CamelAiToolProgressTotalin (operation=progress)The progress so far and the total, if known
CamelAiToolLogLevel, CamelAiToolLoggerin (operation=log)The level (default info) and the logger name
CamelAiToolElicitSchemain (operation=elicit)The JSON Schema of the answer, when the body is the message text
CamelAiToolElicitActionout (operation=elicit)accept, decline or cancel
CamelAiToolSampleMaxTokensin (operation=sample)The maximum tokens of the answer (default 500)
CamelAiToolSampleModelout (operation=sample)The model that answered

Behavior

A call, step by step

  1. Kill switch. A tool named in camel.ai.disabled-tools, or disabled with AiRegistry::disable, is no longer listed and every call is refused.

  2. Authorization. The authorizationPolicy bean may refuse the call; its reason goes back to the model.

  3. Arguments. Arguments whose name starts with camel (any case) are dropped, as in Camel, and so are arguments the schema doesn't declare, unless it allows them (additionalProperties). What is left is validated against the full JSON Schema (2020-12, see json-validator). Camel only checks required fields. A failure returns an error that names each field and shows valid arguments:

    text
    Invalid arguments for tool weather:
    - $.city: is missing but it is required
    - $.unit: does not have a value in the enumeration [celsius, fahrenheit]
    Example of valid arguments: {"city":"text"}
  1. authorize. The predicate sees the arguments as headers and the caller; a call it doesn't match is refused (see Guardrails).
  2. Result cache. With cacheTtl, a result recorded for the same arguments and principal is returned and the route doesn't run.
  3. Rate limit, per principal.
  4. Circuit breaker. While it is open, calls get the fallback text and the route doesn't run.
  5. Approval. A call that needs approval doesn't run. It is parked with a handle and the result says so (CamelAiToolStatus=approval-required). An approver resolves it with AiRegistry::approve(handle, approver), which runs it and returns its result, or AiRegistry::deny(handle, approver, reason), which runs nothing. The approved call's route sees the approver in CamelAiToolApprovedBy. With an approvalEndpoint (approval out of band, by a person who decides later), the caller learns the outcome by making the same call again: while it waits it gets the same handle ("still waiting"), after the decision the result (or the rejection with its reason), without a second request to the approver. Over MCP, a tool without an approval endpoint asks the client's user instead (see MCP server: approvals).
  6. Idempotency. The key is the idempotency_key argument if the schema declares one, else the call id, else a digest of the principal, the conversation (CamelAiToolConversationId; over MCP the session, or io.cameleer/conversationId) and the arguments, so a retry in the same conversation is caught and two conversations that ask for the same thing both run. A repeated call returns the recorded result with CamelAiToolDuplicate=true. A repeat that arrives while the first is running waits for it. A failed call is not recorded, so a retry runs it.
  7. Compensation. The undo step is registered, see Compensation.
  8. The route runs on a new exchange that keeps only the caller's properties and variables (CamelVariable.*), as in Camel. Headers and the body don't leak into the tool.
  9. Timeout. When the caller stops waiting, the call is cancelled: the route stops before its next step (a step that is already running is not interrupted), and the exchange is released. In Camel the call keeps running.
  10. Result. An object or array body is the structured result, with its JSON as the text. An empty body is No result, and any other body is its text. With an outputSchema, a result that doesn't match it is an error.
  11. Redaction. With redact, matching values in the result are replaced.
  12. Audit. One record per call, see Audit log.

Guardrails

Three options put rules on a tool that a prompt can't talk its way around.

authorize is a Simple predicate. It is evaluated after the arguments are validated, on an exchange whose headers are the arguments, the call's own headers (over MCP, CamelMcpSecurityPrincipal, CamelMcpSecurityPrincipalRoles and the others), CamelAiToolPrincipal and CamelAiToolName. If it doesn't match, the route doesn't run and the model gets Not authorized to call refund_small with these arguments. The audit record has the decision denied and the rule. authorizationPolicy (a bean) is checked before it, for rules that need code.

Inside an endpoint URI, & separates options, so write && as %26%26 (and + as %2B, because + decodes to a space). The same goes for YAML parameters:: they are joined into the URI, and a value that contains ${ or {{ is copied as it is, not URL-encoded, so a predicate's && would cut it in two. RAW(...) also works when the predicate has no ).

yaml example: ai-tool-guardrails
- route:
    from:
      uri: ai-tool:refund_small
      parameters:
        description: Refund up to 100 on an order
        parameter.orderId: string
        parameter.orderId.required: true
        parameter.amount: number
        parameter.amount.required: true
        authorize: "${header.amount} <= 100 %26%26 ${header.CamelAiToolPrincipal} == 'support'"
      steps:
        - setBody:
            simple: "Refunded ${header.amount} on order ${header.orderId}"
- route:
    from:
      uri: ai-tool:customer
      parameters:
        description: A customer's record
        parameter.id: string
        parameter.id.required: true
        readOnlyHint: true
        redact: email,phone,secret       # or all, or regex:ticket=TCK-[0-9]+
        cacheTtl: 5m
      steps:
        - to: direct:load-customer
- route:
    from:
      uri: direct:load-customer
      steps:
        - setBody:
            simple: '{"id": "${header.id}", "name": "Ann", "email": "ann@example.com"}'

redact replaces personal data and secrets in a completed result with [REDACTED:kind], in the text, the structured result and the content blocks. It is applied before the result reaches the model, the MCP client or the audit log (the audit record keeps the digest of the redacted result and notes redacted N value(s)). The kinds:

kindmatches
secretAPI keys and tokens: sk-..., sk-ant-..., AWS AKIA..., GitHub ghp_... and similar, Slack xox?-..., JWTs, PEM private key headers
emailE-mail addresses
creditCard13 to 19 digits, with spaces or dashes, that pass the Luhn check
ibanIBANs: two letters, two digits, 11 to 30 letters or digits
ssnUS social security numbers, 123-45-6789
ipv4IPv4 addresses
phonePhone numbers: an optional +, then digits with spaces, dashes, dots or parentheses between them, 9 characters or more
allEvery kind above
regex:NAME=PATTERNYour own pattern, replaced with [REDACTED:NAME]

Custom patterns run first, then the built-in kinds in the order of the table, so a card number is not also taken for a phone number. A pattern can't contain a comma, because commas separate the kinds. An unknown kind or an invalid pattern fails when the route starts.

cacheTtl reuses a successful result for the same arguments and principal (a SHA-256 of both) for that long. Errors are not cached. A cached call doesn't run the route, the rate limit or the approval, and its audit record has the decision cached. Use it for read-only tools whose answer doesn't change by the second; for deduplicating calls with side effects, use idempotency.

Talking to the client: progress, logs, elicitation and sampling

While a tool route runs for an MCP client, it can send progress and log messages to the client, ask the client's user a question (elicitation) or ask the client's model for a completion (sampling). A step to: ai-tool:current?operation=... does it. The name in the URI is not used; the step finds the running call from the exchange.

yaml example: ai-tool-interaction
- route:
    from:
      uri: ai-tool:import_contacts
      parameters:
        description: Import a contact list after the user confirms the target list
        parameter.file: string
        parameter.file.required: true
      steps:
        - setHeader:
            name: CamelAiToolElicitSchema
            constant: '{"type": "object", "properties": {"list": {"type": "string"}}, "required": ["list"]}'
        - setBody:
            constant: Which mailing list should the contacts go to?
        - to: ai-tool:current?operation=elicit
        - choice:
            when:
              - simple: "${header.CamelAiToolElicitAction} != 'accept'"
                steps:
                  - setBody:
                      constant: Import cancelled by the user
                  - stop: {}
        - setProperty:
            name: list
            simple: "${body[list]}"
        - setHeader:
            name: CamelAiToolProgress
            constant: 1
        - setHeader:
            name: CamelAiToolProgressTotal
            constant: 2
        - setBody:
            constant: Reading the file
        - to: ai-tool:current?operation=progress
        - setHeader:
            name: CamelAiToolLogLevel
            constant: info
        - setBody:
            simple: "Importing into ${exchangeProperty.list}"
        - to: ai-tool:current?operation=log
        - setBody:
            simple: "Imported ${header.file} into ${exchangeProperty.list}"
operationinputwhat the client getsafter the step
progressCamelAiToolProgress (required), CamelAiToolProgressTotal, the body as the messagenotifications/progress, only when the request carried a progressTokennothing changes
logCamelAiToolLogLevel (default info), CamelAiToolLogger, the body as datanotifications/message, only when the client asked for logs at that level or belownothing changes
elicitthe body as the message and CamelAiToolElicitSchema, or the body as a whole {message, requestedSchema} objectelicitation/createthe body is the answer's content; CamelAiToolElicitAction is accept, decline or cancel
samplethe body as the user's message and CamelAiToolSampleMaxTokens (default 500), or the body as a whole sampling/createMessage params objectsampling/createMessagethe body is the text of the answer; CamelAiToolSampleModel is the model that answered

Log levels are MCP's: debug, info, notice, warning, error, critical, alert, emergency. In 2026-07-28 the client sets the minimum level in the request's _meta (io.modelcontextprotocol/logLevel); in legacy sessions it calls logging/setLevel. Without either, no log messages are sent. Notifications go on the request's event stream over HTTP and on stdout over stdio.

How a question reaches the client depends on the era:

  • 2025-11-25 and earlier. The server sends elicitation/create or sampling/createMessage to the client during the call, on the response stream (HTTP) or on stdout (stdio), and the step waits up to 10 minutes for the answer.
  • 2026-07-28. The server sends no requests to the client. The call ends with resultType: input_required, the questions in inputRequests and a sealed requestState. The client gets the answers and calls again with inputResponses. The route runs again from the start, and this time each step gets its answer, in order. Keep the steps before a question free of side effects, or make them idempotent.

A client that didn't declare the elicitation or sampling capability can't be asked: the step fails with the error kind org.apache.camel.component.ai.InteractionUnavailableException (the client did not declare the sampling capability), and so does a legacy client that doesn't answer. Handle it with doTry or onException to fall back, for example to a default value. Unhandled, the call fails and the client gets the reason; it is not hidden like other route errors (see camel.server.mcp-expose-errors in the MCP server configuration).

Outside an MCP call (the producer, the agent step, tags on a model component), progress and log do nothing, and elicit and sample fail with InteractionUnavailableException.

Compensation

compensation=direct:undo-x registers the undo step before the call runs, under the call's conversation id (CamelAiToolConversationId; calls without one register nothing). If the conversation fails later, AiRegistry::compensate(conversation) runs every registered undo step, newest first. Each undo step receives the original arguments as headers, the call's result as the body and CamelAiToolCompensating=true. AiRegistry::complete(conversation) ends a conversation and drops its undo steps.

Audit log

Each call appends a record: time, tool, principal, call and conversation ids, a SHA-256 digest of the arguments and of the result (never the data itself), the decision (allowed, invalid, approval-required, approved, rejected, rate-limited, circuit-open, disabled, denied, cached, compensate), the approver, the duplicate flag, the outcome and the duration. Records are hash-chained: each one holds the SHA-256 of the previous record plus its own content, so an edited, removed or reordered record fails cameleer_ai::audit::verify_file.

propertydefaultdescription
camel.ai.audit.enabledtrueKeep audit records
camel.ai.audit.fileAlso append them to this JSON lines file; an existing file is verified and continued
camel.ai.audit.maxRecords1000Records kept in memory (AiRegistry::audit().records())

Pinning

A tool's definition hash covers its name, title, description, schemas and hints. If a route changes a tool's definition but keeps its version, the route fails to start:

text
Tool transfer changed (definition hash 3f2a9c1d04be is now 9b71e0aa52c3) but its version is still 1: set a new version= to publish the change

Within one process the hashes are kept in memory. With camel.ai.manifest=tools.json they are kept in that file, so the check also covers restarts and deployments. A tool's description or schema can't change under a client that already trusts it.

Kill switch

camel.ai.disabled-tools=a,b withdraws tools at startup. AiRegistry::disable(name) and enable(name) do it at runtime. Either way the tool disappears from every list, its calls are refused, and AiRegistry::on_change listeners are told, so MCP clients get notifications/tools/list_changed.

An operator does the same from the command line, against an integration started with cameleer run (it finds the process by name or pid, like cameleer stop):

text
$ cameleer tool list orders
NAME    TAGS    READ-ONLY  DESTRUCTIVE  VERSION  STATUS
lookup  orders  yes                     1        enabled
refund  orders             yes          2        enabled
Pending approvals: 1
$ cameleer tool disable refund orders
tool refund disabled
$ cameleer tool enable refund orders
tool refund enabled

cameleer tool approvals lists the calls waiting for approval (handle, tool, principal, age, reason). cameleer tool approve <handle> [--by NAME] runs one and prints its result, and cameleer tool deny <handle> [--by NAME] [--reason TEXT] rejects it; both go to the audit log with the approver (--by, default the user name). tool list and tool approvals take --json. A name that no route publishes is refused, so a typo doesn't go unnoticed.

With the dev console (camel.server.dev-console-enabled=true), GET /q/dev/tools returns the same list as JSON, and POST /q/dev/tools/{name}/disable and /enable flip the switch. Both need the ai feature in the running binary; without it they say so.

Differences from Apache Camel

  • The hints and policies are enforced, not advisory: deduplication, approval, timeouts that cancel, rate limits, circuit breakers, compensation, audit, pinning and the kill switch are Cameleer extensions.
  • Arguments are validated against the full schema, not only the required fields.
  • Results keep their structure: a map or list is JSON plus structured content, not toString().
  • The producer (calling a tool from a route) is a Cameleer extension.
  • The MCP server serves these tools to MCP clients in both protocol eras (2026-07-28 and 2025-11-25), with the same envelope.
  • A route can return images, audio or embedded resources by setting CamelAiToolContent to a list of MCP content blocks.
  • authorize, redact, cacheTtl and the interaction operations (progress, log, elicit, sample) are Cameleer extensions.
  • Tools of remote MCP servers can be imported and go through the same envelope, see MCP client.

Found a problem?

If something on this page doesn't match what Cameleer does, please open an issue, or fork the repository, fix it, add a test, run cargo test --workspace and open a pull request.

Edit this page on GitHub