Per-agent keys (no plugin)
Bootstrap a long-lived integration without the OpenClaw plugin — provision an agent-scoped credential, identify, and call MCP.
If you're building a Python / Node / Go integration directly against Caura — without the OpenClaw plugin runtime — bind every long-lived agent to its own agent-scoped credential rather than calling under the tenant-scoped key. Agent-scoped credentials give you trust gating, fleet membership, per-agent keystones, and a real identity in the dashboard.
Both kinds of credential use the mc_ prefix on the wire; scope (tenant vs agent) is bound at mint time on the credential row, not encoded in the prefix. (Pre-existing mca_… keys continue to authenticate via back-compat.)
The single tenant-scoped flow on the REST and MCP pages is fine for one-off scripts; the four-step flow here is for anything that ships.
1. Mint an agent-scoped credential
POST /api/v1/admin/agent-keys/provision mints an agent-scoped credential and creates the Agent row eagerly, so a follow-up PATCH /agents/{id}/trust works in the same session without a synthetic first write.
Authenticate with an org-admin session or a tenant-level API key with write capability. Default tenant keys with no explicit capability list also have the full grant. Agent-scoped and read-only keys cannot mint agent credentials.
curl -X POST "https://caura.dev/api/v1/admin/agent-keys/provision" \
-H "X-API-Key: $MC_TENANT_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "quote-agent-na",
"label": "north-america CRM",
"initial_trust": 1,
"initial_fleet": "na-sales"
}'
Response:
{
"id": "…",
"tenant_id": "…",
"agent_id": "quote-agent-na",
"raw_key": "mc_…",
"agent_row_created": true,
"created_at": "…"
}Save raw_key immediately — it is only returned once. agent_row_created: true confirms the Agent row exists. initial_trust and initial_fleet apply when the Agent row is new; reissuing a key preserves an existing Agent's trust and fleet.
Request body fields:
agent_id(required) — stable identifier for this agent. Surfaces in/whoami, audit, and per-agent dashboards.label— human-readable hint in the dashboard.initial_trust—0read-only /1write to home fleet /2cross-fleet read /3cross-fleet write + delete. Default1.initial_fleet— fleet membership; omit for tenant-wide scope.display_name— human-readable name surfaced on the dashboard.
2. Verify identity (/whoami)
Before any real tool call, confirm Caura resolves your credentials the way you expect (the tenant_id it returns names the key's tenant):
curl "https://caura.dev/api/v1/whoami" \
-H "X-API-Key: $AGENT_KEY"
{
"tenant_id": "your-tenant-id",
"agent_id": "quote-agent-na",
"auth_source": "gateway-header",
"via_gateway": true
}If agent_id is null you're sending a tenant-scoped credential, not an agent-scoped one — the gateway only injects X-Agent-ID on the agent-scoped path. (Both kinds share the mc_ prefix. On Connect › Keys, people who manage the organization's credentials see which is which in the Kind column. Agent keys are minted with the provisioning call in step 1, or by the OpenClaw plugin.)
3. Open an MCP session
Caura speaks MCP streamable-http at /mcp (both /mcp and /mcp/ work). The server accepts the API key on either header — use whichever your SDK supports:
| Header | When to use |
|---|---|
X-API-Key: mc_… | Canonical. Use if you control the request shape. |
Authorization: Bearer mc_… | OAuth-style. Required by Anthropic's remote-MCP integration and SDKs that only emit Authorization headers. |
Dashboard-issued JWTs are also accepted via Authorization: Bearer <jwt>; the server tries JWT decode first.
Python (the mcp library)
import asyncio
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def main():
headers = {"X-API-Key": "mc_..."} # agent-scoped credential
url = "https://caura.dev/mcp/"
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print([t.name for t in tools.tools])
result = await session.call_tool(
"caura_write",
{"content": "First memory from the Python harness."},
)
# Always inspect both result.isError and the content envelope.
# Gateway-side refusals (FORBIDDEN, INVALID_ARGUMENTS, NOT_FOUND, ...)
# arrive as isError=True with a JSON {"error":{...}} body — naive
# clients that only check content[0].text for "id" will silently
# treat failure as success.
if result.isError:
print("tool refused:", result.content[0].text)
else:
print(result)
asyncio.run(main())
Anthropic SDK (remote-MCP)
from anthropic import Anthropic
client = Anthropic()
msg = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=[{"role": "user", "content": "Save this note: pricing meets 10 May."}],
extra_body={
"mcp_servers": [
{
"type": "url",
"url": "https://caura.dev/mcp/",
"name": "caura",
"authorization_token": "mc_...", # agent-scoped credential
}
]
},
)
print(msg.content)
The SDK forwards authorization_token as Authorization: Bearer mc_…. Caura recognises that shape and resolves tenant + agent identity from it.
4. Elevate trust (when needed)
If you set initial_trust in step 1, skip this. Otherwise:
curl -X PATCH "https://caura.dev/api/v1/agents/quote-agent-na/trust?tenant_id=$TENANT_ID" \
-H "X-API-Key: $MC_TENANT_KEY" \
-H "Content-Type: application/json" \
-d '{"trust_level": 2}'
Trust levels:
0— read-only.1— write to home fleet.2— cross-fleet read.3— cross-fleet write + delete + update others' memories.
End-to-end bootstrap, one block
TENANT_KEY=mc_...
AGENT_ID="quote-agent-na"
FLEET_ID="na-sales"
# 1. Provision agent + Agent row + trust + fleet in one call.
RESP=$(curl -s -X POST "https://caura.dev/api/v1/admin/agent-keys/provision" \
-H "X-API-Key: $TENANT_KEY" \
-H "Content-Type: application/json" \
-d "{\"agent_id\":\"$AGENT_ID\",\"initial_trust\":1,\"initial_fleet\":\"$FLEET_ID\"}")
AGENT_KEY=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin)['raw_key'])")
# 2. Verify.
curl -s "https://caura.dev/api/v1/whoami" -H "X-API-Key: $AGENT_KEY"
# 3. Use.
curl -s "https://caura.dev/api/v1/memories" \
-H "X-API-Key: $AGENT_KEY" \
-H "Content-Type: application/json" \
-d "{\"agent_id\":\"$AGENT_ID\",\"fleet_id\":\"$FLEET_ID\",\"content\":\"Hello world\"}"
Four steps, one round-trip per agent. No provision → fake-write → patch-trust dance.
Idempotency
A write of identical content (same agent_id, same fleet_id) is retry-safe via MCP:
- First call →
201with the new memory id. - Identical retry →
200with{ "status": "duplicate", "existing_id": "…" }.
Cross-agent writes of identical content no longer collide — each agent gets its own record.
Authoring keystones
When you want an agent to author governance rules (keystones), use caura_keystones_set over MCP:
result = await session.call_tool(
"caura_keystones_set",
{
"op": "set",
"doc_id": "no-rollback-to-cve-versions",
"title": "Refuse rollback to CVE versions",
"content": "Never recommend rolling back to a version listed in any CVE entry.",
"scope": "tenant",
"weight": "high",
},
)Two gotchas worth knowing before you write the call:
agent_idincaura_keystones_setnames the TARGET agent the rule binds to, not the caller. Pass it only forscope=agent. Forscope=tenantorscope=fleet, omit it — passing it returnsINVALID_ARGUMENTS.- The companion read tool
caura_keystonesreturns the merged rule set under the JSON keyrules(notkeystones).
Trust gating is dynamic: self-authoring (scope=agent + target = caller) needs trust ≥ 1; anything else (fleet, tenant, or targeting another agent) needs trust ≥ 2.
Common pitfalls
POST /provisionreturns the raw key once. Save it before the response goes out of scope.PATCH /agents/{id}/trustreturns 404 immediately after provisioning. Means the Agent row was not materialised atomically — should not happen on builds after 2026-05-14. Check/whoamiandGET /api/v1/agents/{id}; ifagent_row_created: falseis in the provision response, the deployment is missingCORE_STORAGE_API_URL.- MCP tool result has
isError=Falsebutcontent[0].textis{"error": …}. Pre-2026-05-15 builds leftisErrorat the default for gateway-side refusals. On current builds, every structured-error envelope arrives withisError=True. - Streaming client hangs on
/mcp(no slash). Older builds redirected/mcp→/mcp/; current builds serve both paths in-process.
Reference
POST /api/v1/admin/agent-keys/provision— atomic provisioning (this guide).GET /api/v1/whoami— identity probe.GET /api/v1/agents/{id}?tenant_id=…— agent detail.PATCH /api/v1/agents/{id}/trust?tenant_id=…— change trust level.POST /api/v1/memories— REST write (mirrorscaura_writeover MCP).mcp://…/mcp/— streamable-http MCP endpoint.