Skip to content

feat: add Docker-from-Docker path remapping and fix cancel-scope isolation#10

Open
nikhilaii93 wants to merge 1 commit intoelusznik:mainfrom
nikhilaii93:nikhilaii93-dfd-and-cancel-scope-fixes
Open

feat: add Docker-from-Docker path remapping and fix cancel-scope isolation#10
nikhilaii93 wants to merge 1 commit intoelusznik:mainfrom
nikhilaii93:nikhilaii93-dfd-and-cancel-scope-fixes

Conversation

@nikhilaii93
Copy link
Copy Markdown

Summary

  • Add Docker-from-Docker (DfD) volume path remapping via MCP_BRIDGE_DOCKER_HOST_PATH_MAP env var
  • Fix anyio cancel-scope stack corruption in PersistentMCPClient by isolating context managers in a dedicated asyncio task

Problem

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() enters stdio_client and ClientSession async 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_MAP accepts a local_prefix:host_prefix mapping (e.g. /tmp:/mnt/disks/data/tmp). A helper function _remap_volume_source() translates volume mount source paths before passing them to docker run -v. When the env var is unset, behavior is unchanged.

Cancel-scope isolation

Refactored PersistentMCPClient so 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

  • Verify bridge works in standard Docker/Podman setup (no DfD) - MCP_BRIDGE_DOCKER_HOST_PATH_MAP unset, behavior unchanged
  • Verify bridge works in Docker-from-Docker setup with MCP_BRIDGE_DOCKER_HOST_PATH_MAP=/tmp:/mnt/disks/data/tmp
  • Verify concurrent MCP tool invocations no longer crash with cancel-scope errors
  • Verify PersistentMCPClient clean shutdown via stop()

…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.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +641 to +642
self._host_task: Optional[asyncio.Task[None]] = None
self._shutdown_event: Optional[asyncio.Event] = None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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()

Comment on lines +2005 to +2006
if path.startswith(local_prefix):
return host_prefix + path[len(local_prefix):]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant