diff --git a/src/ghydramcp/mixins/docker.py b/src/ghydramcp/mixins/docker.py index 9dc1343..e5cae89 100644 --- a/src/ghydramcp/mixins/docker.py +++ b/src/ghydramcp/mixins/docker.py @@ -21,9 +21,9 @@ from typing import Any, Dict, List, Optional from fastmcp import Context from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool -# Port pool configuration +# Port pool configuration (32 ports should handle many concurrent sessions) PORT_POOL_START = 8192 -PORT_POOL_END = 8199 +PORT_POOL_END = 8223 PORT_LOCK_DIR = Path("/tmp/ghydramcp-ports") @@ -204,7 +204,7 @@ class DockerMixin(MCPMixin): with the GhydraMCP plugin pre-installed. Supports multi-process environments with: - - Dynamic port allocation from a pool (8192-8199) + - Dynamic port allocation from a pool (8192-8223) - Session-scoped container naming with UUIDs - Docker label-based tracking for cross-process visibility - Automatic cleanup of orphaned containers @@ -520,7 +520,7 @@ class DockerMixin(MCPMixin): plugin pre-installed. The binary will be imported and analyzed, then the HTTP API will be available. - Ports are automatically allocated from the pool (8192-8199) to + Ports are automatically allocated from the pool (8192-8223) to prevent conflicts between concurrent sessions. Container names are auto-generated with the session ID to ensure uniqueness. @@ -544,7 +544,7 @@ class DockerMixin(MCPMixin): port = self._port_pool.allocate(self.session_id) if port is None: return { - "error": "Port pool exhausted (8192-8199). Stop some containers first.", + "error": "Port pool exhausted (8192-8223). Stop some containers first.", "allocated_ports": self._port_pool.get_allocated_ports(), } @@ -639,6 +639,10 @@ class DockerMixin(MCPMixin): ) -> Dict[str, Any]: """Stop a GhydraMCP Docker container. + For safety, this will only stop containers that belong to the current + MCP session. Attempting to stop another session's container will fail + with an error explaining whose container it is. + Args: name_or_id: Container name or ID remove: Also remove the container (default: True) @@ -649,18 +653,34 @@ class DockerMixin(MCPMixin): if not self._check_docker_available(): return {"error": "Docker is not available on this system"} - # Find the container to get its port for pool release + # Get container's session and port labels for validation container_port = None + container_session = None try: inspect_result = self._run_docker_cmd( - ["inspect", "--format", "{{index .Config.Labels \"" + self.LABEL_PREFIX + ".port\"}}", name_or_id], + [ + "inspect", + "--format", + "{{index .Config.Labels \"" + self.LABEL_PREFIX + ".port\"}}|{{index .Config.Labels \"" + self.LABEL_PREFIX + ".session\"}}", + name_or_id, + ], check=False, ) - if inspect_result.stdout.strip().isdigit(): - container_port = int(inspect_result.stdout.strip()) + parts = inspect_result.stdout.strip().split("|") + if len(parts) >= 2: + if parts[0].isdigit(): + container_port = int(parts[0]) + container_session = parts[1] if parts[1] else None except Exception: pass + # Session validation: only allow stopping own containers + if container_session and container_session != self.session_id: + return { + "error": f"Cannot stop container '{name_or_id}' - it belongs to session '{container_session}', not this session '{self.session_id}'.", + "hint": "Each MCP session can only stop its own containers for safety.", + } + try: # Stop the container self._run_docker_cmd(["stop", name_or_id]) @@ -902,7 +922,7 @@ class DockerMixin(MCPMixin): 3. Optionally waits for the container to become healthy 4. Returns connection info for the instance - Ports are auto-allocated from the pool (8192-8199) to prevent + Ports are auto-allocated from the pool (8192-8223) to prevent conflicts between concurrent sessions. Args: @@ -1003,7 +1023,7 @@ class DockerMixin(MCPMixin): ) async def docker_cleanup( self, - session_only: bool = False, + session_only: bool = True, max_age_hours: float = 24.0, dry_run: bool = False, ctx: Optional[Context] = None, @@ -1013,8 +1033,12 @@ class DockerMixin(MCPMixin): This helps recover from crashed processes that left containers or port locks behind. + By default, only cleans containers from the current session to prevent + accidentally removing another agent's work. Set session_only=False + (with caution) to clean all GhydraMCP containers. + Args: - session_only: Only clean up containers from this session + session_only: Only clean up containers from this session (default: True for safety) max_age_hours: Max age for orphaned containers (default: 24 hours) dry_run: If True, only report what would be cleaned up