stackchan-mcp 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stackchan_mcp-0.1.0/.env.example +33 -0
- stackchan_mcp-0.1.0/.gitignore +30 -0
- stackchan_mcp-0.1.0/LICENSE +39 -0
- stackchan_mcp-0.1.0/PKG-INFO +238 -0
- stackchan_mcp-0.1.0/README.md +207 -0
- stackchan_mcp-0.1.0/pyproject.toml +52 -0
- stackchan_mcp-0.1.0/stackchan_mcp/__init__.py +7 -0
- stackchan_mcp-0.1.0/stackchan_mcp/__main__.py +12 -0
- stackchan_mcp-0.1.0/stackchan_mcp/audio_stream.py +34 -0
- stackchan_mcp-0.1.0/stackchan_mcp/capture_server.py +91 -0
- stackchan_mcp-0.1.0/stackchan_mcp/cli.py +57 -0
- stackchan_mcp-0.1.0/stackchan_mcp/esp32_client.py +340 -0
- stackchan_mcp-0.1.0/stackchan_mcp/gateway.py +123 -0
- stackchan_mcp-0.1.0/stackchan_mcp/handlers/__init__.py +7 -0
- stackchan_mcp-0.1.0/stackchan_mcp/handlers/audio.py +21 -0
- stackchan_mcp-0.1.0/stackchan_mcp/handlers/camera.py +25 -0
- stackchan_mcp-0.1.0/stackchan_mcp/handlers/robot.py +52 -0
- stackchan_mcp-0.1.0/stackchan_mcp/mcp_router.py +126 -0
- stackchan_mcp-0.1.0/stackchan_mcp/protocol.py +95 -0
- stackchan_mcp-0.1.0/stackchan_mcp/server.py +28 -0
- stackchan_mcp-0.1.0/stackchan_mcp/stdio_server.py +344 -0
- stackchan_mcp-0.1.0/stackchan_mcp/tools.py +82 -0
- stackchan_mcp-0.1.0/tests/conftest.py +17 -0
- stackchan_mcp-0.1.0/tests/test_capture_server.py +25 -0
- stackchan_mcp-0.1.0/tests/test_esp32_client.py +278 -0
- stackchan_mcp-0.1.0/tests/test_gateway.py +77 -0
- stackchan_mcp-0.1.0/tests/test_mcp_router.py +124 -0
- stackchan_mcp-0.1.0/tests/test_protocol.py +94 -0
- stackchan_mcp-0.1.0/tests/test_stdio_server.py +66 -0
- stackchan_mcp-0.1.0/uv.lock +1657 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# stackchan-mcp gateway environment variables
|
|
2
|
+
# Copy this file to `.env` and fill in your values.
|
|
3
|
+
|
|
4
|
+
# --- ESP32 authentication ---
|
|
5
|
+
# Token sent by the ESP32 as `Authorization: Bearer <token>`.
|
|
6
|
+
# Set the same value in the firmware's WiFi config UI (token field).
|
|
7
|
+
STACKCHAN_TOKEN=your-secret-token-here
|
|
8
|
+
|
|
9
|
+
# Legacy alias (still supported, prefer STACKCHAN_TOKEN above).
|
|
10
|
+
BEARER_TOKEN=
|
|
11
|
+
|
|
12
|
+
# --- WebSocket server (ESP32 -> gateway) ---
|
|
13
|
+
HOST=0.0.0.0
|
|
14
|
+
WS_PORT=8765
|
|
15
|
+
|
|
16
|
+
# --- HTTP capture server (ESP32 -> gateway, photo upload) ---
|
|
17
|
+
CAPTURE_PORT=8766
|
|
18
|
+
|
|
19
|
+
# Full public capture URL sent to the ESP32.
|
|
20
|
+
# Use this for remote tunnel setups such as Tailscale Funnel.
|
|
21
|
+
# Example: https://stackchan.example.ts.net:8443/capture
|
|
22
|
+
VISION_URL=
|
|
23
|
+
|
|
24
|
+
# Optional separate bearer token for HTTP photo uploads to VISION_URL.
|
|
25
|
+
# If empty, STACKCHAN_TOKEN is reused.
|
|
26
|
+
VISION_TOKEN=
|
|
27
|
+
|
|
28
|
+
# LAN IP of THIS machine, as seen from the ESP32.
|
|
29
|
+
# The gateway tells the ESP32 to POST captured photos to this address.
|
|
30
|
+
# Required if you want take_photo to work and VISION_URL is not set.
|
|
31
|
+
# Example: something like 192.168.x.y on a typical home network
|
|
32
|
+
# (run `ifconfig` / `ip addr` to find your host's LAN IP).
|
|
33
|
+
VISION_HOST=
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
.venv/
|
|
6
|
+
venv/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
|
|
10
|
+
# Environment / secrets
|
|
11
|
+
.env
|
|
12
|
+
.env.local
|
|
13
|
+
*.local
|
|
14
|
+
|
|
15
|
+
# Captures (user photos)
|
|
16
|
+
captures/
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.vscode/
|
|
20
|
+
.idea/
|
|
21
|
+
*.swp
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
|
|
27
|
+
# Build artifacts
|
|
28
|
+
*.zip
|
|
29
|
+
build/
|
|
30
|
+
dist/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Shenzhen Xinzhi Future Technology Co., Ltd.
|
|
4
|
+
Copyright (c) 2025 Project Contributors
|
|
5
|
+
Copyright (c) 2026 kisaragi-mochi
|
|
6
|
+
|
|
7
|
+
NOTE: This MIT License covers the repository as a whole **except** for the
|
|
8
|
+
SCServo_lib files (Feetech serial bus servo SDK), which live in
|
|
9
|
+
`firmware/main/boards/stackchan/` (files: SCS.{cc,h}, SCSCL.{cc,h},
|
|
10
|
+
SCSerial.{cc,h}, INST.h, SCServo.h) and are licensed separately under the
|
|
11
|
+
GNU General Public License v3.0. Their full license text is in
|
|
12
|
+
`firmware/main/boards/stackchan/SCServo_lib_LICENSE.txt`.
|
|
13
|
+
|
|
14
|
+
Because the firmware binary statically links the SCServo_lib code, the
|
|
15
|
+
combined firmware build is effectively distributed under GPL-3.0. The
|
|
16
|
+
gateway/ Python package, which runs in a separate process and communicates
|
|
17
|
+
with the device only over a network socket, remains under the MIT License
|
|
18
|
+
above.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
24
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
25
|
+
in the Software without restriction, including without limitation the rights
|
|
26
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
27
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
28
|
+
furnished to do so, subject to the following conditions:
|
|
29
|
+
|
|
30
|
+
The above copyright notice and this permission notice shall be included in all
|
|
31
|
+
copies or substantial portions of the Software.
|
|
32
|
+
|
|
33
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
34
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
35
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
36
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
37
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
38
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
39
|
+
SOFTWARE.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stackchan-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Two-faced MCP gateway for StackChan (xiaozhi-esp32): bridges stdio MCP clients to the ESP32 over WebSocket + HTTP.
|
|
5
|
+
Project-URL: Homepage, https://github.com/kisaragi-mochi/stackchan-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/kisaragi-mochi/stackchan-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/kisaragi-mochi/stackchan-mcp/issues
|
|
8
|
+
Author: kisaragi-mochi
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: esp32,llm,mcp,robotics,stackchan,xiaozhi
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Communications
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Classifier: Topic :: System :: Hardware
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: aiohttp>=3
|
|
26
|
+
Requires-Dist: mcp>=1.0
|
|
27
|
+
Requires-Dist: pydantic>=2
|
|
28
|
+
Requires-Dist: python-dotenv
|
|
29
|
+
Requires-Dist: websockets>=12
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# gateway
|
|
33
|
+
|
|
34
|
+
Python "two-faced" MCP gateway for the **M5Stack official [StackChan](https://docs.m5stack.com/ja/StackChan)** kit (custom [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) firmware in [`../firmware/main/boards/stackchan/`](../firmware/main/boards/stackchan/)).
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
┌─────────────┐ stdio MCP ┌──────────────┐ WebSocket MCP ┌──────────┐
|
|
38
|
+
│ MCP client │ ──────────▶ │ gateway │ ──────────────▶ │ ESP32 │
|
|
39
|
+
│ (Claude...) │ ◀────────── │ (this dir) │ ◀────────────── │ StackChan│
|
|
40
|
+
└─────────────┘ │ │ └──────────┘
|
|
41
|
+
│ /capture │ ◀─ HTTP POST ──┘ (JPEG)
|
|
42
|
+
└──────────────┘
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The gateway exposes a clean stdio MCP server to the LLM client (left) while
|
|
46
|
+
speaking the xiaozhi-esp32 WebSocket MCP dialect to the device (right). It
|
|
47
|
+
also runs a small HTTP server (`/capture`) so the ESP32 can upload photos.
|
|
48
|
+
|
|
49
|
+
The package name on PyPI, the installed CLI command, and the MCP server id
|
|
50
|
+
in your client config are all `stackchan-mcp`.
|
|
51
|
+
|
|
52
|
+
## Install (end users)
|
|
53
|
+
|
|
54
|
+
The gateway is published to PyPI as `stackchan-mcp`. For end users, install
|
|
55
|
+
it as an isolated CLI tool:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
uv tool install stackchan-mcp
|
|
59
|
+
# or
|
|
60
|
+
pipx install stackchan-mcp
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then run:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
stackchan-mcp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`stackchan-mcp` reads its configuration (`STACKCHAN_TOKEN`, `VISION_HOST`,
|
|
70
|
+
etc.) from environment variables or a `.env` file in the working directory.
|
|
71
|
+
See the [Setup](#setup) section below for the supported variables. For the
|
|
72
|
+
firmware side (WebSocket gateway URL, auth token, NVS configuration), see
|
|
73
|
+
[`../README.md`](../README.md#configuring-the-websocket-gateway-url-and-auth-token).
|
|
74
|
+
|
|
75
|
+
If you prefer a project-managed virtualenv, `pip install stackchan-mcp`
|
|
76
|
+
inside an active venv works as well, and `python -m stackchan_mcp` inside
|
|
77
|
+
that venv is equivalent to `stackchan-mcp`. Just avoid `pip install`
|
|
78
|
+
against the system Python (PEP 668).
|
|
79
|
+
|
|
80
|
+
## Setup
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cd gateway
|
|
84
|
+
cp .env.example .env # then edit .env (see below)
|
|
85
|
+
uv sync
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Edit `.env`:
|
|
89
|
+
- `STACKCHAN_TOKEN`: Bearer token for ESP32 auth (must match firmware setting)
|
|
90
|
+
- `VISION_URL`: full public capture URL for remote access tunnels, such as
|
|
91
|
+
`https://stackchan.example.ts.net:8443/capture`
|
|
92
|
+
- `VISION_TOKEN`: optional separate Bearer token for capture uploads; if empty,
|
|
93
|
+
`STACKCHAN_TOKEN` is reused
|
|
94
|
+
- `VISION_HOST`: LAN IP of this machine, as seen from the ESP32
|
|
95
|
+
(something like `192.168.x.y` on a typical home network — run `ifconfig`
|
|
96
|
+
or `ip addr` to find it). Required for `take_photo` when `VISION_URL` is not
|
|
97
|
+
set.
|
|
98
|
+
|
|
99
|
+
## Run
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv run python -m stackchan_mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Default ports:
|
|
106
|
+
- WebSocket (ESP32 -> gateway): `0.0.0.0:8765`
|
|
107
|
+
- HTTP capture (ESP32 -> gateway): `0.0.0.0:8766`
|
|
108
|
+
|
|
109
|
+
For non-LAN setups, see [`../docs/remote-access.md`](../docs/remote-access.md)
|
|
110
|
+
for the Tailscale Funnel flow.
|
|
111
|
+
|
|
112
|
+
When you restart the gateway during development, an already-connected ESP32
|
|
113
|
+
will notice the dropped WebSocket and retry while idle. The retry delay starts
|
|
114
|
+
at 5 seconds and backs off up to 60 seconds. After the gateway is listening
|
|
115
|
+
again, check `get_status` from the stdio MCP side to confirm the device is back.
|
|
116
|
+
|
|
117
|
+
## Tests
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv run pytest tests/ -v
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Register as MCP server
|
|
124
|
+
|
|
125
|
+
### Claude Code (`~/.claude.json`)
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"stackchan-mcp": {
|
|
131
|
+
"type": "stdio",
|
|
132
|
+
"command": "uv",
|
|
133
|
+
"args": [
|
|
134
|
+
"run",
|
|
135
|
+
"--directory",
|
|
136
|
+
"/absolute/path/to/stackchan-mcp/gateway",
|
|
137
|
+
"python",
|
|
138
|
+
"-m",
|
|
139
|
+
"stackchan_mcp"
|
|
140
|
+
],
|
|
141
|
+
"env": {
|
|
142
|
+
"STACKCHAN_TOKEN": "your-secret-token-here",
|
|
143
|
+
"VISION_HOST": "your.host.lan.ip"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
151
|
+
|
|
152
|
+
Same shape, under `mcpServers`.
|
|
153
|
+
|
|
154
|
+
## Tools exposed to MCP client
|
|
155
|
+
|
|
156
|
+
| Tool | Description |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `get_status` | Gateway connection state (ESP32 connected? device info?) |
|
|
159
|
+
| `get_device_info` | ESP32 device status (battery, volume, WiFi, etc.) |
|
|
160
|
+
| `take_photo(question?)` | Trigger camera capture; returns saved JPEG path |
|
|
161
|
+
| `set_volume(volume)` | Speaker volume 0-100 |
|
|
162
|
+
| `set_brightness(brightness)` | Screen brightness 0-100 |
|
|
163
|
+
| `move_head(yaw, pitch, speed?)` | Drive yaw + pitch servos |
|
|
164
|
+
| `get_head_angles` | Read current yaw + pitch servo angles |
|
|
165
|
+
| `get_touch_state` | Touch sensor state (press/release/stroke) |
|
|
166
|
+
| `set_avatar(face)` | Switch avatar expression (`idle` / `happy` / `thinking` / `sad` / `surprised` / `embarrassed`) |
|
|
167
|
+
| `set_blink(state)` | Blink animation on/off |
|
|
168
|
+
| `set_mouth(state)` | Mouth shape (`closed` / `half` / `open` / `e` / `u`) |
|
|
169
|
+
| `check_vm_en` | Read PY32 VM EN GPIO state (servo power supply diagnostic) |
|
|
170
|
+
|
|
171
|
+
The mapping from these names to ESP32-side `self.*` MCP tools is in
|
|
172
|
+
`stackchan_mcp/stdio_server.py`.
|
|
173
|
+
|
|
174
|
+
## Architecture
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
stackchan_mcp/
|
|
178
|
+
├── __main__.py # entry: starts gateway + stdio server
|
|
179
|
+
├── gateway.py # singleton orchestrator
|
|
180
|
+
├── stdio_server.py # MCP client side (stdio MCP server)
|
|
181
|
+
├── esp32_client.py # ESP32 side (WebSocket MCP client + auth)
|
|
182
|
+
├── capture_server.py # HTTP /capture endpoint for photo uploads
|
|
183
|
+
├── server.py # legacy local WS test server (unused in prod)
|
|
184
|
+
├── mcp_router.py # legacy local stub router (unused in prod)
|
|
185
|
+
├── protocol.py # JSON-RPC 2.0 message helpers
|
|
186
|
+
├── tools.py # ESP32-side tool definitions (stub/test)
|
|
187
|
+
├── audio_stream.py # placeholder for future Opus pipeline
|
|
188
|
+
└── handlers/
|
|
189
|
+
├── robot.py # legacy stubs
|
|
190
|
+
├── camera.py # legacy stubs
|
|
191
|
+
└── audio.py # legacy stubs
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Captures land in `~/.stackchan/captures/` by default.
|
|
195
|
+
|
|
196
|
+
## Manual smoke test (Python)
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
import asyncio, json, websockets
|
|
200
|
+
|
|
201
|
+
async def smoke():
|
|
202
|
+
async with websockets.connect(
|
|
203
|
+
"ws://localhost:8765",
|
|
204
|
+
additional_headers={"Authorization": "Bearer your-secret-token-here"},
|
|
205
|
+
) as ws:
|
|
206
|
+
await ws.send(json.dumps({
|
|
207
|
+
"type": "hello", "version": 1, "audio_params": {},
|
|
208
|
+
}))
|
|
209
|
+
print(await ws.recv())
|
|
210
|
+
|
|
211
|
+
await ws.send(json.dumps({"type": "mcp", "payload": {
|
|
212
|
+
"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {},
|
|
213
|
+
}}))
|
|
214
|
+
print(await ws.recv())
|
|
215
|
+
|
|
216
|
+
await ws.send(json.dumps({"type": "mcp", "payload": {
|
|
217
|
+
"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {},
|
|
218
|
+
}}))
|
|
219
|
+
print(await ws.recv())
|
|
220
|
+
|
|
221
|
+
asyncio.run(smoke())
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Phase roadmap
|
|
225
|
+
|
|
226
|
+
- **Phase 1** (done): stdio MCP shell, ESP32 WebSocket bridge, tool routing
|
|
227
|
+
- **Phase 2** (done): real servo / volume / brightness via ESP32
|
|
228
|
+
- **Phase 3** (done): camera capture (JPEG over HTTP)
|
|
229
|
+
- **Phase 4** (planned): Opus audio stream (STT/TTS pipeline)
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
The gateway is distributed under the MIT License (see `LICENSE`). The
|
|
234
|
+
parent monorepo's `firmware/` directory contains SCServo_lib code under
|
|
235
|
+
GPL-3.0, but those files live only inside
|
|
236
|
+
`firmware/main/boards/stackchan/` and never enter this package. The
|
|
237
|
+
gateway and firmware communicate only over WebSocket, so the GPL/MIT
|
|
238
|
+
boundary is preserved at the process level.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# gateway
|
|
2
|
+
|
|
3
|
+
Python "two-faced" MCP gateway for the **M5Stack official [StackChan](https://docs.m5stack.com/ja/StackChan)** kit (custom [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) firmware in [`../firmware/main/boards/stackchan/`](../firmware/main/boards/stackchan/)).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌─────────────┐ stdio MCP ┌──────────────┐ WebSocket MCP ┌──────────┐
|
|
7
|
+
│ MCP client │ ──────────▶ │ gateway │ ──────────────▶ │ ESP32 │
|
|
8
|
+
│ (Claude...) │ ◀────────── │ (this dir) │ ◀────────────── │ StackChan│
|
|
9
|
+
└─────────────┘ │ │ └──────────┘
|
|
10
|
+
│ /capture │ ◀─ HTTP POST ──┘ (JPEG)
|
|
11
|
+
└──────────────┘
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The gateway exposes a clean stdio MCP server to the LLM client (left) while
|
|
15
|
+
speaking the xiaozhi-esp32 WebSocket MCP dialect to the device (right). It
|
|
16
|
+
also runs a small HTTP server (`/capture`) so the ESP32 can upload photos.
|
|
17
|
+
|
|
18
|
+
The package name on PyPI, the installed CLI command, and the MCP server id
|
|
19
|
+
in your client config are all `stackchan-mcp`.
|
|
20
|
+
|
|
21
|
+
## Install (end users)
|
|
22
|
+
|
|
23
|
+
The gateway is published to PyPI as `stackchan-mcp`. For end users, install
|
|
24
|
+
it as an isolated CLI tool:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv tool install stackchan-mcp
|
|
28
|
+
# or
|
|
29
|
+
pipx install stackchan-mcp
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
stackchan-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`stackchan-mcp` reads its configuration (`STACKCHAN_TOKEN`, `VISION_HOST`,
|
|
39
|
+
etc.) from environment variables or a `.env` file in the working directory.
|
|
40
|
+
See the [Setup](#setup) section below for the supported variables. For the
|
|
41
|
+
firmware side (WebSocket gateway URL, auth token, NVS configuration), see
|
|
42
|
+
[`../README.md`](../README.md#configuring-the-websocket-gateway-url-and-auth-token).
|
|
43
|
+
|
|
44
|
+
If you prefer a project-managed virtualenv, `pip install stackchan-mcp`
|
|
45
|
+
inside an active venv works as well, and `python -m stackchan_mcp` inside
|
|
46
|
+
that venv is equivalent to `stackchan-mcp`. Just avoid `pip install`
|
|
47
|
+
against the system Python (PEP 668).
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd gateway
|
|
53
|
+
cp .env.example .env # then edit .env (see below)
|
|
54
|
+
uv sync
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Edit `.env`:
|
|
58
|
+
- `STACKCHAN_TOKEN`: Bearer token for ESP32 auth (must match firmware setting)
|
|
59
|
+
- `VISION_URL`: full public capture URL for remote access tunnels, such as
|
|
60
|
+
`https://stackchan.example.ts.net:8443/capture`
|
|
61
|
+
- `VISION_TOKEN`: optional separate Bearer token for capture uploads; if empty,
|
|
62
|
+
`STACKCHAN_TOKEN` is reused
|
|
63
|
+
- `VISION_HOST`: LAN IP of this machine, as seen from the ESP32
|
|
64
|
+
(something like `192.168.x.y` on a typical home network — run `ifconfig`
|
|
65
|
+
or `ip addr` to find it). Required for `take_photo` when `VISION_URL` is not
|
|
66
|
+
set.
|
|
67
|
+
|
|
68
|
+
## Run
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv run python -m stackchan_mcp
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Default ports:
|
|
75
|
+
- WebSocket (ESP32 -> gateway): `0.0.0.0:8765`
|
|
76
|
+
- HTTP capture (ESP32 -> gateway): `0.0.0.0:8766`
|
|
77
|
+
|
|
78
|
+
For non-LAN setups, see [`../docs/remote-access.md`](../docs/remote-access.md)
|
|
79
|
+
for the Tailscale Funnel flow.
|
|
80
|
+
|
|
81
|
+
When you restart the gateway during development, an already-connected ESP32
|
|
82
|
+
will notice the dropped WebSocket and retry while idle. The retry delay starts
|
|
83
|
+
at 5 seconds and backs off up to 60 seconds. After the gateway is listening
|
|
84
|
+
again, check `get_status` from the stdio MCP side to confirm the device is back.
|
|
85
|
+
|
|
86
|
+
## Tests
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
uv run pytest tests/ -v
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Register as MCP server
|
|
93
|
+
|
|
94
|
+
### Claude Code (`~/.claude.json`)
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"mcpServers": {
|
|
99
|
+
"stackchan-mcp": {
|
|
100
|
+
"type": "stdio",
|
|
101
|
+
"command": "uv",
|
|
102
|
+
"args": [
|
|
103
|
+
"run",
|
|
104
|
+
"--directory",
|
|
105
|
+
"/absolute/path/to/stackchan-mcp/gateway",
|
|
106
|
+
"python",
|
|
107
|
+
"-m",
|
|
108
|
+
"stackchan_mcp"
|
|
109
|
+
],
|
|
110
|
+
"env": {
|
|
111
|
+
"STACKCHAN_TOKEN": "your-secret-token-here",
|
|
112
|
+
"VISION_HOST": "your.host.lan.ip"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Claude Desktop (`claude_desktop_config.json`)
|
|
120
|
+
|
|
121
|
+
Same shape, under `mcpServers`.
|
|
122
|
+
|
|
123
|
+
## Tools exposed to MCP client
|
|
124
|
+
|
|
125
|
+
| Tool | Description |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `get_status` | Gateway connection state (ESP32 connected? device info?) |
|
|
128
|
+
| `get_device_info` | ESP32 device status (battery, volume, WiFi, etc.) |
|
|
129
|
+
| `take_photo(question?)` | Trigger camera capture; returns saved JPEG path |
|
|
130
|
+
| `set_volume(volume)` | Speaker volume 0-100 |
|
|
131
|
+
| `set_brightness(brightness)` | Screen brightness 0-100 |
|
|
132
|
+
| `move_head(yaw, pitch, speed?)` | Drive yaw + pitch servos |
|
|
133
|
+
| `get_head_angles` | Read current yaw + pitch servo angles |
|
|
134
|
+
| `get_touch_state` | Touch sensor state (press/release/stroke) |
|
|
135
|
+
| `set_avatar(face)` | Switch avatar expression (`idle` / `happy` / `thinking` / `sad` / `surprised` / `embarrassed`) |
|
|
136
|
+
| `set_blink(state)` | Blink animation on/off |
|
|
137
|
+
| `set_mouth(state)` | Mouth shape (`closed` / `half` / `open` / `e` / `u`) |
|
|
138
|
+
| `check_vm_en` | Read PY32 VM EN GPIO state (servo power supply diagnostic) |
|
|
139
|
+
|
|
140
|
+
The mapping from these names to ESP32-side `self.*` MCP tools is in
|
|
141
|
+
`stackchan_mcp/stdio_server.py`.
|
|
142
|
+
|
|
143
|
+
## Architecture
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
stackchan_mcp/
|
|
147
|
+
├── __main__.py # entry: starts gateway + stdio server
|
|
148
|
+
├── gateway.py # singleton orchestrator
|
|
149
|
+
├── stdio_server.py # MCP client side (stdio MCP server)
|
|
150
|
+
├── esp32_client.py # ESP32 side (WebSocket MCP client + auth)
|
|
151
|
+
├── capture_server.py # HTTP /capture endpoint for photo uploads
|
|
152
|
+
├── server.py # legacy local WS test server (unused in prod)
|
|
153
|
+
├── mcp_router.py # legacy local stub router (unused in prod)
|
|
154
|
+
├── protocol.py # JSON-RPC 2.0 message helpers
|
|
155
|
+
├── tools.py # ESP32-side tool definitions (stub/test)
|
|
156
|
+
├── audio_stream.py # placeholder for future Opus pipeline
|
|
157
|
+
└── handlers/
|
|
158
|
+
├── robot.py # legacy stubs
|
|
159
|
+
├── camera.py # legacy stubs
|
|
160
|
+
└── audio.py # legacy stubs
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Captures land in `~/.stackchan/captures/` by default.
|
|
164
|
+
|
|
165
|
+
## Manual smoke test (Python)
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
import asyncio, json, websockets
|
|
169
|
+
|
|
170
|
+
async def smoke():
|
|
171
|
+
async with websockets.connect(
|
|
172
|
+
"ws://localhost:8765",
|
|
173
|
+
additional_headers={"Authorization": "Bearer your-secret-token-here"},
|
|
174
|
+
) as ws:
|
|
175
|
+
await ws.send(json.dumps({
|
|
176
|
+
"type": "hello", "version": 1, "audio_params": {},
|
|
177
|
+
}))
|
|
178
|
+
print(await ws.recv())
|
|
179
|
+
|
|
180
|
+
await ws.send(json.dumps({"type": "mcp", "payload": {
|
|
181
|
+
"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {},
|
|
182
|
+
}}))
|
|
183
|
+
print(await ws.recv())
|
|
184
|
+
|
|
185
|
+
await ws.send(json.dumps({"type": "mcp", "payload": {
|
|
186
|
+
"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {},
|
|
187
|
+
}}))
|
|
188
|
+
print(await ws.recv())
|
|
189
|
+
|
|
190
|
+
asyncio.run(smoke())
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Phase roadmap
|
|
194
|
+
|
|
195
|
+
- **Phase 1** (done): stdio MCP shell, ESP32 WebSocket bridge, tool routing
|
|
196
|
+
- **Phase 2** (done): real servo / volume / brightness via ESP32
|
|
197
|
+
- **Phase 3** (done): camera capture (JPEG over HTTP)
|
|
198
|
+
- **Phase 4** (planned): Opus audio stream (STT/TTS pipeline)
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
The gateway is distributed under the MIT License (see `LICENSE`). The
|
|
203
|
+
parent monorepo's `firmware/` directory contains SCServo_lib code under
|
|
204
|
+
GPL-3.0, but those files live only inside
|
|
205
|
+
`firmware/main/boards/stackchan/` and never enter this package. The
|
|
206
|
+
gateway and firmware communicate only over WebSocket, so the GPL/MIT
|
|
207
|
+
boundary is preserved at the process level.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "stackchan-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Two-faced MCP gateway for StackChan (xiaozhi-esp32): bridges stdio MCP clients to the ESP32 over WebSocket + HTTP."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "kisaragi-mochi" },
|
|
11
|
+
]
|
|
12
|
+
keywords = ["mcp", "stackchan", "esp32", "xiaozhi", "robotics", "llm"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Libraries",
|
|
24
|
+
"Topic :: Communications",
|
|
25
|
+
"Topic :: System :: Hardware",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"websockets>=12",
|
|
29
|
+
"pydantic>=2",
|
|
30
|
+
"python-dotenv",
|
|
31
|
+
"mcp>=1.0",
|
|
32
|
+
"aiohttp>=3",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/kisaragi-mochi/stackchan-mcp"
|
|
37
|
+
Repository = "https://github.com/kisaragi-mochi/stackchan-mcp"
|
|
38
|
+
Issues = "https://github.com/kisaragi-mochi/stackchan-mcp/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
stackchan-mcp = "stackchan_mcp.cli:main"
|
|
42
|
+
|
|
43
|
+
[dependency-groups]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest",
|
|
46
|
+
"pytest-asyncio",
|
|
47
|
+
"ruff>=0.15.12",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["hatchling"]
|
|
52
|
+
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Entry point: ``python -m stackchan_mcp``.
|
|
2
|
+
|
|
3
|
+
The actual implementation lives in :mod:`stackchan_mcp.cli` so that the
|
|
4
|
+
console script and ``python -m`` paths share a single side-effect-free
|
|
5
|
+
import surface.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .cli import main
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Opus audio frame handling — skeleton for Phase 4 (planned).
|
|
2
|
+
|
|
3
|
+
This module will handle:
|
|
4
|
+
- Incoming Opus frames from the device (STT pipeline)
|
|
5
|
+
- Outgoing Opus frames to the device (TTS pipeline)
|
|
6
|
+
|
|
7
|
+
For now, binary frames are logged and discarded.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def handle_audio_frame(data: bytes, session_id: str) -> None:
|
|
18
|
+
"""Process an incoming binary Opus frame (stub).
|
|
19
|
+
|
|
20
|
+
Phase 4 will pipe this into an STT engine.
|
|
21
|
+
"""
|
|
22
|
+
logger.debug(
|
|
23
|
+
"audio_frame session=%s bytes=%d (discarded — Phase 4)",
|
|
24
|
+
session_id,
|
|
25
|
+
len(data),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def send_audio_frame(data: bytes) -> bytes:
|
|
30
|
+
"""Prepare an outgoing Opus frame (stub).
|
|
31
|
+
|
|
32
|
+
Phase 4 will generate this from a TTS engine.
|
|
33
|
+
"""
|
|
34
|
+
return data
|