MCP server that gives AI assistants 37 tools to manage Portainer: deploy stacks, manage containers, volumes, networks, exec commands, pull images, inspect endpoints, monitor resources
Project description
Portainer MCP Server
An MCP (Model Context Protocol) server that gives AI assistants — Claude, Copilot, Cursor, and others — 23 tools to manage Portainer container environments: deploy and update stacks, start/stop/restart containers, pull/remove images, inspect endpoints, and manage users — all through natural language.
For LLM agents: This server connects via stdio transport. Every tool returns JSON. All mutating operations are audit-logged. 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-aware — Automatically detects Docker Swarm clusters and uses the correct API.
- Safe by default — Input validation, path traversal protection, sensitive field filtering, and force-remove disabled by default.
- Works everywhere — Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Continue.dev.
Quick Start
1. Install
pip install portainer-mcp
Or 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_USERNAME": "admin",
"PORTAINER_PASSWORD": "your-password",
"PORTAINER_VERIFY_SSL": "false"
}
}
}
}
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.server
Cursor
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 |
|---|---|---|---|
PORTAINER_URL |
Yes | — | Portainer base URL (e.g. https://portainer.example.com:9443) |
PORTAINER_USERNAME |
Yes | — | Portainer username |
PORTAINER_PASSWORD |
Yes | — | Portainer password |
PORTAINER_DEFAULT_ENDPOINT |
No | 1 |
Default endpoint ID for container/image/stack operations |
PORTAINER_VERIFY_SSL |
No | true |
Set to false for self-signed certificates |
Tools
All 23 tools are listed below with their parameters and descriptions. Every tool returns JSON.
Authentication
| Tool | Description |
|---|---|
portainer_status() |
Check connection and authentication status. Returns version and instance ID. |
Endpoints (Environments)
| Tool | Description |
|---|---|
portainer_endpoints_list() |
List all environments. Returns id, name, type, url, status. |
portainer_endpoint_inspect(endpoint_id) |
Get endpoint details (sensitive fields like TLS certs are filtered). |
Stacks
| Tool | Description |
|---|---|
portainer_stacks_list() |
List all stacks with id, name, type, status, endpoint_id. |
portainer_stack_inspect(stack_id) |
Get stack details including the docker-compose file content. |
portainer_stack_deploy(name, compose_content, endpoint_id?) |
Deploy a new stack. Auto-detects Swarm vs standalone. |
portainer_stack_update(stack_id, compose_content?, endpoint_id?) |
Update a stack. Omit compose_content to redeploy existing. |
portainer_stack_delete(stack_id) |
Delete a stack. |
portainer_stack_start(stack_id) |
Start a stopped stack. |
portainer_stack_stop(stack_id) |
Stop a running stack. |
Containers
| Tool | Description |
|---|---|
portainer_containers_list(endpoint_id?, all?) |
List containers. Set all=true to include stopped. |
portainer_container_inspect(container_id, endpoint_id?) |
Get detailed container info. |
portainer_container_start(container_id, endpoint_id?) |
Start a stopped container. |
portainer_container_stop(container_id, endpoint_id?) |
Stop a running container. |
portainer_container_restart(container_id, endpoint_id?) |
Restart a container. |
portainer_container_remove(container_id, force?, endpoint_id?) |
Remove a container. force defaults to false. |
portainer_container_logs(container_id, tail?, endpoint_id?) |
Get container logs. tail defaults to 100 (max 1000). |
Images
| Tool | Description |
|---|---|
portainer_images_list(endpoint_id?) |
List images with tags and sizes. |
portainer_image_inspect(image_id, endpoint_id?) |
Get detailed image info. |
portainer_image_pull(image_name, tag?, endpoint_id?) |
Pull an image. tag defaults to "latest". |
portainer_image_remove(image_id, endpoint_id?) |
Remove an image. |
Users
| Tool | Description |
|---|---|
portainer_users_list() |
List all Portainer users with id, username, role. |
portainer_user_inspect(user_id) |
Get user details. |
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.
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) to get the current compose file, modify the image tag, then portainer_stack_update(stack_id, compose_content).
Security
- JWT auth with proactive refresh (7h TTL, Portainer default is 8h) plus 401-retry fallback.
- SSL verification enabled by default. Only disable for self-signed certificates.
- Input validation — container IDs, image references, and stack names are validated with regex before API calls. Path traversal (
..) is blocked. - Sensitive field filtering —
endpoint_inspectstrips TLS certificates, Azure credentials, and security settings from responses. - Audit logging — all mutating operations (deploy, delete, remove, pull, start, stop) are logged to stderr with parameters.
- No hardcoded credentials — all secrets come from environment variables.
- Container removal —
forcedefaults tofalseto prevent accidental deletion of running containers. - Log size limits — container logs are capped at 100K characters to prevent memory exhaustion.
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.server
Lint and type-check:
ruff check src/
mypy src/
Requirements
- Python 3.10+
- A running Portainer instance (CE or Business Edition)
- Portainer API access (default port 9443)
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file portainer_mcp-0.1.0.tar.gz.
File metadata
- Download URL: portainer_mcp-0.1.0.tar.gz
- Upload date:
- Size: 14.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6c726766783cdf477666378866f7beb7f003520090b5e037b5e1c214dadb6fba
|
|
| MD5 |
88a0f666f11d91c20ee505d45efcbbd2
|
|
| BLAKE2b-256 |
beabb5cc88c39351584803425f91e3febd76a0e14698152f5d42923d1d930081
|
Provenance
The following attestation bundles were made for portainer_mcp-0.1.0.tar.gz:
Publisher:
publish.yml on ginkida/portainer-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
portainer_mcp-0.1.0.tar.gz -
Subject digest:
6c726766783cdf477666378866f7beb7f003520090b5e037b5e1c214dadb6fba - Sigstore transparency entry: 1214039156
- Sigstore integration time:
-
Permalink:
ginkida/portainer-mcp@e1ae421554744e0d92f689486188b033e6b88b36 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ginkida
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e1ae421554744e0d92f689486188b033e6b88b36 -
Trigger Event:
release
-
Statement type:
File details
Details for the file portainer_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: portainer_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 21.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5a6233bb861866321aeac4daf7517b48ccdcff775d01fad94f62c6b2607801a8
|
|
| MD5 |
b78aa3eb856acd5cbdccd04d4ba35764
|
|
| BLAKE2b-256 |
0f23eeb26fcd5dd02a4f6e4580690a981e54ce63482721c4adb09da87c164b2e
|
Provenance
The following attestation bundles were made for portainer_mcp-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ginkida/portainer-mcp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
portainer_mcp-0.1.0-py3-none-any.whl -
Subject digest:
5a6233bb861866321aeac4daf7517b48ccdcff775d01fad94f62c6b2607801a8 - Sigstore transparency entry: 1214039238
- Sigstore integration time:
-
Permalink:
ginkida/portainer-mcp@e1ae421554744e0d92f689486188b033e6b88b36 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ginkida
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e1ae421554744e0d92f689486188b033e6b88b36 -
Trigger Event:
release
-
Statement type: