Skip to main content
Glama
wenyue

OpenClaw MCP Proxy

by wenyue
README.md
# OpenClaw MCP Proxy

OpenClaw MCP Proxy is a small FastAPI + FastMCP service that exposes app-registered tools as an MCP server for OpenClaw, while forwarding actual tool execution back to the app over a WebSocket bridge.

It has two responsibilities:

- Manage chat-scoped proxy sessions over HTTP and WebSocket.
- Expose the registered tool set as MCP over either stateless HTTP or stdio.

## Architecture

```mermaid
sequenceDiagram
    participant AppClient
    participant ProxyServer
    participant OpenClawClient

    AppClient->>ProxyServer: POST /v1/chat/sessions
    ProxyServer-->>AppClient: mcpSessionId, bridge_url, mcp_url
    AppClient->>ProxyServer: WS /v1/chat/sessions/{session_id}/bridge
    OpenClawClient->>ProxyServer: POST /v1/mcp/{session_id}
    ProxyServer->>AppClient: invoke_tool
    AppClient-->>ProxyServer: invoke_result
    ProxyServer-->>OpenClawClient: MCP tool result
    AppClient->>ProxyServer: DELETE /v1/chat/sessions/{session_id}
```

## Endpoints

### `POST /v1/chat/sessions`

Creates a session and returns the bridge and MCP endpoints for that session.

Request body:

```json
{
  "device_id": "device-1",
  "device_name": "desktop",
  "app_version": "1.0.0",
  "chat_id": "chat-1",
  "tools": [
    {
      "name": "echo_text",
      "path": "/tools/echo_text",
      "description": "Echo text.",
      "input_schema": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          }
        }
      }
    }
  ]
}
```

Response body:

```json
{
  "mcpSessionId": "session-id",
  "bridge_url": "ws://127.0.0.1:8000/v1/chat/sessions/session-id/bridge",
  "mcp_url": "http://127.0.0.1:8000/v1/mcp/session-id"
}
```

### `DELETE /v1/chat/sessions/{session_id}`

Deletes a session.

Response body:

```json
{
  "ok": true
}
```

### `WS /v1/chat/sessions/{session_id}/bridge`

Connects the app-side execution bridge for a registered session.

Bridge messages:

- Proxy -> app: `invoke_tool`
- App -> proxy: `invoke_result`
- Proxy -> app: `ping`
- App -> proxy: `pong`

Proxy -> app:

```json
{
  "type": "invoke_tool",
  "mcpSessionId": "session-id",
  "request_id": "session-id:1",
  "tool_name": "echo_text",
  "arguments": {
    "text": "hello"
  }
}
```

App -> proxy:

```json
{
  "type": "invoke_result",
  "mcpSessionId": "session-id",
  "request_id": "session-id:1",
  "ok": true,
  "content": {
    "echoed_text": "hello"
  }
}
```

### `POST /v1/mcp/{session_id}`

Exposes the registered tools for a specific session as a stateless HTTP MCP endpoint.

This is the simplest way to connect OpenClaw to a specific registered session.

### `POST /v1/mcp/` with `MCP-Session-Id`

The MCP endpoint also supports header-based session routing:

- Header: `MCP-Session-Id: <session_id>`

This is useful when the MCP client configuration prefers a stable URL and injects the session ID through headers.

## MCP Transports

The proxy now supports two MCP-facing transports:

- HTTP: multi-session, routed by path or `MCP-Session-Id`
- stdio: single-session, bound during MCP `initialize` via `mcpSessionId`

The stdio transport is implemented as a local proxy process in front of the HTTP MCP endpoint. The
actual tool execution path is unchanged:

1. The app registers a session over HTTP.
2. The app connects the WebSocket bridge.
3. The stdio process stores `initialize.params.mcpSessionId` and proxies MCP traffic to `POST /v1/mcp` with header `MCP-Session-Id`.
4. The HTTP proxy forwards tool execution to the app over the existing bridge.

### `GET /health`

Returns plain text `ok`.

## Authentication

The proxy uses two independent bearer tokens:

- `OPENCLAW_PROXY_APP_TOKEN`
  Used by:
  - `POST /v1/chat/sessions`
  - `DELETE /v1/chat/sessions/{session_id}`
  - `WS /v1/chat/sessions/{session_id}/bridge`

- `OPENCLAW_PROXY_OPENCLAW_TOKEN`
  Used by:
  - `/v1/mcp/...`

Important:

- If `OPENCLAW_PROXY_APP_TOKEN` is empty, app-facing endpoints accept requests without authentication.
- If `OPENCLAW_PROXY_OPENCLAW_TOKEN` is empty, MCP-facing endpoints accept requests without authentication.

Do not leave either token empty outside local development.

WebSocket close codes:

- `4401`: invalid app token
- `4404`: unknown `session_id`

## Configuration

Environment variables:

| Variable | Default | Description |
| --- | --- | --- |
| `OPENCLAW_PROXY_APP_TOKEN` | `""` | Bearer token for app registration and bridge endpoints. |
| `OPENCLAW_PROXY_OPENCLAW_TOKEN` | `""` | Bearer token for MCP requests from OpenClaw. |
| `OPENCLAW_PROXY_SERVER_URL` | `http://127.0.0.1:8000` | Base URL used by the stdio proxy process to reach the HTTP proxy. |
| `OPENCLAW_PROXY_SESSION_TTL_SECONDS` | `300` | Session time-to-live in seconds. |
| `OPENCLAW_PROXY_TOOL_TIMEOUT_SECONDS` | `120` | Tool call timeout in seconds. |

Example `.env`:

```env
OPENCLAW_PROXY_APP_TOKEN=replace-me
OPENCLAW_PROXY_OPENCLAW_TOKEN=replace-me
OPENCLAW_PROXY_SERVER_URL=http://127.0.0.1:8000
OPENCLAW_PROXY_SESSION_TTL_SECONDS=300
OPENCLAW_PROXY_TOOL_TIMEOUT_SECONDS=120
```

## Run Locally

### Requirements

- Python 3.11+ recommended
- `pip`

### Install dependencies

```bash
pip install -r requirements.txt
```

### Start the server

```bash
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

### Start the stdio MCP proxy

After creating a session and connecting the app bridge, you can expose that session over stdio:

```bash
python -m app.stdio_main
```

Optional flags:

```bash
python -m app.stdio_main \
  --proxy-base-url http://127.0.0.1:8000
```

The stdio client must send `mcpSessionId` in `initialize.params`, for example:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-03-26",
    "capabilities": {},
    "clientInfo": {
      "name": "your-client",
      "version": "1.0.0"
    },
    "mcpSessionId": "<session_id>"
  }
}
```

### Check health

```bash
curl http://127.0.0.1:8000/health
```

Expected output:

```text
ok
```

## OpenClaw MCP Configuration

Header-routed example:

```json
{
  "mcpServers": {
    "otakuroom-chat-mcp": {
      "transport": "http",
      "url": "https://your-proxy-host.example.com/v1/mcp",
      "headers": {
        "Authorization": "Bearer ${OPENCLAW_PROXY_OPENCLAW_TOKEN}",
        "MCP-Session-Id": "${SESSION_ID}"
      }
    }
  }
}
```

You can also connect directly to the session-specific URL returned by session creation, for example:

```text
https://your-proxy-host.example.com/v1/mcp/<session_id>
```

Stdio example:

```json
{
  "mcpServers": {
    "otakuroom-chat-mcp": {
      "transport": "stdio",
      "command": "python",
      "args": [
        "-m",
        "app.stdio_main"
      ],
      "env": {
        "OPENCLAW_PROXY_SERVER_URL": "http://127.0.0.1:8000",
        "OPENCLAW_PROXY_OPENCLAW_TOKEN": "${OPENCLAW_PROXY_OPENCLAW_TOKEN}"
      }
    }
  }
}
```

## How Tool Forwarding Works

1. The app creates a session and sends its available tool definitions.
2. The proxy stores the session in memory.
3. OpenClaw calls the session MCP endpoint.
4. The proxy dynamically builds a FastMCP server for that session and tool set.
5. When OpenClaw invokes a tool, the proxy sends `invoke_tool` over the WebSocket bridge.
6. The app executes the tool locally and sends `invoke_result`.
7. The proxy returns the tool result to the MCP caller.

## Testing

Run the proxy integration tests:

```bash
python -m unittest tests.test_proxy_integration
```

Current coverage includes:

- session-specific MCP routing via `mcp_url`
- header-based MCP routing via `MCP-Session-Id`
- normal WebSocket bridge disconnect without error-level logging
- shared MCP server construction and stdio proxy bootstrap behavior

## Operational Notes

- Sessions are stored in memory only.
  The proxy is not currently designed for stateless multi-instance deployment without sticky routing or shared session state.

- Tool calls require an active bridge connection.
  If the session exists but the bridge is disconnected, tool calls fail with `Bridge is not connected.`

- Sessions expire automatically.
  A background cleanup loop runs every 15 seconds and removes expired sessions.

- Registered tool names must be unique within a session.

- Reverse proxies must support WebSocket upgrade and must forward:
  - `Authorization`
  - `MCP-Session-Id`

- HTTP MCP is served directly from the in-process session registry.
- stdio MCP runs as a separate proxy process and forwards to the HTTP MCP endpoint.
- Tool schemas are registered dynamically from the provided `input_schema`.
  Only trusted app clients should be allowed to register tool definitions.

## Limitations

- No persistent session storage.
- No built-in rate limiting.
- No container or deployment manifests are included in this directory.
- Audit logging is plain application logging, not a full security audit pipeline.