- Add portainer-environments server for environment/endpoint management - Add portainer-docker server for Docker and Swarm container operations - Add merged portainer server combining core + teams functionality - Fix JSON schema issues and API compatibility - Add comprehensive documentation for each server - Add .gitignore and .env.example for security 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1431 lines
61 KiB
Python
Executable File
1431 lines
61 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""MCP server for Portainer Docker and Docker Swarm management."""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import asyncio
|
|
from typing import Any, Dict, List, Optional
|
|
from enum import Enum
|
|
|
|
# Suppress all logging to stderr
|
|
os.environ["MCP_MODE"] = "true"
|
|
|
|
import mcp.server.stdio
|
|
import mcp.types as types
|
|
from mcp.server import NotificationOptions, Server
|
|
from mcp.server.models import InitializationOptions
|
|
|
|
# Create server
|
|
server = Server("portainer-docker")
|
|
|
|
# Store for our state
|
|
portainer_url = os.getenv("PORTAINER_URL", "https://partner.portainer.live")
|
|
api_key = os.getenv("PORTAINER_API_KEY", "")
|
|
|
|
|
|
@server.list_tools()
|
|
async def handle_list_tools() -> list[types.Tool]:
|
|
"""List available tools."""
|
|
return [
|
|
# Container Management
|
|
types.Tool(
|
|
name="list_containers",
|
|
description="List containers in an environment",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"all": {
|
|
"type": "boolean",
|
|
"description": "Show all containers (default shows only running)",
|
|
"default": False
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="inspect_container",
|
|
description="Get detailed information about a container",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"container_id": {
|
|
"type": "string",
|
|
"description": "Container ID or name"
|
|
}
|
|
},
|
|
"required": ["environment_id", "container_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="create_container",
|
|
description="Create a new container",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"image": {
|
|
"type": "string",
|
|
"description": "Docker image to use"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Container name (optional)"
|
|
},
|
|
"command": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Command to run (optional)"
|
|
},
|
|
"env": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Environment variables in KEY=VALUE format (optional)"
|
|
},
|
|
"ports": {
|
|
"type": "object",
|
|
"description": "Port bindings (e.g., {'80/tcp': [{'HostPort': '8080'}]})"
|
|
},
|
|
"volumes": {
|
|
"type": "object",
|
|
"description": "Volume bindings (e.g., {'/host/path': {'bind': '/container/path', 'mode': 'rw'}})"
|
|
},
|
|
"restart_policy": {
|
|
"type": "string",
|
|
"enum": ["no", "always", "unless-stopped", "on-failure"],
|
|
"default": "no"
|
|
}
|
|
},
|
|
"required": ["environment_id", "image"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="start_container",
|
|
description="Start a stopped container",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"container_id": {
|
|
"type": "string",
|
|
"description": "Container ID or name"
|
|
}
|
|
},
|
|
"required": ["environment_id", "container_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="stop_container",
|
|
description="Stop a running container",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"container_id": {
|
|
"type": "string",
|
|
"description": "Container ID or name"
|
|
}
|
|
},
|
|
"required": ["environment_id", "container_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="restart_container",
|
|
description="Restart a container",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"container_id": {
|
|
"type": "string",
|
|
"description": "Container ID or name"
|
|
}
|
|
},
|
|
"required": ["environment_id", "container_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="remove_container",
|
|
description="Remove a container",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"container_id": {
|
|
"type": "string",
|
|
"description": "Container ID or name"
|
|
},
|
|
"force": {
|
|
"type": "boolean",
|
|
"description": "Force removal of running container",
|
|
"default": False
|
|
}
|
|
},
|
|
"required": ["environment_id", "container_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="get_container_logs",
|
|
description="Get logs from a container",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"container_id": {
|
|
"type": "string",
|
|
"description": "Container ID or name"
|
|
},
|
|
"tail": {
|
|
"type": "integer",
|
|
"description": "Number of lines to show from the end",
|
|
"default": 100
|
|
},
|
|
"timestamps": {
|
|
"type": "boolean",
|
|
"description": "Show timestamps",
|
|
"default": False
|
|
}
|
|
},
|
|
"required": ["environment_id", "container_id"]
|
|
}
|
|
),
|
|
# Image Management
|
|
types.Tool(
|
|
name="list_images",
|
|
description="List Docker images in an environment",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="pull_image",
|
|
description="Pull a Docker image",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"image": {
|
|
"type": "string",
|
|
"description": "Image name with optional tag (e.g., nginx:latest)"
|
|
}
|
|
},
|
|
"required": ["environment_id", "image"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="remove_image",
|
|
description="Remove a Docker image",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"image_id": {
|
|
"type": "string",
|
|
"description": "Image ID or name:tag"
|
|
},
|
|
"force": {
|
|
"type": "boolean",
|
|
"description": "Force removal",
|
|
"default": False
|
|
}
|
|
},
|
|
"required": ["environment_id", "image_id"]
|
|
}
|
|
),
|
|
# Volume Management
|
|
types.Tool(
|
|
name="list_volumes",
|
|
description="List Docker volumes",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="create_volume",
|
|
description="Create a Docker volume",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Volume name"
|
|
},
|
|
"driver": {
|
|
"type": "string",
|
|
"description": "Volume driver",
|
|
"default": "local"
|
|
}
|
|
},
|
|
"required": ["environment_id", "name"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="remove_volume",
|
|
description="Remove a Docker volume",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"volume_name": {
|
|
"type": "string",
|
|
"description": "Volume name"
|
|
}
|
|
},
|
|
"required": ["environment_id", "volume_name"]
|
|
}
|
|
),
|
|
# Network Management
|
|
types.Tool(
|
|
name="list_networks",
|
|
description="List Docker networks",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="create_network",
|
|
description="Create a Docker network",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Network name"
|
|
},
|
|
"driver": {
|
|
"type": "string",
|
|
"description": "Network driver",
|
|
"default": "bridge"
|
|
},
|
|
"internal": {
|
|
"type": "boolean",
|
|
"description": "Restrict external access",
|
|
"default": False
|
|
}
|
|
},
|
|
"required": ["environment_id", "name"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="remove_network",
|
|
description="Remove a Docker network",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"network_id": {
|
|
"type": "string",
|
|
"description": "Network ID or name"
|
|
}
|
|
},
|
|
"required": ["environment_id", "network_id"]
|
|
}
|
|
),
|
|
# Docker Swarm Services
|
|
types.Tool(
|
|
name="list_services",
|
|
description="List Docker Swarm services",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the Swarm environment"
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="create_service",
|
|
description="Create a Docker Swarm service",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the Swarm environment"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Service name"
|
|
},
|
|
"image": {
|
|
"type": "string",
|
|
"description": "Docker image"
|
|
},
|
|
"replicas": {
|
|
"type": "integer",
|
|
"description": "Number of replicas",
|
|
"default": 1
|
|
},
|
|
"command": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Command to run (optional)"
|
|
},
|
|
"env": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Environment variables in KEY=VALUE format"
|
|
},
|
|
"ports": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"target": {"type": "integer"},
|
|
"published": {"type": "integer"},
|
|
"protocol": {"type": "string", "enum": ["tcp", "udp"]}
|
|
}
|
|
},
|
|
"description": "Published ports"
|
|
}
|
|
},
|
|
"required": ["environment_id", "name", "image"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="update_service",
|
|
description="Update a Docker Swarm service",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the Swarm environment"
|
|
},
|
|
"service_id": {
|
|
"type": "string",
|
|
"description": "Service ID or name"
|
|
},
|
|
"image": {
|
|
"type": "string",
|
|
"description": "New image (optional)"
|
|
},
|
|
"replicas": {
|
|
"type": "integer",
|
|
"description": "New replica count (optional)"
|
|
},
|
|
"env": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "New environment variables (optional)"
|
|
}
|
|
},
|
|
"required": ["environment_id", "service_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="remove_service",
|
|
description="Remove a Docker Swarm service",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the Swarm environment"
|
|
},
|
|
"service_id": {
|
|
"type": "string",
|
|
"description": "Service ID or name"
|
|
}
|
|
},
|
|
"required": ["environment_id", "service_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="get_service_logs",
|
|
description="Get logs from a Docker Swarm service",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the Swarm environment"
|
|
},
|
|
"service_id": {
|
|
"type": "string",
|
|
"description": "Service ID or name"
|
|
},
|
|
"tail": {
|
|
"type": "integer",
|
|
"description": "Number of lines from the end",
|
|
"default": 100
|
|
}
|
|
},
|
|
"required": ["environment_id", "service_id"]
|
|
}
|
|
),
|
|
# Stack Management
|
|
types.Tool(
|
|
name="list_stacks",
|
|
description="List Docker stacks",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="deploy_stack",
|
|
description="Deploy a Docker stack from compose file",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"description": "Stack name"
|
|
},
|
|
"compose_file": {
|
|
"type": "string",
|
|
"description": "Docker Compose file content (YAML)"
|
|
},
|
|
"env": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {"type": "string"},
|
|
"value": {"type": "string"}
|
|
}
|
|
},
|
|
"description": "Environment variables for the stack"
|
|
}
|
|
},
|
|
"required": ["environment_id", "name", "compose_file"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="remove_stack",
|
|
description="Remove a Docker stack",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
},
|
|
"stack_id": {
|
|
"type": "integer",
|
|
"description": "Stack ID"
|
|
}
|
|
},
|
|
"required": ["environment_id", "stack_id"]
|
|
}
|
|
),
|
|
# System Information
|
|
types.Tool(
|
|
name="get_docker_info",
|
|
description="Get Docker system information",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
),
|
|
types.Tool(
|
|
name="get_docker_version",
|
|
description="Get Docker version information",
|
|
inputSchema={
|
|
"type": "object",
|
|
"properties": {
|
|
"environment_id": {
|
|
"type": "integer",
|
|
"description": "ID of the environment"
|
|
}
|
|
},
|
|
"required": ["environment_id"]
|
|
}
|
|
)
|
|
]
|
|
|
|
|
|
async def make_request(method: str, endpoint: str, json_data: Optional[Dict] = None,
|
|
params: Optional[Dict] = None, text_response: bool = False) -> Dict[str, Any]:
|
|
"""Make HTTP request to Portainer API."""
|
|
import httpx
|
|
|
|
async with httpx.AsyncClient(verify=False, timeout=30.0) as client:
|
|
headers = {"X-API-Key": api_key} if api_key else {}
|
|
|
|
# Add JSON content type for POST/PUT requests
|
|
if method in ["POST", "PUT"] and json_data is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
|
|
if method == "GET":
|
|
response = await client.get(f"{portainer_url}/api{endpoint}", headers=headers, params=params)
|
|
elif method == "POST":
|
|
response = await client.post(f"{portainer_url}/api{endpoint}", headers=headers, json=json_data)
|
|
elif method == "PUT":
|
|
response = await client.put(f"{portainer_url}/api{endpoint}", headers=headers, json=json_data)
|
|
elif method == "DELETE":
|
|
response = await client.delete(f"{portainer_url}/api{endpoint}", headers=headers)
|
|
else:
|
|
raise ValueError(f"Unsupported method: {method}")
|
|
|
|
# Handle text responses (like logs)
|
|
if text_response:
|
|
return {"status_code": response.status_code, "data": None, "text": response.text}
|
|
|
|
# Parse JSON response safely
|
|
try:
|
|
data = response.json() if response.text and response.headers.get("content-type", "").startswith("application/json") else None
|
|
except Exception:
|
|
data = None
|
|
|
|
return {"status_code": response.status_code, "data": data, "text": response.text}
|
|
|
|
|
|
def format_container_state(state: str) -> str:
|
|
"""Format container state for display."""
|
|
state_map = {
|
|
"running": "🟢 Running",
|
|
"paused": "⏸️ Paused",
|
|
"exited": "🔴 Stopped",
|
|
"dead": "💀 Dead",
|
|
"created": "🆕 Created",
|
|
"restarting": "🔄 Restarting",
|
|
"removing": "🗑️ Removing"
|
|
}
|
|
return state_map.get(state.lower(), state)
|
|
|
|
|
|
def format_bytes(bytes_val: int) -> str:
|
|
"""Format bytes to human readable format."""
|
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
if bytes_val < 1024.0:
|
|
return f"{bytes_val:.1f} {unit}"
|
|
bytes_val /= 1024.0
|
|
return f"{bytes_val:.1f} PB"
|
|
|
|
|
|
@server.call_tool()
|
|
async def handle_call_tool(name: str, arguments: dict[str, Any] | None) -> list[types.TextContent]:
|
|
"""Handle tool calls."""
|
|
import httpx
|
|
|
|
try:
|
|
env_id = arguments.get("environment_id") if arguments else None
|
|
|
|
# Container Management
|
|
if name == "list_containers":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
all_containers = arguments.get("all", False)
|
|
params = {"all": "true"} if all_containers else {"all": "false"}
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/containers/json", params=params)
|
|
|
|
if result["status_code"] == 200 and result["data"] is not None:
|
|
containers = result["data"]
|
|
if not containers:
|
|
return [types.TextContent(type="text", text="No containers found")]
|
|
|
|
output = f"Found {len(containers)} container(s):\n"
|
|
for container in containers[:20]: # Limit to 20 containers
|
|
# Container names come with leading slash
|
|
names = ", ".join([n.lstrip("/") for n in container.get("Names", [])])
|
|
state = container.get("State", "unknown")
|
|
status = container.get("Status", "")
|
|
image = container.get("Image", "unknown")
|
|
container_id = container.get("Id", "")[:12] # Short ID
|
|
|
|
output += f"\n- {names} ({container_id})"
|
|
output += f"\n Image: {image}"
|
|
output += f"\n Status: {format_container_state(state)} - {status}"
|
|
|
|
# Show ports if any
|
|
ports = container.get("Ports", [])
|
|
if ports:
|
|
port_info = []
|
|
for port in ports:
|
|
if port.get("PublicPort"):
|
|
port_info.append(f"{port.get('PublicPort')}→{port.get('PrivatePort')}/{port.get('Type')}")
|
|
else:
|
|
port_info.append(f"{port.get('PrivatePort')}/{port.get('Type')}")
|
|
output += f"\n Ports: {', '.join(port_info)}"
|
|
|
|
if len(containers) > 20:
|
|
output += f"\n\n... and {len(containers) - 20} more containers"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to list containers: HTTP {result['status_code']}")]
|
|
|
|
elif name == "inspect_container":
|
|
container_id = arguments.get("container_id")
|
|
if not env_id or not container_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and container_id are required")]
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/containers/{container_id}/json")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
info = result["data"]
|
|
output = f"Container Details:\n"
|
|
output += f"- ID: {info.get('Id', '')[:12]}\n"
|
|
output += f"- Name: {info.get('Name', '').lstrip('/')}\n"
|
|
output += f"- Image: {info.get('Config', {}).get('Image', '')}\n"
|
|
output += f"- Status: {format_container_state(info.get('State', {}).get('Status', ''))}\n"
|
|
output += f"- Created: {info.get('Created', '')}\n"
|
|
|
|
# State details
|
|
state = info.get('State', {})
|
|
if state.get('Running'):
|
|
output += f"- PID: {state.get('Pid')}\n"
|
|
output += f"- Started: {state.get('StartedAt')}\n"
|
|
|
|
# Network info
|
|
networks = info.get('NetworkSettings', {}).get('Networks', {})
|
|
if networks:
|
|
output += "\nNetworks:\n"
|
|
for net_name, net_info in networks.items():
|
|
output += f"- {net_name}: {net_info.get('IPAddress', 'N/A')}\n"
|
|
|
|
# Mounts
|
|
mounts = info.get('Mounts', [])
|
|
if mounts:
|
|
output += "\nMounts:\n"
|
|
for mount in mounts:
|
|
output += f"- {mount.get('Source')} → {mount.get('Destination')} ({mount.get('Mode', 'rw')})\n"
|
|
|
|
# Environment variables
|
|
env_vars = info.get('Config', {}).get('Env', [])
|
|
if env_vars:
|
|
output += "\nEnvironment Variables:\n"
|
|
for env in env_vars[:10]: # Limit to 10
|
|
if '=' in env and not any(secret in env.lower() for secret in ['password', 'secret', 'key', 'token']):
|
|
output += f"- {env}\n"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to inspect container: HTTP {result['status_code']}")]
|
|
|
|
elif name == "create_container":
|
|
image = arguments.get("image")
|
|
if not env_id or not image:
|
|
return [types.TextContent(type="text", text="Error: environment_id and image are required")]
|
|
|
|
# Build container configuration
|
|
config = {
|
|
"Image": image,
|
|
"Hostname": arguments.get("name", ""),
|
|
"AttachStdin": False,
|
|
"AttachStdout": True,
|
|
"AttachStderr": True,
|
|
"Tty": False,
|
|
"OpenStdin": False
|
|
}
|
|
|
|
if arguments.get("command"):
|
|
config["Cmd"] = arguments["command"]
|
|
|
|
if arguments.get("env"):
|
|
config["Env"] = arguments["env"]
|
|
|
|
# Host configuration
|
|
host_config = {
|
|
"RestartPolicy": {"Name": arguments.get("restart_policy", "no")}
|
|
}
|
|
|
|
if arguments.get("ports"):
|
|
config["ExposedPorts"] = {port: {} for port in arguments["ports"].keys()}
|
|
host_config["PortBindings"] = arguments["ports"]
|
|
|
|
if arguments.get("volumes"):
|
|
host_config["Binds"] = []
|
|
for host_path, container_config in arguments["volumes"].items():
|
|
bind = f"{host_path}:{container_config['bind']}"
|
|
if container_config.get('mode'):
|
|
bind += f":{container_config['mode']}"
|
|
host_config["Binds"].append(bind)
|
|
|
|
create_data = {
|
|
**config,
|
|
"HostConfig": host_config
|
|
}
|
|
|
|
# Create container
|
|
params = {"name": arguments.get("name")} if arguments.get("name") else {}
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/containers/create",
|
|
json_data=create_data, params=params)
|
|
|
|
if result["status_code"] in [200, 201] and result["data"]:
|
|
container_id = result["data"].get("Id", "")[:12]
|
|
output = f"✓ Container created: {container_id}"
|
|
|
|
# Auto-start the container
|
|
start_result = await make_request("POST", f"/endpoints/{env_id}/docker/containers/{container_id}/start")
|
|
if start_result["status_code"] in [200, 204]:
|
|
output += "\n✓ Container started successfully"
|
|
else:
|
|
output += f"\n⚠️ Container created but failed to start: HTTP {start_result['status_code']}"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
error_msg = f"Failed to create container: HTTP {result['status_code']}"
|
|
if result.get("text"):
|
|
error_msg += f"\n{result['text']}"
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
elif name == "start_container":
|
|
container_id = arguments.get("container_id")
|
|
if not env_id or not container_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and container_id are required")]
|
|
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/containers/{container_id}/start")
|
|
|
|
if result["status_code"] in [200, 204, 304]: # 304 = already started
|
|
return [types.TextContent(type="text", text=f"✓ Container {container_id} started")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to start container: HTTP {result['status_code']}")]
|
|
|
|
elif name == "stop_container":
|
|
container_id = arguments.get("container_id")
|
|
if not env_id or not container_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and container_id are required")]
|
|
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/containers/{container_id}/stop")
|
|
|
|
if result["status_code"] in [200, 204, 304]: # 304 = already stopped
|
|
return [types.TextContent(type="text", text=f"✓ Container {container_id} stopped")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to stop container: HTTP {result['status_code']}")]
|
|
|
|
elif name == "restart_container":
|
|
container_id = arguments.get("container_id")
|
|
if not env_id or not container_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and container_id are required")]
|
|
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/containers/{container_id}/restart")
|
|
|
|
if result["status_code"] in [200, 204]:
|
|
return [types.TextContent(type="text", text=f"✓ Container {container_id} restarted")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to restart container: HTTP {result['status_code']}")]
|
|
|
|
elif name == "remove_container":
|
|
container_id = arguments.get("container_id")
|
|
if not env_id or not container_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and container_id are required")]
|
|
|
|
params = {"force": "true"} if arguments.get("force") else {}
|
|
result = await make_request("DELETE", f"/endpoints/{env_id}/docker/containers/{container_id}", params=params)
|
|
|
|
if result["status_code"] in [200, 204]:
|
|
return [types.TextContent(type="text", text=f"✓ Container {container_id} removed")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to remove container: HTTP {result['status_code']}")]
|
|
|
|
elif name == "get_container_logs":
|
|
container_id = arguments.get("container_id")
|
|
if not env_id or not container_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and container_id are required")]
|
|
|
|
params = {
|
|
"stdout": "true",
|
|
"stderr": "true",
|
|
"tail": str(arguments.get("tail", 100))
|
|
}
|
|
if arguments.get("timestamps"):
|
|
params["timestamps"] = "true"
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/containers/{container_id}/logs",
|
|
params=params, text_response=True)
|
|
|
|
if result["status_code"] == 200:
|
|
logs = result.get("text", "")
|
|
if logs:
|
|
# Docker logs have special encoding, try to clean them up
|
|
import re
|
|
# Remove ANSI escape codes and Docker log headers
|
|
logs = re.sub(r'\x1b\[[0-9;]*m', '', logs)
|
|
logs = re.sub(r'[\x00-\x08\x0e-\x1f]', '', logs)
|
|
|
|
output = f"Container logs (last {arguments.get('tail', 100)} lines):\n"
|
|
output += "-" * 50 + "\n"
|
|
output += logs
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text="No logs available")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to get logs: HTTP {result['status_code']}")]
|
|
|
|
# Image Management
|
|
elif name == "list_images":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/images/json")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
images = result["data"]
|
|
output = f"Found {len(images)} image(s):\n"
|
|
|
|
for image in images[:20]: # Limit to 20
|
|
tags = image.get("RepoTags", [])
|
|
if tags and tags[0] != "<none>:<none>":
|
|
image_name = tags[0]
|
|
else:
|
|
image_name = image.get("Id", "")[:12]
|
|
|
|
size = format_bytes(image.get("Size", 0))
|
|
created = image.get("Created", 0)
|
|
|
|
output += f"\n- {image_name}"
|
|
output += f"\n ID: {image.get('Id', '')[:12]}"
|
|
output += f"\n Size: {size}"
|
|
|
|
# Additional tags
|
|
if len(tags) > 1:
|
|
output += f"\n Other tags: {', '.join(tags[1:])}"
|
|
|
|
if len(images) > 20:
|
|
output += f"\n\n... and {len(images) - 20} more images"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to list images: HTTP {result['status_code']}")]
|
|
|
|
elif name == "pull_image":
|
|
image = arguments.get("image")
|
|
if not env_id or not image:
|
|
return [types.TextContent(type="text", text="Error: environment_id and image are required")]
|
|
|
|
# Parse image name and tag
|
|
if ":" in image:
|
|
image_name, tag = image.rsplit(":", 1)
|
|
else:
|
|
image_name = image
|
|
tag = "latest"
|
|
|
|
params = {"fromImage": image_name, "tag": tag}
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/images/create", params=params)
|
|
|
|
if result["status_code"] == 200:
|
|
return [types.TextContent(type="text", text=f"✓ Image {image} pulled successfully")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to pull image: HTTP {result['status_code']}")]
|
|
|
|
elif name == "remove_image":
|
|
image_id = arguments.get("image_id")
|
|
if not env_id or not image_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and image_id are required")]
|
|
|
|
params = {"force": "true"} if arguments.get("force") else {}
|
|
result = await make_request("DELETE", f"/endpoints/{env_id}/docker/images/{image_id}", params=params)
|
|
|
|
if result["status_code"] == 200:
|
|
return [types.TextContent(type="text", text=f"✓ Image {image_id} removed")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to remove image: HTTP {result['status_code']}")]
|
|
|
|
# Volume Management
|
|
elif name == "list_volumes":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/volumes")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
volumes = result["data"].get("Volumes", [])
|
|
output = f"Found {len(volumes)} volume(s):\n"
|
|
|
|
for volume in volumes[:20]:
|
|
output += f"\n- {volume.get('Name')}"
|
|
output += f"\n Driver: {volume.get('Driver')}"
|
|
output += f"\n Mountpoint: {volume.get('Mountpoint')}"
|
|
if volume.get('Options'):
|
|
output += f"\n Options: {volume.get('Options')}"
|
|
|
|
if len(volumes) > 20:
|
|
output += f"\n\n... and {len(volumes) - 20} more volumes"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to list volumes: HTTP {result['status_code']}")]
|
|
|
|
elif name == "create_volume":
|
|
volume_name = arguments.get("name")
|
|
if not env_id or not volume_name:
|
|
return [types.TextContent(type="text", text="Error: environment_id and name are required")]
|
|
|
|
volume_data = {
|
|
"Name": volume_name,
|
|
"Driver": arguments.get("driver", "local")
|
|
}
|
|
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/volumes/create", json_data=volume_data)
|
|
|
|
if result["status_code"] in [200, 201]:
|
|
return [types.TextContent(type="text", text=f"✓ Volume '{volume_name}' created")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to create volume: HTTP {result['status_code']}")]
|
|
|
|
elif name == "remove_volume":
|
|
volume_name = arguments.get("volume_name")
|
|
if not env_id or not volume_name:
|
|
return [types.TextContent(type="text", text="Error: environment_id and volume_name are required")]
|
|
|
|
result = await make_request("DELETE", f"/endpoints/{env_id}/docker/volumes/{volume_name}")
|
|
|
|
if result["status_code"] in [200, 204]:
|
|
return [types.TextContent(type="text", text=f"✓ Volume '{volume_name}' removed")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to remove volume: HTTP {result['status_code']}")]
|
|
|
|
# Network Management
|
|
elif name == "list_networks":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/networks")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
networks = result["data"]
|
|
output = f"Found {len(networks)} network(s):\n"
|
|
|
|
for network in networks:
|
|
output += f"\n- {network.get('Name')} ({network.get('Id', '')[:12]})"
|
|
output += f"\n Driver: {network.get('Driver')}"
|
|
output += f"\n Scope: {network.get('Scope')}"
|
|
if network.get('Internal'):
|
|
output += f"\n Internal: Yes"
|
|
if network.get('Containers'):
|
|
output += f"\n Containers: {len(network['Containers'])}"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to list networks: HTTP {result['status_code']}")]
|
|
|
|
elif name == "create_network":
|
|
network_name = arguments.get("name")
|
|
if not env_id or not network_name:
|
|
return [types.TextContent(type="text", text="Error: environment_id and name are required")]
|
|
|
|
network_data = {
|
|
"Name": network_name,
|
|
"Driver": arguments.get("driver", "bridge"),
|
|
"Internal": arguments.get("internal", False)
|
|
}
|
|
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/networks/create", json_data=network_data)
|
|
|
|
if result["status_code"] in [200, 201]:
|
|
return [types.TextContent(type="text", text=f"✓ Network '{network_name}' created")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to create network: HTTP {result['status_code']}")]
|
|
|
|
elif name == "remove_network":
|
|
network_id = arguments.get("network_id")
|
|
if not env_id or not network_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and network_id are required")]
|
|
|
|
result = await make_request("DELETE", f"/endpoints/{env_id}/docker/networks/{network_id}")
|
|
|
|
if result["status_code"] in [200, 204]:
|
|
return [types.TextContent(type="text", text=f"✓ Network '{network_id}' removed")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to remove network: HTTP {result['status_code']}")]
|
|
|
|
# Docker Swarm Services
|
|
elif name == "list_services":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/services")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
services = result["data"]
|
|
output = f"Found {len(services)} service(s):\n"
|
|
|
|
for service in services[:20]:
|
|
spec = service.get("Spec", {})
|
|
mode = spec.get("Mode", {})
|
|
|
|
# Get replica info
|
|
if "Replicated" in mode:
|
|
replicas = mode["Replicated"].get("Replicas", 0)
|
|
mode_str = f"replicated ({replicas} replicas)"
|
|
else:
|
|
mode_str = "global"
|
|
|
|
output += f"\n- {spec.get('Name')} ({service.get('ID', '')[:12]})"
|
|
output += f"\n Image: {spec.get('TaskTemplate', {}).get('ContainerSpec', {}).get('Image', 'unknown')}"
|
|
output += f"\n Mode: {mode_str}"
|
|
|
|
# Get current state
|
|
if service.get("UpdateStatus"):
|
|
status = service["UpdateStatus"].get("State", "")
|
|
if status:
|
|
output += f"\n Update Status: {status}"
|
|
|
|
if len(services) > 20:
|
|
output += f"\n\n... and {len(services) - 20} more services"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
error_msg = f"Failed to list services: HTTP {result['status_code']}"
|
|
if result['status_code'] == 503:
|
|
error_msg += "\nNote: This environment might not be a Docker Swarm cluster"
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
elif name == "create_service":
|
|
service_name = arguments.get("name")
|
|
image = arguments.get("image")
|
|
if not env_id or not service_name or not image:
|
|
return [types.TextContent(type="text", text="Error: environment_id, name, and image are required")]
|
|
|
|
service_spec = {
|
|
"Name": service_name,
|
|
"TaskTemplate": {
|
|
"ContainerSpec": {
|
|
"Image": image
|
|
}
|
|
},
|
|
"Mode": {
|
|
"Replicated": {
|
|
"Replicas": arguments.get("replicas", 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
# Add command if provided
|
|
if arguments.get("command"):
|
|
service_spec["TaskTemplate"]["ContainerSpec"]["Command"] = arguments["command"]
|
|
|
|
# Add environment variables
|
|
if arguments.get("env"):
|
|
service_spec["TaskTemplate"]["ContainerSpec"]["Env"] = arguments["env"]
|
|
|
|
# Add ports
|
|
if arguments.get("ports"):
|
|
service_spec["EndpointSpec"] = {
|
|
"Ports": arguments["ports"]
|
|
}
|
|
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/services/create", json_data=service_spec)
|
|
|
|
if result["status_code"] in [200, 201]:
|
|
return [types.TextContent(type="text", text=f"✓ Service '{service_name}' created")]
|
|
else:
|
|
error_msg = f"Failed to create service: HTTP {result['status_code']}"
|
|
if result.get("text"):
|
|
error_msg += f"\n{result['text']}"
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
elif name == "update_service":
|
|
service_id = arguments.get("service_id")
|
|
if not env_id or not service_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and service_id are required")]
|
|
|
|
# Get current service spec
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/services/{service_id}")
|
|
if result["status_code"] != 200:
|
|
return [types.TextContent(type="text", text=f"Failed to get service: HTTP {result['status_code']}")]
|
|
|
|
service = result["data"]
|
|
spec = service.get("Spec", {})
|
|
version = service.get("Version", {}).get("Index")
|
|
|
|
# Update fields if provided
|
|
if arguments.get("image"):
|
|
spec["TaskTemplate"]["ContainerSpec"]["Image"] = arguments["image"]
|
|
|
|
if arguments.get("replicas") is not None:
|
|
if "Replicated" in spec.get("Mode", {}):
|
|
spec["Mode"]["Replicated"]["Replicas"] = arguments["replicas"]
|
|
|
|
if arguments.get("env"):
|
|
spec["TaskTemplate"]["ContainerSpec"]["Env"] = arguments["env"]
|
|
|
|
# Update service
|
|
result = await make_request("POST", f"/endpoints/{env_id}/docker/services/{service_id}/update",
|
|
json_data=spec, params={"version": version})
|
|
|
|
if result["status_code"] == 200:
|
|
return [types.TextContent(type="text", text=f"✓ Service '{service_id}' updated")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to update service: HTTP {result['status_code']}")]
|
|
|
|
elif name == "remove_service":
|
|
service_id = arguments.get("service_id")
|
|
if not env_id or not service_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and service_id are required")]
|
|
|
|
result = await make_request("DELETE", f"/endpoints/{env_id}/docker/services/{service_id}")
|
|
|
|
if result["status_code"] in [200, 204]:
|
|
return [types.TextContent(type="text", text=f"✓ Service '{service_id}' removed")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to remove service: HTTP {result['status_code']}")]
|
|
|
|
elif name == "get_service_logs":
|
|
service_id = arguments.get("service_id")
|
|
if not env_id or not service_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and service_id are required")]
|
|
|
|
params = {
|
|
"stdout": "true",
|
|
"stderr": "true",
|
|
"tail": str(arguments.get("tail", 100))
|
|
}
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/services/{service_id}/logs",
|
|
params=params, text_response=True)
|
|
|
|
if result["status_code"] == 200:
|
|
logs = result.get("text", "")
|
|
if logs:
|
|
output = f"Service logs (last {arguments.get('tail', 100)} lines):\n"
|
|
output += "-" * 50 + "\n"
|
|
output += logs
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text="No logs available")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to get logs: HTTP {result['status_code']}")]
|
|
|
|
# Stack Management
|
|
elif name == "list_stacks":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
result = await make_request("GET", "/stacks")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
stacks = result["data"]
|
|
# Filter stacks for this environment
|
|
env_stacks = [s for s in stacks if s.get("EndpointId") == env_id]
|
|
|
|
output = f"Found {len(env_stacks)} stack(s) in this environment:\n"
|
|
|
|
for stack in env_stacks:
|
|
output += f"\n- {stack.get('Name')} (ID: {stack.get('Id')})"
|
|
output += f"\n Type: {stack.get('Type', 'unknown')}"
|
|
output += f"\n Status: {stack.get('Status', 'unknown')}"
|
|
if stack.get("GitConfig"):
|
|
output += f"\n Git Repository: Yes"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to list stacks: HTTP {result['status_code']}")]
|
|
|
|
elif name == "deploy_stack":
|
|
stack_name = arguments.get("name")
|
|
compose_file = arguments.get("compose_file")
|
|
if not env_id or not stack_name or not compose_file:
|
|
return [types.TextContent(type="text", text="Error: environment_id, name, and compose_file are required")]
|
|
|
|
stack_data = {
|
|
"Name": stack_name,
|
|
"StackFileContent": compose_file,
|
|
"Env": arguments.get("env", [])
|
|
}
|
|
|
|
params = {
|
|
"type": 2, # Compose stack
|
|
"method": "string", # Stack file provided as string
|
|
"endpointId": env_id
|
|
}
|
|
|
|
result = await make_request("POST", "/stacks", json_data=stack_data, params=params)
|
|
|
|
if result["status_code"] in [200, 201]:
|
|
return [types.TextContent(type="text", text=f"✓ Stack '{stack_name}' deployed successfully")]
|
|
else:
|
|
error_msg = f"Failed to deploy stack: HTTP {result['status_code']}"
|
|
if result.get("text"):
|
|
error_msg += f"\n{result['text']}"
|
|
return [types.TextContent(type="text", text=error_msg)]
|
|
|
|
elif name == "remove_stack":
|
|
stack_id = arguments.get("stack_id")
|
|
if not env_id or not stack_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id and stack_id are required")]
|
|
|
|
result = await make_request("DELETE", f"/stacks/{stack_id}")
|
|
|
|
if result["status_code"] in [200, 204]:
|
|
return [types.TextContent(type="text", text=f"✓ Stack removed successfully")]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to remove stack: HTTP {result['status_code']}")]
|
|
|
|
# System Information
|
|
elif name == "get_docker_info":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/info")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
info = result["data"]
|
|
output = "Docker System Information:\n"
|
|
output += f"- Docker Version: {info.get('ServerVersion', 'unknown')}\n"
|
|
output += f"- API Version: {info.get('ApiVersion', 'unknown')}\n"
|
|
output += f"- Operating System: {info.get('OperatingSystem', 'unknown')}\n"
|
|
output += f"- Architecture: {info.get('Architecture', 'unknown')}\n"
|
|
output += f"- Kernel Version: {info.get('KernelVersion', 'unknown')}\n"
|
|
output += f"- Total Memory: {format_bytes(info.get('MemTotal', 0))}\n"
|
|
output += f"- CPUs: {info.get('NCPU', 0)}\n"
|
|
output += f"- Containers: {info.get('Containers', 0)} ({info.get('ContainersRunning', 0)} running)\n"
|
|
output += f"- Images: {info.get('Images', 0)}\n"
|
|
|
|
# Swarm info
|
|
swarm = info.get('Swarm', {})
|
|
if swarm.get('LocalNodeState') == 'active':
|
|
output += f"\nSwarm Status:\n"
|
|
output += f"- Node ID: {swarm.get('NodeID', '')[:12]}\n"
|
|
output += f"- Is Manager: {swarm.get('ControlAvailable', False)}\n"
|
|
if swarm.get('RemoteManagers'):
|
|
output += f"- Managers: {len(swarm['RemoteManagers'])}\n"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to get Docker info: HTTP {result['status_code']}")]
|
|
|
|
elif name == "get_docker_version":
|
|
if not env_id:
|
|
return [types.TextContent(type="text", text="Error: environment_id is required")]
|
|
|
|
result = await make_request("GET", f"/endpoints/{env_id}/docker/version")
|
|
|
|
if result["status_code"] == 200 and result["data"]:
|
|
version = result["data"]
|
|
output = "Docker Version Information:\n"
|
|
output += f"- Version: {version.get('Version', 'unknown')}\n"
|
|
output += f"- API Version: {version.get('ApiVersion', 'unknown')}\n"
|
|
output += f"- Git Commit: {version.get('GitCommit', 'unknown')}\n"
|
|
output += f"- Go Version: {version.get('GoVersion', 'unknown')}\n"
|
|
output += f"- OS/Arch: {version.get('Os', 'unknown')}/{version.get('Arch', 'unknown')}\n"
|
|
output += f"- Build Time: {version.get('BuildTime', 'unknown')}\n"
|
|
|
|
return [types.TextContent(type="text", text=output)]
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Failed to get Docker version: HTTP {result['status_code']}")]
|
|
|
|
else:
|
|
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
|
|
except httpx.TimeoutException:
|
|
return [types.TextContent(type="text", text="Error: Request timed out. The operation may take longer than expected.")]
|
|
except httpx.ConnectError:
|
|
return [types.TextContent(type="text", text="Error: Could not connect to Portainer server. Please check the URL and network connection.")]
|
|
except Exception as e:
|
|
import traceback
|
|
error_details = f"Error: {str(e)}\nType: {type(e).__name__}"
|
|
return [types.TextContent(type="text", text=error_details)]
|
|
|
|
|
|
async def run():
|
|
"""Run the MCP server."""
|
|
# Use stdio transport
|
|
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
await server.run(
|
|
read_stream,
|
|
write_stream,
|
|
InitializationOptions(
|
|
server_name="portainer-docker",
|
|
server_version="1.0.0",
|
|
capabilities=server.get_capabilities(
|
|
notification_options=NotificationOptions(
|
|
prompts_changed=False,
|
|
resources_changed=False,
|
|
tools_changed=False,
|
|
),
|
|
experimental_capabilities={},
|
|
),
|
|
),
|
|
)
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
asyncio.run(run())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |