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
|
||||
# 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
|
||||
|
||||
# Copy our QMP joystick+parport patch
|
||||
COPY patches/qmp-joystick-parport-v2.patch /build/
|
||||
|
||||
# Configure and build with GDB server support
|
||||
# --enable-remotedebug: Enables C_REMOTEDEBUG flag for GDB/QMP servers
|
||||
# --enable-debug: Enables internal debugger (Alt+Pause)
|
||||
# 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}" && \
|
||||
git clone --branch remotedebug --depth 1 https://github.com/rsp2k/dosbox-x-remotedebug.git dosbox-x && \
|
||||
cd dosbox-x && \
|
||||
echo "Applying QMP joystick+parport patch..." && \
|
||||
patch -p1 < /build/qmp-joystick-parport-v2.patch && \
|
||||
./autogen.sh && \
|
||||
./configure \
|
||||
--prefix=/opt/dosbox-x \
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "dosbox-mcp"
|
||||
version = "2025.01.27"
|
||||
name = "mcdosbox-x"
|
||||
version = "2025.01.28"
|
||||
description = "MCP server for debugging DOS binaries in DOSBox-X via GDB protocol"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
@ -31,17 +31,17 @@ dev = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
dosbox-mcp = "dosbox_mcp.server:main"
|
||||
mcdosbox-x = "mcdosbox_x.server:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/dosbox_mcp"]
|
||||
packages = ["src/mcdosbox_x"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = ["src/dosbox_mcp", "src/dosbox_mcp/fonts"]
|
||||
include = ["src/mcdosbox_x", "src/mcdosbox_x/fonts"]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
@ -41,7 +41,7 @@ def get_font_path(font_name: str) -> Path | None:
|
||||
Example:
|
||||
>>> path = get_font_path("Px437_IBM_VGA_9x16")
|
||||
>>> 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)
|
||||
if filename:
|
||||
@ -22,7 +22,7 @@ These TrueType fonts are from **The Ultimate Oldschool PC Font Pack** by VileR (
|
||||
## Usage in DOSBox-X
|
||||
|
||||
```python
|
||||
from dosbox_mcp.tools import launch
|
||||
from mcdosbox_x.tools import launch
|
||||
|
||||
# Use bundled IBM VGA font
|
||||
launch(
|
||||
@ -23,13 +23,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Get package version
|
||||
try:
|
||||
PACKAGE_VERSION = version("dosbox-mcp")
|
||||
PACKAGE_VERSION = version("mcdosbox-x")
|
||||
except Exception:
|
||||
PACKAGE_VERSION = "2025.01.27"
|
||||
|
||||
# Initialize FastMCP server
|
||||
mcp = FastMCP(
|
||||
name="dosbox-mcp",
|
||||
name="mcdosbox-x",
|
||||
instructions="""
|
||||
DOSBox-X MCP Server for debugging DOS binaries.
|
||||
|
||||
@ -68,7 +68,6 @@ _TOOLS = [
|
||||
tools.step,
|
||||
tools.step_over,
|
||||
tools.quit,
|
||||
tools.fonts_list,
|
||||
# Breakpoints
|
||||
tools.breakpoint_set,
|
||||
tools.breakpoint_list,
|
||||
@ -105,14 +104,16 @@ _TOOLS = [
|
||||
tools.loadstate,
|
||||
tools.memdump,
|
||||
tools.query_status,
|
||||
# Logging (requires QMP logging patch)
|
||||
# Fonts
|
||||
tools.fonts_list,
|
||||
# Logging
|
||||
tools.logging_status,
|
||||
tools.logging_enable,
|
||||
tools.logging_disable,
|
||||
tools.logging_category,
|
||||
tools.log_capture,
|
||||
tools.log_clear,
|
||||
# Network (port mapping and modem)
|
||||
# Network
|
||||
tools.port_list,
|
||||
tools.port_status,
|
||||
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:
|
||||
"""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.
|
||||
Returns PNG image data directly - just read this resource.
|
||||
|
||||
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
|
||||
This is a live capture - no need to call screenshot() first.
|
||||
Simply read this resource to get the current display as PNG.
|
||||
|
||||
Returns:
|
||||
PNG image bytes of the current display
|
||||
PNG image data of the current screen
|
||||
"""
|
||||
data = resources.capture_screen_live()
|
||||
if data is None:
|
||||
@ -153,20 +147,9 @@ def get_screen_resource() -> bytes:
|
||||
|
||||
@mcp.resource("dosbox://screenshots")
|
||||
def list_screenshots_resource() -> str:
|
||||
"""List all saved screenshots with metadata.
|
||||
"""List all available DOSBox-X screenshots.
|
||||
|
||||
Returns JSON array of screenshot info including:
|
||||
- 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
|
||||
Returns a JSON list of screenshot metadata including URIs.
|
||||
"""
|
||||
import json
|
||||
|
||||
@ -174,18 +157,15 @@ def list_screenshots_resource() -> str:
|
||||
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:
|
||||
"""Retrieve a saved screenshot by exact filename.
|
||||
"""Get a specific screenshot by filename.
|
||||
|
||||
Args:
|
||||
filename: Exact filename from screenshot() result or list
|
||||
Example: "screenshot_001.png"
|
||||
filename: Screenshot filename (e.g., "ripterm_001.png")
|
||||
|
||||
Returns:
|
||||
PNG image bytes
|
||||
|
||||
Note: Use dosbox://screenshots to list available files.
|
||||
PNG image data
|
||||
"""
|
||||
data = resources.get_screenshot_data(filename)
|
||||
if data is None:
|
||||
@ -193,6 +173,23 @@ def get_screenshot_resource(filename: str) -> bytes:
|
||||
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
|
||||
# =============================================================================
|
||||
@ -1,7 +1,7 @@
|
||||
"""MCP tools for DOSBox-X debugging.
|
||||
|
||||
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
|
||||
- inspection: registers, memory_read, memory_write, disassemble, stack, status
|
||||
- peripheral: screenshot, serial_send, keyboard_send, mouse_*, joystick_*, parallel_*
|
||||
@ -1,21 +1,4 @@
|
||||
"""Emulator control tools via QMP (QEMU Machine Protocol).
|
||||
|
||||
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
|
||||
"""
|
||||
"""Control tools: pause, resume, reset, savestate, loadstate."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
@ -1,31 +1,46 @@
|
||||
"""Execution control tools for DOSBox-X debugging sessions.
|
||||
|
||||
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
|
||||
"""
|
||||
"""Execution control tools: launch, attach, continue, step, quit."""
|
||||
|
||||
import time
|
||||
|
||||
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 ..state import client, manager
|
||||
from ..utils import format_address
|
||||
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(
|
||||
binary_path: str | None = None,
|
||||
@ -36,20 +51,6 @@ def launch(
|
||||
memsize: int = 16,
|
||||
joystick: str = "auto",
|
||||
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:
|
||||
"""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)
|
||||
parallel1: Parallel port 1 - disabled, file, printer (default: disabled)
|
||||
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:
|
||||
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", joystick="2axis") # With joystick
|
||||
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(
|
||||
gdb_port=gdb_port,
|
||||
@ -102,20 +84,6 @@ def launch(
|
||||
memsize=memsize,
|
||||
joysticktype=joystick,
|
||||
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
|
||||
@ -137,7 +105,7 @@ def launch(
|
||||
launch_method = "native"
|
||||
manager.launch_native(binary_path=binary_path, config=config)
|
||||
|
||||
result = {
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"DOSBox-X launched successfully ({launch_method})",
|
||||
"launch_method": launch_method,
|
||||
@ -147,35 +115,6 @@ def launch(
|
||||
"pid": manager.pid,
|
||||
"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:
|
||||
return {
|
||||
"success": False,
|
||||
@ -246,27 +185,13 @@ def attach(
|
||||
|
||||
|
||||
def continue_execution(timeout: float | None = None) -> dict:
|
||||
"""Continue execution until breakpoint hit or signal received.
|
||||
|
||||
Resumes CPU execution. The debugger will stop when:
|
||||
- A breakpoint is hit
|
||||
- A signal/interrupt occurs
|
||||
- Timeout is reached (if specified)
|
||||
"""Continue execution until breakpoint or signal.
|
||||
|
||||
Args:
|
||||
timeout: Maximum seconds to wait (None = wait forever)
|
||||
Use timeout for programs that may hang or loop
|
||||
timeout: Optional timeout in seconds
|
||||
|
||||
Returns:
|
||||
- stop_reason: Why execution stopped (breakpoint, signal, timeout)
|
||||
- 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
|
||||
Stop event info (reason, address, breakpoint hit)
|
||||
"""
|
||||
try:
|
||||
event = client.continue_execution(timeout=timeout)
|
||||
@ -367,71 +292,34 @@ def quit() -> dict:
|
||||
def fonts_list() -> dict:
|
||||
"""List available bundled TrueType fonts for DOSBox-X TTF output.
|
||||
|
||||
These fonts are from The Ultimate Oldschool PC Font Pack by VileR (int10h.org)
|
||||
and are bundled with dosbox-mcp for convenience. Licensed under CC BY-SA 4.0.
|
||||
These fonts from The Ultimate Oldschool PC Font Pack provide
|
||||
authentic DOS-era rendering. Pass the font name to launch()
|
||||
with output="ttf".
|
||||
|
||||
Returns:
|
||||
Dictionary with:
|
||||
- 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)
|
||||
Dictionary with available fonts and usage hints
|
||||
|
||||
Example:
|
||||
fonts = fonts_list()
|
||||
# Then use with launch():
|
||||
launch(output="ttf", ttf_font="Px437_IBM_VGA_9x16", ttf_ptsize=18)
|
||||
fonts_list()
|
||||
launch(binary_path="/dos/RIPTERM.EXE", ttf_font="Px437_IBM_VGA_9x16")
|
||||
"""
|
||||
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 = []
|
||||
for name in list_fonts():
|
||||
info = font_info.get(name, {"description": name, "best_for": "General use"})
|
||||
path = get_font_path(name)
|
||||
info = _FONT_INFO.get(name, {})
|
||||
font_file = BUNDLED_FONTS.get(name)
|
||||
available = (FONTS_DIR / font_file).exists() if font_file else False
|
||||
fonts.append(
|
||||
{
|
||||
"name": name,
|
||||
"description": info["description"],
|
||||
"best_for": info["best_for"],
|
||||
"available": path is not None and path.exists(),
|
||||
"description": info.get("description", ""),
|
||||
"best_for": info.get("best_for", ""),
|
||||
"available": available,
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"fonts": fonts,
|
||||
"recommended": "Px437_IBM_VGA_9x16",
|
||||
"usage_hint": 'Use with launch(output="ttf", ttf_font="<font_name>", ttf_ptsize=18)',
|
||||
"license": "CC BY-SA 4.0 - The Ultimate Oldschool PC Font Pack by VileR (int10h.org)",
|
||||
"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",
|
||||
}
|
||||
@ -1,26 +1,4 @@
|
||||
"""Peripheral tools for interacting with DOSBox-X I/O.
|
||||
|
||||
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
|
||||
"""
|
||||
"""Peripheral tools: screenshot, serial communication, keyboard input."""
|
||||
|
||||
import json
|
||||
import socket
|
||||
@ -148,35 +126,23 @@ def _qmp_command(
|
||||
|
||||
|
||||
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
|
||||
DOSBox-X's capture directory and registered for access via resources.
|
||||
|
||||
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
|
||||
Uses QMP screendump command which calls DOSBox-X's internal capture function.
|
||||
Returns a resource URI that can be used to fetch the screenshot.
|
||||
|
||||
Args:
|
||||
filename: Output filename (default: DOSBox-X auto-generates name)
|
||||
filename: Optional output filename (DOSBox-X uses auto-naming)
|
||||
|
||||
Returns:
|
||||
- success: True if capture succeeded
|
||||
- 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
|
||||
Screenshot info including resource URI for fetching the image
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Import resources module for registration
|
||||
try:
|
||||
from dosbox_mcp import resources
|
||||
from mcdosbox_x import resources
|
||||
except ImportError:
|
||||
resources = None
|
||||
|
||||
@ -241,12 +207,15 @@ def screenshot(filename: str | None = None) -> dict:
|
||||
"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:
|
||||
response["resource_uri"] = resource_uri
|
||||
if screenshot_filename:
|
||||
response["filename"] = screenshot_filename
|
||||
response["hint"] = f"Access via: dosbox://screenshots/{screenshot_filename}"
|
||||
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:
|
||||
response["filename"] = screenshot_filename
|
||||
|
||||
return response
|
||||
|
||||
@ -264,19 +233,12 @@ def serial_send(data: str, port: int = 1) -> dict:
|
||||
This is useful for RIPscrip testing - send graphics commands
|
||||
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:
|
||||
data: Data to send (text or hex with \\x prefix)
|
||||
port: COM port number (1 or 2)
|
||||
|
||||
Returns:
|
||||
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
|
||||
@ -303,26 +265,20 @@ def serial_send(data: str, port: int = 1) -> dict:
|
||||
i += 1
|
||||
return bytes(result)
|
||||
|
||||
# Validate port number
|
||||
if port not in (1, 2):
|
||||
# Map COM port to TCP port (based on dosbox.conf config)
|
||||
# serial1=nullmodem server:5555
|
||||
tcp_port_map = {
|
||||
1: 5555, # COM1
|
||||
2: 5556, # COM2 (if configured)
|
||||
}
|
||||
|
||||
if port not in tcp_port_map:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid COM port {port}. Supported: 1, 2",
|
||||
}
|
||||
|
||||
# Get TCP port from manager if running, otherwise use defaults
|
||||
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
|
||||
tcp_port = tcp_port_map[port]
|
||||
|
||||
try:
|
||||
byte_data = parse_data(data)
|
||||
@ -347,8 +303,7 @@ def serial_send(data: str, port: int = 1) -> dict:
|
||||
return {
|
||||
"success": False,
|
||||
"error": message,
|
||||
"hint": f'Ensure DOSBox-X is running with serial{port}="nullmodem". '
|
||||
f"Expected TCP port {tcp_port}. Use port_status() to check.",
|
||||
"hint": f"Ensure DOSBox-X is running with serial{port}=nullmodem server:{tcp_port}",
|
||||
}
|
||||
|
||||
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "email-validator"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "mcp"
|
||||
version = "1.26.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user