Skip to main content
Glama
Cuti27

mcp-portainer-lite

by Cuti27
README.md
# mcp-portainer-lite

[![npm version](https://img.shields.io/npm/v/@cuti27/mcp-portainer-lite)](https://www.npmjs.com/package/@cuti27/mcp-portainer-lite)
[![GHCR](https://img.shields.io/badge/GHCR-ghcr.io/cuti27/mcp--portainer--lite-blue)](https://github.com/cuti27/mcp-portainer-lite/pkgs/container/mcp-portainer-lite)
[![CI](https://github.com/cuti27/mcp-portainer-lite/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/cuti27/mcp-portainer-lite/actions/workflows/docker-publish.yml)
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)

Lightweight [MCP](https://modelcontextprotocol.io) server for [Portainer CE](https://github.com/portainer/portainer), focused on **Docker Swarm** environments. Unofficial project.

## Why this project?

The [official Portainer MCP server](https://github.com/portainer/portainer-mcp) is comprehensive but heavy. `mcp-portainer-lite` is:

- **Lightweight**: no unnecessary dependencies, built with Node.js HTTP, Zod, and Pino.
- **Swarm-focused**: tools designed for Swarm environments.
- **Write-protected**: mutation operations are disabled by default.
- **Dual transport**: works over stdio (npx) and HTTP (Docker).

### Comparison with portainer/portainer-mcp

| Aspect | portainer/portainer-mcp | mcp-portainer-lite |
|---|---|---|
| Size | ~25 MB installed | ~8 MB installed |
| Focus | Docker + Swarm | Swarm only |
| Mutations | Always enabled | `ALLOW_MUTATIONS=true` required |
| Transports | stdio | stdio + HTTP |
| Auth | API key or credentials | API key + optional bearer token |

## Requirements

- Node.js >= 20 (for npx usage) or Docker (for container)
- Portainer CE 2.39.x or higher
- Portainer API key with read permissions (and write if using mutations)

## Installation

### Docker Compose (recommended)

```yaml
version: '3.8'
services:
  portainer-mcp:
    image: ghcr.io/cuti27/mcp-portainer-lite:latest
    container_name: portainer-mcp
    ports:
      - "3000:3000"
    environment:
      - PORTAINER_URL=http://portainer:9000
      - PORTAINER_API_KEY=ptr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      - PORTAINER_ENDPOINT_ID=1
      - ALLOW_MUTATIONS=false
      - MCP_AUTH_TOKEN=your-secure-token-here
      - PORT=3000
      - HOST=0.0.0.0
      # Only for plain HTTP Portainer. Use HTTPS in production.
      - PORTAINER_ALLOW_HTTP=true
    networks:
      - portainer-net

networks:
  portainer-net:
    external: true
```

> ⚠️ **Security warning**: `HOST=0.0.0.0` exposes the MCP server to the network. Only use it inside Docker with `MCP_AUTH_TOKEN` set, and put the container behind a TLS-terminating reverse proxy. Never expose the HTTP transport to the internet without authentication and TLS.

### npx

```bash
PORTAINER_URL=http://localhost:9000 \
PORTAINER_API_KEY=ptr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
PORTAINER_ENDPOINT_ID=1 \
PORTAINER_ALLOW_HTTP=true \
npx -y @cuti27/mcp-portainer-lite
```

### Claude Desktop

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "portainer": {
      "command": "npx",
      "args": ["-y", "@cuti27/mcp-portainer-lite"],
      "env": {
        "PORTAINER_URL": "http://localhost:9000",
        "PORTAINER_API_KEY": "ptr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "PORTAINER_ENDPOINT_ID": "1",
        "PORTAINER_ALLOW_HTTP": "true"
      }
    }
  }
}
```

## Environment variables

| Variable | Required | Default | Description |
|---|---|---|---|
| `PORTAINER_URL` | Yes | — | Portainer base URL (e.g. `http://portainer:9000`) |
| `PORTAINER_API_KEY` | Yes | — | Portainer API key |
| `PORTAINER_ENDPOINT_ID` | No | first endpoint | Default endpoint ID |
| `ALLOW_MUTATIONS` | No | `false` | Enable write tools |
| `MCP_TRANSPORT` | No | `stdio` | Transport: `stdio` or `http` |
| `MCP_AUTH_TOKEN` | Only for HTTP | — | Bearer token to authenticate MCP HTTP requests |
| `PORT` | No | `3000` | HTTP port (http transport only) |
| `HOST` | No | `127.0.0.1` | HTTP bind address (http transport only) |
| `PORTAINER_ALLOW_HTTP` | No | `false` | Allow plain HTTP for `PORTAINER_URL` (not recommended for production) |
| `TLS_CERT_PATH` | No | — | Path to TLS certificate (HTTPS transport directly) |
| `TLS_KEY_PATH` | No | — | Path to TLS private key (HTTPS transport directly) |
| `TRUSTED_PROXIES` | No | — | Comma-separated trusted proxy IPs/CIDRs for `X-Forwarded-For` parsing |
| `LOG_LEVEL` | No | `info` | Log level: trace/debug/info/warn/error/fatal |

## Available tools

### Phase 1 — Read (always available)

| Tool | Description |
|---|---|
| `list_endpoints` | List Portainer-managed environments |
| `list_stacks` | List stacks for an endpoint |
| `get_stack` | Get stack details |
| `get_service` | Get service details |
| `list_services` | List Swarm services for an endpoint |
| `get_service_logs` | Get last N log lines from a service (best-effort sensitive data redaction) |
| `get_endpoint_summary` | Get endpoint summary |

### Phase 2 — Mutation (only with `ALLOW_MUTATIONS=true`)

| Tool | Description |
|---|---|
| `restart_service` | Restart a service |
| `start_service` | Scale a service to 1 replica |
| `stop_service` | Scale a service to 0 replicas |
| `restart_stack` | Restart all services in a stack |

## Security

- **Authentication**: When using HTTP transport, `MCP_AUTH_TOKEN` is required. All requests must include `Authorization: Bearer <token>`. Requests without a valid token receive `401 Unauthorized`.
- **API key redaction**: The Portainer API key and Authorization header are automatically redacted from logs by Pino.
- **Write protection**: Mutation operations are **disabled by default**. Enable them only with `ALLOW_MUTATIONS=true`.
- **Input validation**: All service IDs, endpoint IDs, and stack IDs are validated against alphanumeric patterns before being used in API calls, preventing path traversal.
- **Rate limiting**: The HTTP server enforces 60 requests per minute per IP. Excess requests receive `429 Too Many Requests`.
- **Body size limit**: HTTP POST bodies are limited to 1 MB. Larger payloads receive `413 Payload Too Large`.
- **Safe error messages**: Internal error details are logged server-side but never returned to the client.
- **Host binding**: Defaults to `127.0.0.1` (localhost only). If set to `0.0.0.0`, a warning is emitted.
- **HTTPS recommended**: Use `https://` for `PORTAINER_URL` in production. A startup warning is shown if using plain HTTP.
- **Principle of least privilege**: Use a Portainer API key with **minimum required permissions** (read-only if mutations are not needed).

## Compatibility

Tested with **Portainer CE 2.39.x**. Compatible with newer versions barring breaking API changes.

## Local test environment

The repository includes helper scripts to spin up a disposable Docker Swarm sandbox with Portainer CE and dummy services for safe end-to-end testing.

```bash
# Start the test environment (Portainer CE on http://localhost:9000)
./scripts/test-swarm-up.sh

# The script prints credentials and writes a ready-to-use .env.test file.
source .env.test
npm run start:stdio

# Destroy everything when done
./scripts/test-swarm-down.sh
```

These scripts only touch resources named `portainer-test` or `test-apps`; your existing Docker containers, networks, and volumes are left untouched.

## Development

```bash
# Clone
git clone https://github.com/cuti27/mcp-portainer-lite.git
cd mcp-portainer-lite

# Install
npm install

# Watch mode
npm run dev

# Build
npm run build

# Run
npm start
```

## Tests

```bash
# Unit + integration tests
npm test

# With coverage
npm run test:coverage
```

## License

MIT. See [LICENSE](LICENSE).