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:

  1. 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.
  2. towline CLI — A scaffolding tool. towline init <project> creates everything: Portainer team, scoped API key, container stack, agent configuration, compose files, and git repo.
  3. Template packs — Compose files + agent skills + MCP configs bundled as starter kits (default, web-app, api, and the ai-stack and n8n packs, 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 dev

The 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:

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.

ParameterTypeRequiredDescription
servicestringNoService 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.

ParameterTypeRequiredDescription
servicestringYesService name from docker-compose
tailnumberNoLines to return (default 200)
sincestringNoOnly logs after this time: an RFC3339 timestamp (2026-01-02T15:04:05Z), a duration meaning that long ago (10m, 2h), or unix seconds
filterstringNoFilter lines containing this string

towline_env_get

Read stack environment variables. Sensitive values (names containing KEY, SECRET, PASSWORD, TOKEN) are masked.

ParameterTypeRequiredDescription
namestringNoSpecific 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.

ParameterTypeRequiredDescription
namestringYesVariable name. Must match [A-Za-z_][A-Za-z0-9_]*; _TOWLINE_* names are rejected.
valuestringYesVariable value
approvalTokenstringNoToken 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.

ParameterTypeRequiredDescription
servicestringYesService name
domainstringYesDomain name (e.g. app.example.com)
portnumberNoService port (default 80)
methodstringNotraefik, caddy, or cloudflare (auto-detected if omitted)
approvalTokenstringNoToken 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.

ParameterTypeRequiredDescription
servicestringYesService name
domainstringYesDomain to remove
methodstringNotraefik, caddy, or cloudflare (auto-detected if omitted)
approvalTokenstringNoToken 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).

ParameterTypeRequiredDescription
servicestringYesService name
replicasnumberYesNumber of replicas: a whole number from 0 to 20

towline_deployments

View deployment history with diffs and outcomes.

ParameterTypeRequiredDescription
limitnumberNoMax 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.

ParameterTypeRequiredDescription
servicestringYesService name
commandstringYesCommand to run (space-separated args)
approvalTokenstringNoToken for prod tier approval

Upstream Portainer Tools

These are the standard Portainer MCP tools, scoped to your stack:

ToolDescription
listLocalStacksList stacks (filtered to yours)
getLocalStackFileGet compose file content
createLocalStackDeploy a new stack (compose security policy applies)
updateLocalStackUpdate stack (full compose required!). Omitting env keeps the current env vars. Recorded in deployment history.
startLocalStackStart a stopped stack
stopLocalStackStop a running stack
deleteLocalStackDelete a stack. You can createLocalStack again afterwards without restarting the MCP server.
dockerProxyRaw 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:

  1. Call getLocalStackFile to get the current compose content
  2. Modify the YAML as needed
  3. Submit the entire modified file to updateLocalStack

Compose Security Policy

In both dev and prod, createLocalStack and updateLocalStack reject compose files that use:

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)

OperationApproval?Examples
ReadNohealth, logs, env_get, domains_list, deployments, listLocalStacks
OperationalNostartLocalStack, scale, dockerProxy container start/stop/restart
ConfigurationYesenv_set, domains_add, domains_remove
DeployYescreateLocalStack, updateLocalStack
DestructiveYesstopLocalStack, deleteLocalStack
Exec / Docker writeYestowline_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.

  1. You call a tool that requires approval (e.g. updateLocalStack in prod)
  2. towline-mcp sends 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 an approvalToken, which is separate from the server's request ID. Nothing has run yet.
  3. 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> and towline approvals approve <id>, or via a notification)
  4. Re-call the same tool with identical arguments plus the approvalToken parameter
  5. 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:

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 errors

Debug 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 updateLocalStack

Expose 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 healthy

Scale a service

1. towline_scale service=worker replicas=3
2. towline_service_health → verify all replicas running

Roll 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 rollback

CLI 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 role

towline 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 page

Not 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.

// 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 prod

Internal 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

This documentation is designed to be consumed by AI agents. For human-readable docs, see the Getting Started guide.