MemClaw / docs
Integrations

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 MemClaw — 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.

curl -X POST "https://memclaw.net/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 were applied in the same round-trip.

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_trust0 read-only / 1 write to home fleet / 2 cross-fleet read / 3 cross-fleet write + delete. Default 1.
  • 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 MemClaw resolves your credentials the way you expect:

curl "https://memclaw.net/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; the dashboard "Kind" column tells you which is which.)

3. Open an MCP session

MemClaw 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:

HeaderWhen 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://memclaw.net/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(
              "memclaw_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://memclaw.net/mcp/",
              "name": "memclaw",
              "authorization_token": "mc_...",  # agent-scoped credential
          }
      ]
  },
)
print(msg.content)

The SDK forwards authorization_token as Authorization: Bearer mc_…. MemClaw 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://memclaw.net/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://memclaw.net/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://memclaw.net/api/v1/whoami" -H "X-API-Key: $AGENT_KEY"

# 3. Use.
curl -s "https://memclaw.net/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 → 201 with the new memory id.
  • Identical retry → 200 with { "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 memclaw_keystones_set over MCP:

result = await session.call_tool(
    "memclaw_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_id in memclaw_keystones_set names the TARGET agent the rule binds to, not the caller. Pass it only for scope=agent. For scope=tenant or scope=fleet, omit it — passing it returns INVALID_ARGUMENTS.
  • The companion read tool memclaw_keystones returns the merged rule set under the JSON key rules (not keystones).

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 /provision returns the raw key once. Save it before the response goes out of scope.
  • PATCH /agents/{id}/trust returns 404 immediately after provisioning. Means the Agent row was not materialised atomically — should not happen on builds after 2026-05-14. Check /whoami and GET /api/v1/agents/{id}; if agent_row_created: false is in the provision response, the deployment is missing CORE_STORAGE_API_URL.
  • MCP tool result has isError=False but content[0].text is {"error": …}. Pre-2026-05-15 builds left isError at the default for gateway-side refusals. On current builds, every structured-error envelope arrives with isError=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 (mirrors memclaw_write over MCP).
  • mcp://…/mcp/ — streamable-http MCP endpoint.