feat: add Docker-from-Docker path remapping and fix cancel-scope isolation#10
feat: add Docker-from-Docker path remapping and fix cancel-scope isolation#10nikhilaii93 wants to merge 1 commit intoelusznik:mainfrom
Conversation
…ation Add MCP_BRIDGE_DOCKER_HOST_PATH_MAP env var to translate volume mount source paths when running inside a container whose filesystem differs from the Docker daemon's view. Refactor PersistentMCPClient to run stdio_client and ClientSession context managers in a dedicated asyncio task, preventing anyio cancel-scope stack corruption during concurrent MCP tool invocations.
There was a problem hiding this comment.
Code Review
This pull request introduces Docker-from-Docker path remapping support and refactors the PersistentMCPClient to manage its lifecycle within a dedicated background task, ensuring proper isolation of cancel scopes. Feedback includes a recommendation to add an asyncio.Lock to prevent race conditions during concurrent start() calls and a suggestion to use pathlib for more robust path remapping logic to avoid incorrect prefix matching.
| self._host_task: Optional[asyncio.Task[None]] = None | ||
| self._shutdown_event: Optional[asyncio.Event] = None |
There was a problem hiding this comment.
The PersistentMCPClient should be protected against concurrent start() calls. Since start() now spawns a background _host task and initializes several asynchronous resources, multiple concurrent calls could lead to race conditions where multiple tasks are created or state (like _shutdown_event) is overwritten. It is recommended to add an asyncio.Lock and wrap the body of start() with async with self._start_lock: to ensure only one initialization sequence occurs at a time.
| self._host_task: Optional[asyncio.Task[None]] = None | |
| self._shutdown_event: Optional[asyncio.Event] = None | |
| self._host_task: Optional[asyncio.Task[None]] = None | |
| self._shutdown_event: Optional[asyncio.Event] = None | |
| self._start_lock = asyncio.Lock() |
| if path.startswith(local_prefix): | ||
| return host_prefix + path[len(local_prefix):] |
There was a problem hiding this comment.
The current path remapping logic uses a simple string prefix check, which can lead to incorrect translations if a path shares a prefix but is not a sub-directory (e.g., remapping /tmp to /mnt/tmp would incorrectly translate /tmpp/file to /mnt/tmpp/file). Using Path.relative_to ensures that remapping only occurs for actual sub-paths and correctly respects directory boundaries.
| if path.startswith(local_prefix): | |
| return host_prefix + path[len(local_prefix):] | |
| try: | |
| rel_path = Path(path).relative_to(local_prefix) | |
| return str(Path(host_prefix) / rel_path) | |
| except ValueError: | |
| return path |
Summary
MCP_BRIDGE_DOCKER_HOST_PATH_MAPenv varPersistentMCPClientby isolating context managers in a dedicated asyncio taskProblem
Docker-from-Docker path mismatch
When the bridge runs inside a container (e.g. CI environments, Docker-in-Docker setups), the local filesystem paths differ from what the Docker daemon sees on the host. Volume mount source paths like
/tmp/mcp-bridge-state/...are invalid from the daemon's perspective, causing mount failures.Cancel-scope stack corruption
PersistentMCPClient.start()entersstdio_clientandClientSessionasync context managers in the caller's asyncio task. This pushes anyio cancel scopes onto the calling task's stack. When the MCP server's request-handler responder later tries to exit its own cancel scope, the stack is corrupted, causing crashes during concurrent MCP tool invocations.Solution
DfD path remapping
New env var
MCP_BRIDGE_DOCKER_HOST_PATH_MAPaccepts alocal_prefix:host_prefixmapping (e.g./tmp:/mnt/disks/data/tmp). A helper function_remap_volume_source()translates volume mount source paths before passing them todocker run -v. When the env var is unset, behavior is unchanged.Cancel-scope isolation
Refactored
PersistentMCPClientso that all context manager entries (stdio_client.__aenter__(),ClientSession.__aenter__()) run inside a dedicated_host()asyncio task. The task stays alive after initialization (waiting on a_shutdown_event), keeping the cancel-scope stack intact.stop()signals the event so teardown happens in the same task that entered the scopes.Test plan
MCP_BRIDGE_DOCKER_HOST_PATH_MAPunset, behavior unchangedMCP_BRIDGE_DOCKER_HOST_PATH_MAP=/tmp:/mnt/disks/data/tmpPersistentMCPClientclean shutdown viastop()