mirror of
https://github.com/Monadical-SAS/cubbi.git
synced 2025-12-20 12:19:07 +00:00
feat(mcp): first docker proxy working
This commit is contained in:
@@ -4,14 +4,15 @@
|
||||
|
||||
This document specifies the implementation for Model Control Protocol (MCP) server support in the Monadical Container (MC) system. The MCP server feature allows users to connect, build, and manage external MCP servers that can be attached to MC sessions.
|
||||
|
||||
An MCP server is a local (stdio) or remote (HTTP SSE server) service that can be accessed by a driver (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls.
|
||||
An MCP server is a service that can be accessed by a driver (such as Goose or Claude Code) to extend the LLM's capabilities through tool calls. It can be either:
|
||||
- A local stdio-based MCP server running in a container (accessed via an SSE proxy)
|
||||
- A remote HTTP SSE server accessed directly via its URL
|
||||
|
||||
## Key Features
|
||||
|
||||
1. Support multiple types of MCP servers:
|
||||
- Remote HTTP SSE servers
|
||||
- Local container-based servers
|
||||
- Local container with MCP proxy for stdio-to-SSE conversion
|
||||
1. Support two types of MCP servers:
|
||||
- **Proxy-based MCP servers** (default): Container running an MCP stdio server with a proxy that converts to SSE
|
||||
- **Remote MCP servers**: External HTTP SSE servers accessed via URL
|
||||
|
||||
2. Persistent MCP containers that can be:
|
||||
- Started/stopped independently of sessions
|
||||
@@ -26,30 +27,25 @@ The MCP configuration will be stored in the user configuration file and will inc
|
||||
|
||||
```yaml
|
||||
mcps:
|
||||
# Proxy-based MCP server (default type)
|
||||
- name: github
|
||||
type: docker
|
||||
image: mcp/github
|
||||
command: "github-mcp"
|
||||
env:
|
||||
- GITHUB_TOKEN: "your-token-here"
|
||||
|
||||
- name: proxy-example
|
||||
type: proxy
|
||||
base_image: ghcr.io/mcp/github:latest
|
||||
proxy_image: ghcr.io/sparfenyuk/mcp-proxy:latest
|
||||
command: "github-mcp"
|
||||
base_image: mcp/github
|
||||
command: "github-mcp" # Optional command to run in the base image
|
||||
proxy_image: ghcr.io/sparfenyuk/mcp-proxy:latest # Optional, defaults to standard proxy image
|
||||
proxy_options:
|
||||
sse_port: 8080
|
||||
sse_host: "0.0.0.0"
|
||||
allow_origin: "*"
|
||||
env:
|
||||
- GITHUB_TOKEN: "your-token-here"
|
||||
GITHUB_TOKEN: "your-token-here"
|
||||
|
||||
# Remote MCP server
|
||||
- name: remote-mcp
|
||||
type: remote
|
||||
url: "http://mcp-server.example.com/sse"
|
||||
headers:
|
||||
- Authorization: "Bearer your-token-here"
|
||||
Authorization: "Bearer your-token-here"
|
||||
```
|
||||
|
||||
## CLI Commands
|
||||
@@ -57,21 +53,25 @@ mcps:
|
||||
### MCP Management
|
||||
|
||||
```
|
||||
mc mcp list # List all configured MCP servers and their status
|
||||
mc mcp status <name> # Show detailed status of a specific MCP server
|
||||
mc mcp start <name> # Start an MCP server container
|
||||
mc mcp stop <name> # Stop an MCP server container
|
||||
mc mcp restart <name> # Restart an MCP server container
|
||||
mc mcp logs <name> # Show logs for an MCP server container
|
||||
mc mcp list # List all configured MCP servers and their status
|
||||
mc mcp status <name> # Show detailed status of a specific MCP server
|
||||
mc mcp start <name> # Start an MCP server container
|
||||
mc mcp stop <name> # Stop an MCP server container
|
||||
mc mcp restart <name> # Restart an MCP server container
|
||||
mc mcp logs <name> # Show logs for an MCP server container
|
||||
```
|
||||
|
||||
### MCP Configuration
|
||||
|
||||
```
|
||||
mc mcp remote add <name> <url> [--header KEY=VALUE...] # Add a remote MCP server
|
||||
mc mcp docker add <name> <image> [--command CMD] [--env KEY=VALUE...] # Add a Docker-based MCP
|
||||
mc mcp proxy add <name> <base_image> [--proxy-image IMG] [--command CMD] [--sse-port PORT] [--sse-host HOST] [--allow-origin ORIGIN] [--env KEY=VALUE...] # Add a proxied MCP
|
||||
mc mcp remove <name> # Remove an MCP configuration
|
||||
# Add a proxy-based MCP server (default)
|
||||
mc mcp add <name> <base_image> [--command CMD] [--proxy-image IMG] [--sse-port PORT] [--sse-host HOST] [--allow-origin ORIGIN] [--env KEY=VALUE...]
|
||||
|
||||
# Add a remote MCP server
|
||||
mc mcp add-remote <name> <url> [--header KEY=VALUE...]
|
||||
|
||||
# Remove an MCP configuration
|
||||
mc mcp remove <name>
|
||||
```
|
||||
|
||||
### Session Integration
|
||||
@@ -89,14 +89,7 @@ mc session create [--mcp <name>] # Create a session with an MCP server attached
|
||||
3. MCP containers will be persistent across sessions unless explicitly stopped
|
||||
4. MCP containers will be named with a prefix to identify them (`mc_mcp_<name>`)
|
||||
|
||||
### Docker-based MCP Servers
|
||||
|
||||
For Docker-based MCP servers:
|
||||
1. Pull the specified image
|
||||
2. Create a dedicated network if it doesn't exist
|
||||
3. Run the container with the specified environment variables and command
|
||||
|
||||
### Proxy-based MCP Servers
|
||||
### Proxy-based MCP Servers (Default)
|
||||
|
||||
For proxy-based MCP servers:
|
||||
1. Create a custom Dockerfile that:
|
||||
@@ -105,7 +98,16 @@ For proxy-based MCP servers:
|
||||
- Sets up the base MCP server image
|
||||
- Configures the entrypoint to run the MCP proxy with the right parameters
|
||||
2. Build the custom image
|
||||
3. Run the container with the appropriate environment variables
|
||||
3. Run the container with:
|
||||
- The Docker socket mounted to enable Docker-in-Docker
|
||||
- Environment variables from the configuration
|
||||
- The SSE server port exposed
|
||||
|
||||
The proxy container will:
|
||||
1. Pull the base image
|
||||
2. Run the base image with the specified command
|
||||
3. Connect the stdio of the base image to the MCP proxy
|
||||
4. Expose an SSE server that clients can connect to
|
||||
|
||||
### Remote MCP Servers
|
||||
|
||||
|
||||
@@ -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