Cameleer

MCP server

Cameleer serves your ai-tool, ai-resource and ai-prompt routes to MCP clients (Claude Desktop, VS Code, Cursor and agent frameworks) over Streamable HTTP or stdio, in both protocol eras: 2026-07-28 and 2025-11-25. Every call goes through the same envelope: argument checks, deduplication, approvals, timeouts that cancel and an audit log.

Overview and use cases: Cameleer MCP · Cargo feature: ai · Apache Camel: MCP Server (same camel.server.mcp-* property names)

Start it

Write the routes (examples/agents/mcp-server.camel.yaml):

yaml example: mcp-server-lookup-order
- route:
    from:
      uri: ai-tool:lookup_order
      parameters:
        description: Look up an order by id
        tags: support
        parameter.orderId: string
        parameter.orderId.required: true
        readOnlyHint: true
      steps:
        - setBody:
            simple: '{"orderId": "${header.orderId}", "status": "shipped"}'

Serve them over HTTP at http://localhost:8080/mcp:

bash
cameleer run mcp-server.camel.yaml --mcp

Or over stdio, for a client that starts the server itself:

bash
cameleer run mcp-server.camel.yaml --mcp-stdio

A client configuration for stdio (Claude Desktop's claude_desktop_config.json, and similar files in other clients):

json
{
  "mcpServers": {
    "orders": {
      "command": "cameleer",
      "args": ["run", "/path/to/mcp-server.camel.yaml", "--mcp-stdio"]
    }
  }
}

On the command line, --mcp and --mcp-stdio expose every tool, resource and prompt, tagged or not (they default camel.server.mcp-tags to *,untagged), unless camel.server.mcp-tags says otherwise. In a configuration file nothing is exposed until you choose:

properties
camel.server.mcp-enabled=true
camel.server.mcp-tags=support

Expose existing routes without editing them

An integration you already run can serve its routes as tools as they are, with no change to the route files:

bash
cameleer run orders.camel.yaml --mcp-expose rest:*            # every REST DSL operation
cameleer run orders.camel.yaml --mcp-expose direct:lookup-*   # direct routes by name
cameleer run orders.camel.yaml --mcp-stdio --mcp-expose rest:*,route:billing-*

--mcp-expose sets camel.server.mcp-expose-routes and turns on the MCP server (over HTTP, unless --mcp-stdio is given). A pattern selects routes by their consumer, with * as a wildcard:

patternselects
rest:*, rest:/orders*REST DSL operations, by their full path
direct:*, seda:orders-*routes by their consumer endpoint URI (options ignored)
route:billing-*routes by id

Routes that are tools, resources or prompts already (ai-tool:, ai-resource:, ai-prompt:, mcp:) are never exposed twice. The OpenAPI document route and CORS preflight routes of the REST DSL are skipped.

What a model sees.

  • A REST operation becomes a tool whose arguments are its documented parameters: path, query and header parameters with their dataType, description, defaultValue and allowableValues as JSON Schema, and body for a body parameter, POST, PUT and PATCH. A path placeholder without a param is a required string. The tool's name is the operation's route id when you set one (id:), else the method and path (get_orders_id); its description is the operation's description.
  • Any other route takes body (the message body: text or any JSON value) and headers (message headers by name). Its name is the route id, or the direct: name when the id was generated; its description is the route's description.

A call runs the route as its consumer would have delivered the message. A REST tool gets the method, the path with its placeholders filled in, the query and the parameters as headers, and the body as JSON. A reply with a status of 400 or more comes back as a tool error the model can read (HTTP 404: no such order); a JSON reply is structured content. Every call goes through the same reliability envelope as an ai-tool route: argument schema checks, deduplication, timeout, audit.

Safe by default. Exposing code that was never written as a tool deserves care, so:

  • nothing is exposed unless you name a pattern;
  • GET and HEAD operations are read-only (readOnlyHint); every other exposed tool is destructive (destructiveHint), so each call needs approval, and over MCP the client asks its user first (Approvals);
  • exposed tools carry the tag exposed. The server always serves the tools you asked to expose; camel.server.mcp-tags keeps deciding about every other tool.

Overrides, still without touching the routes. Properties keyed by the route id change what a tool looks like and how it is guarded. They take name, title, description, inputSchema, the hints, and every ai-tool envelope option (approval, timeout, rateLimit, idempotency, redact, authorize, cacheTtl, ...). A misspelt option stops start-up with its name.

properties
camel.server.mcp-expose-routes=rest:*,direct:lookup-customer
camel.server.mcp-expose-routes.lookup-customer.name=find_customer
camel.server.mcp-expose-routes.lookup-customer.readOnlyHint=true
camel.server.mcp-expose-routes.lookup-customer.approval=never
camel.server.mcp-expose-routes.lookup-customer.inputSchema={"type":"object","properties":{"body":{"type":"string","description":"The customer's email address"}},"required":["body"]}

Exposed tools are also available to the agent step and the model components, as tags: exposed. For a tool with a hand-written schema, output schema, elicitation or compensation, change the route's from to ai-tool: instead (Start it): one line, and the rest of the route stays.

Run it anywhere

The same route file serves MCP in every way Cameleer runs. Pick the column for how the client reaches the server: HTTP when the server runs on its own (a container, a gateway, a function) and clients connect to http://<host>:8080/mcp; stdio when the client starts the server as a subprocess on the same machine.

where it runsHTTPstdio
the cameleer binarycameleer run tools.camel.yaml --mcpcameleer run tools.camel.yaml --mcp-stdio
Dockerdocker run -p 8080:8080 ... --mcpdocker run -i ... --mcp-stdio
Docker Compose, Kubernetesa service with --mcp
your own Rust programcamel.server.mcp-enabled=truecamel.server.mcp-transport=stdio
Cloud Run, Knative, Azure Container Appsthe image with --mcp
AWS Lambdaa function URL or API Gateway
an edge gatewaytools next to the devicesfor a laptop on site

Docker

The image runs cameleer; mount the folder with your route files at /routes. Over HTTP:

bash
docker run --rm -p 8080:8080 -v "$PWD:/routes:ro" pmsudhi/cameleer \
  run /routes/mcp-server.camel.yaml --mcp

Over stdio, the client starts the container itself. Pass -i (stdin open) and no -t; logs go to stderr, so stdout carries only protocol messages. In a client configuration, use an absolute path for the folder (C:/Users/me/tools on Windows):

json
{
  "mcpServers": {
    "orders": {
      "command": "docker",
      "args": ["run", "-i", "--rm", "-v", "/path/to/tools:/routes:ro",
               "pmsudhi/cameleer", "run", "/routes/mcp-server.camel.yaml", "--mcp-stdio"]
    }
  }
}

To expose only some tools, add --prop camel.server.mcp-tags=support after --mcp or --mcp-stdio. Environment variables reach routes through -e NAME and {{env:NAME}}.

Connect a client

clientHTTP (server already running)stdio (client starts the server)
Claude Codeclaude mcp add --transport http orders http://localhost:8080/mcpclaude mcp add orders -- cameleer run /path/to/mcp-server.camel.yaml --mcp-stdio
VS Code (.vscode/mcp.json){"servers": {"orders": {"type": "http", "url": "http://localhost:8080/mcp"}}}{"servers": {"orders": {"type": "stdio", "command": "cameleer", "args": ["run", "/path/to/mcp-server.camel.yaml", "--mcp-stdio"]}}}
Cursor (~/.cursor/mcp.json){"mcpServers": {"orders": {"url": "http://localhost:8080/mcp"}}}{"mcpServers": {"orders": {"command": "cameleer", "args": ["run", "/path/to/mcp-server.camel.yaml", "--mcp-stdio"]}}}
Claude Desktop (claude_desktop_config.json)the configuration in Start it, or the Docker one above

Any stdio entry works with Docker too: replace cameleer and its arguments with the docker run -i command above.

Docker Compose and Kubernetes

A service that serves the tools next to what they use (here PostgreSQL), with the tags chosen and the health endpoint on:

yaml
services:
  tools:
    image: pmsudhi/cameleer
    command: ["run", "/routes/tools.camel.yaml", "--mcp",
              "--prop", "camel.server.mcp-tags=support",
              "--prop", "camel.server.enabled=true",
              "--prop", "camel.server.healthCheckEnabled=true"]
    ports: ["8080:8080"]
    volumes: ["./routes:/routes:ro"]
    environment:
      DB_URL: postgresql://app:secret@db:5432/shop
    read_only: true
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: shop

On Kubernetes the container takes the same args; probe /q/health/live and /q/health/ready (deployment guide). With more than one replica:

  • clients on 2026-07-28 are stateless, so any replica answers any request;
  • legacy sessions (2025-11-25 and older) live in the replica that created them: route each Mcp-Session-Id to the same replica (session affinity), or run one replica;
  • set camel.server.mcp-request-state-key to the same 64-hex-digit key on every replica, so an approval started on one replica can finish on another. The environment variable CAMEL_SERVER_MCP_REQUEST_STATE_KEY (from a Secret) sets it without a properties file.

In your own Rust program

Depend on cameleer-main (default features, or ai in a reduced build), then turn the server on with properties, as cameleer run --mcp does:

rust
fn main() -> cameleer_core::Result<()> {
    let mut main = cameleer_main::new_main();
    main.add_route_file("tools.camel.yaml");
    main.set_property("camel.server.mcp-enabled", "true");
    main.set_property("camel.server.mcp-tags", "support");
    main.set_property("camel.server.port", "8080");
    main.run()
}

camel.server.mcp-transport=stdio serves stdio instead. Tools can also be Rust routes: b.from("ai-tool:lookup_order?parameter.orderId=string&parameter.orderId.required=true").

Cloud Run and other serverless containers

Build a small image with the route files in it, then deploy it like any container:

dockerfile
FROM pmsudhi/cameleer
COPY tools.camel.yaml /routes/
CMD ["run", "/routes/tools.camel.yaml", "--mcp", "--prop", "camel.server.mcp-tags=support"]
bash
gcloud run deploy orders-tools --source . --port 8080 --no-allow-unauthenticated

Routes are running about 3 ms after the container starts, so scaling to zero costs the client little. Requests on 2026-07-28 are stateless and suit autoscaling; for legacy sessions turn on session affinity (--session-affinity), and set camel.server.mcp-request-state-key as for Kubernetes. Protect the endpoint with the platform's own authentication or with Cameleer's.

On AWS Lambda

cameleer is its own Lambda runtime, and function URL or API Gateway requests for camel.server.mcp-path reach the MCP server in process. Put the routes and an application.properties in the function:

properties
camel.server.mcp-enabled=true
camel.server.mcp-tags=support
bash
scripts/package-lambda.sh --arch arm64 --features ai --out tools.zip \
  mcp-server.camel.yaml application.properties
aws lambda create-function --function-name orders-tools --runtime provided.al2023 \
  --architectures arm64 --handler direct:lambda --zip-file fileb://tools.zip \
  --role arn:aws:iam::<account>:role/<role> \
  --environment "Variables={CAMEL_SERVER_MCP_REQUEST_STATE_KEY=$(openssl rand -hex 32)}"
aws lambda create-function-url-config --function-name orders-tools --auth-type AWS_IAM

The environment variable sets camel.server.mcp-request-state-key (any CAMEL_* variable overrides the matching property), so every sandbox seals approval state with the same key and an approval can finish in another sandbox.

Clients then use https://<url-id>.lambda-url.<region>.on.aws/mcp. Lambda returns each response whole, so use clients on 2026-07-28 (stateless JSON requests); legacy sessions and streamed events need one sandbox at a time (reserved concurrency 1) and are better served from a container. --features must include ai plus the component families the tools use (HTTP is always in the build).

Configuration

propertydefaultdescription
camel.server.mcp-enabledfalseStart the MCP server
camel.server.mcp-transporthttphttp or stdio
camel.server.mcp-tagsnoneTag patterns of what is exposed: *, support, crm-*, untagged (comma separated). Without it nothing is exposed; see Tags
camel.server.mcp-path/mcpThe HTTP path
camel.server.port, camel.server.host8080, 0.0.0.0Shared with platform-http routes and the management endpoints
camel.server.mcp-server-namethe context's nameThe name in serverInfo, as in Camel
camel.server.mcp-server-versionthe Cameleer versionThe version in serverInfo
camel.server.mcp-server-title, -server-description, -server-website-urlMore of serverInfo: a display name, a description and a web site
camel.server.mcp-server-iconsserverInfo.icons, a JSON array such as [{"src": "https://example.com/icon.png", "mimeType": "image/png"}]
camel.server.mcp-instructionsInstructions for the model, sent in server/discover and initialize
camel.server.mcp-tool-timeout20sThe default timeout of tools that set none
camel.server.mcp-expose-routesPatterns of existing routes to serve as tools with no route edits: rest:*, direct:lookup-*, route:billing-* (comma separated); --mcp-expose on the command line. Overrides per route: camel.server.mcp-expose-routes.<routeId>.<option>. See Expose existing routes
camel.server.mcp-resource-toolA tool name (or true for read_document): also list a read-only tool that reads any exposed resource by URI; its description lists them. Most clients attach resources only when the user picks one, so without it the model cannot read your policies or documents by itself
camel.server.mcp-expose-errorsfalseSend route exceptions to clients as they are. By default a client gets Tool NAME failed (error ID). The cause is in the server log. and the cause is logged with the same id
camel.server.mcp-resource-timeout20sHow long a resource read or a prompt may take
camel.server.mcp-page-size100Items per page of the lists
camel.server.mcp-cache-ttl0ttlMs on lists, server/discover and reads (2026-07-28)
camel.server.mcp-allowed-originslocalhostOrigins allowed to call the endpoint from a browser (* for any); other origins get 403
camel.server.mcp-session-keep-alive-interval30sKeep-alive comments on event streams
camel.server.mcp-session-idle-ttl300sWhen an idle legacy session expires
camel.server.mcp-request-state-keyrandom per process64 hex characters: the AES-256-GCM key of requestState; set it when several instances serve one endpoint
camel.server.mcp-request-state-ttl10mHow long an approval request stays valid

Authentication properties are in Authentication.

Tags

camel.server.mcp-tags decides which tools, resources and prompts clients see. Each pattern matches tags, with * as a wildcard: support matches the tag support, crm-* matches crm-orders. As in Camel, * alone matches every item that has a tag, but not items without tags. The pattern untagged (a Cameleer extension) exposes the items without tags. So camel.server.mcp-tags=*,untagged exposes everything, which is what --mcp and --mcp-stdio use by default.

Tools imported from other MCP servers have the tags mcp and mcp:NAME, so mcp:* exposes all of them (see MCP client: gateway).

Protocol versions

Both eras are served on the same endpoint. The server tells them apart by how the client opens, as the specification defines:

  • 2026-07-28. Every request carries _meta with io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities, and over HTTP the MCP-Protocol-Version, Mcp-Method and Mcp-Name headers. The server keeps no session. Every result has resultType, and lists and reads carry ttlMs and cacheScope. Clients learn about changes with subscriptions/listen.
  • 2025-11-25, 2025-06-18 and 2025-03-26. initialize opens a session (Mcp-Session-Id), GET opens its event stream, DELETE ends it, and there are ping and resources/subscribe.
2026-07-282025-11-25 and earlier
Discoveryserver/discoverinitialize
Tools, resources, templates, prompts, completionyesyes
Approvalsinput_required with an elicitation/create formelicitation/create request on the response stream
Change notificationssubscriptions/listenGET event stream
Errors-32020 header mismatch, -32021 missing capability, -32022 unsupported versionJSON-RPC errors, -32002 unknown resource

Approvals

A tool that needs approval (destructiveHint=true, or an approval rule, see ai-tool) doesn't run until someone says yes:

  • If the tool has an approvalEndpoint, approval happens out of band. The client gets a result that says the call is waiting, with its handle.
  • Otherwise the client's user is asked. In 2026-07-28 the result is input_required with an elicitation/create form, and a requestState sealed with AES-256-GCM and bound to the principal, the tool, a digest of the arguments and an expiry. The client retries with the answer. A tampered state, changed arguments or a replay run nothing. In legacy sessions the server sends elicitation/create on the call's response stream (HTTP) or on stdout (stdio). The form has one field, approve, ticked by default: pressing Accept approves, Decline (or unticking the box) refuses.
  • A client that didn't declare the elicitation capability gets -32021 and the call doesn't run.

Out-of-band approvals can also be resolved from the command line: cameleer tool approvals lists the waiting calls of a running integration, and cameleer tool approve <handle> or cameleer tool deny <handle> decides one. These commands reach the process through its local status files, which the container image turns off: run the container with -e CAMEL_LOCAL_STATUS=true (and --tmpfs /tmp with --read-only), then docker exec <container> cameleer tool approve <handle>. cameleer tool disable <name> (or POST /q/dev/tools/{name}/disable on the dev console) withdraws a tool while the server runs: connected clients get notifications/tools/list_changed and the tool's calls are refused. See ai-tool: kill switch.

Progress, logs, elicitation and sampling

A tool route can talk back to the client while it runs, with steps such as to: ai-tool:current?operation=progress:

  • progress sends notifications/progress when the request carried a progressToken.
  • log sends notifications/message at or above the level the client asked for (io.modelcontextprotocol/logLevel in _meta in 2026-07-28, logging/setLevel in legacy sessions).
  • elicit asks the client's user a question with a JSON Schema for the answer (elicitation/create).
  • sample asks the client's model for a completion (sampling/createMessage).

In legacy sessions the questions are requests to the client on the call's response stream or on stdout. In 2026-07-28 the call returns input_required with the questions, and the client calls again with the answers; the route then runs again from the start. A client that didn't declare the elicitation or sampling capability can't be asked, and the step fails with InteractionUnavailableException. The options, headers and an example are in ai-tool: talking to the client.

Rich results

A tool route can return MCP content blocks (images, audio, embedded resources) by setting the CamelAiToolContent header to a list of them. A prompt route can return messages whose content is such a block.

yaml example: mcp-server-screenshot
- route:
    from:
      uri: ai-tool:chart
      parameters:
        description: The sales chart as an image
        readOnlyHint: true
      steps:
        - setHeader:
            name: CamelAiToolContent
            simple: '[{"type": "text", "text": "Sales this week"}, {"type": "image", "mimeType": "image/png", "data": "${body}"}]'

Authentication

propertydescription
camel.server.mcp-authnone (default), api-key, jwt or introspection
camel.server.mcp-api-keysname=key,... for api-key (keys of 16 characters or more; use {{env:...}}). Clients send Authorization: Bearer <key> or X-API-Key
camel.server.mcp-resource-urlThe endpoint's public URL, such as https://tools.example.com/mcp. Tokens must name it as their audience (RFC 8707)
camel.server.mcp-jwt-jwks-urlThe JWKS to verify JWTs with (RS256/384/512, PS256/384/512, ES256/384); keys are cached and refetched when a new kid appears
camel.server.mcp-jwt-secretAn HMAC secret instead (HS256/384/512)
camel.server.mcp-jwt-issuerThe required iss
camel.server.mcp-jwt-audienceAccepted audiences, if not the resource URL
camel.server.mcp-introspection-url, -client-id, -client-secretRFC 7662 introspection
camel.server.mcp-authorization-serversPublished in the protected-resource metadata
camel.server.mcp-required-scopesScopes every token must have; others get 403 with insufficient_scope
camel.server.mcp-principal-claimThe claim that names the principal (default sub)

With jwt or introspection the server publishes RFC 9728 metadata at /.well-known/oauth-protected-resource and /.well-known/oauth-protected-resource/mcp. A request without a valid token gets 401 with WWW-Authenticate: Bearer resource_metadata="...", so clients can find the authorization server.

The client's token is never passed to the routes or downstream. Tool routes see the principal in CamelMcpSecurityPrincipal and CamelMcpSecurityPrincipalName (Camel's name), its scopes as a list in CamelMcpSecurityPrincipalRoles and as one space-separated string in CamelMcpSecurityPrincipalScopes, and the method in CamelMcpSecurityMethod. The same headers are visible to a tool's authorize predicate. Rate limits, the audit log and approvals use the same principal. Over stdio the principal is stdio.

