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:
Ryan Malloy 2026-01-28 12:34:03 -07:00
parent 68e8d3c4c4
commit 6805905287
29 changed files with 151 additions and 324 deletions

View File

@ -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 \

View File

@ -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

View File

@ -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:

View File

@ -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(

View File

@ -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
# =============================================================================

View File

@ -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_*

View File

@ -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

View File

@ -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",
}

View File

@ -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
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
response["hint"] = f"Access via: dosbox://screenshots/{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
View File

@ -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"