portainer/portainer-mcp/portainer-mcp-hygiene
Claude Code and agent skill by portainer/portainer-mcp
- community
Checking that the SKILL.md file is still in the repository.
How to drive the Portainer MCP server's tools correctly — both reading and mutating. Reading: project responses with `select` (JMESPath), where the heavy fields live (snapshots, status blocks, managed fields), how to handle non-JSON Docker/K8s proxy endpoints (container and pod logs, stats, exec), and how to interpret results that are easy to misread (e.g. an edge environment's health comes from its heartbeat, not its `Status` field; typed K8s tools use different field names than the raw proxy). Mutating: deploying, scaling, restarting, and deleting Portainer-managed resources — where success payloads are empty and must be verified out-of-band, and where cleanup (orphaned volumes) and recovery (name-based vs id-based calls) have gotchas. Trigger this whenever you're about to call any Portainer MCP tool — including `docker_proxy`, `kubernetes_proxy`, `EndpointList`, `GetAllKubernetes*`, `StackList`, `StackCreateKubernetes*`, `StackDelete*`, `CreateKubernetes*`, `snapshot*`, `Helm*`, or any other `mcp__portainer__*` tool — and whenever the user asks you to inspect, deploy, scale, restart, or remove Docker containers/images/stacks/networks, Kubernetes resources, or Helm releases managed by Portainer. Use it even if the user doesn't mention Portainer by name, as long as the working answer requires one of these tools.
Install
npx vskill@latest install portainer/portainer-mcp/portainer-mcp-hygieneSKILL.md
Portainer MCP hygiene
The Portainer MCP server returns large JSON payloads by default — a list of environments with snapshots, a list of K8s pods with full status blocks, a stack with its complete manifest. Every tool the server exposes accepts an optional select (JMESPath) parameter applied server-side before the response reaches you. Responses are capped at ~50,000 chars; if you exceed the cap you get a truncation hint that names select and shows an example.
The cost of not projecting is real: 50K chars of dense JSON eats roughly 20K tokens out of your context for a question that usually needed a few hundred. Once truncation fires, you've wasted a round trip and the data past the cap is gone for that call. The default move on any list-shaped Portainer call is to pass select from the start.
Resolve the environment first
Both proxy tools take an environment_id, and the IDs aren't predictable — they're assigned in creation order, not "local = 1". So for any environment-scoped question, resolve the target first with one call:
and use the ID whose name matches what the user means. Guessing the environment is a hard failure, not a silent one: a wrong ID makes the proxy raise an API request failed (HTTP 404) error rather than returning empty data — so a wrong environment is not a source of null projections. Two other things are, though: an unquoted dotted key (see the JMESPath notes below) and a field-name mismatch on the typed GetAllKubernetes* tools (see their note under Where the noise lives). So read an all-null result as a wrong-keys signal to investigate, not as proof the data is absent.
The default pattern
For any call that returns a list of objects, ship a JMESPath that keeps only the fields the user's question actually needs:
JMESPath syntax notes that matter for these surfaces:
- List shape: start with
[]to map over array elements. - Wrapped list (Kubernetes
{items: [...]}): start withitems[]. - Single object:
{field1:path.to.value,field2:other.path}— no leading[]. - Nested paths use dots:
Snapshots[0].RunningContainerCount. But a key that
itself contains a dot or hyphen — compose labels, K8s annotations — collides with that syntax, so quote it as an identifier: Labels."com.docker.compose.project", or in a filter [?Labels."com.docker.compose.project"=='myproj']. Unquoted, JMESPath reads the dots as nested keys and silently returns null — which looks like a missing field rather than a broken expression. Quote with double quotes, not backslashes (\"…\" fails to parse) and not backticks (those denote JSON literals).
- The backslash trap is easy to fall into by accident: the
selectstring is
itself an argument inside a JSON tool call, and escaping the inner quotes a second time sends literal \" to the server. The failure signature is a parse error mentioning Unknown token \. When you hit it, either resend with plain double quotes (encode the tool-call JSON once, not twice), or sidestep quoting entirely with a function filter — [?contains(metadata.name, 'ingress-nginx')] uses only single quotes and survives any transport.
Where the noise lives
These are the fields/sections that dominate Portainer payloads. Either project them out (when you don't need them) or project specifically into them (when they are the answer):
`EndpointList` — `.Snapshots[0]` carries the heavy payload. Each environment includes a full Docker or Kubernetes snapshot — container list, image list, network list, etc. For counts and status questions you almost always want to project into specific snapshot fields rather than fetch them whole:
Status is a reachability signal only for direct agents. For edge environments it doesn't track reachability at all — see Interpreting results, not just shrinking them below before reading it as up/down.
Two EndpointList query params don't surface over MCP: updateInformation (agent-update availability) and k8sEnvAdmin (K8s admin flag). Portainer returns their answer in HTTP response headers (X-Update-Available, X-K8S-Env-Admin), and this server only ever returns the response body — so passing them changes nothing you can read. Don't rely on them through the MCP.
Kubernetes via `kubernetes_proxy` — `metadata.managedFields` and `status` are huge. metadata.managedFields alone is routinely 30-70% of an object. The status block on Deployments, StatefulSets, Pods, and Nodes is similarly verbose. Project them out unless the user is asking about reconciliation state or controller history:
A caveat on that pod projection: status.containerStatuses[0].restartCount — and status.containerStatuses[0].state.waiting.reason — comes back null for pods that never started a container, because Pending and many Failed pods have an empty containerStatuses. That's exactly the unhealthy set you're usually chasing, so don't read a null restart count as "zero restarts, healthy." Lead with status.phase and the pod-level status.reason (e.g. Evicted), and treat restartCount as extra detail that only exists once a container has actually run:
`GetAllKubernetes*` tools — full object body per element, in a Portainer-specific shape. The OpenAPI-generated GetAllKubernetesApplications, GetAllKubernetesPersistentVolumes, GetAllKubernetesConfigMaps, etc. return arrays where each element carries its full body — so project to the fields you need. But mind the field names: these typed tools do not use the raw-K8s paths the proxy uses (metadata.name, status.phase, spec.…), and they don't reliably match the PascalCase of EndpointList (Id, Name) either. They tend to be flattened camelCase, and some fields collapse — e.g. GetAllKubernetesPersistentVolumes returns identity as a top-level name (not metadata.name) and status as a bare string "Bound" (not status.phase):
Because the convention varies by tool, an all-null projection here almost always means you guessed the wrong keys — reusing the proxy's metadata.name or EndpointList's Name — not that the data is absent. When unsure, fetch one element with no `select` first to read the real field names, then project. If you'd rather work in raw-K8s field names, use kubernetes_proxy instead.
`StackList` and `StackInspect` — config and env vars. Stacks carry the full compose/manifest content plus environment variable dictionaries. If the user asked "which stacks exist?", project to {id, name, type, status}. If they asked about a specific stack's config, fetch it directly and only then look at the body. Env values come back redacted by default — see Env values are redacted by default below.
Snapshot inspects (`snapshotInspect`, `snapshotContainersList`, etc.) — entire snapshots. These return the whole snapshot blob by design. Always project.
Helm endpoints — full chart values and manifests. HelmList carries release status + chart metadata; HelmGet returns the rendered manifest. Project to release names and status when listing; only fetch the manifest when the user asked to see it.
`EndpointGetCharts`, `dockerDashboard`, `EndpointSummaryCounts` — already aggregated. These are the lightweight "summary" tools. Prefer them over EndpointList + projection when the user's question is purely a count or rollup — fewer characters, less work, more accurate (server-side aggregation).
Env values are redacted by default. Stack, container, and Kubernetes env values come back as [REDACTED]. The response also carries a one-line summary: [N env value(s) redacted; set PORTAINER_EXPOSE_ENV_VALUES=1 on the MCP server to disclose].
- Don't waste a tool call fishing for them via
select— the projection runs after redaction, so any field path lands on the sentinel. - If the user genuinely needs an env value (troubleshooting a deploy), tell them to set
PORTAINER_EXPOSE_ENV_VALUES=1on the MCP server and reconnect. Don't invoke the toggle yourself. - The sentinel
[REDACTED]is a literal placeholder — never quote it back to the user as if it were the real value. - Redaction covers
Env/EnvVarsshapes (stackEnvpairs, DockerKEY=VALstrings, K8senv[].value). K8svalueFromreferences are preserved — they're references to a Secret/ConfigMap, not the secret material itself.
Interpreting results, not just shrinking them
Projecting the right fields only helps if you read them correctly. The highest-frequency misread on this surface:
Edge environments — `Status` is not the health signal; `Heartbeat` is. The endpoint Status field means up/down only for direct agents (1 = up, 2 = down). For edge agents (Type 4 = EdgeAgentOnDocker, 7 = EdgeAgentOnKubernetes) Status is not a reachability signal in either direction: Portainer sets it to 1 when the edge environment is created and again on every check-in, and never flips it to 2 (down) on its own. So a dead edge agent that last checked in an hour ago still reads Status: 1, and you may also see Status: 0 on an edge env that was provisioned but never completed a normal create/check-in path — neither value reflects current health. Don't read edge Status as up or down. Portainer judges an edge agent by its heartbeat: up if it checked in within 2 × interval + 20 seconds (the interval is EdgeCheckinInterval, falling back to the global Edge check-in setting, for standard agents; the smallest of the ping/command/snapshot intervals for async agents). The server exposes this as a computed Heartbeat boolean — which is exactly what the dashboard's environment badge renders ("Heartbeat" vs "Down").
For a reachability check that's correct across both kinds, project the heartbeat inputs, not just Status:
Read it as: direct agent → trust `status` (1 = up); edge agent (`type` 4/7) → trust `heartbeat` (true = up) and ignore status.
Patterns for common questions
A few high-frequency questions and the projection that gets them in one call:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/portainer-mcp-hygiene/SKILL.md
Popularity
- Repository stars
- 220
- Repository forks
- 45
- 7-day trend score
- 27
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/portainer/portainer-mcp/portainer-mcp-hygiene)