What a call carries

_meta key on tools/callbecomes
io.cameleer/toolCallIdCamelAiToolCallId, the idempotency key
io.cameleer/conversationIdCamelAiToolConversationId, for compensation (legacy sessions use the session id)
traceparent, tracestate, baggageheaders of the tool's exchange

Without a call id, a tool with side effects is deduplicated on its principal, the conversation (the session in 2025-11-25 and earlier, io.cameleer/conversationId in 2026-07-28) and its arguments for idempotencyWindow: a client's retry runs nothing twice, and two conversations that ask for the same thing both run. Declare an idempotency_key argument, or set idempotency=false, when the same arguments may legitimately repeat.

Verification

  • The hermetic tests (crates/cameleer-ai/tests/mcp.rs) drive both eras over real HTTP and stdio connections, including approvals, subscriptions and authentication.
  • scripts/mcp-conformance.sh runs the official conformance suite (@modelcontextprotocol/conformance 0.1.16, pinned and checksum-verified) against scripts/mcp-conformance/fixtures.camel.yaml. It passes all 39 scenarios for 2025-11-25 and all 26 for 2025-06-18, including log and progress notifications during a call, sampling and elicitation. The baseline of known failures, scripts/mcp-conformance/expected-failures.yaml, is empty (server: []); a scenario that fails makes the run fail. This suite version predates 2026-07-28, so that era is covered by the hermetic tests.

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