mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-20 12:19:07 +00:00
feat(mcp): ensure inner mcp environemnt variables are passed
This commit is contained in:
30
README.md
30
README.md
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -25,6 +25,7 @@ dev = [
|
||||
|
||||
[project.scripts]
|
||||
mc = "mcontainer.cli:app"
|
||||
mcx = "mcontainer.cli:session_create_entry_point"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
|
||||
Reference in New Issue
Block a user