Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ __pycache__/
*.pyd

# MCP logs
examples/MCP/*.log
examples/MCP/**/*.log

# Qt build directory
__Builds/
Expand Down
130 changes: 0 additions & 130 deletions examples/MCP/README.md

This file was deleted.

104 changes: 104 additions & 0 deletions examples/MCP/sse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# QTAC MCP Server - SSE

Persistent server supporting multiple concurrent clients. Each client session gets exclusive
ownership of a device.

Use this variant when multiple automation clients need to share a device pool simultaneously.
For single-client use see [`../stdio/`](../stdio/).

## Prerequisites

- Python 3.10+ (architecture must match: x64 or ARM64)
- Project built via `build.bat` / `build.sh`

## Setup

Installs the TACDev library and all dependencies:

```bat
setup.bat # Windows (auto-detects x64/ARM64)
```
```bash
./setup.sh # Linux
```

Or manually:

```bash
pip install interfaces/Python # from repo root
pip install -r examples/MCP/sse/requirements.txt
```

## Configuration

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

| Parameter | Default | Description |
| :-- | :-- | :-- |
| `server.host` | `127.0.0.1` | Bind address |
| `server.port` | `8000` | Port |
| `logging.file` | `tacdev_mcp.log` | Log file path |
| `logging.level` | `INFO` | DEBUG / INFO / WARNING / ERROR |
| `logging.max_bytes` | `10485760` | Rotation size (10 MB) |
| `logging.backup_count` | `5` | Rotated files to keep |

## Running

The server must be started before any client connects:

```bash
python examples/MCP/sse/tacdev_mcp_server.py
```

### Option 1 - Python client

```bash
python examples/MCP/sse/tacdev_mcp_client.py # first available device
python examples/MCP/sse/tacdev_mcp_client.py COM41 # specific port
```

### Option 2 - Claude CLI

Register once (server must already be running):

```bash
claude mcp add --transport sse TACDev-MCP http://127.0.0.1:8000/sse
```

Verify:

```bash
claude mcp list
```

Then ask Claude naturally - no special syntax needed:

```
List connected devices
Power on COM41
Boot COM3 to fastboot
```

> **Note:** The server must be running before starting a Claude session. Claude connects to it
> over SSE; it does not start the server automatically.

## Recovering a locked device

If a client crashes without closing its handle, the device will remain locked. Restart the server
to release all devices cleanly:

```
Ctrl+C
python examples/MCP/sse/tacdev_mcp_server.py
```

## Troubleshooting

| Error | Fix |
| :-- | :-- |
| `No module named 'TACDev'` | `pip install interfaces/Python` from repo root |
| `TACDev library not found` | Run `build.bat` / `build.sh` first |
| `Architecture mismatch` | Use Python matching your OS architecture |
| `No module named 'fastmcp'` | `pip install -r requirements.txt` (requires Python 3.10+) |
| `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` |
| `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` |
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions examples/MCP/setup.bat → examples/MCP/sse/setup.bat
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
setlocal enabledelayedexpansion

set SCRIPT_DIR=%~dp0
pushd "%SCRIPT_DIR%\..\.."
pushd "%SCRIPT_DIR%\..\..\..\"
set REPO_ROOT=%CD%
popd

Expand Down Expand Up @@ -82,4 +82,4 @@ if errorlevel 1 exit /b 1

echo.
echo Setup complete. Start the MCP server:
echo python examples\MCP\tacdev_mcp_server.py
echo python examples\MCP\sse\tacdev_mcp_server.py
4 changes: 2 additions & 2 deletions examples/MCP/setup.sh → examples/MCP/sse/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"

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

echo ""
echo "Setup complete. Start the MCP server:"
echo " python examples/MCP/tacdev_mcp_server.py"
echo " python examples/MCP/sse/tacdev_mcp_server.py"
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,6 @@ def _server_url() -> str:
return f"http://{s['host']}:{s['port']}/sse"


async def _call(client: Client, tool: str, **kwargs: Any) -> Any:
result = await client.call_tool(tool, kwargs)
if result.content:
text = result.content[0].text
try:
return json.loads(text)
except (ValueError, TypeError):
return text
return None


class TACDevClient:
def __init__(self) -> None:
self._client = Client(_server_url())
Expand All @@ -64,7 +53,14 @@ async def __aexit__(self, *args) -> None:
await self._client.__aexit__(*args)

async def _call(self, tool: str, **kwargs: Any) -> Any:
return await _call(self._client, tool, **kwargs)
result = await self._client.call_tool(tool, kwargs)
if result.content:
text = result.content[0].text
try:
return json.loads(text)
except (ValueError, TypeError):
return text
return None

async def list_devices(self) -> dict:
return await self._call("list_devices")
Expand Down Expand Up @@ -275,7 +271,7 @@ async def boot_to_secondary_edl_button(self, handle: int) -> None:
await self._call("boot_to_secondary_edl_button", handle=handle)


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

commands = await tac.list_commands(handle)
if commands:
cmd = commands[0].split(";")[0]
cmd_names = [c.split(";")[0].strip() for c in commands]
print(f"\nCommands : {cmd_names}")

quick_commands = await tac.list_quick_commands(handle)
if quick_commands:
qc_names = [q.split(";")[1].strip() for q in quick_commands]
print(f"Quick commands: {qc_names}")

if commands:
cmd = cmd_names[0]
state_before = await tac.get_command_state(handle, cmd)
print(f"Command '{cmd}' state before toggle: {state_before}")
print(f"\nCommand '{cmd}' state before toggle: {state_before}")
await tac.send_command(handle, cmd, not state_before)
await asyncio.sleep(2)
state_after = await tac.get_command_state(handle, cmd)
print(f"Command '{cmd}' state after toggle: {state_after}")
await tac.send_command(handle, cmd, state_before)
await asyncio.sleep(2)
print(f"Command '{cmd}' restored to : {await tac.get_command_state(handle, cmd)}")

await tac.close_tac_handle(handle)
Expand All @@ -319,4 +326,4 @@ async def _demo(port_name: str | None = None) -> None:


if __name__ == "__main__":
asyncio.run(_demo(sys.argv[1] if len(sys.argv) > 1 else None))
asyncio.run(TACExample(sys.argv[1] if len(sys.argv) > 1 else None))
Loading
Loading