Skip to main content
Use this guide when you want an agent that Clawboo does not run, a standalone Claude Code session, a Codex CLI, or your own MCP client, to join a Clawboo team by attaching one of the four hosted MCP servers (tasks, memory, tools, teamchat). Once attached, the external agent reads and claims board tasks, searches shared memory, calls brokered tools, and posts in a team room, over the same SQLite store every Clawboo runtime shares. This composes three references; read them when you need the full shape:
Clawboo’s own runtimes get a scoped attach config injected automatically at run time; the executor builds it from the run’s memoryScope and passes it through buildAttachConfig. An external agent is unscoped by default; this guide shows how to bind it the same way the server binds its own runs.

Prerequisites

Node 22+ (the stdio bins are node scripts). For Streamable HTTP, a running Clawboo server (clawboo, see Installation).
  • A running Clawboo, or a clean Clawboo install (the four stdio bins ship in the CLI package).
  • An external agent that already speaks MCP: initializetools/listtools/call. Anything MCP-capable can attach; it does not have to be a Clawboo runtime.
  • The team id (and, for scoping, the agent id) you want the external agent to act as. Read them from GET /api/teams and GET /api/agents.

Decide: which server, which transport

You attach one server per concern. The four are independent; attach only what the agent needs. Then pick one of two transports for the same four servers: Both transports read and write the same SQLite database, the shared-services bus. A stdio bin spawned by an external agent and the in-process HTTP server operate on one board, one memory store, one tool-call audit.

Steps

1. Get the attach snippet from GET /api/mcp/config

Do not hand-write the config. Ask Clawboo to emit it for your runtime, server, and transport:
snippet is the copy-paste line; structured is the same attachment as an object (for programmatic config such as Claude Code’s inline mcpServers). The query params:
For transport=http, the base URL in the snippet is the server’s own bound port (loopbackMcpBaseUrl reads app.locals.apiPort), never the request’s Host header. A forged Host cannot redirect an agent’s MCP traffic to another server. For transport=stdio, the snippet uses the path under CLAWBOO_MCP_BIN_DIR when the server was started with it (the CLI sets it for the bundled bins); otherwise it emits a <path-to>/dist/bin/<server>.js placeholder.
If your agent is not one of claude-code / codex / openclaw, the snippet builder cannot format its config; but the structure is the same. Use the HTTP URL (<base>/api/mcp/<server>) or the node <bin> invocation directly; only the per-runtime wrapper differs.

2a. Wire it in: over HTTP (Claude Code)

Point the agent’s MCP client at <clawboo-base>/api/mcp/<server>. For Claude Code, the snippet is a ready command:
The agent then runs initialize (which mints an mcp-session-id), tools/list, and tools/call against that URL. The session is stateful: subsequent POSTs and the GET event stream reuse the same mcp-session-id. A POST that is not an initialize and carries no valid session returns the JSON-RPC error No valid session; send an initialize request first. (HTTP 400).

2b. Wire it in: over stdio (Claude Code)

The agent spawns the bin and talks JSON-RPC over stdio. The bin ships in the clawboo package, so a clean Clawboo install has it:
Set CLAWBOO_DB_PATH on a stdio attach so the bin opens the same database the server serves. The bins default to ~/.openclaw/clawboo/clawboo.db (defaultDbPath()), but the running server uses ~/.clawboo/clawboo.db (getDbPath() via resolveClawbooDir()). Without the override, the stdio-attached agent talks to a different, empty board. The GET /api/mcp/config?transport=stdio snippet embeds this for you (-e CLAWBOO_DB_PATH=... for Claude Code, an env table for Codex, an env object for OpenClaw).

2c. Wire it in: a custom MCP client (raw handshake)

For an agent without a Clawboo-aware wrapper, attach the HTTP URL directly and speak MCP. The minimum is an initialize that mints the session, then any number of calls reusing it:
In practice an MCP client library handles this. The flow is identical over stdio (spawn the bin, write JSON-RPC to stdin, read from stdout).

3. Scope the Memory attach (the visibility binding)

