mcdosbox-x/CLAUDE.md
Ryan Malloy 79c646cf87 Add comprehensive documentation for MCP server
- Update README.md with accurate lokkju fork info and troubleshooting
- Add CLAUDE.md development guide for future sessions
- Document GDB protocol details, architecture, and common issues
2026-01-27 15:26:22 -07:00

11 KiB

DOSBox-X MCP Server - Development Guide

Project Overview

An MCP server that enables Claude to programmatically debug DOS binaries running in DOSBox-X via the GDB Remote Serial Protocol. The primary use case is reverse engineering the unpublished Bezier algorithm in RIPTERM.EXE.


Quick Reference

Running the System

# Start DOSBox-X container (must be running for MCP tools to work)
xhost +local:docker && docker compose up -d

# Verify GDB server is listening
nc -zv localhost 1234

# Check container logs
docker compose logs -f

# Stop container
docker compose down

Testing the MCP Server

# Run full test suite
uv run pytest

# Run specific test file
uv run pytest tests/test_gdb_client.py -v

# Test MCP tools via CLI (important: --allowedTools required!)
claude -p "attach to localhost:1234 and read registers" \
  --allowedTools "mcp__dosbox-mcp__*"

Common Development Commands

# Run server directly (for debugging)
uv run dosbox-mcp

# Lint and format
uv run ruff check src/ && uv run ruff format src/

# Rebuild container after Dockerfile changes
docker compose build --no-cache

Architecture

Component Overview

┌─────────────────────────────────────────────────────────────────┐
│                        Claude Code                               │
└─────────────────────────────────────────────────────────────────┘
                              │ MCP (stdio)
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     FastMCP Server                               │
│  server.py - registers tools from tools/ modules                 │
└─────────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│    state.py     │ │   types.py      │ │   utils.py      │
│  Global client  │ │  Registers      │ │  parse_address  │
│  and manager    │ │  Breakpoint     │ │  format helpers │
└─────────────────┘ │  MemoryRegion   │ └─────────────────┘
        │           └─────────────────┘
        ▼
┌─────────────────────────────────────────────────────────────────┐
│                     gdb_client.py                                │
│  GDB Remote Serial Protocol client                               │
│  - Packet formatting ($cmd#checksum)                             │
│  - Register/memory read/write                                    │
│  - Breakpoint management                                         │
│  - Execution control (step, continue)                            │
└─────────────────────────────────────────────────────────────────┘
                              │ TCP :1234
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│              DOSBox-X Container (lokkju fork)                    │
│  Built with --enable-remotedebug                                 │
│  Ports: 1234 (GDB), 4444 (QMP)                                   │
└─────────────────────────────────────────────────────────────────┘

Key Files

File Purpose
server.py FastMCP entry point, registers all tools
gdb_client.py GDB protocol implementation (~400 lines)
state.py Singleton instances (manager, client) to avoid circular imports
types.py Dataclasses: Registers, Breakpoint, MemoryRegion, etc.
utils.py Address parsing, format conversion
tools/execution.py launch, attach, continue, step, quit
tools/breakpoints.py breakpoint_set, list, delete
tools/inspection.py registers, memory_read/write, disassemble, stack
tools/peripheral.py screenshot, serial_send (stubs)

GDB Remote Serial Protocol

Packet Format

Send:    $<command>#<2-digit-hex-checksum>
Receive: +$<response>#<checksum>
         (+ is ACK, - is NAK)

Key Commands

Command Description Example
qSupported Handshake/capabilities First packet after connect
g Read all registers Returns hex blob
G<hex> Write all registers
m<addr>,<len> Read memory m7c00,10
M<addr>,<len>:<hex> Write memory
Z0,<addr>,1 Set software breakpoint Z0,7c00,1
z0,<addr>,1 Clear breakpoint
c Continue execution
s Step one instruction
? Query stop reason Returns S05 (SIGTRAP) etc.

Register Order (x86)

GDB returns registers in this order:

EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI, EIP, EFLAGS,
CS, SS, DS, ES, FS, GS

Each is 32-bit little-endian hex (8 chars), except segments (16-bit, 4 chars).


DOSBox-X Configuration

Critical: Use [dosbox] Section

The lokkju fork's GDB/QMP config goes in [dosbox], NOT [debug]:

[dosbox]
gdbserver=true
gdbserver port=1234
qmpserver=true
qmpserver port=4444

Why lokkju/dosbox-x-remotedebug?

Mainline DOSBox-X has no GDB stub. The lokkju fork (branch: remotedebug) adds:

  • --enable-remotedebug configure flag
  • gdbserver.cpp and qmp.cpp sources
  • Runtime config options

Build command in Dockerfile:

./configure --enable-remotedebug --enable-debug --enable-sdl2

Real Mode Addressing

DOS uses segment:offset addressing (20-bit physical):

physical = (segment << 4) + offset

# Example: 1234:5678
# physical = 0x12340 + 0x5678 = 0x179B8

The utils.py module handles parsing:

  • "1234:5678" → segment:offset
  • "0x12345" or "12345" → flat hex
  • "#65536" → decimal
  • "CS:IP" → register-based (requires current register state)

Adding New Tools

1. Create the Tool Function

In appropriate tools/*.py file:

from ..state import client, manager
from ..types import SomeType
from ..utils import parse_address

def my_new_tool(address: str, count: int = 10) -> dict:
    """Short description for MCP.

    Args:
        address: Memory address (segment:offset or flat hex)
        count: Number of items (default: 10)

    Returns:
        Result dictionary with data
    """
    if not client.connected:
        return {"error": "Not connected. Use attach() first."}

    addr = parse_address(address, client.read_registers())
    # ... implementation ...
    return {"address": f"{addr:05x}", "data": result}

2. Export from tools/__init__.py

from .mymodule import my_new_tool

__all__ = [
    # ...existing...
    "my_new_tool",
]

3. Register in server.py

mcp.tool()(tools.my_new_tool)

4. Add Tests

In tests/test_tools.py or new file:

def test_my_new_tool():
    # Mock the GDB client
    ...

Unfinished Work

Peripheral Tools (Stubs)

screenshot and serial_send are placeholders. Implementation ideas:

screenshot: DOSBox-X can capture via:

  • Built-in screenshot command (key binding)
  • QMP protocol on port 4444
  • X11 window capture

serial_send: Configure serial port in dosbox.conf:

[serial]
serial1=nullmodem server:1234

Then send data via socket.

Step Over Implementation

step_over in execution.py is basic — it steps but doesn't properly handle CALL instructions. A full implementation would:

  1. Disassemble current instruction
  2. If CALL, set temp breakpoint after it
  3. Continue instead of step
  4. Remove temp breakpoint

Disassembler

Current disassembler in inspection.py is minimal (pattern matching common opcodes). For full disassembly, consider:

  • Capstone library (pip install capstone)
  • External tool like ndisasm

Testing Against Real DOSBox-X

Manual GDB Connection

# From host, after container is running
nc localhost 1234

# Type (no newline):
$qSupported#37

# Should receive capabilities response

Python Quick Test

from dosbox_mcp.gdb_client import GDBClient

client = GDBClient()
client.connect("localhost", 1234)
regs = client.read_registers()
print(f"CS:IP = {regs.cs:04x}:{regs.ip:04x}")
client.disconnect()

Debugging Tips

Enable Verbose Logging

import logging
logging.getLogger("dosbox_mcp").setLevel(logging.DEBUG)

Check Container Logs

# GDB connections show up here
docker logs dosbox-mcp 2>&1 | grep -i gdb

Common Issues

Symptom Cause Fix
"Connection refused" Container not running docker compose up -d
"Connection closed" Wrong DOSBox build Verify lokkju fork
Config not applied Wrong section Use [dosbox] not [debug]
Timeout on continue No breakpoint hit Set breakpoint first
"unhealthy" status Missing nc in image Cosmetic, ignore or add netcat

Future Enhancements

  1. RIPTERM Bezier Tracing

    • Set breakpoints at drawing routines
    • Capture coordinate pairs from registers/stack
    • Build coordinate trace for algorithm analysis
  2. QMP Integration

    • Port 4444 provides machine control
    • Could enable screenshots, state snapshots
  3. Memory Watchpoints

    • GDB protocol supports Z2/z2 for write watchpoints
    • Useful for tracking variable changes
  4. Scripted Automation

    • Create examples/ripterm_bezier.py for the target use case
    • Document coordinate capture methodology