Metadata-Version: 2.4
Name: mcp2xiaozhi
Version: 0.2.0
Summary: Bridge any MCP server (stdio / SSE / StreamableHTTP) to a Xiaozhi server over WebSocket.
Project-URL: Homepage, https://github.com/StanleyChanH/MCP2Xiaozhi
Project-URL: Repository, https://github.com/StanleyChanH/MCP2Xiaozhi
Project-URL: Issues, https://github.com/StanleyChanH/MCP2Xiaozhi/issues
Project-URL: Changelog, https://github.com/StanleyChanH/MCP2Xiaozhi/blob/main/CHANGELOG.md
Author: StanleyChanH
License-Expression: MIT
License-File: LICENSE
Keywords: bridge,mcp,mcp-server,model-context-protocol,sse,stdio,streamable-http,xiaozhi
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: anyio>=4.0.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp>=1.20.0
Requires-Dist: pydantic>=2.7.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: typing-extensions>=4.10.0; python_version < '3.12'
Requires-Dist: websockets>=13.0
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == 'dev'
Requires-Dist: fastmcp>=2.0.0; extra == 'dev'
Requires-Dist: mypy>=1.10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.6.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5.0; extra == 'docs'
Requires-Dist: mkdocs>=1.6.0; extra == 'docs'
Description-Content-Type: text/markdown

# mcp2xiaozhi

[English](README.md) | [中文](README_CN.md)

> Bridge any MCP server (stdio / SSE / StreamableHTTP) to a [Xiaozhi](https://github.com/78/xiaozhi-esp32-server) server over WebSocket.

[![PyPI](https://img.shields.io/pypi/v/mcp2xiaozhi.svg)](https://pypi.org/project/mcp2xiaozhi/)
[![Python](https://img.shields.io/pypi/pyversions/mcp2xiaozhi.svg)](https://pypi.org/project/mcp2xiaozhi/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![CI](https://github.com/StanleyChanH/MCP2Xiaozhi/actions/workflows/ci.yml/badge.svg)](https://github.com/StanleyChanH/MCP2Xiaozhi/actions/workflows/ci.yml)
[![Docs](https://img.shields.io/badge/docs-mkdocs__material-blue)](https://stanleychanh.github.io/MCP2Xiaozhi/)

`mcp2xiaozhi` is a general-purpose bridge that connects **any** [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server to a Xiaozhi server. The Xiaozhi server acts as an MCP *client* over a WebSocket: it sends JSON-RPC tool calls as text frames and expects JSON-RPC replies. This package receives those frames and relays them — at the protocol level — to your MCP server, wherever it runs and whatever transport it speaks.

```
                 JSON-RPC over WebSocket (text frames)
   ┌──────────────────────┐   wss://…    ┌──────────────────────┐
   │   Xiaozhi server     │ ◄──────────► │   mcp2xiaozhi bridge │
   │  (acts as MCP client)│              └──────────┬───────────┘
   └──────────────────────┘                          │  JSON-RPC
                                                     │  over MCP transport
                              ┌──────────────────────┴───────────────┐
                              │   stdio        │   SSE   │   HTTP    │
                              ▼                ▼         ▼           ▼
                         local process    remote server          remote server
```

## Features

- 🔄 **Three transports, one bridge** — `stdio`, `sse`, and `streamablehttp` (`http`), all native, no `mcp-proxy` subprocess required.
- 🧱 **Protocol-level relay** — frames are parsed as `JSONRPCMessage` (wrapped in the SDK's `SessionMessage`), validated, then re-serialized. Malformed frames are logged and dropped instead of crashing the bridge.
- 🔁 **Automatic reconnection** — exponential backoff with jitter on either side dropping; clean closes distinguished from abnormal ones.
- 🗂️ **Multi-server** — run one bridge or every server in your config; each server gets its own endpoint.
- ⚙️ **Config-driven** — drop-in compatible with the `mcp_config.json` shape from Xiaozhi's official `mcp-calculator` demo.
- 🖥️ **Cross-platform** — UTF-8 console handling on Windows, graceful `SIGINT`/`SIGTERM` shutdown.
- 📦 **Real package** — `pyproject.toml`, src layout, type hints, CLI entry point, tests, CI, managed with [uv](https://docs.astral.sh/uv/).

## Install

With [uv](https://docs.astral.sh/uv/) (recommended):

```bash
uv tool install mcp2xiaozhi        # install the CLI as a tool
# or, in a project:
uv add mcp2xiaozhi
```

With pip:

```bash
pip install mcp2xiaozhi
```

From source (for development):

```bash
git clone https://github.com/StanleyChanH/MCP2Xiaozhi.git
cd mcp2xiaozhi
uv sync --extra dev
```

## Quick start

1. Create an MCP server (or use an existing one). A minimal stdio calculator:

   ```python
   # calculator.py
   from mcp.server.fastmcp import FastMCP
   import math

   mcp = FastMCP("Calculator")

   @mcp.tool()
   def calculator(python_expression: str) -> dict:
       """Evaluate a Python math expression."""
       return {"success": True, "result": eval(python_expression, {"math": math})}

   if __name__ == "__main__":
       mcp.run(transport="stdio")
   ```

2. Describe your servers in `mcp_config.json`:

   ```json
   {
     "mcpServers": {
       "calculator": {
         "type": "stdio",
         "command": "python",
         "args": ["calculator.py"]
       }
     }
   }
   ```

3. Set the Xiaozhi WebSocket endpoint and run:

   ```bash
   export MCP_ENDPOINT="wss://api.your-xiaozhi-server.example/mcp/<token>"
   mcp2xiaozhi run calculator
   ```

   Or run every enabled server at once:

   ```bash
   mcp2xiaozhi run            # all enabled servers
   mcp2xiaozhi list           # show configured servers
   mcp2xiaozhi version
   ```

## Configuration

Config discovery order: `--config PATH` → `$MCP_CONFIG` → `./mcp_config.json`.

### Schema

```jsonc
{
  "mcpServers": {
    "my-server": {
      "type": "stdio",              // stdio | sse | streamablehttp | http
      "disabled": false,            // optional; skip when running all

      // stdio-only
      "command": "python",
      "args": ["-m", "my_server"],
      "env": { "FOO": "bar" },      // merged onto the current environment

      // sse / streamablehttp-only
      "url": "https://example.com/mcp",
      "headers": { "Authorization": "Bearer xxx" },
      "timeout": 5.0,               // connect timeout (s)
      "sse_read_timeout": 300.0,    // long-lived read timeout (s)

      // optional per-server Xiaozhi endpoint
      "endpoint": "wss://api.example.com/mcp/<token>"
    }
  }
}
```

### Endpoint resolution

Each server needs the Xiaozhi WebSocket endpoint it should connect to. Resolved in priority:

1. `endpoint` field in the server config
2. `$MCP_ENDPOINT_<NAME>` environment variable (name uppercased, non-alphanumeric → `_`)
3. global `$MCP_ENDPOINT`

When running **multiple** servers, give each its own endpoint — otherwise the Xiaozhi server cannot route tool calls to the right server. The bridge will warn if a server falls back to the global endpoint while others are running.

## Transports

| Type | Use when | Notes |
|------|----------|-------|
| `stdio` | Your MCP server is a local script/binary | Spawns it as a child process; env merged with the current process. |
| `sse` | Legacy remote MCP server using Server-Sent Events | GET `/sse` for the stream, POST for requests — handled by the SDK. |
| `streamablehttp` / `http` | Modern remote MCP server (recommended) | The production HTTP transport. `http` is an alias. |

All three are implemented with the official [`mcp` Python SDK](https://github.com/modelcontextprotocol/python-sdk) transport primitives, which yield `(read, write)` memory streams carrying `SessionMessage` objects. The bridge pumps messages between the WebSocket and those streams — it never spawns `mcp-proxy`.

## CLI

```
mcp2xiaozhi [--config PATH] [--log-level LEVEL] <command>

commands:
  run [SERVER]       Run one server (or all enabled if omitted / --all)
  list               List configured servers
  version            Print version

options:
  --endpoint URL     Override the Xiaozhi endpoint (single-server runs only)
  --log-level        DEBUG | INFO | WARNING | ERROR | CRITICAL
```

`python -m mcp2xiaozhi …` works too.

## Programmatic usage

```python
import asyncio
from mcp2xiaozhi import McpBridge, load_config, resolve_endpoint, get_global_endpoint

async def main():
    config = load_config()
    server = config.require("calculator")
    endpoint = resolve_endpoint(server, global_endpoint=get_global_endpoint())
    bridge = McpBridge(server, endpoint)
    await bridge.run()   # reconnects forever until cancelled

asyncio.run(main())
```

Run several at once with `ServerManager`:

```python
from mcp2xiaozhi import ServerManager, load_config

manager = ServerManager.from_config(load_config())
asyncio.run(manager.run())
```

## Deployment

The bridge is a **long-lived relay** — the host running it must stay online,
because the Xiaozhi server never connects to your MCP server directly. For
remote SSE/HTTP MCP servers you can deploy it to any always-on machine (VPS,
NAS, Raspberry Pi, container) and turn your laptop off.

Quick options:

```bash
# Docker (any platform) — the repo ships Dockerfile + docker-compose.yml
docker compose up -d --build

# Linux systemd — auto-start on boot, restart on crash
sudo systemctl enable --now mcp2xiaozhi

# Windows — NSSM wraps it as a native service
nssm install mcp2xiaozhi mcp2xiaozhi.exe
```

➡️ Full guide (config & secrets, multi-server, logs, upgrade): the
[Deployment docs](https://stanleychanh.github.io/MCP2Xiaozhi/deployment/).

## Development

```bash
uv sync --extra dev          # install dev dependencies
uv run ruff check .          # lint
uv run mypy src              # type check
uv run pytest                # tests
uv build                     # build sdist + wheel
```

See [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT — see [LICENSE](LICENSE).
