mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-21 20:59:05 +00:00
feat(mcp): first docker proxy working
This commit is contained in:
@@ -769,14 +769,6 @@ def remove_volume(
|
||||
|
||||
# MCP Management Commands
|
||||
|
||||
mcp_remote_app = typer.Typer(help="Manage remote MCP servers")
|
||||
mcp_docker_app = typer.Typer(help="Manage Docker-based MCP servers")
|
||||
mcp_proxy_app = typer.Typer(help="Manage proxy-based MCP servers")
|
||||
|
||||
mcp_app.add_typer(mcp_remote_app, name="remote", no_args_is_help=True)
|
||||
mcp_app.add_typer(mcp_docker_app, name="docker", no_args_is_help=True)
|
||||
mcp_app.add_typer(mcp_proxy_app, name="proxy", no_args_is_help=True)
|
||||
|
||||
|
||||
@mcp_app.command("list")
|
||||
def list_mcps() -> None:
|
||||
@@ -1000,7 +992,59 @@ def remove_mcp(name: str = typer.Argument(..., help="MCP server name")) -> None:
|
||||
console.print(f"[red]Error removing MCP server: {e}[/red]")
|
||||
|
||||
|
||||
@mcp_remote_app.command("add")
|
||||
@mcp_app.command("add")
|
||||
def add_mcp(
|
||||
name: str = typer.Argument(..., help="MCP server name"),
|
||||
base_image: str = typer.Argument(..., help="Base MCP Docker image"),
|
||||
proxy_image: str = typer.Option(
|
||||
"ghcr.io/sparfenyuk/mcp-proxy:latest",
|
||||
"--proxy-image",
|
||||
help="Proxy image for MCP",
|
||||
),
|
||||
command: str = typer.Option(
|
||||
"", "--command", "-c", help="Command to run in the container"
|
||||
),
|
||||
sse_port: int = typer.Option(8080, "--sse-port", help="Port for SSE server"),
|
||||
sse_host: str = typer.Option("0.0.0.0", "--sse-host", help="Host for SSE server"),
|
||||
allow_origin: str = typer.Option(
|
||||
"*", "--allow-origin", help="CORS allow-origin header"
|
||||
),
|
||||
env: List[str] = typer.Option(
|
||||
[], "--env", "-e", help="Environment variables (format: KEY=VALUE)"
|
||||
),
|
||||
) -> None:
|
||||
"""Add a proxy-based MCP server (default type)"""
|
||||
# Parse environment variables
|
||||
environment = {}
|
||||
for var in env:
|
||||
if "=" in var:
|
||||
key, value = var.split("=", 1)
|
||||
environment[key] = value
|
||||
else:
|
||||
console.print(
|
||||
f"[yellow]Warning: Ignoring invalid environment variable format: {var}[/yellow]"
|
||||
)
|
||||
|
||||
# Prepare proxy options
|
||||
proxy_options = {
|
||||
"sse_port": sse_port,
|
||||
"sse_host": sse_host,
|
||||
"allow_origin": allow_origin,
|
||||
}
|
||||
|
||||
try:
|
||||
with console.status(f"Adding MCP server '{name}'..."):
|
||||
mcp_manager.add_proxy_mcp(
|
||||
name, base_image, proxy_image, command, proxy_options, environment
|
||||
)
|
||||
|
||||
console.print(f"[green]Added MCP server '{name}'[/green]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error adding MCP server: {e}[/red]")
|
||||
|
||||
|
||||
@mcp_app.command("add-remote")
|
||||
def add_remote_mcp(
|
||||
name: str = typer.Argument(..., help="MCP server name"),
|
||||
url: str = typer.Argument(..., help="URL of the remote MCP server"),
|
||||
@@ -1022,7 +1066,7 @@ def add_remote_mcp(
|
||||
|
||||
try:
|
||||
with console.status(f"Adding remote MCP server '{name}'..."):
|
||||
result = mcp_manager.add_remote_mcp(name, url, headers)
|
||||
mcp_manager.add_remote_mcp(name, url, headers)
|
||||
|
||||
console.print(f"[green]Added remote MCP server '{name}'[/green]")
|
||||
|
||||
@@ -1030,90 +1074,5 @@ def add_remote_mcp(
|
||||
console.print(f"[red]Error adding remote MCP server: {e}[/red]")
|
||||
|
||||
|
||||
@mcp_docker_app.command("add")
|
||||
def add_docker_mcp(
|
||||
name: str = typer.Argument(..., help="MCP server name"),
|
||||
image: str = typer.Argument(..., help="Docker image for the MCP server"),
|
||||
command: str = typer.Option(
|
||||
"", "--command", "-c", help="Command to run in the container"
|
||||
),
|
||||
env: List[str] = typer.Option(
|
||||
[], "--env", "-e", help="Environment variables (format: KEY=VALUE)"
|
||||
),
|
||||
) -> None:
|
||||
"""Add a Docker-based MCP server"""
|
||||
# Parse environment variables
|
||||
environment = {}
|
||||
for var in env:
|
||||
if "=" in var:
|
||||
key, value = var.split("=", 1)
|
||||
environment[key] = value
|
||||
else:
|
||||
console.print(
|
||||
f"[yellow]Warning: Ignoring invalid environment variable format: {var}[/yellow]"
|
||||
)
|
||||
|
||||
try:
|
||||
with console.status(f"Adding Docker-based MCP server '{name}'..."):
|
||||
result = mcp_manager.add_docker_mcp(name, image, command, environment)
|
||||
|
||||
console.print(f"[green]Added Docker-based MCP server '{name}'[/green]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error adding Docker-based MCP server: {e}[/red]")
|
||||
|
||||
|
||||
@mcp_proxy_app.command("add")
|
||||
def add_proxy_mcp(
|
||||
name: str = typer.Argument(..., help="MCP server name"),
|
||||
base_image: str = typer.Argument(..., help="Base MCP Docker image"),
|
||||
proxy_image: str = typer.Option(
|
||||
"ghcr.io/sparfenyuk/mcp-proxy:latest",
|
||||
"--proxy-image",
|
||||
help="Proxy image for MCP",
|
||||
),
|
||||
command: str = typer.Option(
|
||||
"", "--command", "-c", help="Command to run in the container"
|
||||
),
|
||||
sse_port: int = typer.Option(8080, "--sse-port", help="Port for SSE server"),
|
||||
sse_host: str = typer.Option("0.0.0.0", "--sse-host", help="Host for SSE server"),
|
||||
allow_origin: str = typer.Option(
|
||||
"*", "--allow-origin", help="CORS allow-origin header"
|
||||
),
|
||||
env: List[str] = typer.Option(
|
||||
[], "--env", "-e", help="Environment variables (format: KEY=VALUE)"
|
||||
),
|
||||
) -> None:
|
||||
"""Add a proxy-based MCP server"""
|
||||
# Parse environment variables
|
||||
environment = {}
|
||||
for var in env:
|
||||
if "=" in var:
|
||||
key, value = var.split("=", 1)
|
||||
environment[key] = value
|
||||
else:
|
||||
console.print(
|
||||
f"[yellow]Warning: Ignoring invalid environment variable format: {var}[/yellow]"
|
||||
)
|
||||
|
||||
# Prepare proxy options
|
||||
proxy_options = {
|
||||
"sse_port": sse_port,
|
||||
"sse_host": sse_host,
|
||||
"allow_origin": allow_origin,
|
||||
}
|
||||
|
||||
try:
|
||||
with console.status(f"Adding proxy-based MCP server '{name}'..."):
|
||||
result = mcp_manager.add_proxy_mcp(
|
||||
name, base_image, proxy_image, command, proxy_options, environment
|
||||
)
|
||||
|
||||
console.print(f"[green]Added proxy-based MCP server '{name}'[/green]")
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error adding proxy-based MCP server: {e}[/red]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
@@ -288,7 +288,7 @@ class ContainerManager:
|
||||
mcp_names = []
|
||||
|
||||
# Ensure MCP is a list
|
||||
mcps_to_process = mcp or []
|
||||
mcps_to_process = mcp if isinstance(mcp, list) else []
|
||||
|
||||
# Process each MCP
|
||||
for mcp_name in mcps_to_process:
|
||||
|
||||
@@ -242,20 +242,102 @@ class MCPManager:
|
||||
elif mcp_type == "proxy":
|
||||
# For proxy, we need to create a custom Dockerfile and build an image
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
# Create entrypoint script for mcp-proxy that runs the base MCP image
|
||||
entrypoint_script = """#!/bin/sh
|
||||
echo "Starting MCP proxy with base image $MCP_BASE_IMAGE (command: $MCP_COMMAND) on port $SSE_PORT"
|
||||
|
||||
# Verify if Docker socket is available
|
||||
if [ ! -S /var/run/docker.sock ]; then
|
||||
echo "ERROR: Docker socket not available. Cannot run base MCP image."
|
||||
echo "Make sure the Docker socket is mounted from the host."
|
||||
|
||||
# Create a minimal fallback server for testing
|
||||
cat > /tmp/fallback_server.py << 'EOF'
|
||||
import json, sys, time
|
||||
print(json.dumps({"type": "ready", "message": "Fallback server - Docker socket not available"}))
|
||||
sys.stdout.flush()
|
||||
while True:
|
||||
line = sys.stdin.readline().strip()
|
||||
if line:
|
||||
try:
|
||||
data = json.loads(line)
|
||||
if data.get("type") == "ping":
|
||||
print(json.dumps({"type": "pong", "id": data.get("id")}))
|
||||
else:
|
||||
print(json.dumps({"type": "error", "message": "Docker socket not available"}))
|
||||
except:
|
||||
print(json.dumps({"type": "error"}))
|
||||
sys.stdout.flush()
|
||||
time.sleep(1)
|
||||
EOF
|
||||
|
||||
exec mcp-proxy \
|
||||
--sse-port "$SSE_PORT" \
|
||||
--sse-host "$SSE_HOST" \
|
||||
--allow-origin "$ALLOW_ORIGIN" \
|
||||
--pass-environment \
|
||||
-- \
|
||||
python /tmp/fallback_server.py
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Pull the base MCP image
|
||||
echo "Pulling base MCP image: $MCP_BASE_IMAGE"
|
||||
docker pull "$MCP_BASE_IMAGE" || true
|
||||
|
||||
# Prepare the command to run the MCP server
|
||||
if [ -n "$MCP_COMMAND" ]; then
|
||||
CMD="$MCP_COMMAND"
|
||||
else
|
||||
# Default to empty if no command specified
|
||||
CMD=""
|
||||
fi
|
||||
|
||||
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
|
||||
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
|
||||
"""
|
||||
# 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 Dockerfile for the proxy
|
||||
dockerfile_content = f"""
|
||||
FROM {mcp_config["proxy_image"]}
|
||||
FROM {mcp_config["proxy_image"]}
|
||||
|
||||
# Set environment variables for the proxy
|
||||
ENV MCP_BASE_IMAGE={mcp_config["base_image"]}
|
||||
ENV MCP_COMMAND={mcp_config["command"]}
|
||||
ENV SSE_PORT={mcp_config["proxy_options"].get("sse_port", 8080)}
|
||||
ENV SSE_HOST={mcp_config["proxy_options"].get("sse_host", "0.0.0.0")}
|
||||
ENV ALLOW_ORIGIN={mcp_config["proxy_options"].get("allow_origin", "*")}
|
||||
# Install Docker CLI (trying multiple package managers to handle different base images)
|
||||
USER root
|
||||
RUN (apt-get update && apt-get install -y docker.io) || \\
|
||||
(apt-get update && apt-get install -y docker-ce-cli) || \\
|
||||
(apk add --no-cache docker-cli) || \\
|
||||
(yum install -y docker) || \\
|
||||
echo "WARNING: Could not install Docker CLI - will fall back to minimal MCP server"
|
||||
|
||||
# Add environment variables from the configuration
|
||||
{chr(10).join([f'ENV {k}="{v}"' for k, v in mcp_config.get("env", {}).items()])}
|
||||
"""
|
||||
# Set environment variables for the proxy
|
||||
ENV MCP_BASE_IMAGE={mcp_config["base_image"]}
|
||||
ENV MCP_COMMAND={mcp_config.get("command", "")}
|
||||
ENV SSE_PORT={mcp_config["proxy_options"].get("sse_port", 8080)}
|
||||
ENV SSE_HOST={mcp_config["proxy_options"].get("sse_host", "0.0.0.0")}
|
||||
ENV ALLOW_ORIGIN={mcp_config["proxy_options"].get("allow_origin", "*")}
|
||||
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
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
"""
|
||||
|
||||
# Write the Dockerfile
|
||||
dockerfile_path = os.path.join(tmp_dir, "Dockerfile")
|
||||
@@ -271,12 +353,25 @@ class MCPManager:
|
||||
rm=True,
|
||||
)
|
||||
|
||||
# Format command for the Docker entrypoint arguments
|
||||
# The MCP proxy container will handle this internally based on
|
||||
# the MCP_BASE_IMAGE and MCP_COMMAND env vars we set
|
||||
logger.info(
|
||||
f"Starting MCP proxy with base_image={mcp_config['base_image']}, command={mcp_config.get('command', '')}"
|
||||
)
|
||||
|
||||
# Create and start the container
|
||||
container = self.client.containers.run(
|
||||
image=custom_image_name,
|
||||
name=container_name,
|
||||
detach=True,
|
||||
network=network_name,
|
||||
volumes={
|
||||
"/var/run/docker.sock": {
|
||||
"bind": "/var/run/docker.sock",
|
||||
"mode": "rw",
|
||||
}
|
||||
},
|
||||
labels={
|
||||
"mc.mcp": "true",
|
||||
"mc.mcp.name": name,
|
||||
|
||||
Reference in New Issue
Block a user