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.
Files changed (30) hide show
  1. stackchan_mcp-0.1.0/.env.example +33 -0
  2. stackchan_mcp-0.1.0/.gitignore +30 -0
  3. stackchan_mcp-0.1.0/LICENSE +39 -0
  4. stackchan_mcp-0.1.0/PKG-INFO +238 -0
  5. stackchan_mcp-0.1.0/README.md +207 -0
  6. stackchan_mcp-0.1.0/pyproject.toml +52 -0
  7. stackchan_mcp-0.1.0/stackchan_mcp/__init__.py +7 -0
  8. stackchan_mcp-0.1.0/stackchan_mcp/__main__.py +12 -0
  9. stackchan_mcp-0.1.0/stackchan_mcp/audio_stream.py +34 -0
  10. stackchan_mcp-0.1.0/stackchan_mcp/capture_server.py +91 -0
  11. stackchan_mcp-0.1.0/stackchan_mcp/cli.py +57 -0
  12. stackchan_mcp-0.1.0/stackchan_mcp/esp32_client.py +340 -0
  13. stackchan_mcp-0.1.0/stackchan_mcp/gateway.py +123 -0
  14. stackchan_mcp-0.1.0/stackchan_mcp/handlers/__init__.py +7 -0
  15. stackchan_mcp-0.1.0/stackchan_mcp/handlers/audio.py +21 -0
  16. stackchan_mcp-0.1.0/stackchan_mcp/handlers/camera.py +25 -0
  17. stackchan_mcp-0.1.0/stackchan_mcp/handlers/robot.py +52 -0
  18. stackchan_mcp-0.1.0/stackchan_mcp/mcp_router.py +126 -0
  19. stackchan_mcp-0.1.0/stackchan_mcp/protocol.py +95 -0
  20. stackchan_mcp-0.1.0/stackchan_mcp/server.py +28 -0
  21. stackchan_mcp-0.1.0/stackchan_mcp/stdio_server.py +344 -0
  22. stackchan_mcp-0.1.0/stackchan_mcp/tools.py +82 -0
  23. stackchan_mcp-0.1.0/tests/conftest.py +17 -0
  24. stackchan_mcp-0.1.0/tests/test_capture_server.py +25 -0
  25. stackchan_mcp-0.1.0/tests/test_esp32_client.py +278 -0
  26. stackchan_mcp-0.1.0/tests/test_gateway.py +77 -0
  27. stackchan_mcp-0.1.0/tests/test_mcp_router.py +124 -0
  28. stackchan_mcp-0.1.0/tests/test_protocol.py +94 -0
  29. stackchan_mcp-0.1.0/tests/test_stdio_server.py +66 -0
  30. 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,7 @@
1
+ """stackchan-mcp: Two-faced gateway for StackChan (xiaozhi-esp32).
2
+
3
+ MCP client side: stdio MCP server (mcp Python SDK)
4
+ ESP32 side: WebSocket server (MCP client over JSON-RPC 2.0)
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -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