openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
337 lines (263 loc) • 13.7 kB
Markdown
---
summary: "Run OpenClaw as a stdio MCP server so an MCP client can read and send channel conversations"
title: "Run OpenClaw as an MCP server"
read_when:
- Connecting Codex, Claude Code, or another MCP client to OpenClaw-backed channels
- Running `openclaw mcp serve`
- Debugging bridge events, Claude notifications, or missing conversations
---
This page covers the `openclaw mcp serve` path: OpenClaw acting as an MCP
server over stdio, its tools, its event model, and its limits.
## OpenClaw as an MCP server
This is the `openclaw mcp serve` path.
### When to use serve
Use `openclaw mcp serve` when:
- Codex, Claude Code, or another MCP client should talk directly to OpenClaw-backed channel conversations
- you already have a local or remote OpenClaw Gateway with routed sessions
- you want one MCP server that works across OpenClaw's channel backends instead of running separate per-channel bridges
Use [`openclaw acp`](/cli/acp) instead when OpenClaw should host the coding runtime itself and keep the agent session inside OpenClaw.
### How it works
`openclaw mcp serve` starts a stdio MCP server. The MCP client owns that process. While the client keeps the stdio session open, the bridge connects to a local or remote OpenClaw Gateway over WebSocket and exposes routed channel conversations over MCP.
<Steps>
<Step title="Client spawns the bridge">
The MCP client spawns `openclaw mcp serve`.
</Step>
<Step title="Bridge connects to Gateway">
The bridge connects to the OpenClaw Gateway over WebSocket.
</Step>
<Step title="Sessions become MCP conversations">
Routed sessions become MCP conversations and transcript/history tools.
</Step>
<Step title="Live events queue">
Live events are queued in memory while the bridge is connected.
</Step>
<Step title="Optional Claude push">
If Claude channel mode is enabled, the same session can also receive Claude-specific push notifications.
</Step>
</Steps>
<AccordionGroup>
<Accordion title="Important behavior">
- live queue state starts when the bridge connects
- older transcript history is read with `messages_read`
- Claude push notifications only exist while the MCP session is alive
- when the client disconnects, the bridge exits and the live queue is gone
- cancelling an `events_wait` request immediately releases its server-side wait and timeout
- bridge or MCP transport close failures make `openclaw mcp serve` fail instead of reporting a clean shutdown
- one-shot agent entry points such as `openclaw agent` and `openclaw infer model run` retire any bundled MCP runtimes they open when the reply completes, so repeated scripted runs do not accumulate stdio MCP child processes
- stdio MCP servers launched by OpenClaw (bundled or user-configured) are torn down as a process tree on shutdown, so child subprocesses started by the server do not survive after the parent stdio client exits
- deleting or resetting a session disposes that session's MCP clients through the shared runtime cleanup path, so there are no lingering stdio connections tied to a removed session
</Accordion>
</AccordionGroup>
### Choose a client mode
<Tabs>
<Tab title="Generic MCP clients">
Standard MCP tools only. Use `conversations_list`, `messages_read`, `events_poll`, `events_wait`, `messages_send`, and the approval tools.
</Tab>
<Tab title="Claude Code">
Standard MCP tools plus the Claude-specific channel adapter. Enable `--claude-channel-mode on` or leave the default `auto`.
</Tab>
</Tabs>
<Note>
`auto` behaves the same as `on`. There is no client capability detection.
</Note>
### What serve exposes
The bridge uses existing Gateway session route metadata to expose channel-backed conversations. A conversation appears when OpenClaw already has session state with a known route such as:
- `channel`
- recipient or destination metadata
- optional `accountId`
- optional `threadId`
This gives MCP clients one place to:
- list recent routed conversations
- read recent transcript history
- wait for new inbound events
- send a reply back through the same route
- see approval requests that arrive while the bridge is connected
### Usage
<Tabs>
<Tab title="Local Gateway">
```bash
openclaw mcp serve
```
</Tab>
<Tab title="Remote Gateway (token)">
```bash
openclaw mcp serve --url wss://gateway-host:18789 --token-file ~/.openclaw/gateway.token
```
</Tab>
<Tab title="Remote Gateway (password)">
```bash
openclaw mcp serve --url wss://gateway-host:18789 --password-file ~/.openclaw/gateway.password
```
</Tab>
<Tab title="Verbose / Claude off">
```bash
openclaw mcp serve --verbose
openclaw mcp serve --claude-channel-mode off
```
</Tab>
</Tabs>
### Bridge tools
<AccordionGroup>
<Accordion title="conversations_list">
Lists recent session-backed conversations that already have route metadata in Gateway session state.
Filters: `limit` (max 500), `search`, `channel`, `includeDerivedTitles`, `includeLastMessage`.
</Accordion>
<Accordion title="conversation_get">
Returns one conversation by `session_key` using a direct Gateway session lookup.
</Accordion>
<Accordion title="messages_read">
Reads recent transcript messages for one session-backed conversation. `limit` defaults to 20, max 200.
</Accordion>
<Accordion title="attachments_fetch">
Extracts non-text message content blocks and canonical persisted media metadata from one transcript message. Looks up `message_id` directly through the Gateway, so the message does not need to appear in the recent history window. Persisted entries use `{ "type": "openclaw_media", "media": { ... } }`, where `media` can include `url`, `contentType`, `kind`, `fileName`, dimensions, duration, or size. This is a metadata view, not a standalone durable attachment blob store.
</Accordion>
<Accordion title="events_poll">
Reads queued live events since a numeric cursor. `limit` max 200. If the requested cursor predates retained queue history, the result also includes `gap.requested_after_cursor` and `gap.oldest_available_cursor`.
</Accordion>
<Accordion title="events_wait">
Long-polls until the next matching queued event arrives or a timeout expires (default 30s, max 300s).
Use this when a generic MCP client needs near-real-time delivery without a Claude-specific push protocol.
A known cursor gap returns immediately with the same additive `gap` metadata, even when no matching event is currently retained.
</Accordion>
<Accordion title="messages_send">
Sends text back through the same route already recorded on the session.
Current behavior:
- requires an existing conversation route
- uses the session's channel, recipient, account id, and thread id
- sends text only
</Accordion>
<Accordion title="permissions_list_open">
Lists pending exec/plugin approval requests the bridge has observed since it connected to the Gateway.
</Accordion>
<Accordion title="permissions_respond">
Resolves one pending exec/plugin approval request with:
- `allow-once`
- `allow-always`
- `deny`
</Accordion>
</AccordionGroup>
### Event model
The bridge keeps an in-memory event queue while it is connected.
Current event types:
- `message`
- `exec_approval_requested`
- `exec_approval_resolved`
- `plugin_approval_requested`
- `plugin_approval_resolved`
- `claude_permission_request`
<Warning>
- the queue is live-only; it starts when the MCP bridge starts
- `events_poll` and `events_wait` do not replay older Gateway history by themselves
- the queue is bounded; when `gap` is present, read durable history with `messages_read`, then resume with `after_cursor` set to one less than `gap.oldest_available_cursor`
- durable backlog should be read with `messages_read`
</Warning>
### Claude channel notifications
The bridge can also expose Claude-specific channel notifications. This is the OpenClaw equivalent of a Claude Code channel adapter: standard MCP tools remain available, but live inbound messages can also arrive as Claude-specific MCP notifications.
<Tabs>
<Tab title="off">
`--claude-channel-mode off`: standard MCP tools only.
</Tab>
<Tab title="on">
`--claude-channel-mode on`: enable Claude channel notifications.
</Tab>
<Tab title="auto (default)">
`--claude-channel-mode auto`: current default; same bridge behavior as `on`.
</Tab>
</Tabs>
When Claude channel mode is enabled, the server advertises Claude experimental capabilities and can emit:
- `notifications/claude/channel`
- `notifications/claude/channel/permission`
Current bridge behavior:
- inbound `user` transcript messages are forwarded as `notifications/claude/channel`
- Claude permission requests received over MCP are tracked in-memory
- if the command owner in the linked conversation later sends `yes <id>` or `no <id>` (`<id>` is the 5-letter request id, excluding `l`), the bridge converts that to `notifications/claude/channel/permission`
- these notifications are live-session only; if the MCP client disconnects, there is no push target
This is intentionally client-specific. Generic MCP clients should rely on the standard polling tools.
### MCP client config
Example stdio client config:
```json
{
"mcpServers": {
"openclaw": {
"command": "openclaw",
"args": [
"mcp",
"serve",
"--url",
"wss://gateway-host:18789",
"--token-file",
"/path/to/gateway.token"
]
}
}
}
```
For most generic MCP clients, start with the standard tool surface and ignore Claude mode. Turn Claude mode on only for clients that actually understand the Claude-specific notification methods.
### Options
`openclaw mcp serve` supports:
<ParamField path="--url" type="string">
Gateway WebSocket URL. Defaults to `gateway.remote.url` when configured.
</ParamField>
<ParamField path="--token" type="string">
Gateway token.
</ParamField>
<ParamField path="--token-file" type="string">
Read token from file.
</ParamField>
<ParamField path="--password" type="string">
Gateway password.
</ParamField>
<ParamField path="--password-file" type="string">
Read password from file.
</ParamField>
<ParamField path="--claude-channel-mode" type='"auto" | "on" | "off"'>
Claude notification mode. Default `auto`.
</ParamField>
<ParamField path="-v, --verbose" type="boolean">
Verbose logs on stderr.
</ParamField>
<Tip>
Prefer `--token-file` or `--password-file` over inline secrets when possible.
</Tip>
### Security and trust boundary
The bridge does not invent routing. It only exposes conversations that Gateway already knows how to route.
That means:
- sender allowlists, pairing, and channel-level trust still belong to the underlying OpenClaw channel configuration
- `messages_send` can only reply through an existing stored route
- approval state is live/in-memory only for the current bridge session
- bridge auth should use the same Gateway token or password controls you would trust for any other remote Gateway client
If a conversation is missing from `conversations_list`, the usual cause is not MCP configuration. It is missing or incomplete route metadata in the underlying Gateway session.
### Testing
OpenClaw ships a deterministic Docker smoke for this bridge:
```bash
pnpm test:docker:mcp-channels
```
That smoke runs a single container: it seeds conversation state, starts the Gateway, then spawns `openclaw mcp serve` as a stdio child process and drives it as an MCP client. It verifies conversation discovery, transcript reads, attachment metadata reads, live event queue behavior, and Claude-style channel and permission notifications over the real stdio MCP bridge. Outbound send routing (`messages_send` reusing the stored conversation route) is covered separately by unit tests in `src/mcp/channel-server.test.ts`.
This is the fastest way to prove the bridge works without wiring a real Telegram, Discord, or iMessage account into the test run.
For broader testing context, see [Testing](/help/testing).
### Troubleshooting
<AccordionGroup>
<Accordion title="No conversations returned">
Usually means the Gateway session is not already routable. Confirm that the underlying session has stored channel/provider, recipient, and optional account/thread route metadata.
</Accordion>
<Accordion title="events_poll or events_wait misses older messages">
The live queue starts when the bridge connects and retains a bounded window. If a result includes `gap`, read durable transcript history with `messages_read`, then resume with `after_cursor` set to one less than `gap.oldest_available_cursor`.
</Accordion>
<Accordion title="Claude notifications do not show up">
Check all of these:
- the client kept the stdio MCP session open
- `--claude-channel-mode` is `on` or `auto`
- the client actually understands the Claude-specific notification methods
- the inbound message happened after the bridge connected
</Accordion>
<Accordion title="Approvals are missing">
`permissions_list_open` only shows approval requests observed while the bridge was connected. It is not a durable approval history API.
</Accordion>
</AccordionGroup>
## Current limits
The bridge has these limits:
- conversation discovery depends on existing Gateway session route metadata
- no generic push protocol beyond the Claude-specific adapter
- no message edit or react tools
- HTTP/SSE/streamable-http transport connects to a single remote server; upstreams are not multiplexed
- `permissions_list_open` only includes approvals observed while the bridge is connected