-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Plan: Migrate ALAS MCP Server to FastMCP 3.0 and Eliminate Anti-Patterns
Context
The ALAS project (C:/_projects/ALAS) has a hand-rolled JSON-RPC MCP server (agent_orchestrator/alas_mcp_server.py, 230 lines) that was verified working on 2026-01-26 but implements 2024-era MCP patterns. The 2026 standard is FastMCP 3.0 (currently in beta).
Additionally, C:/_projects/ALASGetFunctional/mcp-servers/Android-Mobile-MCP/ is a generic Android automation server with no ALAS-specific functionality and should be deleted.
Critical Anti-Patterns Identified (10 Total)
- Manual JSON-RPC envelope construction (lines 19-26, 156-196) - High severity
- Giant if/elif tool dispatch (lines 104-154) - High severity
- Zero type safety (lines 113-152) - High severity
- Schema/implementation drift risk (lines 28-92 vs 104-152) - High severity
- Generic error codes (lines 190-196) - Medium severity
- 65 lines of schema boilerplate (lines 28-92) - Medium severity
- Untestable stdin/stdout loop (lines 199-225) - Medium severity
- No parameter pre-validation (lines 113-152) - Medium severity
- Inconsistent response formats (lines 106-152) - Low severity
- Direct state machine coupling (lines 16-17, 130-152) - Low severity
Grade: C+ (Functional but technically outdated)
Migration Strategy: 3-Phase Refactor
Phase 1: Evaluate Android-Mobile-MCP Usefulness (15 minutes)
Rationale: The Android-Mobile-MCP has generic Android automation tools. Need to determine if any are worth keeping for ALAS use cases.
Useful functionality found:
- App launching:
mobile_launch_app(package_name)- Could be used for "start emulator → launch Azur Lane" - System app filtering: Pattern matching to exclude Android system apps from listings
- UI coordinate validation: Pre-dump validation before clicks (prevents invalid coordinates)
ALAS already has better versions of:
- App launching (multi-backend: WSA, uiautomator2, ADB)
- UI detection (semantic matching with OCR/color/templates)
- Device control (integrated into ALAS device layer)
Decision: KEEP Android-Mobile-MCP but add ALAS-aware wrapper
Actions:
-
Keep Android-Mobile-MCP as-is (generic Android automation)
-
Create ALAS-specific wrapper (
my_tools/alas_launcher.py):- Detects Azur Lane package name for current server (EN:
com.YoStarEN.AzurLane, CN:com.bilibili.azurlane, etc.) - Combines emulator start + app launch + wait for game to initialize
- Falls back to ALAS's native
app_start()if Android-Mobile-MCP unavailable
- Detects Azur Lane package name for current server (EN:
-
Document integration in CLAUDE.md:
- When to use Android-Mobile-MCP (generic Android tasks)
- When to use ALAS MCP (game-specific automation)
- How the two servers complement each other
Phase 2: Setup FastMCP 3.0 Environment (30 minutes)
FastMCP 3.0 Status: Currently in beta (announced Jan 2026), rebuilds framework around:
- Components: Tools, resources, prompts
- Providers: Context sources (local, remote, OpenAPI)
- Transforms: Composition layers
Installation:
-
Create isolated environment in
C:/_projects/ALAS/agent_orchestrator/:cd C:/_projects/ALAS/agent_orchestrator uv init --python 3.10 uv add "fastmcp>=3.0.0b1" # Beta version uv add pillow lz4 # For screenshot encoding
-
Verify FastMCP version:
uv run python -c "import fastmcp; print(fastmcp.__version__)" # Expected: 3.0.0b1 or higher
-
Create pyproject.toml:
[project] name = "alas-mcp-server" version = "1.0.0" requires-python = ">=3.10" dependencies = [ "fastmcp>=3.0.0b1", "pillow>=10.0.0", "lz4>=4.0.0", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build"
Compatibility note: The MCP server imports alas.AzurLaneAutoScript from alas_wrapped/ (Python 3.7 code). This is safe because:
- ALAS is installed as a package (not just source files)
- Python 3.10 can import Python 3.7 modules (forward compatibility)
- Only the device/state machine interfaces are used (stable API)
Phase 3: Refactor to FastMCP 3.0 (2 hours)
File: C:/_projects/ALAS/agent_orchestrator/alas_mcp_server.py
Refactor approach: Incremental migration, tool-by-tool.
Step 1: Create FastMCP skeleton (preserve existing logic)
New structure:
from fastmcp import FastMCP
from typing import Optional
import argparse
import base64
import io
from PIL import Image
# Initialize FastMCP server
mcp = FastMCP("alas-mcp", version="1.0.0")
# Global context (initialized in main())
class ALASContext:
def __init__(self, config_name: str):
from alas import AzurLaneAutoScript
self.script = AzurLaneAutoScript(config_name=config_name)
self._state_machine = self.script.state_machine
def encode_screenshot_png_base64(self) -> str:
"""Preserve existing PNG encoding logic (lines 94-102)."""
image = self.script.device.screenshot()
if getattr(image, "shape", None) is not None and len(image.shape) == 3 and image.shape[2] == 3:
img = Image.fromarray(image[:, :, ::-1]) # BGR→RGB
else:
img = Image.fromarray(image)
buf = io.BytesIO()
img.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("ascii")
ctx: Optional[ALASContext] = None
# Tools implemented in Steps 2-8...
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="alas")
args = parser.parse_args()
ctx = ALASContext(config_name=args.config)
mcp.run(transport="stdio") # FastMCP handles stdin/stdout loopEliminates:
- Lines 19-26: Manual
_result()and_error()methods - Lines 156-196: Manual
handle()method - Lines 199-225: Manual stdin/stdout loop
- Lines 28-92: Manual
_tool_specs()(schemas auto-generated)
Step 2: Migrate ADB tools (3 tools)
Tool 1: adb.screenshot
@mcp.tool()
def adb_screenshot() -> dict:
"""Take a screenshot from the connected emulator/device.
Returns a base64-encoded PNG image. Requires lz4 package for decompression.
"""
data = ctx.encode_screenshot_png_base64()
return {
"content": [
{"type": "image", "mimeType": "image/png", "data": data}
]
}Tool 2: adb.tap
@mcp.tool()
def adb_tap(x: int, y: int) -> str:
"""Tap a coordinate using ADB input tap.
Args:
x: X coordinate (integer)
y: Y coordinate (integer)
Returns:
Confirmation message
"""
ctx.script.device.click_adb(x, y)
return f"tapped {x},{y}"Tool 3: adb.swipe
@mcp.tool()
def adb_swipe(
x1: int,
y1: int,
x2: int,
y2: int,
duration_ms: int = 100
) -> str:
"""Swipe between coordinates using ADB input swipe.
Args:
x1: Starting X coordinate
y1: Starting Y coordinate
x2: Ending X coordinate
y2: Ending Y coordinate
duration_ms: Duration in milliseconds (default: 100)
Returns:
Confirmation message
"""
duration = duration_ms / 1000.0
ctx.script.device.swipe_adb((x1, y1), (x2, y2), duration=duration)
return f"swiped {x1},{y1}->{x2},{y2}"Type safety gained:
duration_ms: int = 100ensures type checking + default value- No more
arguments.get("duration_ms")with silent None handling
Step 3: Migrate State tools (2 tools)
Tool 4: alas.get_current_state
@mcp.tool()
def alas_get_current_state() -> str:
"""Return the current ALAS UI Page name.
Returns:
Page name (e.g., 'page_main', 'page_exercise')
"""
page = ctx._state_machine.get_current_state()
return str(page)Tool 5: alas.goto
@mcp.tool()
def alas_goto(page: str) -> str:
"""Navigate to a target ALAS UI Page by name.
Args:
page: Page name (e.g., 'page_main')
Returns:
Confirmation message
Raises:
ValueError: If page name is unknown
"""
from module.ui.page import Page
destination = Page.all_pages.get(page)
if destination is None:
raise ValueError(f"unknown page: {page}")
ctx._state_machine.transition(destination)
return f"navigated to {page}"Error handling improvement:
ValueErrorinstead ofKeyError(more semantically correct)- FastMCP converts Python exceptions to proper JSON-RPC error codes
Step 4: Migrate Tool Discovery tools (2 tools)
Tool 6: alas.list_tools
@mcp.tool()
def alas_list_tools() -> list[dict]:
"""List deterministic ALAS tools registered in the state machine.
Returns:
List of tool specifications (name, description, parameters)
"""
tools = [
{
"name": t.name,
"description": t.description,
"parameters": t.parameters
}
for t in ctx._state_machine.get_all_tools()
]
return toolsTool 7: alas.call_tool
@mcp.tool()
def alas_call_tool(name: str, arguments: dict = None) -> dict:
"""Invoke a deterministic ALAS tool by name.
Args:
name: Tool name (from alas.list_tools)
arguments: Tool arguments (default: empty dict)
Returns:
Tool result (structure varies by tool)
Raises:
KeyError: If tool name is unknown
"""
args = arguments or {}
result = ctx._state_machine.call_tool(name, **args)
return resultResponse format improvement:
- Returns native Python
list[dict]instead of JSON-as-string - FastMCP handles serialization automatically
- Clients get structured data, not nested JSON strings
Step 5: Update launch configuration
Old launch (manual invocation):
cd C:/_projects/ALAS
PYTHONIOENCODING=utf-8 python agent_orchestrator/alas_mcp_server.py --config alasNew launch (with uv):
cd C:/_projects/ALAS/agent_orchestrator
uv run alas_mcp_server.py --config alasCreate wrapper script (agent_orchestrator/run_server.sh):
#!/bin/bash
cd "$(dirname "$0")"
export PYTHONIOENCODING=utf-8
uv run alas_mcp_server.py --config "${1:-alas}"Code Metrics Comparison
| Metric | Before (Hand-rolled) | After (FastMCP 3.0) | Improvement |
|---|---|---|---|
| Total lines | 230 | ~140 | -39% |
| Boilerplate lines | 90 (39%) | ~10 (7%) | -89% |
| Schema definitions | 65 lines manual | 0 (auto-generated) | -100% |
| Type safety | None | Full (function signatures) | ∞ |
| Tool dispatch | 50-line if/elif chain | Decorator-based | N/A |
| Error handling | Generic -32000 | Structured by exception type | Qualitative |
| Testability | Low (stdio mocking) | High (plain functions) | Qualitative |
Testing Strategy
Unit Tests (new file: agent_orchestrator/test_alas_mcp.py)
import pytest
from alas_mcp_server import ALASContext, adb_tap, alas_goto
@pytest.fixture
def mock_context(monkeypatch):
"""Mock ALAS context to avoid real device."""
ctx = ALASContext(config_name="test")
# Mock device methods
monkeypatch.setattr(ctx.script.device, "click_adb", lambda x, y: None)
return ctx
def test_adb_tap(mock_context, monkeypatch):
import alas_mcp_server
monkeypatch.setattr(alas_mcp_server, "ctx", mock_context)
result = adb_tap(100, 200)
assert result == "tapped 100,200"
def test_alas_goto_invalid_page(mock_context, monkeypatch):
import alas_mcp_server
monkeypatch.setattr(alas_mcp_server, "ctx", mock_context)
with pytest.raises(ValueError, match="unknown page"):
alas_goto("invalid_page_name")Integration Tests (verify against running MEmu)
Test sequence (same as Phase 0 verification):
- Start MEmu emulator on
127.0.0.1:21503 - Start ALAS MCP server:
uv run alas_mcp_server.py --config alas - Use MCP client to invoke tools:
from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def test_all_tools(): server_params = StdioServerParameters( command="uv", args=["run", "alas_mcp_server.py", "--config", "alas"], cwd="C:/_projects/ALAS/agent_orchestrator" ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: # Initialize await session.initialize() # Test 1: Screenshot result = await session.call_tool("adb.screenshot", {}) assert result["content"][0]["type"] == "image" # Test 2: Tap result = await session.call_tool("adb.tap", {"x": 100, "y": 100}) assert "tapped" in result["content"][0]["text"] # Test 3: Get current state result = await session.call_tool("alas.get_current_state", {}) assert "page_" in result["content"][0]["text"] # Test 4: List tools result = await session.call_tool("alas.list_tools", {}) tools = json.loads(result["content"][0]["text"]) assert len(tools) == 9 # ALAS internal tools # Test 5: Goto result = await session.call_tool("alas.goto", {"page": "page_main"}) assert "navigated" in result["content"][0]["text"]
Files Modified
| File | Change | Lines Changed |
|---|---|---|
C:/_projects/ALAS/agent_orchestrator/alas_mcp_server.py |
Complete rewrite to FastMCP 3.0 | 230 → ~140 |
C:/_projects/ALAS/agent_orchestrator/pyproject.toml |
NEW - FastMCP dependencies | +15 |
C:/_projects/ALAS/agent_orchestrator/run_server.sh |
NEW - Wrapper script | +4 |
C:/_projects/ALAS/agent_orchestrator/test_alas_mcp.py |
NEW - Unit tests | +30 |
C:/_projects/ALASGetFunctional/my_tools/alas_launcher.py |
NEW - ALAS app launcher wrapper | +50 |
C:/_projects/ALASGetFunctional/CLAUDE.md |
Document Android-Mobile-MCP + ALAS MCP integration | +10 |
C:/_projects/ALASGetFunctional/AGENTS.md |
Update MCP server descriptions | ~5 |
Documentation Updates
Update CLAUDE.md (C:/_projects/ALAS/CLAUDE.md)
Replace the "MCP Tool Status" section with:
## MCP Tool Status (Migrated to FastMCP 3.0, 2026-01-26)
All 7 MCP tools refactored from hand-rolled JSON-RPC to **FastMCP 3.0** framework.
**Improvements:**
- ✅ Type-safe function signatures (automatic schema generation)
- ✅ Structured error handling (ValueError, KeyError → proper JSON-RPC error codes)
- ✅ 39% code reduction (230 → 140 lines)
- ✅ Unit testable (tools are plain Python functions)
| Tool | Category | Status | Notes |
|------|----------|--------|-------|
| `adb.screenshot` | ADB | Working | Returns base64 PNG. Requires `lz4` package. |
| `adb.tap` | ADB | Working | Type-safe coordinates (`x: int, y: int`). |
| `adb.swipe` | ADB | Working | Default duration 100ms. |
| `alas.get_current_state` | State | Working | Returns current page via StateMachine. |
| `alas.goto` | State | Working | Raises `ValueError` if page unknown. |
| `alas.list_tools` | Tool | Working | Returns structured list (not JSON string). |
| `alas.call_tool` | Tool | Working | Invokes registered tool by name. |
### Launch Command
```bash
cd C:/_projects/ALAS/agent_orchestrator
uv run alas_mcp_server.py --config alas
### Update CHANGELOG.md (C:/_projects/ALAS/CHANGELOG.md)
Add under `[Unreleased]`:
```markdown
### Changed
- **MCP Server**: Migrated from hand-rolled JSON-RPC to FastMCP 3.0 framework
- Eliminated 90 lines of protocol boilerplate (39% code reduction)
- Added full type safety via function signature validation
- Improved error handling (structured exception types → JSON-RPC error codes)
- All 7 tools remain functionally identical, now with better maintainability
### Added
- **ALAS Launcher Wrapper** (`my_tools/alas_launcher.py`): Combines emulator start + Azur Lane app launch
- Detects correct package name for server region (EN/CN/JP/etc.)
- Uses Android-Mobile-MCP's `mobile_launch_app()` when available
- Falls back to ALAS native `app_start()` if MCP unavailable
Rollback Plan
If FastMCP 3.0 has critical bugs (beta software risk):
-
Preserve legacy implementation:
cd C:/_projects/ALAS/agent_orchestrator git mv alas_mcp_server.py alas_mcp_server_legacy.py # After migration, keep both files until FastMCP 3.0 is stable
-
Fallback instructions in CLAUDE.md:
### Rollback to Legacy (if needed) If FastMCP 3.0 has issues, use the legacy hand-rolled server: ```bash cd C:/_projects/ALAS PYTHONIOENCODING=utf-8 python agent_orchestrator/alas_mcp_server_legacy.py --config alas
-
Delete legacy after 1 week of stable FastMCP operation.
Success Criteria
✅ Phase 1 Complete when:
- Android-Mobile-MCP directory deleted
- Documentation updated (4 files)
- No broken references in codebase
✅ Phase 2 Complete when:
- FastMCP 3.0 installed via
uv pyproject.tomlcreated- Version confirmed (
fastmcp>=3.0.0b1)
✅ Phase 3 Complete when:
- All 7 tools refactored to
@mcp.tool()pattern - Integration tests pass against MEmu emulator
- Code metrics show 39% reduction
- Unit tests cover all tools
- Legacy implementation archived
✅ Overall Success when:
- MCP client can invoke all 7 tools via FastMCP server
- No functional regressions vs. hand-rolled implementation
- Type safety verified (wrong parameter types rejected at schema level)
- Error messages are structured (not generic -32000 codes)
Risk Assessment
| Risk | Severity | Mitigation |
|---|---|---|
| FastMCP 3.0 beta instability | High | Keep legacy implementation as rollback |
| Python 3.7 (alas_wrapped) ↔ 3.10 (MCP) incompatibility | Low | Tested - imports work due to stable API |
| Breaking changes in tool behavior | Medium | Integration tests catch regressions |
| Missing FastMCP 3.0 features from 2.x | Low | 2.x → 3.x migration path documented |
| ALAS startup penalty regression | Low | Persistent process model preserved |
Timeline Estimate
- Phase 1 (Delete Android-Mobile-MCP): 15 minutes
- Phase 2 (Setup FastMCP 3.0): 30 minutes
- Phase 3 (Refactor + Test): 2 hours
- Documentation: 30 minutes
- Buffer for issues: 1 hour
Total: 4-5 hours
Questions for User
None - plan is ready for execution.