debugpy_attach
Attach debugpy to running Python processes inside Docker containers for debugging and inspection. Enables process injection and breakpoint planning based on logs.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| container | Yes | ||
| pid | No | ||
| port | No | ||
| host | No | 0.0.0.0 | |
| python_bin | No | python | |
| wait_for_client | No | ||
| log_to | No | /tmp/debugpy-logs | |
| configure_subprocess | No |
Output Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/debugpy_mcp/server.py:458-500 (handler)The main handler function for the 'debugpy_attach' tool. It checks container status, verifies debugpy installation, auto-discovers target PID if not provided, checks if port is already listening, builds and executes the debugpy attach command via docker exec, and returns a structured result with status and next steps.
@mcp.tool() def debugpy_attach(container: str, pid: int | None = None, port: int = DEFAULT_PORT, host: str = DEFAULT_HOST, python_bin: str = "python", wait_for_client: bool = False, log_to: str | None = DEFAULT_DEBUGPY_LOG_DIR, configure_subprocess: bool = False) -> dict[str, Any]: notes: list[str] = [] if not docker_inspect_running(container): return DebugpyAttachResult(ok=False, container=container, port=port, host=host, notes=["Container is not running or does not exist."]).model_dump() installed, version = detect_debugpy_installed(container, python_bin=python_bin) if not installed: return DebugpyAttachResult(ok=False, container=container, port=port, host=host, notes=["debugpy is not installed inside the container.", "Install it in the image or running container before attach."]).model_dump() if version: notes.append(f"Detected debugpy {version} inside the container.") processes = get_process_table(container) if pid is None: pid, pid_notes = choose_pid(processes) notes.extend(pid_notes) if pid is None: return DebugpyAttachResult(ok=False, container=container, port=port, host=host, notes=["No candidate Python process was found to attach to."]).model_dump() if port_is_listening(container, port): mapped = docker_port_mapping(container, port) return DebugpyAttachResult(ok=True, container=container, port=port, host=host, pid=pid, already_listening=True, attached=False, mapped_port=mapped, notes=[f"Port {port} is already listening inside the container.", "Skipping injection because debugpy likely already attached."], next_steps=["Start your existing Attach configuration in Cursor.", "Verify your pathMappings match the container source path."]).model_dump() if log_to: docker_exec(container, f"mkdir -p {shlex.quote(log_to)}", timeout=10, check=False) cmd = build_debugpy_attach_cmd(python_bin=python_bin, host=host, port=port, pid=pid, wait_for_client=wait_for_client, log_to=log_to, configure_subprocess=configure_subprocess) proc = docker_exec(container, cmd, timeout=60, check=False) mapped_port = docker_port_mapping(container, port) listening = port_is_listening(container, port) if not listening: notes.append("debugpy did not appear to open the listening port after attach.") notes.append("Common causes: ptrace restrictions, wrong PID, or missing process privileges.") if "operation not permitted" in (proc.stderr or "").lower(): notes.append("The container likely lacks ptrace permission for PID attach.") next_steps = [ "Inspect stderr from the command output.", "Check container capabilities such as SYS_PTRACE and seccomp settings.", "Verify you attached to the worker process rather than a supervisor or master process.", ] else: notes.append(f"debugpy is now listening on {host}:{port} inside the container.") next_steps = [ "Start your existing Attach configuration in Cursor.", "Set breakpoints in the relevant FastAPI route, dependency, or middleware path.", "If breakpoints do not bind, verify localRoot and remoteRoot path mappings.", ] return DebugpyAttachResult(ok=listening, container=container, port=port, host=host, pid=pid, attached=listening, mapped_port=mapped_port, command=cmd, stdout=(proc.stdout or "").strip() or None, stderr=(proc.stderr or "").strip() or None, notes=notes, next_steps=next_steps).model_dump() - src/debugpy_mcp/server.py:52-66 (schema)Pydantic model DebugpyAttachResult that defines the output schema for the debugpy_attach tool, including fields for ok status, container, port, host, pid, already_listening, attached, mapped_port, command, stdout, stderr, notes, and next_steps.
class DebugpyAttachResult(BaseModel): ok: bool container: str port: int host: str pid: int | None = None already_listening: bool = False attached: bool = False mapped_port: str | None = None command: str | None = None stdout: str | None = None stderr: str | None = None notes: list[str] = Field(default_factory=list) next_steps: list[str] = Field(default_factory=list) - src/debugpy_mcp/server.py:316-326 (helper)Helper function build_debugpy_attach_cmd that constructs the shell command string for running debugpy in listen mode with the specified parameters (python binary, host, port, pid, wait_for_client, log_to, configure_subprocess).
def build_debugpy_attach_cmd(*, python_bin: str, host: str, port: int, pid: int, wait_for_client: bool, log_to: str | None, configure_subprocess: bool) -> str: parts = [ shlex.quote(python_bin), "-m", "debugpy", "--listen", f"{shlex.quote(host)}:{port}", "--configure-subProcess", "true" if configure_subprocess else "false" ] if wait_for_client: parts.append("--wait-for-client") if log_to: parts.extend(["--log-to", shlex.quote(log_to)]) parts.extend(["--pid", str(pid)]) return " ".join(parts)