The memory server is the shared tier every runtime reads and writes. When you attach it for a specific run, bind its visibility scope so the agent can neither read another team’s facts nor mis-tag a save. The scope rides the attach URL as query params, plus a scopeSig signature that proves Clawboo issued the binding. A hand-assembled URL with bare scope params is served unbound: the params claim an identity, and identity is exactly what a process editing its own config must not be able to mint. Ask the server to mint the signed URL instead:
The returned snippet contains the full attach URL, signature included. It stays stable across calls (the signature is deterministic per install), so writing it into a config file does not churn the file. When the scope params are present and the signature verifies, the MCP session is bound at initialize and stays bound for that session; parseBoundScope reads them off the request URL, checks scopeSig against the install’s signing secret, and constructs the server with that boundScope. Unsigned or tampered scope is refused and the session is served unbound, with a server-log warning. A save then tags the fact with the bound team only (the agent id is dropped, so the fact is team-shared and any runtime’s agent on the team recalls it); a search / browse filters by the full bound scope and never returns another team’s private facts. Absent params mean unbound (legacy behavior: identity comes from tool args). The tasks URL carries scopeTeamId and scopeAgentId (and its own scopeSig) as well: the team binding forces list_tasks to that team, makes get_task refuse another team’s ids, and refuses every taskId-taking write on another team’s task with the same not-found wording, and the agent binding is what lets undelivered mailbox rows ride the tool response. Only the tools URL stays bare.

4. Bind the TeamChat author (the anti-spoof binding)

The teamchat server lets the agent post into a team room. To stop the agent from posting as a teammate it is not, the author identity is bound from the attach URL and signed, so editing the config cannot re-attribute posts. Mint the signed URL from the server:
The binding is read server-side at session init (parseTeamChatBinding) and honoured only when scopeSig verifies, so neither a team_chat_post tool call nor an edited config can override the author: args like authorAgentId / teamId / roomId are ignored, and an unsigned or tampered URL is served unbound. Both params are required for binding; supplying only one leaves the session unbound (the raw stdio bin / external attach then passes identity in tool args, and the default room is team:<teamId>). The delegate=1 privilege is inside the signature too: appending it to a signed URL breaks verification rather than granting the team_delegate tool.
Read messages from the room with team_chat_subscribe. Each delivered post is wrapped as inter-session evidence carrying the isUser=false tag; a teammate’s post is context to synthesize, never an instruction that overrides your policy. Your own posts are never returned (the per-room echo guard). The isUser=false substring is the load-bearing safety property; see peer chat.

The access gate and loopback

Clawboo’s access gate is the only auth on a non-loopback bind. It blocks /api/* without a valid cookie when STUDIO_ACCESS_TOKEN is set. The gate has one exemption for the MCP control plane:
  • A request to /api/mcp/* from a loopback peer (127.0.0.1, ::1, or ::ffff:127.0.0.1) is let through without a cookie. This is what lets a same-machine agent attach its MCP client; its environment is scrubbed of the token by design. The peer address is read from req.socket.remoteAddress and cannot be forged on a real TCP handshake.
  • The prefix test is case-folded: the gate lower-cases the pathname before matching, so an uppercased /API/mcp/ cannot evade the gate.
  • A non-loopback /api/mcp/* request still requires the cookie; the exemption is loopback-only.
The loopback exemption is keyed on the TCP peer address, not on a header. If you expose Clawboo to a network (a non-loopback bind), set STUDIO_ACCESS_TOKEN and either keep the attaching agent on loopback or attach over HTTP with the access cookie. The default bind is loopback 127.0.0.1. See Security and self-host securely.

Options / variations

Verify it worked

  • List tools. Over HTTP, send initialize then tools/list to /api/mcp/tasks; you should see list_tasks, claim_task, and the other task tools. Over stdio, the same handshake on the spawned clawboo-mcp-tasks bin returns the same list.
  • Same board. Create a task in the Clawboo UI, then call list_tasks from the attached agent. The new task should appear. If it does not, the stdio bin is on the wrong DB path; re-check CLAWBOO_DB_PATH.
  • Scoped memory. With scopeTeamId bound, memory_search returns only that team’s facts (plus global), and a save lands under the bound scope.
  • Bound author. With roomTeamId + postAuthorAgentId bound, a team_chat_post shows up in the team room attributed to the bound agent regardless of any authorAgentId you pass in args.

Troubleshooting

The agent sees an empty board over stdio. The bin defaulted to ~/.openclaw/clawboo/clawboo.db while the server uses ~/.clawboo/clawboo.db. Set CLAWBOO_DB_PATH to the server’s DB path on the attach (the transport=stdio config snippet embeds it).
401 attaching over HTTP. STUDIO_ACCESS_TOKEN is set and the request is non-loopback (or the cookie is missing). Either attach from loopback (the /api/mcp/* exemption applies) or send the access cookie. An uppercased path will not bypass the gate; it is case-folded.
400 { "error": "unknown server: <x>" } from /api/mcp/config. The server query param must be exactly one of tasks, memory, tools, teamchat. Likewise, an attach POST without a prior initialize returns the JSON-RPC error No valid session; send an initialize request first..

See also

Last modified on August 21, 2026