ta-studio-mcp 1.2.4 → 1.4.0

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 (38) hide show
  1. package/README.md +24 -95
  2. package/__init__.py +14 -0
  3. package/auth.py +53 -0
  4. package/bin/cli.js +69 -0
  5. package/emulator_relay.py +241 -0
  6. package/main.py +105 -0
  7. package/package.json +14 -48
  8. package/stream.py +110 -0
  9. package/ws_client.py +135 -0
  10. package/LICENSE +0 -22
  11. package/dist/index.d.ts +0 -3
  12. package/dist/index.d.ts.map +0 -1
  13. package/dist/index.js +0 -28
  14. package/dist/index.js.map +0 -1
  15. package/dist/knowledge/codebase-map.d.ts +0 -6
  16. package/dist/knowledge/codebase-map.d.ts.map +0 -1
  17. package/dist/knowledge/codebase-map.js +0 -121
  18. package/dist/knowledge/codebase-map.js.map +0 -1
  19. package/dist/knowledge/conventions.d.ts +0 -6
  20. package/dist/knowledge/conventions.d.ts.map +0 -1
  21. package/dist/knowledge/conventions.js +0 -101
  22. package/dist/knowledge/conventions.js.map +0 -1
  23. package/dist/knowledge/known-issues.d.ts +0 -17
  24. package/dist/knowledge/known-issues.d.ts.map +0 -1
  25. package/dist/knowledge/known-issues.js +0 -103
  26. package/dist/knowledge/known-issues.js.map +0 -1
  27. package/dist/knowledge/methodology.d.ts +0 -6
  28. package/dist/knowledge/methodology.d.ts.map +0 -1
  29. package/dist/knowledge/methodology.js +0 -180
  30. package/dist/knowledge/methodology.js.map +0 -1
  31. package/dist/knowledge/workflows.d.ts +0 -7
  32. package/dist/knowledge/workflows.d.ts.map +0 -1
  33. package/dist/knowledge/workflows.js +0 -146
  34. package/dist/knowledge/workflows.js.map +0 -1
  35. package/dist/tools/register-all.d.ts +0 -6
  36. package/dist/tools/register-all.d.ts.map +0 -1
  37. package/dist/tools/register-all.js +0 -104
  38. package/dist/tools/register-all.js.map +0 -1
package/README.md CHANGED
@@ -1,98 +1,27 @@
1
- # ta-studio-mcp 🚀
2
-
3
- **The definitive domain knowledge layer for AI agents building mobile test automation at TA Studio.**
4
-
5
- AI agents often struggle with project-specific context, unique navigation patterns, and "tribal knowledge" about past bugs. `ta-studio-mcp` solves this by giving your agent (Claude, Cursor, Windsurf) structured, programmatic access to the team's methodologies, codebase maps, and verified bug fixes.
6
-
7
- ---
8
-
9
- ## 📋 Prerequisites
10
-
11
- - **Node.js**: `v18.0.0` or higher.
12
- - **MCP Client**: An IDE or tool that supports the [Model Context Protocol](https://modelcontextprotocol.io) (e.g., Claude Desktop, Cursor, Windsurf, VS Code).
13
-
14
- ---
15
-
16
- ## ⚡ Quick Start
17
-
18
- ```bash
19
- npx ta-studio-mcp
1
+ # ta-studio-mcp
2
+
3
+ Thin local relay for TA Studio. Connects your machine to the TA Studio server via outbound WebSocket.
4
+
5
+ ## Quick Start
6
+
7
+ Add to your Claude Code `.mcp.json`:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "ta-studio": {
13
+ "command": "npx",
14
+ "args": ["ta-studio-mcp@latest"],
15
+ "env": {
16
+ "TA_API_KEY": "sk-your-key"
17
+ }
18
+ }
19
+ }
20
+ }
20
21
  ```
21
22
 
22
- ---
23
-
24
- ## 🧠 Expert Knowledge & Deep Technical Lore
25
-
26
- This section documents the state-of-the-art implementations used by the TA Studio team.
27
-
28
- ### 1. Set-of-Mark (SoM) Screenshot Annotation
29
- Based on OmniParser's SoM approach, we use color-coded, type-aware bounding boxes to provide visual anchors.
30
- - **Type-Aware Palette**: 9 distinct colors (e.g., **Dodger Blue** for buttons, **Orange** for inputs, **Purple** for toggles).
31
- - **PIL Threading**: `asyncio.to_thread(_draw_bounding_boxes_threaded)` for non-blocking UI drawing.
32
- - **TOON Optimization**: **Token Optimized Object Notation** reduces prompt tokens by 40% by stripping redundant XML.
33
- - **Scaling Correction**: Screenshots are compressed to ~45% resolution. We apply `native_coord * (img_width / native_width)` for pixel-perfect alignment.
34
-
35
- ### 2. Deep Subagent Handoff Protocol
36
- Our "Deep Agent Pattern" orchestrates specialized specialists via a strict chain of custody:
37
- 1. **Perceptor** (`Screen Classifier`): Returns structured state and **TOON** elements.
38
- 2. **Planner** (`Device Agent`): Proposes action based on the identified UI state.
39
- 3. **Guardrail** (`Action Verifier`): Applies **Boolean Verification** (Safe/Relevant/Executable).
40
- 4. **Actor** (`Mobile MCP`): Executes the approved action on the target device.
41
- 5. **Doctor** (`Failure Diagnosis`): Categorizes failures and suggests recovery (Jitter/Wait/Backtrack).
42
-
43
- ### 3. Boolean Verification vs. Numerical Scoring
44
- We reject "0.85 confidence" scores. Every action must pass three binary checks:
45
- - **is_safe**: Does this action cause data loss or unauthorized access? (YES/NO)
46
- - **is_relevant**: Does this move the needle on the task goal? (YES/NO)
47
- - **is_executable**: Is the target realistically reachable on the current screen? (YES/NO)
48
- - **Logic**: Action executes ONLY if ALL checks are YES. Reject and propose an `alternative_action` otherwise.
49
-
50
- ### 4. Real-Time HUD & Parallel Execution
51
- - **Observation Pipeline**: Achieve `<200ms` lag via `on_step` async callbacks that emit SSE events to the frontend.
52
- - **Concurrency Control**: `asyncio.Semaphore` and per-simulation `asyncio.Lock` manage multiple parallel device streams without resource collision.
53
- - **Retention**: Automated 24h age or 100 total simulations cleanup before auto-purge.
54
-
55
- ### 5. Model Tiering (Jan 2026 Standard)
56
- - **Thinking Tier (GPT-5.2)**: High-level orchestration (Coordinator) and complex visual reasoning. reasoning effort: `high`.
57
- - **Core Tier (GPT-5-mini)**: Specialized agents (Classifier, Verifier, Diagnosis). *Never use nano for classification.*
58
- - **Utility Tier (GPT-5-nano)**: MCP tool call formatting and data distillation.
59
-
60
- ---
61
-
62
- ## 🐞 Critical Bug Fixes (Implementation Level)
63
-
64
- | Severity | Issue | Root Cause & Expert Fix |
65
- |----------|-------|-------------------------|
66
- | **CRITICAL** | Bbox Misalignment | **RC**: 45% Scaling Delta. **Fix**: Apply `img_width / native_width` factor. |
67
- | **CRITICAL** | Async to_thread | **RC**: CORO vs CALL mismatch. **Fix**: Remove `async` from functions passed to `asyncio.to_thread`. |
68
- | **CRITICAL** | Race Condition | **RC**: Parallel session state collision. **Fix**: Set `parallel_tool_calls=False`. |
69
- | **HIGH** | Simulation Leak | **RC**: Memory persistence. **Fix**: 24h/100-run auto-purge with `asyncio.Lock`. |
70
- | **HIGH** | Mobile MCP Bug | **RC**: Offline device fail (v0.0.36). **Fix**: Full ADB bridge fallback (screencap/uiautomator). |
71
-
72
- ---
73
-
74
- ## 🔄 Core Workflows
75
-
76
- ### The Ralph Loop (Closed-Loop Verification)
77
- 1. **CODE** → Implement feature or fix.
78
- 2. **LINT** → `mypy` / `eslint` verification.
79
- 3. **UNIT TEST** → Specific module verification.
80
- 4. **CHECK ASYNC** → Confirm `to_thread` safety.
81
- 5. **VERIFY HUD** → Watch the emulator stream while agent runs autonomously.
82
-
83
- ---
84
-
85
- ## 📦 Installation & Setup
86
-
87
- ### Claude Desktop
88
- ```bash
89
- claude mcp add ta-studio -- npx -y ta-studio-mcp@latest
90
- ```
91
-
92
- ### Cursor / Windsurf
93
- Add `npx -y ta-studio-mcp@latest` as a command-type MCP server.
94
-
95
- ---
23
+ ## Requirements
96
24
 
97
- ## 📜 License
98
- MIT © 2026 TA Studios.
25
+ - Python 3.10+
26
+ - `websockets` pip package (`pip install websockets`)
27
+ - Android SDK with ADB (for emulator control)
package/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ TA Studio MCP — Thin local relay package.
3
+
4
+ This package runs on the user's machine and connects OUT to the TA Studio
5
+ server via outbound WebSocket. No ports are opened. No tunnel required.
6
+
7
+ Components:
8
+ - ws_client: Outbound WebSocket connection with auto-reconnect
9
+ - auth: API key authentication for the handshake
10
+ - emulator_relay: Receives and executes ADB commands from server
11
+ - stream: Captures and streams emulator frames to server
12
+ """
13
+
14
+ __version__ = "1.0.0"
package/auth.py ADDED
@@ -0,0 +1,53 @@
1
+ """
2
+ TA Studio — API key authentication for outbound WebSocket handshake.
3
+
4
+ The thin relay authenticates with the TA Studio server by sending an API key
5
+ during the WebSocket upgrade. The server validates the key and associates the
6
+ connection with the user's account.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import json
13
+ import logging
14
+ from typing import Optional
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # API key resolution
20
+ # ---------------------------------------------------------------------------
21
+
22
+ def get_api_key() -> str:
23
+ """Return the TA Studio API key from environment or local config.
24
+
25
+ Resolution order:
26
+ 1. TA_API_KEY environment variable
27
+ 2. TA_MCP_TOKEN environment variable (legacy compat)
28
+ 3. ~/.ta-studio/config.json → api_key field
29
+ """
30
+ key = os.getenv("TA_API_KEY") or os.getenv("TA_MCP_TOKEN", "")
31
+ if key:
32
+ return key.strip()
33
+
34
+ config_path = os.path.expanduser("~/.ta-studio/config.json")
35
+ try:
36
+ with open(config_path) as f:
37
+ data = json.load(f)
38
+ return (data.get("api_key") or "").strip()
39
+ except (FileNotFoundError, json.JSONDecodeError, KeyError):
40
+ pass
41
+
42
+ return ""
43
+
44
+
45
+ def build_auth_headers(api_key: Optional[str] = None) -> dict[str, str]:
46
+ """Return headers dict for the WebSocket upgrade request."""
47
+ key = api_key or get_api_key()
48
+ if not key:
49
+ raise ValueError(
50
+ "No TA Studio API key found. Set TA_API_KEY env var or run the "
51
+ "install script to configure ~/.ta-studio/config.json"
52
+ )
53
+ return {"Authorization": f"Bearer {key}"}
package/bin/cli.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const { spawn, execFileSync } = require("child_process");
5
+ const path = require("path");
6
+
7
+ const PKG_DIR = path.resolve(__dirname, "..");
8
+
9
+ // --- Locate a working Python 3 interpreter -------------------------------- //
10
+
11
+ function findPython() {
12
+ for (const bin of ["python3", "python"]) {
13
+ try {
14
+ const ver = execFileSync(bin, ["--version"], {
15
+ encoding: "utf8",
16
+ stdio: ["ignore", "pipe", "ignore"],
17
+ }).trim();
18
+ const major = parseInt((ver.match(/(\d+)\./) || [])[1], 10);
19
+ if (major >= 3) return bin;
20
+ } catch {
21
+ // not found — try next
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+
27
+ // --- Main ----------------------------------------------------------------- //
28
+
29
+ const python = findPython();
30
+
31
+ if (!python) {
32
+ process.stderr.write(
33
+ [
34
+ "",
35
+ " ta-studio-mcp: Python 3 not found.",
36
+ "",
37
+ " This relay requires Python 3.10+ with the 'websockets' package.",
38
+ " Install Python from https://www.python.org/downloads/ and then run:",
39
+ "",
40
+ " pip install websockets",
41
+ "",
42
+ "",
43
+ ].join("\n")
44
+ );
45
+ process.exit(1);
46
+ }
47
+
48
+ const child = spawn(python, [path.join(PKG_DIR, "main.py")], {
49
+ stdio: "inherit",
50
+ env: {
51
+ ...process.env,
52
+ PYTHONPATH: PKG_DIR + (process.env.PYTHONPATH ? path.delimiter + process.env.PYTHONPATH : ""),
53
+ },
54
+ });
55
+
56
+ // Forward signals so the Python process shuts down cleanly.
57
+ for (const sig of ["SIGINT", "SIGTERM"]) {
58
+ process.on(sig, () => {
59
+ child.kill(sig);
60
+ });
61
+ }
62
+
63
+ child.on("exit", (code, signal) => {
64
+ if (signal) {
65
+ process.kill(process.pid, signal);
66
+ } else {
67
+ process.exit(code ?? 1);
68
+ }
69
+ });
@@ -0,0 +1,241 @@
1
+ """
2
+ TA Studio — Emulator relay.
3
+
4
+ Receives emulator commands from the TA Studio server via WebSocket and
5
+ executes them locally via ADB. Results are sent back through the same
6
+ connection.
7
+
8
+ Supported commands:
9
+ - adb_shell: run an ADB shell command
10
+ - adb_install: install an APK
11
+ - launch_emulator: start an emulator AVD
12
+ - list_devices: list connected ADB devices
13
+ - tap / swipe / type_text / press_key: input events
14
+ - screenshot: capture and return a screenshot
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import base64
21
+ import logging
22
+ import os
23
+ import shutil
24
+ import subprocess
25
+ from typing import Any
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ def _adb_path() -> str:
31
+ """Resolve ADB binary path."""
32
+ android_home = os.environ.get(
33
+ "ANDROID_HOME",
34
+ os.path.expanduser("~/Library/Android/sdk"),
35
+ )
36
+ adb = os.path.join(android_home, "platform-tools", "adb")
37
+ if os.path.isfile(adb):
38
+ return adb
39
+ # Fallback to PATH
40
+ found = shutil.which("adb")
41
+ return found or "adb"
42
+
43
+
44
+ async def _run_adb(*args: str, device: str | None = None) -> dict[str, Any]:
45
+ """Run an ADB command and return stdout/stderr/returncode."""
46
+ cmd = [_adb_path()]
47
+ if device:
48
+ cmd += ["-s", device]
49
+ cmd += list(args)
50
+
51
+ proc = await asyncio.create_subprocess_exec(
52
+ *cmd,
53
+ stdout=asyncio.subprocess.PIPE,
54
+ stderr=asyncio.subprocess.PIPE,
55
+ )
56
+ stdout, stderr = await proc.communicate()
57
+ return {
58
+ "stdout": stdout.decode(errors="replace"),
59
+ "stderr": stderr.decode(errors="replace"),
60
+ "returncode": proc.returncode,
61
+ }
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Command handlers
66
+ # ---------------------------------------------------------------------------
67
+
68
+
69
+ async def handle_list_devices(_payload: dict[str, Any]) -> dict[str, Any]:
70
+ """List connected ADB devices."""
71
+ result = await _run_adb("devices", "-l")
72
+ lines = result["stdout"].strip().split("\n")[1:] # skip header
73
+ devices = []
74
+ for line in lines:
75
+ parts = line.split()
76
+ if len(parts) >= 2:
77
+ devices.append({"serial": parts[0], "state": parts[1]})
78
+ return {"devices": devices}
79
+
80
+
81
+ async def handle_adb_shell(payload: dict[str, Any]) -> dict[str, Any]:
82
+ """Run an ADB shell command."""
83
+ command = payload.get("command", "")
84
+ device = payload.get("device")
85
+ if not command:
86
+ return {"error": "No command provided"}
87
+ return await _run_adb("shell", command, device=device)
88
+
89
+
90
+ async def handle_adb_install(payload: dict[str, Any]) -> dict[str, Any]:
91
+ """Install an APK on a device."""
92
+ apk_path = payload.get("apk_path", "")
93
+ device = payload.get("device")
94
+ if not apk_path or not os.path.isfile(apk_path):
95
+ return {"error": f"APK not found: {apk_path}"}
96
+ return await _run_adb("install", "-r", apk_path, device=device)
97
+
98
+
99
+ async def handle_launch_emulator(payload: dict[str, Any]) -> dict[str, Any]:
100
+ """Launch an Android emulator AVD."""
101
+ avd_name = payload.get("avd_name", "")
102
+ android_home = os.environ.get(
103
+ "ANDROID_HOME",
104
+ os.path.expanduser("~/Library/Android/sdk"),
105
+ )
106
+ emulator_bin = os.path.join(android_home, "emulator", "emulator")
107
+ if not os.path.isfile(emulator_bin):
108
+ return {"error": "Emulator binary not found"}
109
+
110
+ if not avd_name:
111
+ # List available AVDs and pick the first
112
+ proc = await asyncio.create_subprocess_exec(
113
+ emulator_bin, "-list-avds",
114
+ stdout=asyncio.subprocess.PIPE,
115
+ stderr=asyncio.subprocess.PIPE,
116
+ )
117
+ stdout, _ = await proc.communicate()
118
+ avds = [l.strip() for l in stdout.decode().strip().split("\n") if l.strip()]
119
+ if not avds:
120
+ return {"error": "No AVDs available"}
121
+ avd_name = avds[0]
122
+
123
+ # Launch in background
124
+ proc = await asyncio.create_subprocess_exec(
125
+ emulator_bin,
126
+ "-avd", avd_name,
127
+ "-no-snapshot",
128
+ "-gpu", "swiftshader_indirect",
129
+ "-no-boot-anim",
130
+ stdout=asyncio.subprocess.DEVNULL,
131
+ stderr=asyncio.subprocess.DEVNULL,
132
+ )
133
+ return {"avd_name": avd_name, "pid": proc.pid, "status": "launching"}
134
+
135
+
136
+ async def handle_screenshot(payload: dict[str, Any]) -> dict[str, Any]:
137
+ """Capture a screenshot and return as base64 PNG."""
138
+ device = payload.get("device")
139
+ result = await _run_adb("exec-out", "screencap", "-p", device=device)
140
+ if result["returncode"] != 0:
141
+ return {"error": result["stderr"]}
142
+ # screencap -p via exec-out returns raw PNG bytes in stdout
143
+ # Re-run with raw output
144
+ cmd = [_adb_path()]
145
+ if device:
146
+ cmd += ["-s", device]
147
+ cmd += ["exec-out", "screencap", "-p"]
148
+
149
+ proc = await asyncio.create_subprocess_exec(
150
+ *cmd,
151
+ stdout=asyncio.subprocess.PIPE,
152
+ stderr=asyncio.subprocess.PIPE,
153
+ )
154
+ stdout, stderr = await proc.communicate()
155
+ if proc.returncode != 0:
156
+ return {"error": stderr.decode(errors="replace")}
157
+
158
+ return {"image_base64": base64.b64encode(stdout).decode(), "format": "png"}
159
+
160
+
161
+ async def handle_tap(payload: dict[str, Any]) -> dict[str, Any]:
162
+ """Tap at coordinates."""
163
+ x, y = payload.get("x", 0), payload.get("y", 0)
164
+ device = payload.get("device")
165
+ return await _run_adb("shell", f"input tap {x} {y}", device=device)
166
+
167
+
168
+ async def handle_swipe(payload: dict[str, Any]) -> dict[str, Any]:
169
+ """Swipe between coordinates."""
170
+ x1, y1 = payload.get("x1", 0), payload.get("y1", 0)
171
+ x2, y2 = payload.get("x2", 0), payload.get("y2", 0)
172
+ duration = payload.get("duration", 300)
173
+ device = payload.get("device")
174
+ return await _run_adb(
175
+ "shell", f"input swipe {x1} {y1} {x2} {y2} {duration}",
176
+ device=device,
177
+ )
178
+
179
+
180
+ async def handle_type_text(payload: dict[str, Any]) -> dict[str, Any]:
181
+ """Type text on the device."""
182
+ text = payload.get("text", "")
183
+ device = payload.get("device")
184
+ # Escape spaces for ADB input
185
+ escaped = text.replace(" ", "%s")
186
+ return await _run_adb("shell", f"input text '{escaped}'", device=device)
187
+
188
+
189
+ async def handle_press_key(payload: dict[str, Any]) -> dict[str, Any]:
190
+ """Press a key event (e.g., KEYCODE_HOME)."""
191
+ keycode = payload.get("keycode", "KEYCODE_HOME")
192
+ device = payload.get("device")
193
+ return await _run_adb("shell", f"input keyevent {keycode}", device=device)
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Command dispatcher
198
+ # ---------------------------------------------------------------------------
199
+
200
+ COMMAND_HANDLERS: dict[str, Any] = {
201
+ "list_devices": handle_list_devices,
202
+ "adb_shell": handle_adb_shell,
203
+ "adb_install": handle_adb_install,
204
+ "launch_emulator": handle_launch_emulator,
205
+ "screenshot": handle_screenshot,
206
+ "tap": handle_tap,
207
+ "swipe": handle_swipe,
208
+ "type_text": handle_type_text,
209
+ "press_key": handle_press_key,
210
+ }
211
+
212
+
213
+ async def dispatch_command(msg: dict[str, Any]) -> dict[str, Any]:
214
+ """Dispatch an incoming command message to the appropriate handler.
215
+
216
+ Expected message format:
217
+ {"type": "command", "command": "<name>", "request_id": "...", ...payload}
218
+
219
+ Returns a response dict with the same request_id.
220
+ """
221
+ command = msg.get("command", "")
222
+ request_id = msg.get("request_id", "")
223
+ handler = COMMAND_HANDLERS.get(command)
224
+
225
+ if not handler:
226
+ return {
227
+ "type": "response",
228
+ "request_id": request_id,
229
+ "error": f"Unknown command: {command}",
230
+ }
231
+
232
+ try:
233
+ result = await handler(msg)
234
+ return {"type": "response", "request_id": request_id, **result}
235
+ except Exception as e:
236
+ logger.exception("Command '%s' failed", command)
237
+ return {
238
+ "type": "response",
239
+ "request_id": request_id,
240
+ "error": str(e),
241
+ }
package/main.py ADDED
@@ -0,0 +1,105 @@
1
+ """
2
+ TA Studio MCP — Entry point for the thin local relay.
3
+
4
+ Usage:
5
+ python -m ta-studio-mcp
6
+ # or
7
+ npx ta-studio-mcp@latest
8
+
9
+ Connects outbound to the TA Studio server via WebSocket. Receives emulator
10
+ commands, executes them locally via ADB, and streams results back.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import logging
17
+ import os
18
+ import sys
19
+
20
+ from .auth import build_auth_headers
21
+ from .ws_client import TAWebSocketClient, DEFAULT_SERVER_URL
22
+ from .emulator_relay import dispatch_command
23
+ from .stream import FrameStreamer
24
+
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
28
+ datefmt="%Y-%m-%d %H:%M:%S",
29
+ )
30
+ logger = logging.getLogger("ta-studio-mcp")
31
+
32
+
33
+ async def main() -> None:
34
+ """Start the thin relay — connect out to TA Studio server."""
35
+ server_url = os.getenv("TA_STUDIO_URL", "").rstrip("/")
36
+ if server_url:
37
+ # Convert HTTP URL to WebSocket URL
38
+ ws_url = server_url.replace("https://", "wss://").replace("http://", "ws://")
39
+ if not ws_url.endswith("/ws/agent-relay"):
40
+ ws_url += "/ws/agent-relay"
41
+ else:
42
+ ws_url = DEFAULT_SERVER_URL
43
+
44
+ logger.info("TA Studio thin relay starting")
45
+ logger.info("Server: %s", ws_url)
46
+
47
+ # Build auth headers
48
+ try:
49
+ auth_headers = build_auth_headers()
50
+ except ValueError as e:
51
+ logger.error(str(e))
52
+ sys.exit(1)
53
+
54
+ client = TAWebSocketClient(server_url=ws_url, auth_headers=auth_headers)
55
+ streamer = FrameStreamer(send_fn=client.send)
56
+
57
+ # -- Handle commands from server -----------------------------------------
58
+
59
+ async def on_command(msg: dict) -> None:
60
+ """Handle command messages from the TA agent."""
61
+ response = await dispatch_command(msg)
62
+ await client.send(response)
63
+
64
+ async def on_stream_start(msg: dict) -> None:
65
+ """Start frame streaming when server requests it."""
66
+ session_id = msg.get("session_id", "default")
67
+ device = msg.get("device")
68
+ fps = msg.get("fps", 2)
69
+ streamer._device = device
70
+ streamer._fps = min(fps, 10)
71
+ await streamer.start(session_id)
72
+
73
+ async def on_stream_stop(_msg: dict) -> None:
74
+ """Stop frame streaming."""
75
+ await streamer.stop()
76
+
77
+ async def on_connected(_msg: dict) -> None:
78
+ """Send identity info on connection."""
79
+ from .emulator_relay import handle_list_devices
80
+ devices = await handle_list_devices({})
81
+ await client.send({
82
+ "type": "relay_ready",
83
+ "version": "1.0.0",
84
+ "devices": devices.get("devices", []),
85
+ })
86
+
87
+ client.on("command", on_command)
88
+ client.on("stream_start", on_stream_start)
89
+ client.on("stream_stop", on_stream_stop)
90
+ client.on("connected", on_connected)
91
+
92
+ # Run until interrupted
93
+ try:
94
+ await client.connect()
95
+ except KeyboardInterrupt:
96
+ await client.disconnect()
97
+
98
+
99
+ def run() -> None:
100
+ """Synchronous entry point."""
101
+ asyncio.run(main())
102
+
103
+
104
+ if __name__ == "__main__":
105
+ run()
package/package.json CHANGED
@@ -1,59 +1,25 @@
1
1
  {
2
2
  "name": "ta-studio-mcp",
3
- "version": "1.2.4",
4
- "description": "TA Studio MCP — Domain knowledge, patterns, bug fixes, and workflows for AI agents working on the TA Studio mobile test automation platform.",
5
- "type": "module",
3
+ "version": "1.4.0",
4
+ "description": "TA Studio MCP — thin local relay for AI-driven mobile QA",
6
5
  "bin": {
7
- "ta-studio-mcp": "dist/index.js"
6
+ "ta-studio-mcp": "bin/cli.js"
8
7
  },
9
- "main": "./dist/index.js",
10
- "files": [
11
- "dist",
12
- "README.md",
13
- "LICENSE"
14
- ],
15
- "scripts": {
16
- "build": "tsc",
17
- "dev": "tsc --watch",
18
- "start": "node dist/index.js",
19
- "prepublishOnly": "npm run build"
20
- },
21
- "repository": {
22
- "type": "git",
23
- "url": "git+https://github.com/TA-Studios-AI-Avengers/ta-agent-examples.git",
24
- "directory": "packages/ta-studio-mcp"
25
- },
26
- "homepage": "https://github.com/TA-Studios-AI-Avengers/ta-agent-examples/tree/main/packages/ta-studio-mcp#readme",
27
- "bugs": {
28
- "url": "https://github.com/TA-Studios-AI-Avengers/ta-agent-examples/issues"
29
- },
30
- "author": "TA Studios",
31
8
  "keywords": [
32
9
  "mcp",
33
- "model-context-protocol",
34
- "mobile-testing",
10
+ "testing",
11
+ "qa",
35
12
  "android",
36
- "test-automation",
37
- "ai-agents",
38
- "ta-studio",
39
- "claude",
40
- "openai",
41
- "agentic",
42
- "oavr",
43
- "som-annotation",
44
- "device-testing",
45
- "screenshot-annotation"
13
+ "emulator",
14
+ "claude-code"
46
15
  ],
47
16
  "license": "MIT",
48
- "dependencies": {
49
- "@modelcontextprotocol/sdk": "^1.21.1",
50
- "zod": "^3.23.8"
51
- },
52
- "devDependencies": {
53
- "@types/node": "^22.0.0",
54
- "typescript": "^5.5.4"
55
- },
56
17
  "engines": {
57
18
  "node": ">=18"
58
- }
59
- }
19
+ },
20
+ "files": [
21
+ "bin/",
22
+ "*.py",
23
+ "README.md"
24
+ ]
25
+ }