portainer-mcp
Portainer MCP Server
An MCP (Model Context Protocol) server that gives AI assistants — Claude, Copilot, Cursor, and others — 53 tools to manage Portainer container environments: deploy and update stacks (Env preserved, re-pull on demand), inspect Swarm services/tasks/nodes, manage containers/images/volumes/networks, exec commands, analyze logs, and inspect endpoints — all through natural language. Two optional Laravel helpers bring the total to 55.
For LLM agents: This server connects via stdio transport and ships server instructions (start with services on Swarm, inspect before update). Every tool returns JSON. All mutating operations are audit-logged. Credential-looking values are masked as
[REDACTED]unless you ask forreveal_env=true. Credentials are passed via environment variables, never hardcoded.
Why Use This
Natural language DevOps — Ask your AI assistant to deploy a stack, check container logs, or pull an image.
Swarm-native — Services, tasks and nodes as first-class tools (
docker service ls/ps/logs/updateequivalents plus a per-stack health summary); stack deploys auto-detect Swarm vs standalone.Safe by default — Input validation, path traversal protection, sensitive field filtering, credential masking in stack/service inspect,
[REDACTED]values can never be written back, and force-remove disabled by default.Token auth — Use a Portainer access token (
PORTAINER_API_KEY) instead of an admin password.Works everywhere — Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Continue.dev.
Quick Start
1. Install
pip install portainer-mcpOr from source:
git clone https://github.com/ginkida/portainer-mcp.git
cd portainer-mcp
pip install -e .2. Configure your AI client
Pick your client below, paste the config, and replace the placeholder values with your Portainer credentials.
Client Configuration
Claude Desktop
File: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
"mcpServers": {
"portainer": {
"command": "python3",
"args": ["-m", "portainer_mcp.server"],
"env": {
"PORTAINER_URL": "https://your-portainer:9443",
"PORTAINER_API_KEY": "ptr_your-access-token",
"PORTAINER_VERIFY_SSL": "false"
}
}
}
}PORTAINER_API_KEY is a Portainer access token (My account → Access tokens). You can use PORTAINER_USERNAME + PORTAINER_PASSWORD instead, as in the examples below.
Claude Code
File: .mcp.json in your project root (project-scope) or ~/.claude.json (user-scope)
{
"mcpServers": {
"portainer": {
"type": "stdio",
"command": "python3",
"args": ["-m", "portainer_mcp.server"],
"env": {
"PORTAINER_URL": "https://your-portainer:9443",
"PORTAINER_USERNAME": "admin",
"PORTAINER_PASSWORD": "${PORTAINER_PASSWORD}",
"PORTAINER_VERIFY_SSL": "false"
}
}
}
}Or via CLI:
claude mcp add portainer -- python3 -m portainer_mcp.serverCursor
File: ~/.cursor/mcp.json (global) or .cursor/mcp.json (project)
{
"mcpServers": {
"portainer": {
"command": "python3",
"args": ["-m", "portainer_mcp.server"],
"env": {
"PORTAINER_URL": "https://your-portainer:9443",
"PORTAINER_USERNAME": "admin",
"PORTAINER_PASSWORD": "your-password",
"PORTAINER_VERIFY_SSL": "false"
}
}
}
}Windsurf
File: ~/.codeium/windsurf/mcp_config.json
{
"mcpServers": {
"portainer": {
"command": "python3",
"args": ["-m", "portainer_mcp.server"],
"env": {
"PORTAINER_URL": "https://your-portainer:9443",
"PORTAINER_USERNAME": "admin",
"PORTAINER_PASSWORD": "your-password",
"PORTAINER_VERIFY_SSL": "false"
}
}
}
}VS Code (GitHub Copilot)
File: .vscode/mcp.json in your workspace
{
"servers": {
"portainer": {
"type": "stdio",
"command": "python3",
"args": ["-m", "portainer_mcp.server"],
"env": {
"PORTAINER_URL": "${input:portainer-url}",
"PORTAINER_USERNAME": "${input:portainer-username}",
"PORTAINER_PASSWORD": "${input:portainer-password}",
"PORTAINER_VERIFY_SSL": "false"
}
}
},
"inputs": [
{ "type": "promptString", "id": "portainer-url", "description": "Portainer base URL" },
{ "type": "promptString", "id": "portainer-username", "description": "Portainer username" },
{ "type": "promptString", "id": "portainer-password", "description": "Portainer password", "password": true }
]
}Continue.dev
File: ~/.continue/config.yaml or .continue/config.yaml
mcpServers:
- name: portainer
type: stdio
command: python3
args:
- -m
- portainer_mcp.server
env:
PORTAINER_URL: "https://your-portainer:9443"
PORTAINER_USERNAME: "admin"
PORTAINER_PASSWORD: "your-password"
PORTAINER_VERIFY_SSL: "false"Environment Variables
Variable | Required | Default | Description |
| Yes | — | Portainer base URL, e.g. |
| One of | — | Portainer access token, sent as |
| One of | — | Portainer username (with |
| One of | — | Portainer password |
| No |
| Default endpoint ID for container/image/stack operations |
| No |
| Set to |
| No |
| Timeout (seconds) for ordinary API calls |
| No |
| Timeout (seconds) for long-running operations: image pull, container exec, large log scans |
| No |
| Max concurrent HTTP connections to Portainer |
| No |
| Max idle keep-alive connections |
| No |
| Proactive JWT refresh interval (seconds). Set below Portainer's session timeout (default 8h) to avoid per-call 401 re-auth round-trips. Unused with |
| No |
| Register the opinionated Laravel helpers ( |
All values are validated at startup — a malformed URL, a non-numeric timeout, or a non-positive limit fails fast with a clear error instead of breaking later.
Tools
All 53 tools are listed below with their parameters and descriptions (55 with the Laravel helpers enabled). Every tool returns JSON.
Authentication
Tool | Description |
| Check connection and authentication status. Returns version, instance ID, auth mode, the number of endpoints and, for the default endpoint, its name/status and |
Endpoints (Environments)
Tool | Description |
| List all environments. Returns id, name, type, url, status. |
| Get endpoint details (sensitive fields like TLS certs are filtered; the bulky raw |
Stacks
Tool | Description |
| List all stacks with id, name, type ( |
| Get stack details, its Env variables and the compose file. Credential-looking values ( |
| Deploy a new stack with optional Env variables. Auto-detects Swarm vs standalone. |
| Redeploy a stack. The stored Env variables are always preserved (Portainer replaces the whole list on every update; the tool reads it first and merges |
| Health summary: every service with running/desired replicas, update state and the task failures newer than its last good task. Cron-driven services (swarm-cronjob labels) are judged on their last run, not on replicas. Falls back to container states on a standalone endpoint (or for a Compose project on a manager). |
| Poll |
| Delete a stack (endpoint derived from the stack; a different |
| Start a stopped stack (endpoint derived from the stack). |
| Stop a running stack (endpoint derived from the stack). |
Swarm
Tool | Description |
|
|
| Full service definition (spec, previous spec, update status). Credential-looking |
|
|
| Aggregated logs of all the service's tasks across nodes. |
|
|
|
|
| Poll until the service is healthy and its update finished, the update paused on failure, or the timeout elapses. Returns the service summary with |
| Swarm secrets: names and metadata only, never values. |
| Swarm configs: names and metadata only, never content. |
| Swarm nodes: hostname, role, availability, state, leader, engine version, CPUs/memory, labels. |
Containers
Tool | Description |
| List containers with their |
| Get detailed container info. |
| Start a stopped container. |
| Stop a running container. |
| Restart a container. |
| Remove a container. |
| Get container logs as a JSON envelope ( |
| Server-side regex over logs. Returns only matching lines (with optional context) — saves bandwidth on noisy logs. |
| Point-in-time CPU%, memory, network and block I/O stats (not a stream). |
| Run a shell command inside a running container and return its stdout/stderr + exit code. Audit-logged. |
| Concurrent scan of every running container in a stack for HTTP 4xx/5xx, exceptions, fatal/critical levels, panics, OOM, PHP errors, etc. |
Laravel (opt-in: PORTAINER_ENABLE_LARAVEL_TOOLS=true)
Tool | Description |
| Read |
| Execute PHP via |
Images
Tool | Description |
| List images with tags and sizes. |
| Get detailed image info. Accepts |
| Pull an image. |
| Remove an image. |
| Registries configured in Portainer (id, name, URL, type, authentication flag) — the |
Volumes
Tool | Description |
| List Docker volumes. |
| Get detailed volume info. |
| Create a volume. |
| Remove a volume. |
Networks
Tool | Description |
| List Docker networks with driver, scope and attached container count. |
| Get detailed network info. |
| Create a network. |
| Remove a network. |
| Attach a container to a network. |
| Detach a container from a network. |
System
Tool | Description |
| OS, CPU, memory, container/image counts, |
| Per-category disk usage (containers, images, volumes, build cache) with reclaimable size. |
| Reclaim disk: |
Users
Tool | Description |
| List all Portainer users with id, username, role. |
| Get user details. Sensitive fields (password hash, TFA material, tokens) are filtered out. |
Example Workflows
Deploy a new service:
"Deploy a stack called 'redis' with Redis 7 on port 6379"
The agent will call portainer_stack_deploy(name="redis", compose_content="...") with the generated compose YAML.
Debug a failing container:
"Why is the nginx container crashing?"
The agent will call portainer_containers_list() to find the container, then portainer_container_logs(container_id) to inspect the logs.
Roll out a new build on Swarm:
"Deploy the latest arena-etl image"
The agent will call portainer_stack_update(stack_id, pull_image=true) — the stack's Env variables are preserved — or portainer_service_update(service_id, image="registry/app:latest") for a single service, then portainer_stack_wait("arena-etl") to confirm the rollout converged (and portainer_service_rollback if it did not).
Update an existing stack:
"Update the arena-etl stack to use the new image tag v2.1"
The agent will call portainer_stack_inspect(stack_id, reveal_env=true) to get the current compose file, modify the image tag, then portainer_stack_update(stack_id, compose_content).
Why are replicas down?
"arena-etl_worker shows 0/2"
The agent will call portainer_service_tasks("arena-etl_worker") and read the task error (no suitable node, task: non-zero exit (137), image pull failures), then portainer_service_logs for the application side.
Security
Access-token auth (
PORTAINER_API_KEY, sent asX-API-Key) — no admin password on disk, no session to refresh; Portainer skips CSRF checks for token requests. Falls back to JWT auth with proactive refresh (7h TTL, Portainer default is 8h),asyncio.Lock-guarded re-authentication for safe concurrent use, and 401/403-CSRF retry fallback.CSRF handling for Portainer 2.39+ — Referer +
X-CSRF-Tokenare sent only on mutating methods; CSRF token is harvested fromX-CSRF-Tokenresponse headers and refreshed automatically.SSL verification enabled by default. Only disable for self-signed certificates.
Input validation — container IDs, image references (incl. digests), stack names, volume/network names are regex-validated before any API call. Path traversal (
..) is blocked.Sensitive field filtering —
endpoint_inspectstrips TLS certificates, Azure credentials and security settings;user_inspectwhitelists safe fields and hides password/TFA material;registries_listnever returns registry credentials.Credential masking —
stack_inspectandservice_inspectmask Env values whose name looks like a credential (and anyuser:pass@inside URLs) as[REDACTED];reveal_env=trueshows them. A compose file or Env value containing[REDACTED]is refused bystack_update, so a masked value can never overwrite a real one.stack_updatepreserves the stack's stored Env on every redeploy.Audit logging — every mutating operation (deploy, delete, remove, pull, start, stop, exec, tinker) is logged to stderr with parameters.
No hardcoded credentials — all secrets come from environment variables. Optional
X-Registry-Authfor private-registry image pulls is passed in via parameter, never persisted.Container removal —
forcedefaults tofalseto prevent accidental deletion of running containers.Log/exec size limits — output is capped at 100K characters to prevent memory exhaustion.
Non-JSON responses are surfaced, not swallowed — a reverse proxy's HTML login page in place of the API is reported as "Unexpected response from Portainer" with the content type and the first bytes of the body.
Development
git clone https://github.com/ginkida/portainer-mcp.git
cd portainer-mcp
pip install -e ".[dev]"Run locally:
export PORTAINER_URL=https://your-portainer:9443
export PORTAINER_USERNAME=admin
export PORTAINER_PASSWORD=your-password
python3 -m portainer_mcp.serverLint, type-check and test (CI runs the same on Python 3.10–3.13 for every push and pull request):
ruff check src/ tests/
mypy src/ tests/
pytestRequirements
Python 3.10+
A running Portainer instance (CE or Business Edition)
Portainer API access (default port 9443)
License
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/ginkida/portainer-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server