Skip to content
Open
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
11 changes: 11 additions & 0 deletions apps/backend/agents/tools_pkg/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
# CLI E2E harness (pseudo-terminal runner for terminal apps)
TOOL_RUN_CLI_SESSION = "mcp__auto-claude__run_cli_session"

# Android QA harness (adb-driven; registered for Android projects only)
TOOL_ANDROID_SCREENSHOT = "mcp__auto-claude__android_screenshot"
TOOL_ANDROID_INPUT = "mcp__auto-claude__android_input"
TOOL_ANDROID_LOGCAT = "mcp__auto-claude__android_logcat"

# =============================================================================
# External MCP Tools
# =============================================================================
Expand Down Expand Up @@ -286,6 +291,9 @@ def is_electron_mcp_enabled() -> bool:
TOOL_GET_SESSION_CONTEXT,
TOOL_SEARCH_TEAM_DOCS, # Verify against team standards
TOOL_RUN_CLI_SESSION, # E2E-verify terminal apps
TOOL_ANDROID_SCREENSHOT, # Android E2E (registered only for Android projects)
TOOL_ANDROID_INPUT,
TOOL_ANDROID_LOGCAT,
],
"thinking_default": "high",
},
Expand All @@ -299,6 +307,9 @@ def is_electron_mcp_enabled() -> bool:
TOOL_UPDATE_QA_STATUS,
TOOL_RECORD_GOTCHA,
TOOL_RUN_CLI_SESSION, # Verify CLI fixes end-to-end
TOOL_ANDROID_SCREENSHOT, # Android E2E (registered only for Android projects)
TOOL_ANDROID_INPUT,
TOOL_ANDROID_LOGCAT,
],
"thinking_default": "medium",
},
Expand Down
2 changes: 2 additions & 0 deletions apps/backend/agents/tools_pkg/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
create_sdk_mcp_server = None

