Skip to content

Migrate ALAS MCP Server to FastMCP 3.0 and Eliminate Anti-Patterns #6

@Coldaine

Description

@Coldaine

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)

  1. Manual JSON-RPC envelope construction (lines 19-26, 156-196) - High severity
  2. Giant if/elif tool dispatch (lines 104-154) - High severity
  3. Zero type safety (lines 113-152) - High severity
  4. Schema/implementation drift risk (lines 28-92 vs 104-152) - High severity
  5. Generic error codes (lines 190-196) - Medium severity
  6. 65 lines of schema boilerplate (lines 28-92) - Medium severity
  7. Untestable stdin/stdout loop (lines 199-225) - Medium severity
  8. No parameter pre-validation (lines 113-152) - Medium severity
  9. Inconsistent response formats (lines 106-152) - Low severity
  10. 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:

  1. App launching: mobile_launch_app(package_name) - Could be used for "start emulator → launch Azur Lane"
  2. System app filtering: Pattern matching to exclude Android system apps from listings
  3. 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:

  1. Keep Android-Mobile-MCP as-is (generic Android automation)

  2. 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
  3. 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:

  1. 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
  2. Verify FastMCP version:

    uv run python -c "import fastmcp; print(fastmcp.__version__)"
    # Expected: 3.0.0b1 or higher
  3. 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 loop

Eliminates:

  • 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 = 100 ensures 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:

  • ValueError instead of KeyError (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 tools

Tool 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 result

Response 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 alas

New launch (with uv):

cd C:/_projects/ALAS/agent_orchestrator
uv run alas_mcp_server.py --config alas

Create 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):

  1. Start MEmu emulator on 127.0.0.1:21503
  2. Start ALAS MCP server: uv run alas_mcp_server.py --config alas
  3. 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):

  1. 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
  2. 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
    
    
  3. 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.toml created
  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    JulesAssigned to Jules (GitHub Copilot agent)

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions