Documentation
¶
Overview ¶
Package server — `GET /v1/biam/subscribe` SSE handler (ADR-024 Phase 4: A2A asynchronous push).
Wire shape:
GET /v1/biam/subscribe?task_id=<id>
Accept: text/event-stream (advisory — we always
emit SSE for this path)
Last-Event-ID: <u64> (optional; resume after a
prior disconnect)
Response:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
id: 1
event: task
data: {"task_id":"…","status":"active",…}
id: 2
event: frame
data: {"task_id":"…","line":"hello","kind":"stdout","ts":"…"}
…
Lifecycle:
- On connect, replay every event for task_id whose ID is greater than the parsed Last-Event-ID header (default 0 = full ring).
- Then block on the per-task notify channel; on each wake re-read the ring's tail and stream new events.
- Close on (a) terminal-status event (kind == "terminal"), (b) client disconnect (ctx.Done from r.Context()), or (c) daemon shutdown propagated through the same ctx chain.
The buffer is fed by the broadcast hooks installed at daemon boot (see internal/agents/biam.WirePushHooks). The handler itself touches biam.Events only as a reader + subscriber.
Package server — HTTP gateway (ADR-014 Phase 2, v0.11).
`clawtool serve --listen :8080 --token-file <path>` mounts a thin HTTP surface that proxies prompts to the supervisor and exposes the agent registry. Every dispatch goes through Supervisor.Send (same call site as the CLI / MCP). Auth is bearer-token at the edge — non-negotiable; the relay opens an exec-arbitrary-code-on-host surface.
TLS is not terminated here. Operators front this with nginx / caddy / Cloudflare Tunnel — we do not invent a cert story (see ADR-014 Rationale).
Mcp-Method / Mcp-Name HTTP header handling for the Streamable HTTP transport (SEP-2243, finalized 2026-04-17).
SEP-2243 adds two request headers on Streamable HTTP POSTs so load balancers, rate-limiters, and metrics pipelines can route without parsing the JSON-RPC body:
- Mcp-Method: <method-name> e.g. "tools/call", "tools/list"
- Mcp-Name: <tool-or-prompt> e.g. "mcp__clawtool__SendMessage"
Mcp-Name is only meaningful for methods that carry a sub-target (`tools/call` → params.name, `prompts/get` → params.name); for methods like `notifications/initialized` we OMIT the header rather than send empty — the spec language ("for methods with a sub-target") reads as "don't include otherwise" and an absent header is unambiguous to proxies, where empty-string is a matchable value that complicates rules.
Server side (mcpHeaderMiddleware): reads incoming Mcp-Method, falls back to body inspection when absent, prefers the body's JSON-RPC method when the two disagree (logging a stderr warning), exposes the resolved values via context, and echoes them on the response so clients can see what we processed.
Client side (BuildMCPRequest): sets Mcp-Method from the JSON-RPC body's method field and sets Mcp-Name from params.name only when the method is one that carries a sub-target. Used by any code in clawtool that POSTs to an upstream Streamable HTTP MCP server (today: tests + future outbound transports).
MCP-over-HTTP Accept-header content negotiation shim.
The mark3labs/mcp-go StreamableHTTPServer (v0.49.0) always replies to a single-response /mcp POST with `Content-Type: application/json` and a bare JSON-RPC body, regardless of the client's Accept header (see streamable_http.go:546 in that release).
rmcp (the Rust MCP SDK that codex's HTTP client is built on) opens initialize with `Accept: text/event-stream` only. When the upstream answers with raw JSON the rmcp parser tries to decode the body as SSE-framed (`data: <json>\n\n`), finds no `event:` lines, and surfaces the misleading
Deserialize error: data did not match any variant of untagged enum JsonRpcMessage when send initialize request
MCP Streamable-HTTP (2025-06-18) says the server SHOULD honor the client's Accept by responding with `text/event-stream` framed as `data: <json>\n\n`; a single SSE event is a valid response shape.
mcpAcceptShim wraps the streamable handler and post-processes the outgoing response when the client asked for SSE — buffer the `application/json` body the inner handler emits, then write it back as a single `data: ...\n\n` SSE event with the right Content-Type. When the inner handler already chose `text/event-stream` (multi-event drain path, the upgradedHeader branch in mcp-go) we pass through unchanged.
Package server — `/v1/peers` REST surface (ADR-024 Phase 1).
Four endpoints, all bearer-authed by the same authMiddleware every other /v1/* path uses:
GET /v1/peers — list with status / backend / circle / path filters
POST /v1/peers/register — body: a2a.RegisterInput; returns the assigned Peer
POST /v1/peers/{peer_id}/heartbeat — refresh last_seen + status
DELETE /v1/peers/{peer_id} — explicit deregister on session end
Wire shape mirrors prassanna-ravishankar/repowire's /peers + /peers/by-pane endpoints so an existing repowire dashboard can be re-pointed at a clawtool daemon with a one-line URL change. Difference: clawtool's auth model is bearer-token (the daemon-wide token in ~/.config/clawtool/listener-token), not repowire's per-peer auth_token; we already have the daemon-shared token so a second layer is unnecessary at this phase.
Registry lifecycle: the handlers fetch a2a.GetGlobal() on every request. buildMCPServer's Phase-1 boot installs a registry into the global slot (with persistence at ~/.config/clawtool/peers.json); daemon shutdown clears it. Handlers return 503 when the global is nil so a misconfigured boot doesn't 500 — operator gets a clear "registry not initialised" hint instead.
Package server starts the clawtool MCP server.
Per ADR-004, clawtool exposes itself as one MCP server over stdio. Per ADR-006, core tools use PascalCase names (Bash, Read, Edit, ...). Per ADR-008, configured sources spawn as child MCP servers and their tools are aggregated under `<instance>__<tool>` wire names.
Boot order on every `clawtool serve`:
- Load config + secrets.
- Build sources.Manager and start each configured source. Failures on individual sources are non-fatal; their tools just don't show up.
- Build a search.Index from descriptors of every tool we plan to register: enabled core tools + ToolSearch + aggregated source tools. This index powers the ToolSearch primitive — see ADR-005 for why search-first is the prerequisite that lets a 50+ tool catalog scale.
- Register all tools on the parent MCP server. ToolSearch closes over the index reference; aggregated source-tool handlers route via the manager.
Index ¶
- Constants
- func BuildMCPRequest(ctx context.Context, url string, body []byte) (*http.Request, error)
- func InitTokenFile(path string) (string, error)
- func MCPMethodFromContext(ctx context.Context) string
- func MCPNameFromContext(ctx context.Context) string
- func ServeHTTP(ctx context.Context, opts HTTPOptions) error
- func ServeStdio(ctx context.Context) error
- type HTTPOptions
Constants ¶
const ( HeaderMcpMethod = "Mcp-Method" HeaderMcpName = "Mcp-Name" )
HTTP header names. Exported so callers building requests can reference them without re-declaring the constants.
Variables ¶
This section is empty.
Functions ¶
func BuildMCPRequest ¶ added in v0.22.116
BuildMCPRequest constructs an *http.Request for a Streamable HTTP MCP POST with SEP-2243 headers populated from `body`.
- body must be a JSON-encoded JSON-RPC request (object). Method + (when applicable) params.name are extracted by re-parsing — caller doesn't have to pass them separately.
- Mcp-Method is always set when the body has a non-empty method field.
- Mcp-Name is set only when the method is `tools/call` or `prompts/get` AND params.name is non-empty. For any other method, the header is OMITTED entirely (not sent as empty-string) — this matches the spec wording and keeps proxy rule-matching unambiguous.
- Content-Type is always set to application/json so the caller doesn't have to remember.
Returns a request ready to hand to http.Client.Do.
func InitTokenFile ¶ added in v0.20.0
InitTokenFile generates a fresh 32-byte (256-bit) hex token and writes it to path with 0600. Used by `clawtool serve init-token` and by tests that need a working credential.
func MCPMethodFromContext ¶ added in v0.22.116
MCPMethodFromContext returns the JSON-RPC method resolved by the middleware (body wins on mismatch). Empty string when the middleware did not run or the body was unparseable.
func MCPNameFromContext ¶ added in v0.22.116
MCPNameFromContext returns the sub-target (tool or prompt name) for tools/call / prompts/get. Empty string for other methods or when params.name is missing.
func ServeHTTP ¶ added in v0.20.0
func ServeHTTP(ctx context.Context, opts HTTPOptions) error
ServeHTTP runs clawtool as an HTTP gateway. Blocks until the listener returns. Mirrors ServeStdio's lifecycle: build the MCP server (so the same agents/recipes/tools are available), then route HTTP requests through it.
MCP-over-HTTP (`--mcp-http`) mounts the full toolset at /mcp via mark3labs/mcp-go's StreamableHTTPServer (the persistent shared daemon every host fans into; see internal/daemon).
func ServeStdio ¶
ServeStdio runs clawtool as an MCP server speaking over stdio. It blocks until stdin closes (the conventional MCP shutdown signal) or an unrecoverable error occurs.
Types ¶
type HTTPOptions ¶ added in v0.20.0
type HTTPOptions struct {
Listen string // ":8080" or "0.0.0.0:8080" — passed to http.ListenAndServe.
TokenFile string // path to a 0600 file containing the bearer token. Refused if missing/empty unless NoAuth is set.
MCPHTTP bool // when true, mount the MCP toolset at /mcp via mcp-go's Streamable HTTP transport.
// NoAuth runs the listener without bearer-token enforcement. The
// shared local daemon flips this on by default — the operator's
// machine is the trust boundary, codex / gemini hit /mcp over
// loopback without pre-setting CLAWTOOL_TOKEN. Daemon / relay
// deployments (multi-user, exposed beyond loopback) keep auth on
// by leaving NoAuth false and supplying TokenFile.
NoAuth bool
}
HTTPOptions configures the listener.