feat(mcp): ensure inner mcp environemnt variables are passed

This commit is contained in:
2025-03-26 16:44:35 +01:00
parent 7805aa720e
commit 0d75bfc3d8
6 changed files with 269 additions and 155 deletions

View File

@@ -5,6 +5,14 @@ containers that run AI tools and development environments. It works with both
local Docker and a dedicated remote web service that manages containers in a
Docker-in-Docker (DinD) environment. MC also supports connecting to MCP (Model Control Protocol) servers to extend AI tools with additional capabilities.
## Quick Reference
- `mc session create` - Create a new session
- `mcx` - Shortcut for `mc session create` (mount directories or clone repos)
- `mcx .` - Mount the current directory
- `mcx /path/to/dir` - Mount a specific directory
- `mcx https://github.com/user/repo` - Clone a repository
## Requirements
- [uv](https://docs.astral.sh/uv/)
@@ -27,10 +35,12 @@ mc --help
## Basic Usage
```bash
# Create a new session with the default driver
# mc create session -- is the full command
# Show help message (displays available commands)
mc
# Create a new session with the default driver
mc session create
# List all active sessions
mc session list
@@ -50,17 +60,27 @@ mc session create -e VAR1=value1 -e VAR2=value2
mc session create -v /local/path:/container/path
mc session create -v ~/data:/data -v ./configs:/etc/app/config
# Mount a local directory (current directory or specific path)
mc session create .
mc session create /path/to/project
# Connect to external Docker networks
mc session create --network teamnet --network dbnet
# Connect to MCP servers for extended capabilities
mc session create --mcp github --mcp jira
# Shorthand for creating a session with a project repository
mc github.com/username/repo
# Clone a Git repository
mc session create https://github.com/username/repo
# Using the mcx shortcut (equivalent to mc session create)
mcx # Creates a session without mounting anything
mcx . # Mounts the current directory
mcx /path/to/project # Mounts the specified directory
mcx https://github.com/username/repo # Clones the repository
# Shorthand with MCP servers
mc github.com/username/repo --mcp github
mcx https://github.com/username/repo --mcp github
```
## Driver Management

View File

@@ -3,6 +3,7 @@ CLI for Monadical Container Tool.
"""
import os
import logging
from typing import List, Optional
import typer
from rich.console import Console
@@ -15,11 +16,18 @@ from .user_config import UserConfigManager
from .session import SessionManager
from .mcp import MCPManager
app = typer.Typer(help="Monadical Container Tool")
session_app = typer.Typer(help="Manage MC sessions")
# Configure logging - will only show logs if --verbose flag is used
logging.basicConfig(
level=logging.WARNING, # Default to WARNING level
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
handlers=[logging.StreamHandler()],
)
app = typer.Typer(help="Monadical Container Tool", no_args_is_help=True)
session_app = typer.Typer(help="Manage MC sessions", no_args_is_help=True)
driver_app = typer.Typer(help="Manage MC drivers", no_args_is_help=True)
config_app = typer.Typer(help="Manage MC configuration")
mcp_app = typer.Typer(help="Manage MCP servers")
config_app = typer.Typer(help="Manage MC configuration", no_args_is_help=True)
mcp_app = typer.Typer(help="Manage MCP servers", no_args_is_help=True)
app.add_typer(session_app, name="session", no_args_is_help=True)
app.add_typer(driver_app, name="driver", no_args_is_help=True)
app.add_typer(config_app, name="config", no_args_is_help=True)
@@ -33,21 +41,21 @@ container_manager = ContainerManager(config_manager, session_manager, user_confi
mcp_manager = MCPManager(config_manager=user_config)
@app.callback(invoke_without_command=True)
def main(ctx: typer.Context) -> None:
"""Monadical Container Tool"""
# If no command is specified, create a session
if ctx.invoked_subcommand is None:
create_session(
driver=None,
project=None,
env=[],
volume=[],
network=[],
name=None,
no_connect=False,
no_mount=False,
)
@app.callback()
def main(
ctx: typer.Context,
verbose: bool = typer.Option(
False, "--verbose", "-v", help="Enable verbose logging"
),
) -> None:
"""Monadical Container Tool
Run 'mc session create' to create a new session.
Use 'mcx' as a shortcut for 'mc session create'.
"""
# Set log level based on verbose flag
if verbose:
logging.getLogger().setLevel(logging.INFO)
@app.command()
@@ -120,8 +128,10 @@ def list_sessions() -> None:
@session_app.command("create")
def create_session(
driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"),
project: Optional[str] = typer.Option(
None, "--project", "-p", help="Project repository URL"
project: Optional[str] = typer.Argument(
None,
help="Local directory path to mount or repository URL to clone",
show_default=False,
),
env: List[str] = typer.Option(
[], "--env", "-e", help="Environment variables (KEY=VALUE)"
@@ -136,11 +146,6 @@ def create_session(
no_connect: bool = typer.Option(
False, "--no-connect", help="Don't automatically connect to the session"
),
no_mount: bool = typer.Option(
False,
"--no-mount",
help="Don't mount local directory to /app (ignored if --project is used)",
),
mcp: List[str] = typer.Option(
[],
"--mcp",
@@ -148,7 +153,12 @@ def create_session(
help="Attach MCP servers to the session (can be specified multiple times)",
),
) -> None:
"""Create a new MC session"""
"""Create a new MC session
If a local directory path is provided, it will be mounted at /app in the container.
If a repository URL is provided, it will be cloned into /app during initialization.
If no path or URL is provided, no local volume will be mounted.
"""
# Use default driver from user configuration
if not driver:
driver = user_config.get(
@@ -209,7 +219,7 @@ def create_session(
if not all_mcps:
default_mcps = user_config.get("defaults.mcps", [])
all_mcps = default_mcps
if default_mcps:
console.print(f"Using default MCP servers: {', '.join(default_mcps)}")
@@ -223,12 +233,18 @@ def create_session(
console.print(f" {host_path} -> {mount_info['bind']}")
with console.status(f"Creating session with driver '{driver}'..."):
# If project is a local directory, we should mount it
# If it's a Git URL or doesn't exist, handle accordingly
mount_local = False
if project and os.path.isdir(os.path.expanduser(project)):
mount_local = True
session = container_manager.create_session(
driver_name=driver,
project=project,
environment=environment,
session_name=name,
mount_local=not no_mount and user_config.get("defaults.mount_local", True),
mount_local=mount_local,
volumes=volume_mounts,
networks=all_networks,
mcp=all_mcps,
@@ -362,61 +378,6 @@ def stop() -> None:
os.system("kill 1") # Send SIGTERM to PID 1 (container's init process)
# Main CLI entry point that handles project repository URLs
@app.command(name="")
def quick_create(
project: Optional[str] = typer.Argument(..., help="Project repository URL"),
driver: Optional[str] = typer.Option(None, "--driver", "-d", help="Driver to use"),
env: List[str] = typer.Option(
[], "--env", "-e", help="Environment variables (KEY=VALUE)"
),
volume: List[str] = typer.Option(
[], "--volume", "-v", help="Mount volumes (LOCAL_PATH:CONTAINER_PATH)"
),
network: List[str] = typer.Option(
[], "--network", "-N", help="Connect to additional Docker networks"
),
name: Optional[str] = typer.Option(None, "--name", "-n", help="Session name"),
no_connect: bool = typer.Option(
False, "--no-connect", help="Don't automatically connect to the session"
),
no_mount: bool = typer.Option(
False,
"--no-mount",
help="Don't mount local directory to /app (ignored if a project is specified)",
),
mcp: List[str] = typer.Option(
[],
"--mcp",
"-m",
help="Attach MCP servers to the session (can be specified multiple times)",
),
) -> None:
"""Create a new MC session with a project repository"""
# Use user config for defaults if not specified
if not driver:
driver = user_config.get("defaults.driver")
# Get default MCPs if none specified
all_mcps = mcp if isinstance(mcp, list) else []
if not all_mcps:
default_mcps = user_config.get("defaults.mcps", [])
if default_mcps:
all_mcps = default_mcps
create_session(
driver=driver,
project=project,
env=env,
volume=volume,
network=network,
name=name,
no_connect=no_connect,
no_mount=no_mount,
mcp=all_mcps,
)
@driver_app.command("list")
def list_drivers() -> None:
"""List available MC drivers"""
@@ -537,6 +498,7 @@ config_app.add_typer(volume_app, name="volume", no_args_is_help=True)
config_mcp_app = typer.Typer(help="Manage default MCP servers")
config_app.add_typer(config_mcp_app, name="mcp", no_args_is_help=True)
# MCP configuration commands
@config_mcp_app.command("list")
def list_default_mcps() -> None:
@@ -555,6 +517,7 @@ def list_default_mcps() -> None:
console.print(table)
@config_mcp_app.command("add")
def add_default_mcp(
name: str = typer.Argument(..., help="MCP server name to add to defaults"),
@@ -576,6 +539,7 @@ def add_default_mcp(
user_config.set("defaults.mcps", default_mcps)
console.print(f"[green]Added MCP server '{name}' to defaults[/green]")
@config_mcp_app.command("remove")
def remove_default_mcp(
name: str = typer.Argument(..., help="MCP server name to remove from defaults"),
@@ -1017,8 +981,15 @@ def mcp_status(name: str = typer.Argument(..., help="MCP server name")) -> None:
def start_mcp(
name: Optional[str] = typer.Argument(None, help="MCP server name"),
all_servers: bool = typer.Option(False, "--all", help="Start all MCP servers"),
verbose: bool = typer.Option(
False, "--verbose", "-v", help="Enable verbose logging"
),
) -> None:
"""Start an MCP server or all servers"""
# Set log level based on verbose flag
if verbose:
logging.getLogger().setLevel(logging.INFO)
# Check if we need to start all servers
if all_servers:
# Get all configured MCP servers
@@ -1281,6 +1252,26 @@ def mcp_logs(
def remove_mcp(name: str = typer.Argument(..., help="MCP server name")) -> None:
"""Remove an MCP server configuration"""
try:
# Check if any active sessions might be using this MCP
active_sessions = container_manager.list_sessions()
affected_sessions = []
for session in active_sessions:
if session.mcps and name in session.mcps:
affected_sessions.append(session)
# Just warn users about affected sessions
if affected_sessions:
console.print(
f"[yellow]Warning: Found {len(affected_sessions)} active sessions using MCP '{name}'[/yellow]"
)
console.print(
"[yellow]You may need to restart these sessions for changes to take effect:[/yellow]"
)
for session in affected_sessions:
console.print(f" - Session: {session.id} ({session.name})")
# Remove the MCP from configuration
with console.status(f"Removing MCP server '{name}'..."):
result = mcp_manager.remove_mcp(name)
@@ -1365,7 +1356,7 @@ def add_mcp(
console.print(
f"Container port {sse_port} will be bound to host port {assigned_port}"
)
if not no_default:
console.print(f"MCP server '{name}' added to defaults")
else:
@@ -1400,10 +1391,12 @@ def add_remote_mcp(
try:
with console.status(f"Adding remote MCP server '{name}'..."):
mcp_manager.add_remote_mcp(name, url, headers, add_as_default=not no_default)
mcp_manager.add_remote_mcp(
name, url, headers, add_as_default=not no_default
)
console.print(f"[green]Added remote MCP server '{name}'[/green]")
if not no_default:
console.print(f"MCP server '{name}' added to defaults")
else:
@@ -1861,5 +1854,27 @@ exec npm start
console.print("[green]MCP Inspector stopped[/green]")
def session_create_entry_point():
"""Entry point that directly invokes 'mc session create'.
This provides a convenient shortcut:
- 'mcx' runs as if you typed 'mc session create'
- 'mcx .' mounts the current directory
- 'mcx /path/to/project' mounts the specified directory
- 'mcx repo-url' clones the repository
All command-line options are passed through to 'session create'.
"""
import sys
# Save the program name (e.g., 'mcx')
prog_name = sys.argv[0]
# Insert 'session' and 'create' commands before any other arguments
sys.argv.insert(1, "session")
sys.argv.insert(2, "create")
# Run the app with the modified arguments
app(prog_name=prog_name)
if __name__ == "__main__":
app()

View File

@@ -141,7 +141,7 @@ class ContainerManager:
project: Optional[str] = None,
environment: Optional[Dict[str, str]] = None,
session_name: Optional[str] = None,
mount_local: bool = True,
mount_local: bool = False,
volumes: Optional[Dict[str, Dict[str, str]]] = None,
networks: Optional[List[str]] = None,
mcp: Optional[List[str]] = None,
@@ -150,10 +150,10 @@ class ContainerManager:
Args:
driver_name: The name of the driver to use
project: Optional project repository URL
project: Optional project repository URL or local directory path
environment: Optional environment variables
session_name: Optional session name
mount_local: Whether to mount the current directory to /app
mount_local: Whether to mount the specified local directory to /app (ignored if project is None)
volumes: Optional additional volumes to mount (dict of {host_path: {"bind": container_path, "mode": mode}})
networks: Optional list of additional Docker networks to connect to
mcp: Optional list of MCP server names to attach to the session
@@ -203,16 +203,29 @@ class ContainerManager:
# Set up volume mounts
session_volumes = {}
# If project URL is provided, don't mount local directory (will clone into /app)
# If no project URL and mount_local is True, mount local directory to /app
if not project and mount_local:
# Mount current directory to /app in the container
current_dir = os.getcwd()
session_volumes[current_dir] = {"bind": "/app", "mode": "rw"}
print(f"Mounting local directory {current_dir} to /app")
elif project:
# Determine if project is a local directory or a Git repository
is_local_directory = False
is_git_repo = False
if project:
# Check if project is a local directory
if os.path.isdir(os.path.expanduser(project)):
is_local_directory = True
else:
# If not a local directory, assume it's a Git repo URL
is_git_repo = True
# Handle mounting based on project type
if is_local_directory and mount_local:
# Mount the specified local directory to /app in the container
local_dir = os.path.abspath(os.path.expanduser(project))
session_volumes[local_dir] = {"bind": "/app", "mode": "rw"}
print(f"Mounting local directory {local_dir} to /app")
# Clear project for container environment since we're mounting
project = None
elif is_git_repo:
print(
f"Project URL provided - container will clone {project} into /app during initialization"
f"Git repository URL provided - container will clone {project} into /app during initialization"
)
# Add user-specified volumes
@@ -220,13 +233,13 @@ class ContainerManager:
for host_path, mount_spec in volumes.items():
container_path = mount_spec["bind"]
# Check for conflicts with /app mount
if container_path == "/app" and not project and mount_local:
if container_path == "/app" and is_local_directory and mount_local:
print(
"[yellow]Warning: Volume mount to /app conflicts with automatic local directory mount. User-specified mount takes precedence.[/yellow]"
"[yellow]Warning: Volume mount to /app conflicts with local directory mount. User-specified mount takes precedence.[/yellow]"
)
# Remove the automatic mount if there's a conflict
if current_dir in session_volumes:
del session_volumes[current_dir]
# Remove the local directory mount if there's a conflict
if local_dir in session_volumes:
del session_volumes[local_dir]
# Add the volume
session_volumes[host_path] = mount_spec
@@ -309,9 +322,11 @@ class ContainerManager:
try:
print(f"Ensuring MCP server '{mcp_name}' is running...")
self.mcp_manager.start_mcp(mcp_name)
# Store container name for later network connection
container_name = self.mcp_manager.get_mcp_container_name(mcp_name)
container_name = self.mcp_manager.get_mcp_container_name(
mcp_name
)
mcp_container_names.append(container_name)
# Get MCP status to extract endpoint information
@@ -438,25 +453,29 @@ class ContainerManager:
)
except DockerException as e:
print(f"Error connecting to network {network_name}: {e}")
# Reload the container to get updated network information
container.reload()
# Connect directly to each MCP's dedicated network
for mcp_name in mcp_names:
try:
# Get the dedicated network for this MCP
dedicated_network_name = f"mc-mcp-{mcp_name}-network"
try:
network = self.client.networks.get(dedicated_network_name)
# Connect the session container to the MCP's dedicated network
network.connect(container, aliases=[session_name])
print(f"Connected session to MCP '{mcp_name}' via dedicated network: {dedicated_network_name}")
print(
f"Connected session to MCP '{mcp_name}' via dedicated network: {dedicated_network_name}"
)
except DockerException as e:
print(f"Error connecting to MCP dedicated network '{dedicated_network_name}': {e}")
print(
f"Error connecting to MCP dedicated network '{dedicated_network_name}': {e}"
)
except Exception as e:
print(f"Error connecting session to MCP '{mcp_name}': {e}")
@@ -464,7 +483,13 @@ class ContainerManager:
if networks:
for network_name in networks:
# Check if already connected to this network
if network_name not in [net.name for net in container.attrs.get("NetworkSettings", {}).get("Networks", {}).values()]:
# NetworkSettings.Networks contains a dict where keys are network names
existing_networks = (
container.attrs.get("NetworkSettings", {})
.get("Networks", {})
.keys()
)
if network_name not in existing_networks:
try:
# Get or create the network
try:

View File

@@ -42,7 +42,8 @@ COPY update-goose-config.sh /usr/local/bin/update-goose-config.sh
# Extend env via bashrc
# Make scripts executable
RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh /usr/local/bin/update-goose-config.sh
RUN chmod +x /mc-init.sh /entrypoint.sh /init-status.sh \
/usr/local/bin/update-goose-config.sh
# Set up initialization status check on login
RUN echo 'export PATH=/root/.local/bin:$PATH' >> /etc/bash.bashrc

View File

@@ -43,13 +43,13 @@ class MCPManager:
if not networks:
self.client.networks.create(network_name, driver="bridge")
return network_name
def _get_mcp_dedicated_network(self, mcp_name: str) -> str:
"""Get or create a dedicated network for direct session-to-MCP connections.
Args:
mcp_name: The name of the MCP server
Returns:
The name of the dedicated network
"""
@@ -74,16 +74,20 @@ class MCPManager:
return None
def add_remote_mcp(
self, name: str, url: str, headers: Dict[str, str] = None, add_as_default: bool = True
self,
name: str,
url: str,
headers: Dict[str, str] = None,
add_as_default: bool = True,
) -> Dict[str, Any]:
"""Add a remote MCP server.
Args:
name: Name of the MCP server
url: URL of the remote MCP server
headers: HTTP headers to use when connecting
add_as_default: Whether to add this MCP to the default MCPs list
Returns:
The MCP configuration dictionary
"""
@@ -106,7 +110,7 @@ class MCPManager:
# Save the configuration
self.config_manager.set("mcps", mcps)
# Add to default MCPs if requested
if add_as_default:
default_mcps = self.config_manager.get("defaults.mcps", [])
@@ -117,17 +121,22 @@ class MCPManager:
return mcp_config
def add_docker_mcp(
self, name: str, image: str, command: str, env: Dict[str, str] = None, add_as_default: bool = True
self,
name: str,
image: str,
command: str,
env: Dict[str, str] = None,
add_as_default: bool = True,
) -> Dict[str, Any]:
"""Add a Docker-based MCP server.
Args:
name: Name of the MCP server
image: Docker image for the MCP server
command: Command to run in the container
env: Environment variables to set in the container
add_as_default: Whether to add this MCP to the default MCPs list
Returns:
The MCP configuration dictionary
"""
@@ -151,7 +160,7 @@ class MCPManager:
# Save the configuration
self.config_manager.set("mcps", mcps)
# Add to default MCPs if requested
if add_as_default:
default_mcps = self.config_manager.get("defaults.mcps", [])
@@ -173,7 +182,7 @@ class MCPManager:
add_as_default: bool = True,
) -> Dict[str, Any]:
"""Add a proxy-based MCP server.
Args:
name: Name of the MCP server
base_image: Base Docker image running the actual MCP server
@@ -183,7 +192,7 @@ class MCPManager:
env: Environment variables to set in the container
host_port: Host port to bind the MCP server to (auto-assigned if not specified)
add_as_default: Whether to add this MCP to the default MCPs list
Returns:
The MCP configuration dictionary
"""
@@ -228,7 +237,7 @@ class MCPManager:
# Save the configuration
self.config_manager.set("mcps", mcps)
# Add to default MCPs if requested
if add_as_default:
default_mcps = self.config_manager.get("defaults.mcps", [])
@@ -240,10 +249,10 @@ class MCPManager:
def remove_mcp(self, name: str) -> bool:
"""Remove an MCP server configuration.
Args:
name: Name of the MCP server to remove
Returns:
True if the MCP was successfully removed, False otherwise
"""
@@ -258,7 +267,7 @@ class MCPManager:
# Save the updated configuration
self.config_manager.set("mcps", updated_mcps)
# Also remove from default MCPs if it's there
default_mcps = self.config_manager.get("defaults.mcps", [])
if name in default_mcps:
@@ -370,20 +379,22 @@ class MCPManager:
},
)
# Connect to the inspector network
# Connect to the inspector network
network = self.client.networks.get(network_name)
network.connect(container, aliases=[name])
logger.info(
f"Connected MCP server '{name}' to inspector network {network_name} with alias '{name}'"
)
# Create and connect to a dedicated network for session connections
dedicated_network_name = self._get_mcp_dedicated_network(name)
try:
dedicated_network = self.client.networks.get(dedicated_network_name)
except DockerException:
dedicated_network = self.client.networks.create(dedicated_network_name, driver="bridge")
dedicated_network = self.client.networks.create(
dedicated_network_name, driver="bridge"
)
dedicated_network.connect(container, aliases=[name])
logger.info(
f"Connected MCP server '{name}' to dedicated network {dedicated_network_name} with alias '{name}'"
@@ -400,6 +411,7 @@ class MCPManager:
with tempfile.TemporaryDirectory() as tmp_dir:
# Create entrypoint script for mcp-proxy that runs the base MCP image
entrypoint_script = """#!/bin/sh
set -x
echo "Starting MCP proxy with base image $MCP_BASE_IMAGE (command: $MCP_COMMAND) on port $SSE_PORT"
# Verify if Docker socket is available
@@ -453,19 +465,44 @@ echo "Running MCP server from image $MCP_BASE_IMAGE with command: $CMD"
# Run the actual MCP server in the base image and pipe its I/O to mcp-proxy
# Using docker run without -d to keep stdio connected
# Build env vars string to pass through to the inner container
ENV_ARGS=""
# Check if the environment variable names file exists
if [ -f "/mcp-envs.txt" ]; then
# Read env var names from file and pass them to docker
while read -r var_name; do
# Skip empty lines
if [ -n "$var_name" ]; then
# Simply add the env var - Docker will only pass it if it exists
ENV_ARGS="$ENV_ARGS -e $var_name"
fi
done < "/mcp-envs.txt"
echo "Passing environment variables from mcp-envs.txt: $ENV_ARGS"
fi
exec mcp-proxy \
--sse-port "$SSE_PORT" \
--sse-host "$SSE_HOST" \
--allow-origin "$ALLOW_ORIGIN" \
--pass-environment \
-- \
docker run --rm -i "$MCP_BASE_IMAGE" $CMD
docker run --rm -i $ENV_ARGS "$MCP_BASE_IMAGE" $CMD
"""
# Write the entrypoint script
entrypoint_path = os.path.join(tmp_dir, "entrypoint.sh")
with open(entrypoint_path, "w") as f:
f.write(entrypoint_script)
# Create a file with environment variable names (no values)
env_names_path = os.path.join(tmp_dir, "mcp-envs.txt")
with open(env_names_path, "w") as f:
# Write one env var name per line
for env_name in mcp_config.get("env", {}).keys():
f.write(f"{env_name}\n")
# Create a Dockerfile for the proxy
dockerfile_content = f"""
FROM {mcp_config["proxy_image"]}
@@ -489,7 +526,8 @@ ENV DEBUG=1
# Add environment variables from the configuration
{chr(10).join([f'ENV {k}="{v}"' for k, v in mcp_config.get("env", {}).items()])}
# Add entrypoint script
# Add env names file and entrypoint script
COPY mcp-envs.txt /mcp-envs.txt
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
@@ -545,20 +583,22 @@ ENTRYPOINT ["/entrypoint.sh"]
ports=port_bindings, # Bind the SSE port to the host if configured
)
# Connect to the inspector network
# Connect to the inspector network
network = self.client.networks.get(network_name)
network.connect(container, aliases=[name])
logger.info(
f"Connected MCP server '{name}' to inspector network {network_name} with alias '{name}'"
)
# Create and connect to a dedicated network for session connections
dedicated_network_name = self._get_mcp_dedicated_network(name)
try:
dedicated_network = self.client.networks.get(dedicated_network_name)
except DockerException:
dedicated_network = self.client.networks.create(dedicated_network_name, driver="bridge")
dedicated_network = self.client.networks.create(
dedicated_network_name, driver="bridge"
)
dedicated_network.connect(container, aliases=[name])
logger.info(
f"Connected MCP server '{name}' to dedicated network {dedicated_network_name} with alias '{name}'"
@@ -574,14 +614,25 @@ ENTRYPOINT ["/entrypoint.sh"]
raise ValueError(f"Unsupported MCP type: {mcp_type}")
def stop_mcp(self, name: str) -> bool:
"""Stop an MCP server container."""
if not self.client:
raise Exception("Docker client is not available")
"""Stop an MCP server container.
# Get the MCP configuration
Args:
name: The name of the MCP server to stop
Returns:
True if the operation was successful (including cases where the container doesn't exist)
"""
if not self.client:
logger.warning("Docker client is not available")
return False
# Get the MCP configuration - don't raise an exception if not found
mcp_config = self.get_mcp(name)
if not mcp_config:
raise ValueError(f"MCP server '{name}' not found")
logger.warning(
f"MCP server '{name}' not found, but continuing with removal"
)
return True
# Remote MCPs don't have containers to stop
if mcp_config.get("type") == "remote":
@@ -605,12 +656,13 @@ ENTRYPOINT ["/entrypoint.sh"]
return True
except NotFound:
# Container doesn't exist
# Container doesn't exist - this is fine when removing
logger.info(f"MCP container '{name}' not found, nothing to stop or remove")
return False
return True
except Exception as e:
# Log the error but don't fail the removal operation
logger.error(f"Error stopping/removing MCP container: {e}")
return False
return True # Return true anyway to continue with removal
def restart_mcp(self, name: str) -> Dict[str, Any]:
"""Restart an MCP server container."""

View File

@@ -25,6 +25,7 @@ dev = [
[project.scripts]
mc = "mcontainer.cli:app"
mcx = "mcontainer.cli:session_create_entry_point"
[tool.ruff]
line-length = 88