from .tools import (
create_android_tools,
create_background_task_tools,
create_cli_harness_tools,
create_debugging_tools,
Expand Down Expand Up @@ -52,6 +53,7 @@ def create_all_tools(spec_dir: Path, project_dir: Path) -> list:
all_tools.extend(create_statistics_tools(spec_dir, project_dir))
all_tools.extend(create_background_task_tools(spec_dir, project_dir))
all_tools.extend(create_cli_harness_tools(spec_dir, project_dir))
all_tools.extend(create_android_tools(spec_dir, project_dir))
all_tools.extend(create_debugging_tools(spec_dir, project_dir))
all_tools.extend(create_knowledge_base_tools(spec_dir, project_dir))

Expand Down
2 changes: 2 additions & 0 deletions apps/backend/agents/tools_pkg/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Individual tool implementations organized by functionality.
"""

from .android_harness import create_android_tools
from .background_task import create_background_task_tools
from .cli_harness import create_cli_harness_tools
from .debugging import create_debugging_tools
Expand All @@ -21,6 +22,7 @@
"create_memory_tools",
"create_qa_tools",
"create_statistics_tools",
"create_android_tools",
"create_background_task_tools",
"create_cli_harness_tools",
"create_debugging_tools",
Expand Down
183 changes: 183 additions & 0 deletions apps/backend/agents/tools_pkg/tools/android_harness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""
Android QA Harness Tools
========================

MCP tools that let QA agents drive an Android app on an emulator or
attached device via adb: screenshots, input injection, and logcat reads.
The Android analog of the Electron MCP toolset.

Only registered when the project is an Android Gradle project, keeping
tool context clean for everything else.
"""

import base64
import logging
from pathlib import Path
from typing import Any

try:
from claude_agent_sdk import tool

SDK_AVAILABLE = True
except ImportError:
SDK_AVAILABLE = False

LOGCAT_TAIL_CHARS = 8_000


def create_android_tools(spec_dir: Path, project_dir: Path) -> list:

Check failure on line 28 in apps/backend/agents/tools_pkg/tools/android_harness.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 44 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ9ZvRmqIVdFCJxEId0W&open=AZ9ZvRmqIVdFCJxEId0W&pullRequest=423

Check warning on line 28 in apps/backend/agents/tools_pkg/tools/android_harness.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "spec_dir".

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ9ZvRmqIVdFCJxEId0V&open=AZ9ZvRmqIVdFCJxEId0V&pullRequest=423
"""
Create Android device tools for QA agents.

Args:
spec_dir: Path to the spec directory (unused, kept for registry symmetry)
project_dir: Path to the project root; gates registration

Returns:
List of tool functions (empty for non-Android projects)
"""
if not SDK_AVAILABLE:
return []

try:
from core.android_device import is_android_project
except ImportError:
return []

if not is_android_project(project_dir):
return []

tools = []

@tool(
"android_screenshot",
"Take a screenshot of the Android emulator or attached device via "
"adb. Use it to visually verify UI state after launching the app or "
"interacting with it. Optionally pass a device serial when several "
"devices are connected.",
{"serial": str},
)
Comment on lines +52 to +59

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

pip download --no-deps --no-binary=:all: "claude-agent-sdk==0.2.87" --dest "$tmpdir"
archive="$(fd -t f -e gz . "$tmpdir" | head -n 1)"

tar -xOf "$archive" --wildcards '*/src/claude_agent_sdk/__init__.py' |
  rg -n -C3 '"required": list\(properties\.keys\(\)\)'

Repository: OBenner/Auto-Coding

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and nearby definitions.
git ls-files 'apps/backend/agents/tools_pkg/tools/android_harness.py' \
  'apps/backend/**/claude_agent_sdk*' \
  'pyproject.toml' 'uv.lock' 'poetry.lock' 'requirements*.txt' 2>/dev/null || true

echo '--- outline ---'
ast-grep outline apps/backend/agents/tools_pkg/tools/android_harness.py --view expanded || true

echo '--- focused reads ---'
sed -n '1,240p' apps/backend/agents/tools_pkg/tools/android_harness.py

echo '--- sdk schema search ---'
rg -n '"required":|properties\.keys\(\)|`@tool`\(' -S . \
  -g '!**/.git/**' -g '!**/node_modules/**' -g '!**/dist/**' -g '!**/build/**'

Repository: OBenner/Auto-Coding

Length of output: 23954


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- tool-schema docs ---'
sed -n '1,220p' docs/architecture/tool-schema-translator.md

echo '--- Android harness surrounding lines ---'
sed -n '40,180p' apps/backend/agents/tools_pkg/tools/android_harness.py

echo '--- compare other `@tool` usages with optional params ---'
sed -n '1,220p' apps/backend/agents/tools_pkg/tools/cli_harness.py
sed -n '1,260p' apps/backend/agents/tools_pkg/tools/debugging.py

Repository: OBenner/Auto-Coding

Length of output: 26494


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json
from pathlib import Path

path = Path("docs/architecture/tool-schema-translator.md")
text = path.read_text()
for needle in ["required", "optional", "`@tool`", "schema"]:
    print(f"=== {needle} ===")
    for i, line in enumerate(text.splitlines(), 1):
        if needle.lower() in line.lower():
            print(f"{i}: {line}")
PY

Repository: OBenner/Auto-Coding

Length of output: 7356


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- tool-schema docs ---'
sed -n '1,240p' docs/architecture/tool-schema-translator.md

echo '--- android harness ---'
sed -n '40,180p' apps/backend/agents/tools_pkg/tools/android_harness.py

echo '--- nearby tool patterns ---'
sed -n '1,220p' apps/backend/agents/tools_pkg/tools/cli_harness.py
sed -n '1,260p' apps/backend/agents/tools_pkg/tools/debugging.py

Repository: OBenner/Auto-Coding

Length of output: 27099


🌐 Web query:

claude-agent-sdk @tool dict schema required properties optional JSON schema tool decorator

💡 Result:

In the Claude Agent SDK for Python, the @tool decorator uses the input_schema parameter to define the tool's arguments [1][2]. You have three ways to define this schema [2]: 1. Dictionary mapping (Simple): You provide a dictionary where keys are parameter names and values are Python types (e.g., {"name": str, "age": int}) [2][3]. In this mode, all parameters are treated as required [4]. 2. TypedDict (Complex): You can provide a TypedDict class for more structured, complex schemas [2]. 3. JSON Schema (Full Control): You can provide a full JSON Schema dictionary [1][2]. This is necessary if you need features like enums, specific ranges, nested objects, or explicit control over required/optional fields [5][4]. For JSON Schema definitions, the standard JSON schema structure applies, where you define your properties under the "properties" key and specify which are required using the "required" array [1][6]. To handle optional parameters in Python when using the simple dictionary mapping: Because the dictionary schema treats every key as required, the recommended pattern for optional parameters is to omit them from the schema entirely, mention them in the tool's description string, and access them using args.get within your handler function [4]. When using the full JSON Schema approach, you simply define your properties and omit the parameter name from the "required" list in the schema definition [1][6]. Top Results: [1][5][2][4]

Citations:


Make the optional tool args optional. android_screenshot, android_input, and android_logcat currently use the dict-based @tool schema, which makes every listed property required. That forces serial, x2/y2/duration_ms, text, key, and filter_text to be passed even though the handlers already treat them as optional. Switch to full JSON Schema (or a TypedDict) and mark only the action-specific fields as required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/agents/tools_pkg/tools/android_harness.py` around lines 52 - 59,
Update the `@tool` schemas for android_screenshot, android_input, and
android_logcat so optional handler arguments are not required by validation.
Replace the dict-based schemas with full JSON Schema or TypedDict definitions,
keeping serial, x2/y2/duration_ms, text, key, and filter_text optional and
marking only fields required for the selected action.

async def android_screenshot(args: dict[str, Any]) -> dict[str, Any]:
"""Capture and return a device screenshot."""
from core.android_device import take_screenshot

try:
image, mime_or_error = take_screenshot(
serial=(args.get("serial") or "").strip() or None
)
Comment on lines +64 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Offload blocking ADB calls from the MCP event loop.

Each async handler synchronously waits in subprocess.run for up to 30 seconds, blocking other MCP work and cancellation. Invoke the device helper through await asyncio.to_thread(...).

As per path instructions, focus on proper “async patterns.”

Also applies to: 118-133, 157-162

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/agents/tools_pkg/tools/android_harness.py` around lines 64 - 67,
Update the async handlers around take_screenshot and the additional
device-helper call sites to invoke blocking ADB helpers via await
asyncio.to_thread(...). Preserve their existing arguments, result handling, and
error behavior while ensuring subprocess.run does not execute on the MCP event
loop.

Source: Path instructions

except Exception as e:
logging.exception("Error during android_screenshot")
return _text_result(f"Error taking screenshot: {e}")

if image is None:
return _text_result(f"Screenshot failed: {mime_or_error}")

return {
"content": [
{
"type": "image",
"data": base64.b64encode(image).decode("ascii"),
"mimeType": mime_or_error,
}
]
}

tools.append(android_screenshot)

@tool(
"android_input",
"Send input to the Android emulator or attached device via adb. "
"Actions: 'tap' (x, y), 'swipe' (x, y, x2, y2, optional "
"duration_ms), 'text' (text typed into the focused field), "
"'keyevent' (key: KEYCODE_* name or numeric code, e.g. KEYCODE_BACK). "
"Use android_screenshot before and after to verify the effect.",
{
"action": str,
"x": int,
"y": int,
"x2": int,
"y2": int,
"duration_ms": int,
"text": str,
"key": str,
"serial": str,
},
)
async def android_input(args: dict[str, Any]) -> dict[str, Any]:
"""Inject a tap, swipe, text, or key event."""
from core.android_device import (
send_keyevent,
send_swipe,
send_tap,
send_text,
)

action = (args.get("action") or "").strip().lower()
serial = (args.get("serial") or "").strip() or None

try:
if action == "tap":
result = send_tap(args.get("x") or 0, args.get("y") or 0, serial)
elif action == "swipe":
result = send_swipe(
args.get("x") or 0,
args.get("y") or 0,
args.get("x2") or 0,
args.get("y2") or 0,
args.get("duration_ms") or 300,
serial,
Comment on lines +120 to +128

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve explicit zero-valued inputs.

or converts duration_ms=0 to 300 and lines=0 to 200, bypassing the core layer’s intended line clamping.

Proposed fix
-                    args.get("duration_ms") or 300,
+                    args.get("duration_ms", 300),
...
-                lines=args.get("lines") or 200,
+                lines=args.get("lines", 200),

Also applies to: 159-159

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/agents/tools_pkg/tools/android_harness.py` around lines 120 -
128, Update the argument defaults in the swipe and related line-handling paths
to preserve explicit zero values instead of using truthiness-based fallbacks.
Use presence-aware defaulting for duration_ms and lines so omitted arguments
still receive their defaults while 0 reaches send_swipe and the core
line-clamping logic unchanged.

)
elif action == "text":
result = send_text(args.get("text") or "", serial)
elif action == "keyevent":
result = send_keyevent(args.get("key") or "", serial)
else:
return _text_result("Unknown action: use tap, swipe, text, or keyevent")
except Exception as e:
logging.exception("Error during android_input")
return _text_result(f"Error sending input: {e}")

status = "OK" if result.ok else "FAILED"
detail = result.output.strip()
return _text_result(f"{status}: {action}" + (f"\n{detail}" if detail else ""))

tools.append(android_input)

@tool(
"android_logcat",
"Read recent Android log output (adb logcat) for debugging. "
"Optionally filter lines by a substring (e.g. your app's tag or "
"package name) and limit the number of recent lines.",
{"lines": int, "filter_text": str, "serial": str},
)
async def android_logcat(args: dict[str, Any]) -> dict[str, Any]:
"""Fetch recent logcat lines."""
from core.android_device import read_logcat

try:
result = read_logcat(
lines=args.get("lines") or 200,
filter_text=(args.get("filter_text") or "").strip() or None,
serial=(args.get("serial") or "").strip() or None,
)
except Exception as e:
logging.exception("Error during android_logcat")
return _text_result(f"Error reading logcat: {e}")

if not result.ok:
return _text_result(f"logcat failed: {result.output}")

output = result.output[-LOGCAT_TAIL_CHARS:]
return _text_result(output if output.strip() else "(no matching log lines)")
Comment on lines +157 to +171

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not return unscoped device logs to the agent.

With no filter, this forwards raw device-wide logcat output into the agent context. Attached-device logs may contain identifiers, tokens, or other user data. Require an app/package scope by default and redact sensitive patterns before returning results.

As per path instructions, check for proper “security considerations.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/backend/agents/tools_pkg/tools/android_harness.py` around lines 157 -
171, Update the android_logcat handling around read_logcat to require an
app/package scope when no filter is provided, using the existing package-scope
configuration or symbol rather than allowing device-wide logs. Before
_text_result returns successful or error logcat output, redact sensitive
patterns such as identifiers, tokens, and user data; preserve the existing
empty-output behavior and ensure the security considerations are covered.

Source: Path instructions


tools.append(android_logcat)

return tools


def _text_result(text: str) -> dict[str, Any]:
"""Wrap text in the MCP tool result envelope."""
return {"content": [{"type": "text", "text": text}]}


__all__ = ["create_android_tools"]
Loading
Loading