#
tokens: 2830/50000 10/10 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── .python-version
├── LICENSE
├── pyproject.toml
├── README.md
├── src
│   └── mcpandroidbuild
│       ├── __init__.py
│       ├── __main__.py
│       ├── build.sh
│       ├── instrumentedTest.sh
│       ├── server.py
│       └── test.sh
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.13
2 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | .DS_Store
 2 | # Python-generated files
 3 | __pycache__/
 4 | *.py[oc]
 5 | build/
 6 | dist/
 7 | wheels/
 8 | *.egg-info
 9 | 
10 | # Virtual environments
11 | .venv
12 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Android Project MCP Server
 2 | 
 3 | A Model Context Protocol server that builds Android project that enables seamless workflow working with Android projects in Visual Studio Code using extensions like Cline or Roo Code.
 4 | 
 5 | <a href="https://glama.ai/mcp/servers/@ShenghaiWang/androidbuild">
 6 |   <img width="380" height="200" src="https://glama.ai/mcp/servers/@ShenghaiWang/androidbuild/badge" alt="Android Project Server MCP server" />
 7 | </a>
 8 | 
 9 | ### Available Tools
10 | 
11 | - `build` - Build Android project
12 |     - `folder` (string, required): The full path of the current folder that the Android project sits
13 | - `test` - Run unit test
14 |     - `folder` (string, required): The full path of the current folder that the Android project sits
15 | - `instrumentedTest` - Run Instrumented test
16 |   - `folder` (string, required): The full path of the current folder that the Android project sits
17 | 
18 | ## Installation
19 | 
20 | 
21 | ### Using uv (recommended)
22 | 
23 | When using [`uv`](https://docs.astral.sh/uv/) no specific installation is needed. We will
24 | use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to directly run *mcpandroidbuild*.
25 | 
26 | ### Using PIP
27 | 
28 | Alternatively you can install `mcpandroidbuild` via pip:
29 | 
30 | ```
31 | pip install mcpandroidbuild
32 | ```
33 | 
34 | After installation, you can run it as a script using:
35 | 
36 | ```
37 | python -m  mcpandroidbuild
38 | ```
39 | 
40 | ## Configuration
41 | 
42 | ### Configure for Claude.app
43 | 
44 | Add to your Claude settings:
45 | 
46 | <details>
47 | <summary>Using uvx</summary>
48 | 
49 | ```json
50 | "mcpServers": {
51 |   "mcpandroidbuild": {
52 |     "command": "uvx",
53 |     "args": ["mcpandroidbuild"]
54 |   }
55 | }
56 | ```
57 | </details>
58 | 
59 | <details>
60 | <summary>Using pip installation</summary>
61 | 
62 | ```json
63 | "mcpServers": {
64 |   "mcpandroidbuild": {
65 |     "command": "python",
66 |     "args": ["-m", "mcpandroidbuild"]
67 |   }
68 | }
69 | ```
70 | </details>
71 | 
72 | 
73 | ## License
74 | 
75 | mcpandroidbuild MCP tool is licensed under the MIT License. This means you are free to use, modify, and distribute the software, subject to the terms and conditions of the MIT License. For more details, please see the LICENSE file in the project repository.
```

--------------------------------------------------------------------------------
/src/mcpandroidbuild/test.sh:
--------------------------------------------------------------------------------

```bash
1 | #!/bin/bash
2 | 
3 | cd $1
4 | pwd
5 | ./gradlew test
```

--------------------------------------------------------------------------------
/src/mcpandroidbuild/build.sh:
--------------------------------------------------------------------------------

```bash
1 | #!/bin/bash
2 | 
3 | cd $1
4 | pwd
5 | ./gradlew build
```

--------------------------------------------------------------------------------
/src/mcpandroidbuild/__main__.py:
--------------------------------------------------------------------------------

```python
1 | from mcpandroidbuild import main
2 | 
3 | if __name__ == "__main__":
4 |     main()
5 | 
```

--------------------------------------------------------------------------------
/src/mcpandroidbuild/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | from .server import run
 2 | 
 3 | def main():
 4 |     """Android Project MCP Server - Building Android project"""
 5 |     import asyncio
 6 |     asyncio.run(run())
 7 | 
 8 | 
 9 | if __name__ == "__main__":
10 |     main()
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | requires-python = ">=3.13"
 3 | name = "mcpandroidbuild"
 4 | version = "0.10.0"
 5 | description = "A MCP tool that builds Android project and send error back. It also helps to run test and feed the error back to llm."
 6 | authors = [{ name = "Tim Wang" }]
 7 | maintainers = [{ name = "Tim Wang", email = "[email protected]" }]
 8 | keywords = ["http", "mcp", "llm", "automation", "Android"]
 9 | license = { text = "MIT" }
10 | classifiers = [
11 |     "Development Status :: 4 - Beta",
12 |     "Intended Audience :: Developers",
13 |     "License :: OSI Approved :: MIT License",
14 |     "Programming Language :: Python :: 3",
15 |     "Programming Language :: Python :: 3.10",
16 | ]
17 | readme = "README.md"
18 | dependencies = [
19 |     "httpx>=0.28.1",
20 |     "mcp[cli]>=1.3.0",
21 | ]
22 | 
23 | [project.scripts]
24 | mcpandroidbuild = "mcpandroidbuild:main"
25 | 
26 | [build-system]
27 | requires = ["hatchling"]
28 | build-backend = "hatchling.build"
29 | 
30 | [tool.uv]
31 | dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.3"]
```

--------------------------------------------------------------------------------
/src/mcpandroidbuild/instrumentedTest.sh:
--------------------------------------------------------------------------------

```bash
 1 | #!/bin/bash
 2 | 
 3 | cd $1
 4 | pwd
 5 | echo "Checking for connected devices..."
 6 | DEVICE=$(adb devices | awk 'NR>1 {print $1}' | head -n 1)
 7 | 
 8 | if [ -z "$DEVICE" ]; then
 9 |     echo "No device found. Searching for available emulators..."
10 |     
11 |     # List available emulators and select the first one
12 |     EMULATOR_NAME=$($ANDROID_HOME/emulator/emulator -list-avds | head -n 1)
13 | 
14 |     if [ -z "$EMULATOR_NAME" ]; then
15 |         echo "No emulators found! Please create one using AVD Manager."
16 |         exit 1
17 |     fi
18 | 
19 |     echo "Starting emulator: $EMULATOR_NAME..."
20 |     
21 |     # Start the selected emulator in the background
22 |     $ANDROID_HOME/emulator/emulator -avd "$EMULATOR_NAME" -no-window -no-audio -no-snapshot-load &
23 | 
24 |     echo "Waiting for emulator to boot..."
25 |     adb wait-for-device
26 |     
27 |     # Wait until the emulator is fully booted
28 |     while [[ "$(adb shell getprop sys.boot_completed | tr -d '\r')" != "1" ]]; do
29 |         echo "Still booting..."
30 |         sleep 5
31 |     done
32 |     
33 |     DEVICE=$(adb devices | awk 'NR>1 {print $1}' | head -n 1)
34 | fi
35 | 
36 | if [ -n "$DEVICE" ]; then
37 |     MODEL=$(adb -s "$DEVICE" shell getprop ro.product.model)
38 |     ANDROID_VERSION=$(adb -s "$DEVICE" shell getprop ro.build.version.release)
39 |     echo "Using device: $MODEL (Android $ANDROID_VERSION) - $DEVICE"
40 | else
41 |     echo "Failed to detect or launch a device!"
42 |     exit 1
43 | fi
44 | 
45 | # Run Android tests
46 | echo "Running Android tests on device: $DEVICE..."
47 | ./gradlew connectedAndroidTest
48 | 
```

--------------------------------------------------------------------------------
/src/mcpandroidbuild/server.py:
--------------------------------------------------------------------------------

```python
 1 | from mcp.server.lowlevel import Server
 2 | from mcp.server import Server
 3 | from mcp.server.stdio import stdio_server
 4 | from mcp.types import (
 5 |     ErrorData,
 6 |     TextContent,
 7 |     Tool,
 8 |     Annotations,
 9 |     Field,
10 |     Annotated,
11 |     INVALID_PARAMS,
12 | )
13 | from pydantic import BaseModel
14 | import subprocess
15 | import os, json
16 | from mcp.shared.exceptions import McpError
17 | 
18 | 
19 | class Folder(BaseModel):
20 |     """Parameters"""
21 |     folder: Annotated[str, Field(description="The full path of the current folder that the Android project sits")]
22 | 
23 | server = Server("build")
24 | 
25 | @server.list_tools()
26 | async def list_tools() -> list[Tool]:
27 |     return [
28 |         Tool(
29 |             name = "build",
30 |             description = "Build the Android project in the folder",
31 |             inputSchema = Folder.model_json_schema(),
32 |         ),
33 |         Tool(
34 |             name="test",
35 |             description="Run test for the Android project in the folder",
36 |             inputSchema=Folder.model_json_schema(),
37 |         ),
38 |         Tool(
39 |             name="instrumentedTest",
40 |             description="Run instrumented test for the Android project in the folder",
41 |             inputSchema=Folder.model_json_schema(),
42 |         )
43 |     ]
44 | @server.call_tool()
45 | async def call_tool(name, arguments: dict) -> list[TextContent]:
46 |     try:
47 |         args = Folder(**arguments)
48 |     except ValueError as e:
49 |         raise McpError(ErrorData(code=INVALID_PARAMS, message=str(e)))
50 |     # os.chdir(args.folder)
51 |     script_dir = os.path.dirname(os.path.abspath(__file__))
52 | 
53 |     command = [""]
54 |     if name == "build":
55 |         command = [os.path.join(script_dir, "build.sh"), args.folder]
56 |     elif name == "test":
57 |         command = [os.path.join(script_dir, "test.sh"), args.folder]
58 |     else:
59 |         command = [os.path.join(script_dir, "instrumentedTest.sh"), args.folder]
60 | 
61 |     result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False)
62 |     stdout_lines = result.stdout.decode("utf-8").splitlines()
63 |     stderr_lines = result.stderr.decode("utf-8").splitlines()
64 |     all_lines = stdout_lines + stderr_lines
65 |     
66 |     
67 |     error_lines = [line for line in all_lines if "failure: " in line.lower() or "e: " in line.lower() or " failed" in line.lower()]
68 |     error_message = "\n".join(error_lines)
69 |     if not error_message:
70 |         error_message = "Successful"
71 |     return [
72 |         TextContent(type="text", text=f"{error_message}")
73 |         ]
74 | 
75 | 
76 | async def run():
77 |     options = server.create_initialization_options()
78 |     async with stdio_server() as (read_stream, write_stream):
79 |         await server.run(
80 |             read_stream,
81 |             write_stream,
82 |             options,
83 |             raise_exceptions=True,
84 |         )
85 | 
86 | if __name__ == "__main__":
87 |     import asyncio
88 |     asyncio.run(run())
89 | 
```