Towline Agent Documentation
This document is designed for AI agents (Claude Code, Cursor, Codex, Gemini CLI, Windsurf, etc.) that need to understand what Towline is, how it works, and how to use its tools to build, deploy, and operate software on Portainer-managed infrastructure.
TL;DR: Towline gives you an isolated container stack on Portainer with 10 high-level MCP tools for deployment, health checks, logs, domain routing, scaling, and exec. You can go from "write code" to "running in production" without leaving your MCP session.
What is Towline?
Towline is the agentic engineering SDK for Portainer. It has three components:
- towline-mcp — An MCP server (Go binary) that runs per-project, providing your agent with scoped access to a single Portainer stack. It adds stack isolation, tier-based approval gating, and 10 high-level operational tools on top of the upstream Portainer MCP.
- towline CLI — A scaffolding tool.
towline init <project>creates everything: Portainer team, scoped API key, container stack, agent configuration, compose files, and git repo. - Template packs — Compose files + agent skills + MCP configs bundled as starter kits (
default,web-app,api, and theai-stackandn8npacks, or your own).
The CLI also ships an approval server for prod (towline approvals serve), and towline-mcp has a gateway mode that serves projects remotely to apps with no terminal (see Connected Through the Remote Gateway).
How Your MCP Session Works
When your user runs towline init my-project, it creates a .mcp.json (or equivalent for your agent) that tells your MCP client to launch towline-mcp with these flags:
TOWLINE_PORTAINER_TOKEN=<scoped-api-key> \
towline-mcp \
-server https://portainer.example.com \
-stack my-project-dev \
-tier devThe token is passed via the TOWLINE_PORTAINER_TOKEN environment variable (the config's env block), not on the command line. Prod-tier configs also pass -approval-mode human|agent (plus -approval-webhook <url> and a TOWLINE_APPROVAL_WEBHOOK_TOKEN env var in human mode). -skip-tls-verify is added only when the user said Portainer uses a self-signed certificate, and -allow-bind-mounts, -allow-networks, -allow-volumes or -compose-policy off only when the user relaxed the compose policy for the project.
This MCP server process is your gateway to the Portainer environment. Every tool call you make goes through it, and it enforces:
- Stack scoping — You can only see and modify the
my-projectstack. Other stacks are invisible. - Container ownership — Docker proxy calls are filtered to containers belonging to your stack, and only a small set of container lifecycle mutations is allowed (see Using the Docker Proxy).
- Compose security policy — compose files that could reach the host (privileged mode, host bind mounts, host networking, etc.) are rejected in every tier (see Compose Security Policy).
- Tier gating — In
devtier, you have full autonomy. Inprodtier, deploys, configuration changes, exec, and destructive operations require approval: from a human via an approval server (human mode), or your own explicit confirmation (agent mode).
MCP Tools Reference
You have access to two categories of tools: Towline tools (high-level, one-call answers) and upstream Portainer tools (stack CRUD and Docker proxy).
Towline Tools
towline_service_health
Get health snapshot for one or all services in the stack.
| Parameter | Type | Required | Description |
|---|---|---|---|
service | string | No | Service name from docker-compose. Omit for all services. |
Returns: state, uptime, restart count, CPU/memory usage, exit code. If stats or inspect calls fail, stats_error or inspect_error fields will contain the error message.
towline_service_logs
Fetch recent log output for a named service.
| Parameter | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name from docker-compose |
tail | number | No | Lines to return (default 200) |
since | string | No | Only logs after this time: an RFC3339 timestamp (2026-01-02T15:04:05Z), a duration meaning that long ago (10m, 2h), or unix seconds |
filter | string | No | Filter lines containing this string |
towline_env_get
Read stack environment variables. Sensitive values (names containing KEY, SECRET, PASSWORD, TOKEN) are masked.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | No | Specific variable name. Omit for all. |
towline_env_set
Set a stack environment variable. A service restart may be needed to pick up the change. Recorded in towline_deployments (name only, never the value). Requires approval in prod tier.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Variable name. Must match [A-Za-z_][A-Za-z0-9_]*; _TOWLINE_* names are rejected. |
value | string | Yes | Variable value |
approvalToken | string | No | Token for prod tier approval |
towline_domains_list
List all domain/routing mappings for services in the stack. No parameters.
towline_domains_add
Add domain routing to a service. Requires approval in prod tier.
- Traefik (labels in the compose file, then a redeploy): one domain per service. If the service already has a Towline-managed route, remove that domain before adding another.
- Caddy (admin API): the service must exist in your compose file, and a hostname already routed by another route cannot be claimed. Routes dial
<service>.<stack>.towline:<port>; that network alias is added to the service (and the stack redeployed) if it is missing. - Cloudflare: returns instructions to carry out with the Cloudflare MCP.
| Parameter | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name |
domain | string | Yes | Domain name (e.g. app.example.com) |
port | number | No | Service port (default 80) |
method | string | No | traefik, caddy, or cloudflare (auto-detected if omitted) |
approvalToken | string | No | Token for prod tier approval |
towline_domains_remove
Remove domain routing from a service. With Traefik, the service's router rule must be exactly Host(`domain`); for any other rule nothing is changed and you get an error (edit the labels in the compose file instead). Requires approval in prod tier.
| Parameter | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name |
domain | string | Yes | Domain to remove |
method | string | No | traefik, caddy, or cloudflare (auto-detected if omitted) |
approvalToken | string | No | Token for prod tier approval |
towline_scale
Scale a service to N replicas by setting deploy.replicas and redeploying. Warns if the service has persistent volumes. Refuses services whose definition or deploy block comes from YAML anchors, aliases or merge keys (<<), since the change would affect other services or drop inherited settings; give the service its own deploy block first. No approval required (operational).
| Parameter | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name |
replicas | number | Yes | Number of replicas: a whole number from 0 to 20 |
towline_deployments
View deployment history with diffs and outcomes.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | No | Max entries (default 10) |
towline_exec
Execute a command inside a running container and capture stdout/stderr. Commands get 30 seconds; if one runs longer, you get the output so far with a note that it may still be running. Requires approval in prod tier.
| Parameter | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Service name |
command | string | Yes | Command to run (space-separated args) |
approvalToken | string | No | Token for prod tier approval |
Upstream Portainer Tools
These are the standard Portainer MCP tools, scoped to your stack:
| Tool | Description |
|---|---|
listLocalStacks | List stacks (filtered to yours) |
getLocalStackFile | Get compose file content |
createLocalStack | Deploy a new stack (compose security policy applies) |
updateLocalStack | Update stack (full compose required!). Omitting env keeps the current env vars. Recorded in deployment history. |
startLocalStack | Start a stopped stack |
stopLocalStack | Stop a running stack |
deleteLocalStack | Delete a stack. You can createLocalStack again afterwards without restarting the MCP server. |
dockerProxy | Raw Docker API proxy: reads plus limited container lifecycle mutations |
Critical: Partial Compose Files
When calling updateLocalStack, you MUST submit the COMPLETE compose file. Portainer removes any service not included in the update. If you only include the service you changed, all other services will be destroyed.
Safe workflow:
- Call
getLocalStackFileto get the current compose content - Modify the YAML as needed
- Submit the entire modified file to
updateLocalStack
Compose Security Policy
In both dev and prod, createLocalStack and updateLocalStack reject compose files that use:
privileged(also onpost_start/pre_stophooks),cap_add,devicesdeploy.resources.reservations.devicesother than GPU reservationsnetwork_modeother thanbridgeornone; host orcontainer:...forpid,ipc,uts,userns_mode,cgroupsecurity_optwithunconfinedordisable- Host bind mounts: absolute, relative (
./),~, or${VAR}sources, and long-syntaxtype: bind volumes_from- Volumes with
driver_opts, a non-local driver,externalorname(unless allowed, see below) - Networks with
externalorname(unless allowed),driver_opts, or a driver other thanbridge/overlay secrets/configswithfile:env_fileoutside the stack directoryexternal_linksbuildfrom a local context (use a git/https URL or a prebuilt image)- Top-level
include/extends - More than one YAML document (
---) in the file - Service names, container names and network aliases under
.towline, which are reserved for Caddy routing (a service may only use its own<service>.<stack>.towline)
Named volumes and tmpfs are fine. A volume named <stack>_<key> (compose's default) is always allowed. If a compose file is rejected, fix it rather than working around the policy. If the stack genuinely needs a host path, an external network or another named volume, ask the human: they can allow specific bind mounts (allow_bind_mounts), external networks (allow_networks; never host, bridge or none) and volumes (allow_volumes), or turn the policy off, under compose_policy in the project's towline.json and run towline refresh, which passes them to towline-mcp as -allow-bind-mounts, -allow-networks, -allow-volumes or -compose-policy off. You cannot change the policy yourself. The other rules still apply when something is allowed, and .., ~ and ${VAR} sources are always rejected.
Tier System & Approval Flow
The -tier flag controls what you can do without human approval:
Dev Tier (-tier dev)
Full autonomy. Every tool call executes immediately. Use this for development and testing.
Prod Tier (-tier prod)
| Operation | Approval? | Examples |
|---|---|---|
| Read | No | health, logs, env_get, domains_list, deployments, listLocalStacks |
| Operational | No | startLocalStack, scale, dockerProxy container start/stop/restart |
| Configuration | Yes | env_set, domains_add, domains_remove |
| Deploy | Yes | createLocalStack, updateLocalStack |
| Destructive | Yes | stopLocalStack, deleteLocalStack |
| Exec / Docker write | Yes | towline_exec, other allowed dockerProxy mutations |
How Approval Works
The human chooses one of two approval modes (-approval-mode human|agent). The first call to a gated tool runs nothing and returns an approvalToken with one of two messages; the message tells you which mode is active.
"Production operation requires human approval" (human mode)
Approval comes from a human via an approval server (Towline's built-in towline approvals serve, or the user's own). You cannot approve your own requests, and a "yes" in the chat does not approve anything.
- You call a tool that requires approval (e.g.
updateLocalStackin prod) towline-mcpsends the request to the approval server under a random request ID. The response says the request is "shown to your partner as" a short 8-character ID, and contains anapprovalToken, which is separate from the server's request ID. Nothing has run yet.- Tell the human what the change does (and the short ID, so they can find it), then wait until they tell you they have approved or rejected it (in the approval UI, with
towline approvals show <id>andtowline approvals approve <id>, or via a notification) - Re-call the same tool with identical arguments plus the
approvalTokenparameter - If approved, the call executes. If still pending, nothing runs — wait for the human again; do not re-call in a loop. If rejected, the call is refused — do not retry without discussing it. Requests not decided within 30 minutes expire and are reported as rejected.
If human mode is active but no approval server is configured, prod operations that need approval are refused — ask the human to run towline approvals setup or to make the change themselves.
"Production operation requires confirmation" (agent mode)
For autonomous setups, the human has put you in charge of prod operations. This is an explicit confirmation step, not a formality: review what the change does to production (the exact arguments, the full compose file for deploys, possible downtime or data loss), then confirm by re-calling the same tool with identical arguments plus the approvalToken. The operation then executes.
In both modes, tokens are single-use, expire after 30 minutes, and are bound to the specific tool and arguments. You cannot reuse a token with different arguments; call again without approvalToken to get a new one. In human mode, a token is claimed while its re-call runs, so concurrent re-calls with the same token are rejected and the operation runs at most once. Requests whose arguments are too large to show the approver (over 64 KiB, not counting the compose file) are refused.
Connected Through the Remote Gateway
If you reach Towline as a connector (Claude, ChatGPT/Codex or another app, at a URL such as https://mcp.example.com/mcp), you are talking to the human's self-hosted Towline gateway, not a local towline-mcp process. The same stack scoping, compose policy and container checks apply, with these differences:
- Call
towline_guidefirst. It lists the projects you can use, each project's tier, your access (read or deploy) and approval policy, and includes the operations guide, since you can't readCLAUDE.mdor skills from disk. - Tool names. On
/mcp, tools are named<project>__<tool>(e.g.shop-prod__towline_service_health); use the tools of the project you are working on. On/p/<project>/mcpthey have their plain names. - No shell or files. Pass complete compose files inline and deploy images that already exist in a registry.
- Read-only connections don't see tools that change anything. If a call is refused because the client only has read access, ask the human for a connection with deploy access instead of looking for another route.
- Approvals. By default every change (deploy, configuration, exec, destructive action) needs the human's approval on the gateway page, on dev projects too. The first call runs nothing and returns an
approvalToken. Tell the human what the change does and that it is waiting on the gateway page, wait until they say they decided, then re-call with identical arguments plus the token. You can never confirm your own changes through the gateway, and a "yes" in the chat approves nothing. Tokens are bound to your connection, the tool and the exact arguments. - Slow calls. A call that takes longer than 80 seconds (for example a deploy pulling large images) answers that it is still running and finishes in the background. Check the outcome with
towline_deploymentsandtowline_service_healthrather than calling it again.
Common Agent Workflows
Deploy a new service
1. getLocalStackFile → get current compose
2. Add your new service to the YAML
3. updateLocalStack → deploy (submit FULL compose)
4. towline_service_health → verify it started
5. towline_service_logs → check for errorsDebug a failing service
1. towline_service_health → check state, restarts, exit code
2. towline_service_logs service=failing-svc tail=100 → read logs
3. towline_exec service=failing-svc command="cat /app/config.json" → inspect files
4. towline_env_get → check environment variables
5. Fix the issue, redeploy via updateLocalStackExpose a service on a domain
1. towline_domains_add service=web domain=app.example.com port=3000
(auto-detects Traefik/Caddy/Cloudflare)
2. towline_domains_list → verify routing
3. towline_service_health → ensure service is healthyScale a service
1. towline_scale service=worker replicas=3
2. towline_service_health → verify all replicas runningRoll back a broken deploy
1. towline_deployments → see history, find last working version
2. getLocalStackFile → get current (broken) compose
3. Revert to previous compose content
4. updateLocalStack → redeploy
5. towline_service_health → verify rollbackCLI Reference
These are commands the human user runs (not MCP tools). You may need to instruct them to run these:
towline init <project>
Create a new project with full Portainer scaffolding.
towline init --template api --tier dev my-project
Flags (must come before the project name; flags after it are rejected):
--template Compose template or pack (default, web-app, api, ai-stack, n8n, or a custom pack)
--tier Deployment tier (dev or prod, default: dev)Project names must match ^[a-z0-9][a-z0-9_-]{0,62}$. Each project has a single tier.
Run inside an existing codebase, init adds Towline to that directory: existing agent configs are merged rather than replaced, CLAUDE.md is appended to, and missing entries are added to .gitignore. It refuses to run if the directory already has a towline.json (use towline refresh) or if git tracks any file that would hold the project token.
towline list
List all Towline projects on this machine.
towline destroy <project>
Remove a project: deletes the Portainer stack, service user, and team. Each ID in towline.json is verified against Portainer first and skipped on mismatch. Local files are kept.
towline setup
Interactive setup: connects to a Portainer instance (verifying its TLS certificate unless the user says it is self-signed), asks how prod operations are approved (human or agent), and saves credentials. Re-running it keeps existing approval settings.
towline approvals
Configure and run prod-tier approvals. Only the human should run these.
towline approvals setup [--mode human|agent] [--listen addr] [--url external-server]
[--webhook-token t] [--ntfy topic-url] [--public-url url]
towline approvals serve # built-in approval server, web UI at /ui (default 127.0.0.1:8787)
towline approvals list # these four prompt for the human's approver token
towline approvals show <id>
towline approvals approve <id>
towline approvals reject <id>The approver token is shown once at setup and read from a terminal prompt, never from env vars or flags. Never ask the human for it or try to obtain it.
towline refresh
Brings existing projects up to date after towline update: rewrites the towline-<tier> entry in the MCP configs (keeping other servers and custom flags), moves the token into an env var, applies the approval mode and compose policy, fixes permissions and .gitignore, and moves the project team to the Standard user role. On first run it also allows the deployed stack's existing bind mounts (except host-control paths such as /, docker.sock or /etc), external networks and external or named volumes in compose_policy, and prints any remaining policy violations. The human restarts agent sessions afterwards.
towline refresh --all # every project
towline refresh <name>... # specific projects (or no argument inside a project)
--keep-role # don't change the team roletowline remote
Configures and deploys the remote gateway that serves projects to connector apps. Only the human should run these; the owner token printed by setup and owner-token is the human's, and you should never ask for it.
TUNNEL_TOKEN=<cloudflare-tunnel-token> \
towline remote setup --url https://mcp.example.com --all # or name projects
[--approval always|prod] [--ntfy-url url] [--portainer-url url]
[--no-tunnel --port 8080] [--compose-dir dir]
towline remote add <name>... # serve more projects (redeploys)
towline remote remove <name>... # stop serving projects (redeploys)
towline remote deploy # redeploy, e.g. after towline update
towline remote status # configuration and health check
towline remote owner-token # new owner token for the /ui pageNot yet implemented
towline promote, towline rotate-keys and towline status exist as commands but only print "not yet implemented". Don't suggest them to the human as a way to promote a project, rotate its key or check its health; use towline_service_health for health.
Generated Project Structure
~/projects/<name>/
├── .mcp.json # MCP server configuration (0600, gitignored)
├── .claude/settings.json # Claude Code permissions
├── .cursor/mcp.json # Cursor MCP config (0600, gitignored)
├── .gemini/settings.json # Gemini CLI MCP config (0600, gitignored)
├── towline.json # Project config: stack, tier, compose_policy (0600, gitignored)
├── CLAUDE.md # Agent instructions for this project
├── docker-compose.yml # From template
├── .env.example # Env var template
├── skills/ # Agent skills library
└── .git/The .mcp.json file tells your MCP client how to launch towline-mcp. The CLAUDE.md file contains project-specific instructions including available tools, the stack name, and operational guidelines.
Using the Docker Proxy
The dockerProxy tool gives you raw Docker API access, scoped to your stack's containers. Use it for operations not covered by the Towline tools.
dockerAPIPathmust be a plain path: query strings, fragments,%-encoding, backslashes, and./..or empty segments are rejected. Pass query parameters viaqueryParams. API version prefixes like/v1.43/are normalized before scoping.- The only mutating requests allowed are
POST /containers/{id}/{start|stop|restart|kill|pause|unpause|wait|resize|rename|update}andDELETE /containers/{id}, on your stack's own containers. - Container create, exec, archive upload, and anything on networks, volumes, images, system, etc. are rejected. Use
updateLocalStackandtowline_execinstead.
// List containers
dockerProxy method=GET dockerAPIPath=/containers/json queryParams=[{key: "all", value: "true"}]
// Inspect a container
dockerProxy method=GET dockerAPIPath=/containers/<id>/json
// Restart a container (no approval in prod — operational)
dockerProxy method=POST dockerAPIPath=/containers/<id>/restart
// Other allowed mutations (kill, pause, rename, update, DELETE, ...) require approval in prodInternal Environment Variables
Variables prefixed with _TOWLINE_ are managed internally by Towline (e.g. deployment history). You cannot set or override them — they are silently filtered from createLocalStack and updateLocalStack requests, and the stack's own internal variables and deployment history are preserved on update. Omitting env from updateLocalStack keeps the stack's current variables. Every updateLocalStack and towline_env_set is recorded in towline_deployments (env changes record the variable name only, never the value).
Prerequisites
- Portainer v2.28+ with API access
- Docker on the container host
- Network connectivity from the dev machine to the Portainer API
- An MCP-compatible agent (Claude Code, Cursor, Codex, Gemini CLI, Windsurf)
Links
This documentation is designed to be consumed by AI agents. For human-readable docs, see the Getting Started guide.