Skip to content

Commit 3cd33a1

Browse files
committed
finalize mcp stdio and sse example
1 parent ee58794 commit 3cd33a1

7 files changed

Lines changed: 49 additions & 21 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ __pycache__/
2929
*.pyd
3030

3131
# MCP logs
32-
examples/MCP/*.log
32+
examples/MCP/**/*.log
3333

3434
# Qt build directory
3535
__Builds/

examples/MCP/sse/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# QTAC MCP Server SSE
1+
# QTAC MCP Server - SSE
22

33
Persistent server supporting multiple concurrent clients. Each client session gets exclusive
44
ownership of a device.
@@ -31,7 +31,7 @@ pip install -r examples/MCP/sse/requirements.txt
3131

3232
## Configuration
3333

34-
`config.yaml` all fields optional, defaults shown:
34+
`config.yaml` - all fields optional, defaults shown:
3535

3636
| Parameter | Default | Description |
3737
| :-- | :-- | :-- |
@@ -50,14 +50,14 @@ The server must be started before any client connects:
5050
python examples/MCP/sse/tacdev_mcp_server.py
5151
```
5252

53-
### Option 1 Python client
53+
### Option 1 - Python client
5454

5555
```bash
5656
python examples/MCP/sse/tacdev_mcp_client.py # first available device
5757
python examples/MCP/sse/tacdev_mcp_client.py COM41 # specific port
5858
```
5959

60-
### Option 2 Claude CLI
60+
### Option 2 - Claude CLI
6161

6262
Register once (server must already be running):
6363

@@ -71,7 +71,7 @@ Verify:
7171
claude mcp list
7272
```
7373

74-
Then ask Claude naturally no special syntax needed:
74+
Then ask Claude naturally - no special syntax needed:
7575

7676
```
7777
List connected devices
@@ -100,5 +100,5 @@ python examples/MCP/sse/tacdev_mcp_server.py
100100
| `TACDev library not found` | Run `build.bat` / `build.sh` first |
101101
| `Architecture mismatch` | Use Python matching your OS architecture |
102102
| `No module named 'fastmcp'` | `pip install -r requirements.txt` (requires Python 3.10+) |
103-
| `cryptography` build failure on ARM64 | Re-run `setup.bat` downloads OpenSSL automatically. Manual fallback: install [Win64ARMOpenSSL-4_0_1.msi](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` |
103+
| `cryptography` build failure on ARM64 | Re-run `setup.bat` - downloads OpenSSL automatically. Manual fallback: install [Win64ARMOpenSSL-4_0_1.msi](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` |
104104
| `get_device_count` returns 0 | Check board is connected. Windows: run `FTDICheck.exe` from `__Builds\x64\Release\bin`. Linux: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` |

examples/MCP/sse/tacdev_mcp_client.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,17 +299,24 @@ async def TACExample(port_name: str | None = None) -> None:
299299

300300
commands = await tac.list_commands(handle)
301301
if commands:
302-
# Extract short command names (e.g. "sedl" from "sedl(30), Secondary EDL")
303-
cmd_names = [c.split("(")[0].strip() for c in commands]
304-
print(f"\nCommand names: {cmd_names}")
302+
cmd_names = [c.split(";")[0].strip() for c in commands]
303+
print(f"\nCommands : {cmd_names}")
305304

305+
quick_commands = await tac.list_quick_commands(handle)
306+
if quick_commands:
307+
qc_names = [q.split(";")[1].strip() for q in quick_commands]
308+
print(f"Quick commands: {qc_names}")
309+
310+
if commands:
306311
cmd = cmd_names[0]
307312
state_before = await tac.get_command_state(handle, cmd)
308313
print(f"\nCommand '{cmd}' state before toggle: {state_before}")
309314
await tac.send_command(handle, cmd, not state_before)
315+
await asyncio.sleep(2)
310316
state_after = await tac.get_command_state(handle, cmd)
311317
print(f"Command '{cmd}' state after toggle: {state_after}")
312318
await tac.send_command(handle, cmd, state_before)
319+
await asyncio.sleep(2)
313320
print(f"Command '{cmd}' restored to : {await tac.get_command_state(handle, cmd)}")
314321

315322
await tac.close_tac_handle(handle)

examples/MCP/sse/tacdev_mcp_server.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import asyncio
77
import logging
88
import logging.handlers
9+
import time
910
from pathlib import Path
1011

1112
import yaml
@@ -32,8 +33,9 @@ def _setup_logging() -> logging.Logger:
3233
logger = logging.getLogger("tacdev_mcp")
3334
logger.setLevel(log_cfg["level"].upper())
3435

36+
log_path = _CONFIG_PATH.with_name(log_cfg["file"])
3537
handler = logging.handlers.RotatingFileHandler(
36-
log_cfg["file"],
38+
log_path,
3739
maxBytes=log_cfg["max_bytes"],
3840
backupCount=log_cfg["backup_count"],
3941
)
@@ -43,6 +45,14 @@ def _setup_logging() -> logging.Logger:
4345

4446
log = _setup_logging()
4547

48+
# Suppress uvicorn SSE shutdown race: starlette sends http.response.start after
49+
# uvicorn has already torn down the connection on Ctrl+C.
50+
logging.getLogger("uvicorn.error").addFilter(
51+
type("_suppress_sse_shutdown", (logging.Filter,), {
52+
"filter": lambda self, r: "Expected ASGI message" not in r.getMessage()
53+
})()
54+
)
55+
4656
# --------------------------------------------------------------------------- #
4757
# Device ownership registry
4858
# --------------------------------------------------------------------------- #
@@ -80,6 +90,7 @@ def _release_all() -> None:
8090
try:
8191
device.Close()
8292
log.info("session=%s handle=%d port=%s closed on shutdown", session_id, handle, port)
93+
time.sleep(2)
8394
except Exception as e:
8495
log.warning("session=%s handle=%d port=%s failed to close on shutdown: %s", session_id, handle, port, e)
8596
handles.clear()
@@ -594,12 +605,13 @@ async def boot_to_secondary_edl_button(handle: int, ctx: Context) -> dict:
594605
if __name__ == "__main__":
595606
server_cfg = cfg["server"]
596607
log.info("Starting TACDev MCP server on %s:%d", server_cfg["host"], server_cfg["port"])
597-
log.info("Tip: if a client crashes and leaves a device locked, press Ctrl+C to restart the server — all devices will be released cleanly on shutdown.")
608+
log.info("Tip: if a client crashes and leaves a device locked, press Ctrl+C to restart the server. All devices will be released cleanly on shutdown.")
598609
try:
599610
mcp.run(
600611
transport="sse",
601612
host=server_cfg["host"],
602613
port=server_cfg["port"],
614+
uvicorn_config={"timeout_graceful_shutdown": 0},
603615
)
604616
except KeyboardInterrupt:
605617
pass

examples/MCP/stdio/README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# QTAC MCP Server stdio
1+
# QTAC MCP Server - stdio
22

33
Server is spawned automatically per-client process. No standalone server to manage.
44

@@ -30,16 +30,16 @@ pip install -r examples/MCP/stdio/requirements.txt
3030

3131
## Running
3232

33-
No separate server process needed the server is spawned automatically.
33+
No separate server process needed - the server is spawned automatically.
3434

35-
### Option 1 Python client
35+
### Option 1 - Python client
3636

3737
```bash
3838
python examples/MCP/stdio/tacdev_mcp_client.py # first available device
3939
python examples/MCP/stdio/tacdev_mcp_client.py COM41 # specific port
4040
```
4141

42-
### Option 2 Claude CLI
42+
### Option 2 - Claude CLI
4343

4444
Register once from repo root:
4545

@@ -53,7 +53,7 @@ Verify:
5353
claude mcp list
5454
```
5555

56-
Then ask Claude naturally no special syntax needed:
56+
Then ask Claude naturally - no special syntax needed:
5757

5858
```
5959
List connected devices
@@ -79,5 +79,5 @@ Boot COM3 to fastboot
7979
| `TACDev library not found` | Run `build.bat` / `build.sh` first |
8080
| `Architecture mismatch` | Use Python matching your OS architecture |
8181
| `No module named 'fastmcp'` | `pip install -r requirements.txt` (requires Python 3.10+) |
82-
| `cryptography` build failure on ARM64 | Re-run `setup.bat` downloads OpenSSL automatically. Manual fallback: install [Win64ARMOpenSSL-4_0_1.msi](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` |
82+
| `cryptography` build failure on ARM64 | Re-run `setup.bat` - downloads OpenSSL automatically. Manual fallback: install [Win64ARMOpenSSL-4_0_1.msi](https://slproweb.com/download/Win64ARMOpenSSL-4_0_1.msi) (full, not Light) and set `OPENSSL_DIR=C:\Program Files\OpenSSL-Win64-ARM` |
8383
| `get_device_count` returns 0 | Check board is connected. Windows: run `FTDICheck.exe` from `__Builds\x64\Release\bin`. Linux: `sudo cp udev-rules/99-QTAC-USB.rules /etc/udev/rules.d/ && sudo udevadm control --reload` |

examples/MCP/stdio/tacdev_mcp_client.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,17 +293,24 @@ async def TACExample(port_name: str | None = None) -> None:
293293

294294
commands = await tac.list_commands(handle)
295295
if commands:
296-
# Extract short command names (e.g. "sedl" from "sedl(30), Secondary EDL")
297-
cmd_names = [c.split("(")[0].strip() for c in commands]
298-
print(f"\nCommand names: {cmd_names}")
296+
cmd_names = [c.split(";")[0].strip() for c in commands]
297+
print(f"\nCommands : {cmd_names}")
299298

299+
quick_commands = await tac.list_quick_commands(handle)
300+
if quick_commands:
301+
qc_names = [q.split(";")[1].strip() for q in quick_commands]
302+
print(f"Quick commands: {qc_names}")
303+
304+
if commands:
300305
cmd = cmd_names[0]
301306
state_before = await tac.get_command_state(handle, cmd)
302307
print(f"\nCommand '{cmd}' state before toggle: {state_before}")
303308
await tac.send_command(handle, cmd, not state_before)
309+
await asyncio.sleep(2)
304310
state_after = await tac.get_command_state(handle, cmd)
305311
print(f"Command '{cmd}' state after toggle: {state_after}")
306312
await tac.send_command(handle, cmd, state_before)
313+
await asyncio.sleep(2)
307314
print(f"Command '{cmd}' restored to : {await tac.get_command_state(handle, cmd)}")
308315

309316
await tac.close_tac_handle(handle)

examples/MCP/stdio/tacdev_mcp_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,5 +494,7 @@ def boot_to_secondary_edl_button(handle: int) -> dict:
494494
if __name__ == "__main__":
495495
try:
496496
mcp.run(transport="stdio")
497+
except KeyboardInterrupt:
498+
pass
497499
finally:
498500
_release_all()

0 commit comments

Comments
 (0)