Skip to content

Commit 45cae39

Browse files
authored
Merge pull request #53 from qualcomm/feature/mcp-stdio
Finalize MCP SSE and STDIO examples
2 parents 9316bfa + 3cd33a1 commit 45cae39

15 files changed

Lines changed: 1299 additions & 278 deletions

.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/README.md

Lines changed: 0 additions & 130 deletions
This file was deleted.

examples/MCP/sse/README.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# QTAC MCP Server - SSE
2+
3+
Persistent server supporting multiple concurrent clients. Each client session gets exclusive
4+
ownership of a device.
5+
6+
Use this variant when multiple automation clients need to share a device pool simultaneously.
7+
For single-client use see [`../stdio/`](../stdio/).
8+
9+
## Prerequisites
10+
11+
- Python 3.10+ (architecture must match: x64 or ARM64)
12+
- Project built via `build.bat` / `build.sh`
13+
14+
## Setup
15+
16+
Installs the TACDev library and all dependencies:
17+
18+
```bat
19+
setup.bat # Windows (auto-detects x64/ARM64)
20+
```
21+
```bash
22+
./setup.sh # Linux
23+
```
24+
25+
Or manually:
26+
27+
```bash
28+
pip install interfaces/Python # from repo root
29+
pip install -r examples/MCP/sse/requirements.txt
30+
```
31+
32+
## Configuration
33+
34+
`config.yaml` - all fields optional, defaults shown:
35+
36+
| Parameter | Default | Description |
37+
| :-- | :-- | :-- |
38+
| `server.host` | `127.0.0.1` | Bind address |
39+
| `server.port` | `8000` | Port |
40+
| `logging.file` | `tacdev_mcp.log` | Log file path |
41+
| `logging.level` | `INFO` | DEBUG / INFO / WARNING / ERROR |
42+
| `logging.max_bytes` | `10485760` | Rotation size (10 MB) |
43+
| `logging.backup_count` | `5` | Rotated files to keep |
44+
45+
## Running
46+
47+
The server must be started before any client connects:
48+
49+
```bash
50+
python examples/MCP/sse/tacdev_mcp_server.py
51+
```
52+
53+
### Option 1 - Python client
54+
55+
```bash
56+
python examples/MCP/sse/tacdev_mcp_client.py # first available device
57+
python examples/MCP/sse/tacdev_mcp_client.py COM41 # specific port
58+
```
59+
60+
### Option 2 - Claude CLI
61+
62+
Register once (server must already be running):
63+
64+
```bash
65+
claude mcp add --transport sse TACDev-MCP http://127.0.0.1:8000/sse
66+
```
67+
68+
Verify:
69+
70+
```bash
71+
claude mcp list
72+
```
73+
74+
Then ask Claude naturally - no special syntax needed:
75+
76+
```
77+
List connected devices
78+
Power on COM41
79+
Boot COM3 to fastboot
80+
```
81+
82+
> **Note:** The server must be running before starting a Claude session. Claude connects to it
83+
> over SSE; it does not start the server automatically.
84+
85+
## Recovering a locked device
86+
87+
If a client crashes without closing its handle, the device will remain locked. Restart the server
88+
to release all devices cleanly:
89+
90+
```
91+
Ctrl+C
92+
python examples/MCP/sse/tacdev_mcp_server.py
93+
```
94+
95+
## Troubleshooting
96+
97+
| Error | Fix |
98+
| :-- | :-- |
99+
| `No module named 'TACDev'` | `pip install interfaces/Python` from repo root |
100+
| `TACDev library not found` | Run `build.bat` / `build.sh` first |
101+
| `Architecture mismatch` | Use Python matching your OS architecture |
102+
| `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` |
104+
| `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` |
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
setlocal enabledelayedexpansion
66

77
set SCRIPT_DIR=%~dp0
8-
pushd "%SCRIPT_DIR%\..\.."
8+
pushd "%SCRIPT_DIR%\..\..\..\"
99
set REPO_ROOT=%CD%
1010
popd
1111

@@ -82,4 +82,4 @@ if errorlevel 1 exit /b 1
8282

8383
echo.
8484
echo Setup complete. Start the MCP server:
85-
echo python examples\MCP\tacdev_mcp_server.py
85+
echo python examples\MCP\sse\tacdev_mcp_server.py
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
set -e
77

88
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9-
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
9+
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
1010

1111
if [ ! -d "$REPO_ROOT/__Builds" ]; then
1212
echo "[1/3] Build not found. Running root build..."
@@ -23,4 +23,4 @@ pip install -r "$SCRIPT_DIR/requirements.txt"
2323

2424
echo ""
2525
echo "Setup complete. Start the MCP server:"
26-
echo " python examples/MCP/tacdev_mcp_server.py"
26+
echo " python examples/MCP/sse/tacdev_mcp_server.py"
Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,6 @@ def _server_url() -> str:
4141
return f"http://{s['host']}:{s['port']}/sse"
4242

4343

44-
async def _call(client: Client, tool: str, **kwargs: Any) -> Any:
45-
result = await client.call_tool(tool, kwargs)
46-
if result.content:
47-
text = result.content[0].text
48-
try:
49-
return json.loads(text)
50-
except (ValueError, TypeError):
51-
return text
52-
return None
53-
54-
5544
class TACDevClient:
5645
def __init__(self) -> None:
5746
self._client = Client(_server_url())
@@ -64,7 +53,14 @@ async def __aexit__(self, *args) -> None:
6453
await self._client.__aexit__(*args)
6554

6655
async def _call(self, tool: str, **kwargs: Any) -> Any:
67-
return await _call(self._client, tool, **kwargs)
56+
result = await self._client.call_tool(tool, kwargs)
57+
if result.content:
58+
text = result.content[0].text
59+
try:
60+
return json.loads(text)
61+
except (ValueError, TypeError):
62+
return text
63+
return None
6864

6965
async def list_devices(self) -> dict:
7066
return await self._call("list_devices")
@@ -275,7 +271,7 @@ async def boot_to_secondary_edl_button(self, handle: int) -> None:
275271
await self._call("boot_to_secondary_edl_button", handle=handle)
276272

277273

278-
async def _demo(port_name: str | None = None) -> None:
274+
async def TACExample(port_name: str | None = None) -> None:
279275
async with TACDevClient() as tac:
280276
print(f" Alpaca version : {await tac.get_alpaca_version()}")
281277
print(f" TAC version : {await tac.get_tac_version()}")
@@ -303,13 +299,24 @@ async def _demo(port_name: str | None = None) -> None:
303299

304300
commands = await tac.list_commands(handle)
305301
if commands:
306-
cmd = commands[0].split(";")[0]
302+
cmd_names = [c.split(";")[0].strip() for c in commands]
303+
print(f"\nCommands : {cmd_names}")
304+
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:
311+
cmd = cmd_names[0]
307312
state_before = await tac.get_command_state(handle, cmd)
308-
print(f"Command '{cmd}' state before toggle: {state_before}")
313+
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)
@@ -319,4 +326,4 @@ async def _demo(port_name: str | None = None) -> None:
319326

320327

321328
if __name__ == "__main__":
322-
asyncio.run(_demo(sys.argv[1] if len(sys.argv) > 1 else None))
329+
asyncio.run(TACExample(sys.argv[1] if len(sys.argv) > 1 else None))

0 commit comments

Comments
 (0)