Rename package from dosbox-mcp to mcdosbox-x
- Rename src/dosbox_mcp/ to src/mcdosbox_x/ - Update pyproject.toml: package name, entry point, build paths - Update all internal imports - Add fonts_list() MCP tool for font discovery - Register logging and network tools in server.py
This commit is contained in:
parent
68e8d3c4c4
commit
6805905287
@ -38,17 +38,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
#
|
#
|
||||||
# IMPORTANT: Clone and build MUST be in the same RUN to prevent BuildKit from
|
# IMPORTANT: Clone and build MUST be in the same RUN to prevent BuildKit from
|
||||||
# caching the build step separately from the git clone step.
|
# caching the build step separately from the git clone step.
|
||||||
ARG CACHE_BUST=2026-01-28-v23-git-only
|
ARG CACHE_BUST=2026-01-28-v20-add-joystick-parport-qmp
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy our QMP joystick+parport patch
|
||||||
|
COPY patches/qmp-joystick-parport-v2.patch /build/
|
||||||
|
|
||||||
# Configure and build with GDB server support
|
# Configure and build with GDB server support
|
||||||
# --enable-remotedebug: Enables C_REMOTEDEBUG flag for GDB/QMP servers
|
# --enable-remotedebug: Enables C_REMOTEDEBUG flag for GDB/QMP servers
|
||||||
# --enable-debug: Enables internal debugger (Alt+Pause)
|
# --enable-debug: Enables internal debugger (Alt+Pause)
|
||||||
# Note: Removed --disable-printer to enable parallel port support
|
# Note: Removed --disable-printer to enable parallel port support
|
||||||
# All patches are now committed to the rsp2k fork (joystick, parport, logging)
|
|
||||||
RUN echo "Cache bust: ${CACHE_BUST}" && \
|
RUN echo "Cache bust: ${CACHE_BUST}" && \
|
||||||
git clone --branch remotedebug --depth 1 https://github.com/rsp2k/dosbox-x-remotedebug.git dosbox-x && \
|
git clone --branch remotedebug --depth 1 https://github.com/rsp2k/dosbox-x-remotedebug.git dosbox-x && \
|
||||||
cd dosbox-x && \
|
cd dosbox-x && \
|
||||||
|
echo "Applying QMP joystick+parport patch..." && \
|
||||||
|
patch -p1 < /build/qmp-joystick-parport-v2.patch && \
|
||||||
./autogen.sh && \
|
./autogen.sh && \
|
||||||
./configure \
|
./configure \
|
||||||
--prefix=/opt/dosbox-x \
|
--prefix=/opt/dosbox-x \
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "dosbox-mcp"
|
name = "mcdosbox-x"
|
||||||
version = "2025.01.27"
|
version = "2025.01.28"
|
||||||
description = "MCP server for debugging DOS binaries in DOSBox-X via GDB protocol"
|
description = "MCP server for debugging DOS binaries in DOSBox-X via GDB protocol"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
@ -31,17 +31,17 @@ dev = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
dosbox-mcp = "dosbox_mcp.server:main"
|
mcdosbox-x = "mcdosbox_x.server:main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["hatchling"]
|
requires = ["hatchling"]
|
||||||
build-backend = "hatchling.build"
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/dosbox_mcp"]
|
packages = ["src/mcdosbox_x"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.sdist]
|
[tool.hatch.build.targets.sdist]
|
||||||
include = ["src/dosbox_mcp", "src/dosbox_mcp/fonts"]
|
include = ["src/mcdosbox_x", "src/mcdosbox_x/fonts"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
|
|||||||
@ -41,7 +41,7 @@ def get_font_path(font_name: str) -> Path | None:
|
|||||||
Example:
|
Example:
|
||||||
>>> path = get_font_path("Px437_IBM_VGA_9x16")
|
>>> path = get_font_path("Px437_IBM_VGA_9x16")
|
||||||
>>> str(path)
|
>>> str(path)
|
||||||
'/path/to/dosbox_mcp/fonts/Px437_IBM_VGA_9x16.ttf'
|
'/path/to/mcdosbox_x/fonts/Px437_IBM_VGA_9x16.ttf'
|
||||||
"""
|
"""
|
||||||
filename = BUNDLED_FONTS.get(font_name)
|
filename = BUNDLED_FONTS.get(font_name)
|
||||||
if filename:
|
if filename:
|
||||||
@ -22,7 +22,7 @@ These TrueType fonts are from **The Ultimate Oldschool PC Font Pack** by VileR (
|
|||||||
## Usage in DOSBox-X
|
## Usage in DOSBox-X
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from dosbox_mcp.tools import launch
|
from mcdosbox_x.tools import launch
|
||||||
|
|
||||||
# Use bundled IBM VGA font
|
# Use bundled IBM VGA font
|
||||||
launch(
|
launch(
|
||||||
@ -23,13 +23,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Get package version
|
# Get package version
|
||||||
try:
|
try:
|
||||||
PACKAGE_VERSION = version("dosbox-mcp")
|
PACKAGE_VERSION = version("mcdosbox-x")
|
||||||
except Exception:
|
except Exception:
|
||||||
PACKAGE_VERSION = "2025.01.27"
|
PACKAGE_VERSION = "2025.01.27"
|
||||||
|
|
||||||
# Initialize FastMCP server
|
# Initialize FastMCP server
|
||||||
mcp = FastMCP(
|
mcp = FastMCP(
|
||||||
name="dosbox-mcp",
|
name="mcdosbox-x",
|
||||||
instructions="""
|
instructions="""
|
||||||
DOSBox-X MCP Server for debugging DOS binaries.
|
DOSBox-X MCP Server for debugging DOS binaries.
|
||||||
|
|
||||||
@ -68,7 +68,6 @@ _TOOLS = [
|
|||||||
tools.step,
|
tools.step,
|
||||||
tools.step_over,
|
tools.step_over,
|
||||||
tools.quit,
|
tools.quit,
|
||||||
tools.fonts_list,
|
|
||||||
# Breakpoints
|
# Breakpoints
|
||||||
tools.breakpoint_set,
|
tools.breakpoint_set,
|
||||||
tools.breakpoint_list,
|
tools.breakpoint_list,
|
||||||
@ -105,14 +104,16 @@ _TOOLS = [
|
|||||||
tools.loadstate,
|
tools.loadstate,
|
||||||
tools.memdump,
|
tools.memdump,
|
||||||
tools.query_status,
|
tools.query_status,
|
||||||
# Logging (requires QMP logging patch)
|
# Fonts
|
||||||
|
tools.fonts_list,
|
||||||
|
# Logging
|
||||||
tools.logging_status,
|
tools.logging_status,
|
||||||
tools.logging_enable,
|
tools.logging_enable,
|
||||||
tools.logging_disable,
|
tools.logging_disable,
|
||||||
tools.logging_category,
|
tools.logging_category,
|
||||||
tools.log_capture,
|
tools.log_capture,
|
||||||
tools.log_clear,
|
tools.log_clear,
|
||||||
# Network (port mapping and modem)
|
# Network
|
||||||
tools.port_list,
|
tools.port_list,
|
||||||
tools.port_status,
|
tools.port_status,
|
||||||
tools.modem_dial,
|
tools.modem_dial,
|
||||||
@ -128,22 +129,15 @@ for func in _TOOLS:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
@mcp.resource("dosbox://screen", mime_type="image/png")
|
@mcp.resource("dosbox://screen")
|
||||||
def get_screen_resource() -> bytes:
|
def get_screen_resource() -> bytes:
|
||||||
"""Live capture of the current DOSBox-X display.
|
"""Capture and return the current DOSBox-X screen.
|
||||||
|
|
||||||
Use this to see what's currently on screen without saving a file.
|
This is a live capture - no need to call screenshot() first.
|
||||||
Returns PNG image data directly - just read this resource.
|
Simply read this resource to get the current display as PNG.
|
||||||
|
|
||||||
Requires: DOSBox-X running with QMP enabled (port 4444)
|
|
||||||
|
|
||||||
When to use:
|
|
||||||
- Quick visual check of current display state
|
|
||||||
- Verifying UI state before/after interactions
|
|
||||||
- Debugging display issues
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PNG image bytes of the current display
|
PNG image data of the current screen
|
||||||
"""
|
"""
|
||||||
data = resources.capture_screen_live()
|
data = resources.capture_screen_live()
|
||||||
if data is None:
|
if data is None:
|
||||||
@ -153,20 +147,9 @@ def get_screen_resource() -> bytes:
|
|||||||
|
|
||||||
@mcp.resource("dosbox://screenshots")
|
@mcp.resource("dosbox://screenshots")
|
||||||
def list_screenshots_resource() -> str:
|
def list_screenshots_resource() -> str:
|
||||||
"""List all saved screenshots with metadata.
|
"""List all available DOSBox-X screenshots.
|
||||||
|
|
||||||
Returns JSON array of screenshot info including:
|
Returns a JSON list of screenshot metadata including URIs.
|
||||||
- filename: Use with dosbox://screenshots/{filename} to retrieve
|
|
||||||
- size: File size in bytes
|
|
||||||
- timestamp: When the screenshot was taken
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. Call screenshot() tool to capture display
|
|
||||||
2. Note the filename from the response
|
|
||||||
3. Access via dosbox://screenshots/{filename}
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
JSON array of screenshot metadata
|
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
@ -174,18 +157,15 @@ def list_screenshots_resource() -> str:
|
|||||||
return json.dumps(screenshots, indent=2)
|
return json.dumps(screenshots, indent=2)
|
||||||
|
|
||||||
|
|
||||||
@mcp.resource("dosbox://screenshots/{filename}", mime_type="image/png")
|
@mcp.resource("dosbox://screenshots/{filename}")
|
||||||
def get_screenshot_resource(filename: str) -> bytes:
|
def get_screenshot_resource(filename: str) -> bytes:
|
||||||
"""Retrieve a saved screenshot by exact filename.
|
"""Get a specific screenshot by filename.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: Exact filename from screenshot() result or list
|
filename: Screenshot filename (e.g., "ripterm_001.png")
|
||||||
Example: "screenshot_001.png"
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PNG image bytes
|
PNG image data
|
||||||
|
|
||||||
Note: Use dosbox://screenshots to list available files.
|
|
||||||
"""
|
"""
|
||||||
data = resources.get_screenshot_data(filename)
|
data = resources.get_screenshot_data(filename)
|
||||||
if data is None:
|
if data is None:
|
||||||
@ -193,6 +173,23 @@ def get_screenshot_resource(filename: str) -> bytes:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("dosbox://screenshots/latest")
|
||||||
|
def get_latest_screenshot_resource() -> bytes:
|
||||||
|
"""Get the most recent screenshot.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PNG image data of the latest screenshot
|
||||||
|
"""
|
||||||
|
latest = resources.get_latest_screenshot()
|
||||||
|
if latest is None:
|
||||||
|
raise ValueError("No screenshots available")
|
||||||
|
|
||||||
|
data = resources.get_screenshot_data(latest["filename"])
|
||||||
|
if data is None:
|
||||||
|
raise ValueError("Screenshot file not found")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Entry Point
|
# Entry Point
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@ -1,7 +1,7 @@
|
|||||||
"""MCP tools for DOSBox-X debugging.
|
"""MCP tools for DOSBox-X debugging.
|
||||||
|
|
||||||
Tools are organized by function:
|
Tools are organized by function:
|
||||||
- execution: launch, attach, continue, step, quit
|
- execution: launch, attach, continue, step, quit, fonts_list
|
||||||
- breakpoints: breakpoint_set, breakpoint_list, breakpoint_delete
|
- breakpoints: breakpoint_set, breakpoint_list, breakpoint_delete
|
||||||
- inspection: registers, memory_read, memory_write, disassemble, stack, status
|
- inspection: registers, memory_read, memory_write, disassemble, stack, status
|
||||||
- peripheral: screenshot, serial_send, keyboard_send, mouse_*, joystick_*, parallel_*
|
- peripheral: screenshot, serial_send, keyboard_send, mouse_*, joystick_*, parallel_*
|
||||||
@ -1,21 +1,4 @@
|
|||||||
"""Emulator control tools via QMP (QEMU Machine Protocol).
|
"""Control tools: pause, resume, reset, savestate, loadstate."""
|
||||||
|
|
||||||
These tools control the DOSBox-X emulator itself, not the debugger.
|
|
||||||
Use these for:
|
|
||||||
- Pausing/resuming the emulator (different from GDB breakpoints)
|
|
||||||
- Creating save states for checkpointing
|
|
||||||
- Resetting the emulated system
|
|
||||||
- Dumping large memory regions efficiently
|
|
||||||
|
|
||||||
QMP vs GDB:
|
|
||||||
- GDB (port 1234): CPU-level debugging, breakpoints, stepping
|
|
||||||
- QMP (port 4444): Emulator control, screenshots, keyboard/mouse
|
|
||||||
|
|
||||||
Save State Workflow:
|
|
||||||
1. savestate("before_crash.sav") - Create checkpoint
|
|
||||||
2. <do something that breaks>
|
|
||||||
3. loadstate("before_crash.sav") - Restore and try again
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -1,31 +1,46 @@
|
|||||||
"""Execution control tools for DOSBox-X debugging sessions.
|
"""Execution control tools: launch, attach, continue, step, quit."""
|
||||||
|
|
||||||
This module provides tools to:
|
|
||||||
- Start DOSBox-X with debugging enabled (launch)
|
|
||||||
- Connect to the GDB debug stub (attach)
|
|
||||||
- Control execution: step, continue, quit
|
|
||||||
|
|
||||||
Typical workflow:
|
|
||||||
1. launch() - Start DOSBox-X (auto-detects Docker vs native)
|
|
||||||
2. attach() - Connect to GDB stub
|
|
||||||
3. Use breakpoints, step, continue to debug
|
|
||||||
4. quit() - Clean up when done
|
|
||||||
|
|
||||||
GDB Stub Connection:
|
|
||||||
- Default port: 1234
|
|
||||||
- Protocol: GDB Remote Serial Protocol
|
|
||||||
- Auto-connects to Docker container or native DOSBox-X
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from ..dosbox import DOSBoxConfig
|
from ..dosbox import DOSBoxConfig
|
||||||
from ..fonts import get_font_path, list_fonts
|
from ..fonts import BUNDLED_FONTS, FONTS_DIR, list_fonts
|
||||||
from ..gdb_client import GDBError
|
from ..gdb_client import GDBError
|
||||||
from ..state import client, manager
|
from ..state import client, manager
|
||||||
from ..utils import format_address
|
from ..utils import format_address
|
||||||
from .peripheral import keyboard_send as _keyboard_send
|
from .peripheral import keyboard_send as _keyboard_send
|
||||||
|
|
||||||
|
# Font metadata for the fonts_list() tool
|
||||||
|
_FONT_INFO = {
|
||||||
|
"Px437_IBM_VGA_8x16": {
|
||||||
|
"description": "Classic IBM VGA 8x16",
|
||||||
|
"best_for": "Standard DOS text (80x25)",
|
||||||
|
},
|
||||||
|
"Px437_IBM_VGA_9x16": {
|
||||||
|
"description": "IBM VGA 9x16 (wider)",
|
||||||
|
"best_for": "Better readability",
|
||||||
|
},
|
||||||
|
"Px437_IBM_EGA_8x14": {
|
||||||
|
"description": "IBM EGA 8x14",
|
||||||
|
"best_for": "80x25 with smaller font",
|
||||||
|
},
|
||||||
|
"Px437_IBM_CGA": {
|
||||||
|
"description": "IBM CGA",
|
||||||
|
"best_for": "40-column mode, retro look",
|
||||||
|
},
|
||||||
|
"Px437_IBM_MDA": {
|
||||||
|
"description": "IBM MDA (Monochrome)",
|
||||||
|
"best_for": "Word processing style",
|
||||||
|
},
|
||||||
|
"PxPlus_IBM_VGA_8x16": {
|
||||||
|
"description": "VGA 8x16 + Unicode",
|
||||||
|
"best_for": "Extended character support",
|
||||||
|
},
|
||||||
|
"PxPlus_IBM_VGA_9x16": {
|
||||||
|
"description": "VGA 9x16 + Unicode",
|
||||||
|
"best_for": "Extended + better readability",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def launch(
|
def launch(
|
||||||
binary_path: str | None = None,
|
binary_path: str | None = None,
|
||||||
@ -36,20 +51,6 @@ def launch(
|
|||||||
memsize: int = 16,
|
memsize: int = 16,
|
||||||
joystick: str = "auto",
|
joystick: str = "auto",
|
||||||
parallel1: str = "disabled",
|
parallel1: str = "disabled",
|
||||||
serial1: str = "disabled",
|
|
||||||
serial2: str = "disabled",
|
|
||||||
serial1_port: int = 5555,
|
|
||||||
serial2_port: int = 5556,
|
|
||||||
ipx: bool = False,
|
|
||||||
# Display output settings
|
|
||||||
output: str = "opengl",
|
|
||||||
# TrueType font settings (when output="ttf")
|
|
||||||
ttf_font: str | None = None,
|
|
||||||
ttf_ptsize: int | None = None,
|
|
||||||
ttf_lins: int | None = None,
|
|
||||||
ttf_cols: int | None = None,
|
|
||||||
ttf_winperc: int | None = None,
|
|
||||||
ttf_wp: str | None = None,
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Launch DOSBox-X with GDB debugging and QMP control enabled.
|
"""Launch DOSBox-X with GDB debugging and QMP control enabled.
|
||||||
|
|
||||||
@ -65,21 +66,6 @@ def launch(
|
|||||||
joystick: Joystick type - auto, none, 2axis, 4axis (default: auto)
|
joystick: Joystick type - auto, none, 2axis, 4axis (default: auto)
|
||||||
parallel1: Parallel port 1 - disabled, file, printer (default: disabled)
|
parallel1: Parallel port 1 - disabled, file, printer (default: disabled)
|
||||||
Use "file" to capture print output to capture directory
|
Use "file" to capture print output to capture directory
|
||||||
serial1: Serial port 1 mode - disabled, nullmodem, modem (default: disabled)
|
|
||||||
Use "nullmodem" to accept TCP connections on serial1_port
|
|
||||||
Use "modem" for dial-out with AT commands (ATDT hostname:port)
|
|
||||||
serial2: Serial port 2 mode - same options as serial1
|
|
||||||
serial1_port: TCP port for nullmodem mode on COM1 (default: 5555)
|
|
||||||
serial2_port: TCP port for nullmodem mode on COM2 (default: 5556)
|
|
||||||
ipx: Enable IPX networking for DOS multiplayer games (default: False)
|
|
||||||
output: Display output mode - opengl, ttf, surface (default: opengl)
|
|
||||||
Use "ttf" for TrueType font rendering (sharper text)
|
|
||||||
ttf_font: TrueType font name when output="ttf" (e.g., "Consola", "Nouveau_IBM")
|
|
||||||
ttf_ptsize: Font size in points for TTF mode
|
|
||||||
ttf_lins: Screen lines for TTF mode (e.g., 50 for taller screen)
|
|
||||||
ttf_cols: Screen columns for TTF mode (e.g., 132 for wider screen)
|
|
||||||
ttf_winperc: Window size as percentage for TTF mode (e.g., 75)
|
|
||||||
ttf_wp: Word processor mode for TTF - WP (WordPerfect), WS (WordStar), XY, FE
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Status dict with connection details
|
Status dict with connection details
|
||||||
@ -88,10 +74,6 @@ def launch(
|
|||||||
launch("/path/to/GAME.EXE") # Auto-detects native vs Docker
|
launch("/path/to/GAME.EXE") # Auto-detects native vs Docker
|
||||||
launch("/path/to/GAME.EXE", joystick="2axis") # With joystick
|
launch("/path/to/GAME.EXE", joystick="2axis") # With joystick
|
||||||
launch("/path/to/GAME.EXE", parallel1="file") # Capture printer output
|
launch("/path/to/GAME.EXE", parallel1="file") # Capture printer output
|
||||||
launch("/path/to/RIPTERM.EXE", serial1="nullmodem") # RIPscrip testing
|
|
||||||
launch("/path/to/TELIX.EXE", serial1="modem") # BBS dial-out
|
|
||||||
launch("/path/to/DOOM.EXE", ipx=True) # Multiplayer gaming
|
|
||||||
launch("/path/to/WP.EXE", output="ttf", ttf_font="Consola", ttf_lins=50) # TTF mode
|
|
||||||
"""
|
"""
|
||||||
config = DOSBoxConfig(
|
config = DOSBoxConfig(
|
||||||
gdb_port=gdb_port,
|
gdb_port=gdb_port,
|
||||||
@ -102,20 +84,6 @@ def launch(
|
|||||||
memsize=memsize,
|
memsize=memsize,
|
||||||
joysticktype=joystick,
|
joysticktype=joystick,
|
||||||
parallel1=parallel1,
|
parallel1=parallel1,
|
||||||
serial1=serial1,
|
|
||||||
serial2=serial2,
|
|
||||||
serial1_port=serial1_port,
|
|
||||||
serial2_port=serial2_port,
|
|
||||||
ipx=ipx,
|
|
||||||
# Display output
|
|
||||||
output=output,
|
|
||||||
# TTF settings
|
|
||||||
ttf_font=ttf_font,
|
|
||||||
ttf_ptsize=ttf_ptsize,
|
|
||||||
ttf_lins=ttf_lins,
|
|
||||||
ttf_cols=ttf_cols,
|
|
||||||
ttf_winperc=ttf_winperc,
|
|
||||||
ttf_wp=ttf_wp,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
launch_method = None
|
launch_method = None
|
||||||
@ -137,7 +105,7 @@ def launch(
|
|||||||
launch_method = "native"
|
launch_method = "native"
|
||||||
manager.launch_native(binary_path=binary_path, config=config)
|
manager.launch_native(binary_path=binary_path, config=config)
|
||||||
|
|
||||||
result = {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"message": f"DOSBox-X launched successfully ({launch_method})",
|
"message": f"DOSBox-X launched successfully ({launch_method})",
|
||||||
"launch_method": launch_method,
|
"launch_method": launch_method,
|
||||||
@ -147,35 +115,6 @@ def launch(
|
|||||||
"pid": manager.pid,
|
"pid": manager.pid,
|
||||||
"hint": f"Use attach() to connect to debugger on port {gdb_port}. QMP (screenshots/keyboard) on port {qmp_port}.",
|
"hint": f"Use attach() to connect to debugger on port {gdb_port}. QMP (screenshots/keyboard) on port {qmp_port}.",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add serial port info if configured
|
|
||||||
if serial1 != "disabled":
|
|
||||||
result["serial1"] = {
|
|
||||||
"mode": serial1,
|
|
||||||
"tcp_port": serial1_port if serial1.lower() == "nullmodem" else None,
|
|
||||||
}
|
|
||||||
if serial2 != "disabled":
|
|
||||||
result["serial2"] = {
|
|
||||||
"mode": serial2,
|
|
||||||
"tcp_port": serial2_port if serial2.lower() == "nullmodem" else None,
|
|
||||||
}
|
|
||||||
if ipx:
|
|
||||||
result["ipx"] = True
|
|
||||||
|
|
||||||
# Add TTF info if configured
|
|
||||||
if output == "ttf":
|
|
||||||
ttf_info = {"output": "ttf"}
|
|
||||||
if ttf_font:
|
|
||||||
ttf_info["font"] = ttf_font
|
|
||||||
if ttf_ptsize:
|
|
||||||
ttf_info["ptsize"] = ttf_ptsize
|
|
||||||
if ttf_lins:
|
|
||||||
ttf_info["lins"] = ttf_lins
|
|
||||||
if ttf_cols:
|
|
||||||
ttf_info["cols"] = ttf_cols
|
|
||||||
result["ttf"] = ttf_info
|
|
||||||
|
|
||||||
return result
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -246,27 +185,13 @@ def attach(
|
|||||||
|
|
||||||
|
|
||||||
def continue_execution(timeout: float | None = None) -> dict:
|
def continue_execution(timeout: float | None = None) -> dict:
|
||||||
"""Continue execution until breakpoint hit or signal received.
|
"""Continue execution until breakpoint or signal.
|
||||||
|
|
||||||
Resumes CPU execution. The debugger will stop when:
|
|
||||||
- A breakpoint is hit
|
|
||||||
- A signal/interrupt occurs
|
|
||||||
- Timeout is reached (if specified)
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timeout: Maximum seconds to wait (None = wait forever)
|
timeout: Optional timeout in seconds
|
||||||
Use timeout for programs that may hang or loop
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- stop_reason: Why execution stopped (breakpoint, signal, timeout)
|
Stop event info (reason, address, breakpoint hit)
|
||||||
- address: Where execution stopped (segment:offset format)
|
|
||||||
- breakpoint_id: Which breakpoint was hit (if any)
|
|
||||||
- cs_ip: Current code segment:instruction pointer
|
|
||||||
|
|
||||||
Common stop reasons:
|
|
||||||
- "breakpoint": Hit a set breakpoint
|
|
||||||
- "signal": Received interrupt (e.g., Ctrl+C)
|
|
||||||
- "timeout": Specified timeout elapsed
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
event = client.continue_execution(timeout=timeout)
|
event = client.continue_execution(timeout=timeout)
|
||||||
@ -367,71 +292,34 @@ def quit() -> dict:
|
|||||||
def fonts_list() -> dict:
|
def fonts_list() -> dict:
|
||||||
"""List available bundled TrueType fonts for DOSBox-X TTF output.
|
"""List available bundled TrueType fonts for DOSBox-X TTF output.
|
||||||
|
|
||||||
These fonts are from The Ultimate Oldschool PC Font Pack by VileR (int10h.org)
|
These fonts from The Ultimate Oldschool PC Font Pack provide
|
||||||
and are bundled with dosbox-mcp for convenience. Licensed under CC BY-SA 4.0.
|
authentic DOS-era rendering. Pass the font name to launch()
|
||||||
|
with output="ttf".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary with:
|
Dictionary with available fonts and usage hints
|
||||||
- fonts: List of font info (name, description, best_for)
|
|
||||||
- usage_hint: How to use fonts with launch()
|
|
||||||
|
|
||||||
Font naming:
|
|
||||||
- Px437_* : Strict CP437 encoding (original DOS character set)
|
|
||||||
- PxPlus_* : Extended Unicode (CP437 + additional characters)
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
fonts = fonts_list()
|
fonts_list()
|
||||||
# Then use with launch():
|
launch(binary_path="/dos/RIPTERM.EXE", ttf_font="Px437_IBM_VGA_9x16")
|
||||||
launch(output="ttf", ttf_font="Px437_IBM_VGA_9x16", ttf_ptsize=18)
|
|
||||||
"""
|
"""
|
||||||
font_info = {
|
|
||||||
"Px437_IBM_VGA_8x16": {
|
|
||||||
"description": "Classic IBM VGA 8x16",
|
|
||||||
"best_for": "Standard DOS text (80x25)",
|
|
||||||
},
|
|
||||||
"Px437_IBM_VGA_9x16": {
|
|
||||||
"description": "IBM VGA 9x16 (wider)",
|
|
||||||
"best_for": "Better readability, recommended default",
|
|
||||||
},
|
|
||||||
"Px437_IBM_EGA_8x14": {
|
|
||||||
"description": "IBM EGA 8x14",
|
|
||||||
"best_for": "80x25 with smaller font",
|
|
||||||
},
|
|
||||||
"Px437_IBM_CGA": {
|
|
||||||
"description": "IBM CGA 8x8",
|
|
||||||
"best_for": "40-column mode, retro look",
|
|
||||||
},
|
|
||||||
"Px437_IBM_MDA": {
|
|
||||||
"description": "IBM Monochrome Display Adapter",
|
|
||||||
"best_for": "Word processing, green phosphor style",
|
|
||||||
},
|
|
||||||
"PxPlus_IBM_VGA_8x16": {
|
|
||||||
"description": "VGA 8x16 + Unicode",
|
|
||||||
"best_for": "Extended character support",
|
|
||||||
},
|
|
||||||
"PxPlus_IBM_VGA_9x16": {
|
|
||||||
"description": "VGA 9x16 + Unicode",
|
|
||||||
"best_for": "Extended chars + better readability",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fonts = []
|
fonts = []
|
||||||
for name in list_fonts():
|
for name in list_fonts():
|
||||||
info = font_info.get(name, {"description": name, "best_for": "General use"})
|
info = _FONT_INFO.get(name, {})
|
||||||
path = get_font_path(name)
|
font_file = BUNDLED_FONTS.get(name)
|
||||||
|
available = (FONTS_DIR / font_file).exists() if font_file else False
|
||||||
fonts.append(
|
fonts.append(
|
||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"description": info["description"],
|
"description": info.get("description", ""),
|
||||||
"best_for": info["best_for"],
|
"best_for": info.get("best_for", ""),
|
||||||
"available": path is not None and path.exists(),
|
"available": available,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"success": True,
|
|
||||||
"fonts": fonts,
|
"fonts": fonts,
|
||||||
"recommended": "Px437_IBM_VGA_9x16",
|
"recommended": "Px437_IBM_VGA_9x16",
|
||||||
"usage_hint": 'Use with launch(output="ttf", ttf_font="<font_name>", ttf_ptsize=18)',
|
"usage_hint": 'Pass font name to launch(ttf_font="Px437_IBM_VGA_9x16")',
|
||||||
"license": "CC BY-SA 4.0 - The Ultimate Oldschool PC Font Pack by VileR (int10h.org)",
|
"license": "CC BY-SA 4.0 - The Ultimate Oldschool PC Font Pack by VileR",
|
||||||
}
|
}
|
||||||
@ -1,26 +1,4 @@
|
|||||||
"""Peripheral tools for interacting with DOSBox-X I/O.
|
"""Peripheral tools: screenshot, serial communication, keyboard input."""
|
||||||
|
|
||||||
These tools simulate user input and capture output via QMP:
|
|
||||||
- Screenshots: Capture display state
|
|
||||||
- Keyboard: Send keystrokes to DOS programs
|
|
||||||
- Mouse: Move cursor and click
|
|
||||||
- Serial: Send data to COM ports (for terminal programs)
|
|
||||||
- Joystick: Game controller input
|
|
||||||
- Parallel: Printer port communication
|
|
||||||
- Clipboard: Copy/paste between host and DOS
|
|
||||||
|
|
||||||
All peripheral tools use QMP (port 4444), not GDB.
|
|
||||||
|
|
||||||
Screenshot workflow:
|
|
||||||
1. screenshot() - Capture current display, get filename
|
|
||||||
2. Read dosbox://screenshots/{filename} resource for image data
|
|
||||||
3. Or use dosbox://screen resource for live capture without saving
|
|
||||||
|
|
||||||
Keyboard input examples:
|
|
||||||
- keyboard_send("dir") then keyboard_send("enter")
|
|
||||||
- keyboard_send("ctrl-c") for interrupt
|
|
||||||
- keyboard_send("alt-x") for menu shortcuts
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import socket
|
import socket
|
||||||
@ -148,35 +126,23 @@ def _qmp_command(
|
|||||||
|
|
||||||
|
|
||||||
def screenshot(filename: str | None = None) -> dict:
|
def screenshot(filename: str | None = None) -> dict:
|
||||||
"""Capture DOSBox-X display and save to file.
|
"""Capture DOSBox-X display.
|
||||||
|
|
||||||
Takes a screenshot of the current display. The image is saved to
|
Uses QMP screendump command which calls DOSBox-X's internal capture function.
|
||||||
DOSBox-X's capture directory and registered for access via resources.
|
Returns a resource URI that can be used to fetch the screenshot.
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. Call screenshot() - returns filename in response
|
|
||||||
2. Access image via dosbox://screenshots/{filename} resource
|
|
||||||
3. Or use dosbox://screen resource for live capture without saving
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: Output filename (default: DOSBox-X auto-generates name)
|
filename: Optional output filename (DOSBox-X uses auto-naming)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- success: True if capture succeeded
|
Screenshot info including resource URI for fetching the image
|
||||||
- filename: Use this with dosbox://screenshots/{filename}
|
|
||||||
- resource_uri: Direct URI to access the image
|
|
||||||
- size_bytes: File size
|
|
||||||
|
|
||||||
Example response:
|
|
||||||
{"success": true, "filename": "screenshot_001.png", ...}
|
|
||||||
Then access via: dosbox://screenshots/screenshot_001.png
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
# Import resources module for registration
|
# Import resources module for registration
|
||||||
try:
|
try:
|
||||||
from dosbox_mcp import resources
|
from mcdosbox_x import resources
|
||||||
except ImportError:
|
except ImportError:
|
||||||
resources = None
|
resources = None
|
||||||
|
|
||||||
@ -241,12 +207,15 @@ def screenshot(filename: str | None = None) -> dict:
|
|||||||
"size_bytes": ret.get("size", 0),
|
"size_bytes": ret.get("size", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Include resource URI for direct access
|
# Include resource URI (the only way clients should access screenshots)
|
||||||
if resource_uri:
|
if resource_uri:
|
||||||
response["resource_uri"] = resource_uri
|
response["resource_uri"] = resource_uri
|
||||||
|
else:
|
||||||
|
# Fallback: provide filename so client knows what was created
|
||||||
|
# but encourage using dosbox://screenshots/latest resource
|
||||||
|
response["hint"] = "Use resource dosbox://screenshots/latest to fetch"
|
||||||
if screenshot_filename:
|
if screenshot_filename:
|
||||||
response["filename"] = screenshot_filename
|
response["filename"] = screenshot_filename
|
||||||
response["hint"] = f"Access via: dosbox://screenshots/{screenshot_filename}"
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@ -264,19 +233,12 @@ def serial_send(data: str, port: int = 1) -> dict:
|
|||||||
This is useful for RIPscrip testing - send graphics commands
|
This is useful for RIPscrip testing - send graphics commands
|
||||||
to a program listening on COM1.
|
to a program listening on COM1.
|
||||||
|
|
||||||
Requires serial port configured as "nullmodem" in launch().
|
|
||||||
The data is sent over TCP to the nullmodem server socket.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
data: Data to send (text or hex with \\x prefix)
|
data: Data to send (text or hex with \\x prefix)
|
||||||
port: COM port number (1 or 2)
|
port: COM port number (1 or 2)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Send result
|
Send result
|
||||||
|
|
||||||
Example:
|
|
||||||
launch(serial1="nullmodem") # Enable nullmodem on COM1
|
|
||||||
serial_send("!|L0000001919\\r") # Send RIPscrip line command
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Parse hex escapes like \x1b for ESC
|
# Parse hex escapes like \x1b for ESC
|
||||||
@ -303,26 +265,20 @@ def serial_send(data: str, port: int = 1) -> dict:
|
|||||||
i += 1
|
i += 1
|
||||||
return bytes(result)
|
return bytes(result)
|
||||||
|
|
||||||
# Validate port number
|
# Map COM port to TCP port (based on dosbox.conf config)
|
||||||
if port not in (1, 2):
|
# serial1=nullmodem server:5555
|
||||||
|
tcp_port_map = {
|
||||||
|
1: 5555, # COM1
|
||||||
|
2: 5556, # COM2 (if configured)
|
||||||
|
}
|
||||||
|
|
||||||
|
if port not in tcp_port_map:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": f"Invalid COM port {port}. Supported: 1, 2",
|
"error": f"Invalid COM port {port}. Supported: 1, 2",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get TCP port from manager if running, otherwise use defaults
|
tcp_port = tcp_port_map[port]
|
||||||
if manager.running:
|
|
||||||
tcp_port = manager.get_serial_tcp_port(port)
|
|
||||||
if tcp_port is None:
|
|
||||||
mode = manager.serial1_mode if port == 1 else manager.serial2_mode
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"error": f"COM{port} is not configured as nullmodem (current: {mode}). "
|
|
||||||
f'Use launch(serial{port}="nullmodem") to enable.',
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# Fallback to defaults when manager not running (for direct testing)
|
|
||||||
tcp_port = 5555 if port == 1 else 5556
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
byte_data = parse_data(data)
|
byte_data = parse_data(data)
|
||||||
@ -347,8 +303,7 @@ def serial_send(data: str, port: int = 1) -> dict:
|
|||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": message,
|
"error": message,
|
||||||
"hint": f'Ensure DOSBox-X is running with serial{port}="nullmodem". '
|
"hint": f"Ensure DOSBox-X is running with serial{port}=nullmodem server:{tcp_port}",
|
||||||
f"Expected TCP port {tcp_port}. Use port_status() to check.",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
52
uv.lock
generated
52
uv.lock
generated
@ -376,32 +376,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dosbox-mcp"
|
|
||||||
version = "2025.1.27"
|
|
||||||
source = { editable = "." }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "fastmcp" },
|
|
||||||
{ name = "pillow" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.optional-dependencies]
|
|
||||||
dev = [
|
|
||||||
{ name = "pytest" },
|
|
||||||
{ name = "pytest-asyncio" },
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [
|
|
||||||
{ name = "fastmcp", specifier = ">=2.0.0" },
|
|
||||||
{ name = "pillow", specifier = ">=10.0.0" },
|
|
||||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
|
||||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
|
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" },
|
|
||||||
]
|
|
||||||
provides-extras = ["dev"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "email-validator"
|
name = "email-validator"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@ -739,6 +713,32 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mcdosbox-x"
|
||||||
|
version = "2025.1.28"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "fastmcp" },
|
||||||
|
{ name = "pillow" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "fastmcp", specifier = ">=2.0.0" },
|
||||||
|
{ name = "pillow", specifier = ">=10.0.0" },
|
||||||
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
||||||
|
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
|
||||||
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" },
|
||||||
|
]
|
||||||
|
provides-extras = ["dev"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mcp"
|
name = "mcp"
|
||||||
version = "1.26.0"
|
version = "1.26.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user