This page covers the apus mcp serve path: Apus acting as an MCP server over stdio, its tools, its event model, and its limits.

Apus as an MCP server

This is the apus mcp serve path.

When to use serve

Use apus mcp serve when:

Use apus acp instead when Apus should host the coding runtime itself and keep the agent session inside Apus.

How it works

apus 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 Apus Gateway over WebSocket and exposes routed channel conversations over MCP.

Client spawns the bridge

The MCP client spawns `apus mcp serve`.

Bridge connects to Gateway

The bridge connects to the Apus Gateway over WebSocket.

Sessions become MCP conversations

Routed sessions become MCP conversations and transcript/history tools.

Live events queue

Live events are queued in memory while the bridge is connected.

Optional Claude push

If Claude channel mode is enabled, the same session can also receive Claude-specific push notifications.
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 `apus mcp serve` fail instead of reporting a clean shutdown
- one-shot agent entry points such as `apus agent` and `apus 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 Apus (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

Choose a client mode

Generic MCP clients

Standard MCP tools only. Use `conversations_list`, `messages_read`, `events_poll`, `events_wait`, `messages_send`, and the approval tools.

Claude Code

Standard MCP tools plus the Claude-specific channel adapter. Enable `--claude-channel-mode on` or leave the default `auto`.

auto behaves the same as on. There is no client capability detection.

What serve exposes

The bridge uses existing Gateway session route metadata to expose channel-backed conversations. A conversation appears when Apus already has session state with a known route such as:

This gives MCP clients one place to:

Usage

Local Gateway

```bash
apus mcp serve
```

Remote Gateway (token)

```bash
apus mcp serve --url wss://gateway-host:18789 --token-file ~/.apus/gateway.token
```

Remote Gateway (password)

```bash
apus mcp serve --url wss://gateway-host:18789 --password-file ~/.apus/gateway.password
```

Verbose / Claude off

```bash
apus mcp serve --verbose
apus mcp serve --claude-channel-mode off
```

Bridge tools

conversations_list
Lists recent session-backed conversations that already have route metadata in Gateway session state.

Filters: `limit` (max 500), `search`, `channel`, `includeDerivedTitles`, `includeLastMessage`.
conversation_get
Returns one conversation by `session_key` using a direct Gateway session lookup.
messages_read
Reads recent transcript messages for one session-backed conversation. `limit` defaults to 20, max 200.
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": "apus_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.
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`.
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.
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
permissions_list_open
Lists pending exec/plugin approval requests the bridge has observed since it connected to the Gateway.
permissions_respond
Resolves one pending exec/plugin approval request with:

- `allow-once`
- `allow-always`
- `deny`

Event model

The bridge keeps an in-memory event queue while it is connected.

Current event types:

  • 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

Claude channel notifications

The bridge can also expose Claude-specific channel notifications. This is the Apus equivalent of a Claude Code channel adapter: standard MCP tools remain available, but live inbound messages can also arrive as Claude-specific MCP notifications.

off

`--claude-channel-mode off`: standard MCP tools only.

on

`--claude-channel-mode on`: enable Claude channel notifications.

auto (default)

`--claude-channel-mode auto`: current default; same bridge behavior as `on`.

When Claude channel mode is enabled, the server advertises Claude experimental capabilities and can emit:

Current bridge behavior:

This is intentionally client-specific. Generic MCP clients should rely on the standard polling tools.

MCP client config

Example stdio client config:

{
  "mcpServers": {
    "apus": {
      "command": "apus",
      "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

apus mcp serve supports:

--url string

Gateway WebSocket URL. Defaults to gateway.remote.url when configured.

--token string

Gateway token.

--token-file string

Read token from file.

--password string

Gateway password.

--password-file string

Read password from file.

--claude-channel-mode "auto" | "on" | "off"

Claude notification mode. Default auto.

-v, --verbose boolean

Verbose logs on stderr.

Prefer --token-file or --password-file over inline secrets when possible.

Security and trust boundary

The bridge does not invent routing. It only exposes conversations that Gateway already knows how to route.

That means:

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

Apus ships a deterministic Docker smoke for this bridge:

pnpm test:docker:mcp-channels

That smoke runs a single container: it seeds conversation state, starts the Gateway, then spawns apus 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/agent/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.

Troubleshooting

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.
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`.
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
Approvals are missing
`permissions_list_open` only shows approval requests observed while the bridge was connected. It is not a durable approval history API.

Current limits

The bridge has these limits: