MCP client and gateway
Cameleer connects to remote MCP servers, over Streamable HTTP or stdio, and imports their tools into its own tool registry. The imported tools go through the same envelope as
ai-toolroutes: argument checks, approvals for destructive tools, deduplication, timeouts, audit and pinning. Models use them from theagentstep or the model components, routes call them with themcpcomponent, and Cameleer's own MCP server can serve them again as a gateway.
Overview and use cases: Cameleer MCP · Cargo feature: ai · Apache Camel: none (Cameleer extension)
Connect a server
One block of properties per server. The block's name (github below) names the server
everywhere else.
# Streamable HTTP
camel.ai.mcp.github.url=https://mcp.example.com/github
camel.ai.mcp.github.bearerToken={{env:GITHUB_TOKEN}}
camel.ai.mcp.github.include=issue_*,pr_*
# stdio: Cameleer starts the program and talks to it over stdin and stdout
camel.ai.mcp.files.command=npx
camel.ai.mcp.files.args=-y,@modelcontextprotocol/server-filesystem,/data
camel.ai.mcp.files.env.NODE_ENV=productionWhen Cameleer starts, after the routes, it connects to each server, detects the protocol era and imports the tools. The log says how many:
Imported 4 tools from MCP server githubA server that is down doesn't stop the integration. Cameleer logs
MCP server github is not reachable (...); retrying every 30s and connects in the background
until it succeeds. Until then, that server's tools are not in the registry.
Configuration
| property | default | description |
|---|---|---|
camel.ai.mcp.NAME.url | The Streamable HTTP endpoint. Either url or command is required | |
camel.ai.mcp.NAME.command | stdio: the program to start | |
camel.ai.mcp.NAME.args | stdio: its arguments, comma separated | |
camel.ai.mcp.NAME.env.VAR | stdio: an environment variable of the program | |
camel.ai.mcp.NAME.header.H | HTTP: an extra request header, such as header.X-Tenant=acme | |
camel.ai.mcp.NAME.bearerToken | HTTP: sends Authorization: Bearer TOKEN | |
camel.ai.mcp.NAME.toolPrefix | NAME_ | The prefix of the imported tools' names; set it empty for none |
camel.ai.mcp.NAME.tags | Extra tags of the imported tools, comma separated | |
camel.ai.mcp.NAME.include, .exclude | Which tools to import: name patterns with *, comma separated. exclude wins | |
camel.ai.mcp.NAME.timeout | 30s | How long one request may take; also the imported tools' timeout |
camel.ai.mcp.NAME.protocol | auto | auto, 2026-07-28 or 2025-11-25 |
camel.ai.mcp.NAME.refreshInterval | Re-list the tools this often, in addition to list_changed | |
camel.ai.mcp.NAME.OPTION | as on ai-tool | An envelope option for every imported tool of the server: rateLimit, rateLimitPeriod, circuitBreakerThreshold, circuitBreakerDelay, circuitBreakerFallback, approval, approvalEndpoint, approvalTimeout, idempotency, idempotencyWindow, idempotentRepository, compensation, audit, authorize, redact, cacheTtl, authorizationPolicy (ai-tool) |
camel.ai.mcp.NAME.tools.TOOL.OPTION | The same options for one tool (TOOL is the remote server's name for it); they override the server-wide ones |
Use {{env:...}} placeholders for tokens. A stdio program's standard error is discarded.
Protocol eras
With protocol=auto, the client sends server/discover first. If the server lists
2026-07-28, the client uses that era: no session, _meta on every request. Any other answer,
including an error, falls back to initialize with 2025-11-25, and the client keeps the
Mcp-Session-Id the server returns. A legacy session that expires (HTTP 404) is reopened
once, and the request is sent again. protocol=2026-07-28 fails if server/discover fails;
protocol=2025-11-25 skips it.
On stop, the client ends a legacy HTTP session with DELETE and stops a stdio program.
Imported tools
Each remote tool becomes a local tool named PREFIX + its remote name: create_issue on
github becomes github_create_issue. The local tool keeps the remote title, description,
input and output schemas and annotations, and gets these tags:
mcp, on every imported tool,mcp:NAME, the server's own tag,- the tags in
camel.ai.mcp.NAME.tags.
A call to an imported tool runs through the envelope (see ai-tool: a call, step by step) and then goes to the remote server:
The arguments are validated against the remote schema locally. Invalid arguments never leave the process.
A tool with
destructiveHint: trueneeds approval (approval=always). Other tools need none.A tool that is neither
readOnlyHintnoridempotentHintis deduplicated: a repeated call with the same call id, or the same principal and arguments, returns the recorded result.The call is cancelled after the server's
timeout.The kill switch and the audit log work as for local tools:
cameleer tool disable github_create_issuewithdraws an imported tool too.Every other envelope option of
ai-toolcan be set for all the server's tools or for one tool: a rate limit per principal, a circuit breaker, anauthorizerule,redacton what comes back, a result cache. For example:propertiescamel.ai.mcp.github.rateLimit=30 camel.ai.mcp.github.redact=secret,email camel.ai.mcp.github.tools.delete_repository.authorize=${header.CamelAiToolPrincipal} == 'admin'A re-sync (
list_changed, a reconnect,refreshInterval) keeps tools whose definition and options didn't change, so their rate-limit counters, breaker state and cache survive it.
A remote result with isError is a failed call. A structured result (structuredContent)
stays structured; content blocks other than text (images, audio, resources) are passed on as
CamelAiToolContent.
A remote server that asks for input during a call (2026-07-28 input_required, or an
elicitation/create or sampling/createMessage request) gets a refusal: the client has no
user to ask on that connection.
Changes and rug-pull protection
The client follows the server's notifications/tools/list_changed: over stdio on the
program's output, over HTTP with subscriptions/listen (2026-07-28) or the session's GET
stream (legacy), reconnecting when the stream drops. With refreshInterval it also re-lists
on a timer. On each change it imports the list again: new tools appear, removed tools
disappear, and connected MCP clients of Cameleer's own server get list_changed in turn.
Imported tools are pinned like local ones (see ai-tool: pinning). If a server changes a tool's description, schema or annotations after it was imported, the new definition is refused and the tool is withdrawn. The log says why:
Tool weather of MCP server remote not imported: Tool remote_weather changed (definition hash 3f2a9c1d04be is now 9b71e0aa52c3) but its version is still 1: set a new version= to publish the changeThis stops a server that was trusted when it was added from rewriting a tool into an
instruction for the model later ("a rug pull"). In memory the pins last for the process; with
camel.ai.manifest=tools.json they also survive restarts. To accept a changed tool, remove its
entry from the manifest (or restart, without a manifest).
Use the tools from a model
The agent step offers a server's tools with tools: mcp:NAME, next to local tags:
- route:
from:
uri: direct:triage
steps:
- agent:
model: "openai:chat-completion?model=gpt-5.4-mini"
tools: mcp:github,support # the github server's tools and local support tools
systemMessage: You triage bug reports. Search for duplicates before you open an issue.
maxTurns: 8The model components take the same tags in tags:
- route:
from:
uri: direct:ask
steps:
- to: "openai:chat-completion?model={{env:OPENAI_MODEL:gpt-4o-mini}}&apiKey={{env:OPENAI_API_KEY}}&tags=mcp:github"A destructive remote tool pauses the conversation for approval like a local one (see the agent step).
To call a remote tool from a route without a model, use the mcp component:
to: mcp:github/create_issue.
Gateway
Cameleer's MCP server serves imported tools like local ones. So one Cameleer process can sit between your MCP clients and several third-party servers:
camel.ai.mcp.github.url=https://mcp.example.com/github
camel.ai.mcp.github.bearerToken={{env:GITHUB_TOKEN}}
camel.ai.mcp.files.command=files-server
camel.server.mcp-enabled=true
camel.server.mcp-tags=mcp:*,support
camel.server.mcp-auth=jwt
camel.server.mcp-jwt-jwks-url=https://login.example.com/.well-known/jwks.json
camel.server.mcp-resource-url=https://tools.example.com/mcpWhat that gives you:
- One endpoint. Clients configure
https://tools.example.com/mcpinstead of every server. They seegithub_*andfiles_*next to your ownsupporttools. - One authentication. Clients authenticate to Cameleer. The third-party credentials stay in Cameleer's configuration and are never sent to clients. The remote servers see Cameleer's credentials, not the end user's.
- One audit log. Every call, local or remote, gets a hash-chained audit record with the principal of the client.
- Your rules on their tools. Destructive remote tools need approval; the kill switch,
deduplication, schema checks and timeouts apply, and you can add rate limits, circuit
breakers,
authorizerules and redaction per server or per tool. Clients in either protocol era can use servers of either era. - Pinned definitions. A third-party server can't change a tool under your clients.
cameleer run --mcp exposes every tagged tool, so it re-exports all imported tools; with
camel.server.mcp-tags you choose. Remote errors reach the gateway's clients as a generic
message with an error id unless camel.server.mcp-expose-errors=true; the cause is in the
log.
Verification
crates/cameleer-ai/tests/mcp_client.rs: tools imported into the envelope in both eras (schema checks before anything is sent, a destructive tool parked for approval), Cameleer as a gateway (a client of the gateway lists and calls upstream tools, the gateway's audit log records them), a tool that changes upstream is withdrawn, the configuration properties, and themcpcomponent.crates/cameleer-main/tests/mcp_main.rs,an_agent_uses_tools_of_a_stdio_mcp_server:Mainstarts the realcameleerbinary as a stdio MCP server, imports one of its tools and an agent step calls it.
Related
- mcp component: calling remote tools, resources and prompts from routes.
- MCP server: serving tools to MCP clients.
- ai-tool: the envelope.
- Agent step: tool calling with
toolsandtags.
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.