@mseep/mcp-swarmpit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +128 -0
- package/README.md +416 -0
- package/dist/client.d.ts +107 -0
- package/dist/client.js +297 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +41 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/sanitize.d.ts +41 -0
- package/dist/sanitize.js +165 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/test/config.test.d.ts +1 -0
- package/dist/test/config.test.js +103 -0
- package/dist/test/config.test.js.map +1 -0
- package/dist/test/file-ref.test.d.ts +1 -0
- package/dist/test/file-ref.test.js +163 -0
- package/dist/test/file-ref.test.js.map +1 -0
- package/dist/test/helpers.test.d.ts +1 -0
- package/dist/test/helpers.test.js +133 -0
- package/dist/test/helpers.test.js.map +1 -0
- package/dist/test/sanitize.test.d.ts +1 -0
- package/dist/test/sanitize.test.js +207 -0
- package/dist/test/sanitize.test.js.map +1 -0
- package/dist/tools/admin.d.ts +3 -0
- package/dist/tools/admin.js +64 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/configs.d.ts +4 -0
- package/dist/tools/configs.js +70 -0
- package/dist/tools/configs.js.map +1 -0
- package/dist/tools/dashboard.d.ts +3 -0
- package/dist/tools/dashboard.js +41 -0
- package/dist/tools/dashboard.js.map +1 -0
- package/dist/tools/helpers.d.ts +16 -0
- package/dist/tools/helpers.js +74 -0
- package/dist/tools/helpers.js.map +1 -0
- package/dist/tools/networks.d.ts +3 -0
- package/dist/tools/networks.js +70 -0
- package/dist/tools/networks.js.map +1 -0
- package/dist/tools/nodes.d.ts +3 -0
- package/dist/tools/nodes.js +59 -0
- package/dist/tools/nodes.js.map +1 -0
- package/dist/tools/register.d.ts +3 -0
- package/dist/tools/register.js +30 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/tools/secrets.d.ts +4 -0
- package/dist/tools/secrets.js +70 -0
- package/dist/tools/secrets.js.map +1 -0
- package/dist/tools/services.d.ts +4 -0
- package/dist/tools/services.js +198 -0
- package/dist/tools/services.js.map +1 -0
- package/dist/tools/stacks.d.ts +4 -0
- package/dist/tools/stacks.js +196 -0
- package/dist/tools/stacks.js.map +1 -0
- package/dist/tools/tasks.d.ts +3 -0
- package/dist/tools/tasks.js +23 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/timeseries.d.ts +3 -0
- package/dist/tools/timeseries.js +41 -0
- package/dist/tools/timeseries.js.map +1 -0
- package/dist/tools/util.d.ts +3 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/util.js.map +1 -0
- package/dist/tools/volumes.d.ts +3 -0
- package/dist/tools/volumes.js +59 -0
- package/dist/tools/volumes.js.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/client.ts +391 -0
- package/src/config.ts +57 -0
- package/src/index.ts +49 -0
- package/src/sanitize.ts +218 -0
- package/src/test/config.test.ts +118 -0
- package/src/test/file-ref.test.ts +191 -0
- package/src/test/helpers.test.ts +147 -0
- package/src/test/sanitize.test.ts +234 -0
- package/src/tools/admin.ts +93 -0
- package/src/tools/configs.ts +101 -0
- package/src/tools/dashboard.ts +65 -0
- package/src/tools/helpers.ts +91 -0
- package/src/tools/networks.ts +99 -0
- package/src/tools/nodes.ts +88 -0
- package/src/tools/register.ts +36 -0
- package/src/tools/secrets.ts +101 -0
- package/src/tools/services.ts +283 -0
- package/src/tools/stacks.ts +282 -0
- package/src/tools/tasks.ts +37 -0
- package/src/tools/timeseries.ts +65 -0
- package/src/tools/util.ts +20 -0
- package/src/tools/volumes.ts +88 -0
- package/src/types.ts +131 -0
- package/swagger.json +1 -0
- package/swarmpit-config.example.json +9 -0
- package/tsconfig.json +15 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# mcp-swarmpit
|
|
2
|
+
|
|
3
|
+
MCP server that wraps the Swarmpit REST API so MCP clients (Claude Code, opencode, etc.) can manage Docker Swarm clusters. Runs locally over stdio, holds the API token in its own process environment — tokens never enter the LLM conversation context.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
index.ts — entry: load config, startup health check, register tools, connect stdio
|
|
10
|
+
config.ts — SwarmpitConfig + redact mode loaded from env (SWARMPIT_URL/TOKEN/REDACT/REDACT_PATTERNS)
|
|
11
|
+
client.ts — SwarmpitClient: typed HTTP methods per API endpoint (native fetch)
|
|
12
|
+
sanitize.ts — redaction utilities for services, compose YAML; $env: resolver; [REDACTED] round-trip
|
|
13
|
+
types.ts — TypeScript types for Swarmpit API responses
|
|
14
|
+
tools/
|
|
15
|
+
register.ts — wires all tool registrations, creates single SwarmpitClient instance
|
|
16
|
+
helpers.ts — toolResult/toolError, resolveEnvRef, resolveData ($env/$file), prepareServiceForUpdate
|
|
17
|
+
services.ts — list/get/create/update/redeploy/rollback/stop/scale/delete + env management
|
|
18
|
+
stacks.ts — list/get/create/update/redeploy/rollback/deactivate/delete + stack-file variants
|
|
19
|
+
networks.ts — list/get/create/delete + services-using
|
|
20
|
+
nodes.ts — list/get/get_tasks/edit/delete
|
|
21
|
+
tasks.ts — list/get
|
|
22
|
+
volumes.ts — list/get/create/delete + services-using
|
|
23
|
+
secrets.ts — list/get (redacted)/create ($env/$file)/delete + services-using
|
|
24
|
+
configs.ts — list/get (redacted)/create ($env/$file)/delete + services-using
|
|
25
|
+
admin.ts — list/get/create/edit/delete users
|
|
26
|
+
dashboard.ts — pin/unpin node or service
|
|
27
|
+
timeseries.ts — nodes/services cpu/memory/task metrics
|
|
28
|
+
util.ts — swarmpit_info
|
|
29
|
+
test/ — node:test suites (run `npm test`)
|
|
30
|
+
swagger.json — saved from a running Swarmpit instance for reference
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Tools are each under ~50 lines, all follow the same pattern: zod input schema, try/catch, `toolResult` or `toolError`. Full API coverage (79/79 non-timeseries endpoints + all timeseries/dashboard/admin).
|
|
34
|
+
|
|
35
|
+
## Build & test
|
|
36
|
+
|
|
37
|
+
- Node >= 18, TypeScript, ES modules (`"type": "module"`)
|
|
38
|
+
- `npm run build` — compile to `dist/`
|
|
39
|
+
- `npm run dev` — watch mode
|
|
40
|
+
- `npm test` — runs `tsc` (`pretest`) then `node --test dist/test/*.test.js`
|
|
41
|
+
|
|
42
|
+
Tests use Node's built-in test runner. Pattern for integration tests (see `test/file-ref.test.ts`): hand-rolled mock server that captures handlers registered via `server.tool(...)`, plus a mock client that records calls. Invoke the handler directly, assert on captured arguments. Avoids the MCP transport layer entirely.
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
Three env vars drive the server, loaded by `config.ts`:
|
|
47
|
+
|
|
48
|
+
| Env | Required | Notes |
|
|
49
|
+
|-----|----------|-------|
|
|
50
|
+
| `SWARMPIT_URL` | Yes | Base URL, trailing slashes stripped |
|
|
51
|
+
| `SWARMPIT_TOKEN` | Yes | Accepts with or without `Bearer ` prefix |
|
|
52
|
+
| `SWARMPIT_REDACT` | No | `all` (default), `sensitive`, `none` |
|
|
53
|
+
| `SWARMPIT_REDACT_PATTERNS` | No | Comma-separated extra regex patterns for sensitive mode |
|
|
54
|
+
|
|
55
|
+
Config validation runs at startup; fatal errors exit with a message. Immediately after, `checkConnection()` calls `listNodes()` to verify auth and warn (not fail) if the token is wrong.
|
|
56
|
+
|
|
57
|
+
## Security model — secrets never in LLM context
|
|
58
|
+
|
|
59
|
+
Three layers keep user secrets out of the conversation:
|
|
60
|
+
|
|
61
|
+
1. **Response sanitization** (`sanitize.ts`)
|
|
62
|
+
- `sanitizeService`: env var values replaced with `[REDACTED]`. `all` redacts all, `sensitive` matches name patterns (`pass`, `secret`, `token`, `key`, `auth`, `credential`, `private`, `dsn`, `connection.?string`) + user-provided `SWARMPIT_REDACT_PATTERNS`.
|
|
63
|
+
- `sanitizeComposeYaml`: same redaction applied to YAML env vars (both `KEY: val` and `- KEY=val` forms).
|
|
64
|
+
- Secrets/configs `data` field zeroed unless mode is `none`.
|
|
65
|
+
|
|
66
|
+
2. **`$env:` / `$file` references in tool inputs** (`helpers.ts::resolveEnvRef`, `resolveData`)
|
|
67
|
+
- `update_service_env` accepts `{ $env: "MY_SECRET" }` per-var — resolved from `process.env` server-side.
|
|
68
|
+
- `create_stack` / `update_stack` / `create_stack_file` accept YAML with `$env:VAR_NAME` tokens (string or `{ $file: /path }`) — `resolveComposeEnvRefs` replaces them before sending.
|
|
69
|
+
- `create_secret` / `create_config` accept `{ $file: /path }` to read payload from disk (saves thousands of tokens on HTML pages, certs, large compose files).
|
|
70
|
+
|
|
71
|
+
3. **`[REDACTED]` round-trip on stack update** (`sanitize.ts::restoreRedactedValues`)
|
|
72
|
+
- `get_stack` returns compose with sensitive values as `[REDACTED]`.
|
|
73
|
+
- `update_stack` fetches the current raw compose, restores `[REDACTED]` values from it, then applies `$env:` resolution. Lets the user edit only what they need without accidentally overwriting secrets.
|
|
74
|
+
|
|
75
|
+
4. **`SWARMPIT_TOKEN_FILE` to keep the token out of MCP client configs** (`config.ts::loadToken`)
|
|
76
|
+
- If `SWARMPIT_TOKEN_FILE` is set, token is read from the referenced path at startup. Takes precedence over `SWARMPIT_TOKEN`.
|
|
77
|
+
- Matters because Claude Code and similar clients can accidentally dump `.mcp.json` (with inlined `SWARMPIT_TOKEN`) to conversation via their `Read` tool. Users are directed to add a `permissions.deny` rule on `.mcp.json` and use `SWARMPIT_TOKEN_FILE` — see README "Token handling".
|
|
78
|
+
|
|
79
|
+
If you add a new tool that takes user-provided data, use `resolveData` (supports plain string, `$env`, `$file`) instead of accepting raw strings for any non-trivial payload.
|
|
80
|
+
|
|
81
|
+
## Swarmpit API quirks (learned the hard way)
|
|
82
|
+
|
|
83
|
+
Real Swarmpit behaviour diverges from its own Swagger spec in several places. Keep these in mind if you extend the client:
|
|
84
|
+
|
|
85
|
+
- **`POST /api/secrets` and `/api/configs` want base64** — the Swagger types `data: string` but the server rejects raw strings with `invalid JSON: illegal base64 data at input byte 0`. `client.ts` encodes on the way in. Not yet filed upstream.
|
|
86
|
+
|
|
87
|
+
- **`GET /api/services/{id}/logs` requires `since`** as a Go duration string (`30s`, `5m`, `1h`), NOT a unix timestamp or ISO 8601. Default in the client is `5m`. Filed [swarmpit#730](https://github.com/swarmpit/swarmpit/issues/730) for the lack of documentation (closed).
|
|
88
|
+
|
|
89
|
+
- **Nested `{ spec: { compose } }`** — `getStackFile`, `getStackCompose`, `getServiceCompose` all wrap the compose string under `spec.compose`, but the swagger types it as flat `{ compose: string }`. The client normalises to `{ compose }`. Don't trust the swagger. (I initially filed [swarmpit#729](https://github.com/swarmpit/swarmpit/issues/729) thinking the endpoint was broken — it wasn't, just poorly documented.)
|
|
90
|
+
|
|
91
|
+
- **Service edit used to reject its own GET response** — [swarmpit#724](https://github.com/swarmpit/swarmpit/issues/724) (now fixed): `POST /api/services/{id}` complained about `imageDigest` being empty (`"nginx:alpine@"`). `prepareServiceForUpdate()` in `helpers.ts` strips nulls, strips read-only fields (`id`, `createdAt`, `updatedAt`, `state`, `status`), and trims `repository` to `{ name, tag }`. Keep this logic — even if the server bug is fixed, the read-only fields and nulls still cause issues.
|
|
92
|
+
|
|
93
|
+
- **`POST /api/stacks/{name}` edit wants `name` in the body** — not just the path param. `updateStack()` adds it automatically.
|
|
94
|
+
|
|
95
|
+
- **Timeseries endpoints (`/api/nodes/ts`, `/api/services/ts/cpu|memory`, `/api/tasks/{name}/ts`) return huge payloads** — often >100KB. Use the tools sparingly or expect token truncation in clients.
|
|
96
|
+
|
|
97
|
+
## Adding a new tool
|
|
98
|
+
|
|
99
|
+
1. Add a typed method to `SwarmpitClient` (`client.ts`) — use `this.request<T>(method, path, body?)`.
|
|
100
|
+
2. Add a registration function in a resource file under `tools/` (follow the shape of existing ones).
|
|
101
|
+
3. Wire it in `tools/register.ts` alongside the others.
|
|
102
|
+
4. If it accepts user data that could be large or sensitive, accept `string | { $env } | { $file }` and call `resolveData` first.
|
|
103
|
+
5. If it returns data that could contain secrets, pass it through the appropriate sanitizer. Respect the `redact` mode.
|
|
104
|
+
6. If it's destructive (`DELETE`, data loss), require `confirm: z.boolean()` and return a `toolError` if not set.
|
|
105
|
+
7. Add tests: unit test for any new sanitizer logic, integration test using the mock-server pattern if it touches `$file`/`$env`/redaction.
|
|
106
|
+
|
|
107
|
+
## Publishing / distribution
|
|
108
|
+
|
|
109
|
+
Not on npm (yet) — users install via `npx github:swarmpit/mcp` in their MCP client's config. Works because `package.json` has a `bin` entry pointing at `dist/index.js` and npm installs fetch+build on first run. If we publish to npm later, just flip the `mcp-swarmpit` name for `@swarmpit/mcp` and add `publishConfig.access: public`.
|
|
110
|
+
|
|
111
|
+
## Useful commands
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Hit the live Swarmpit API directly (bypassing the MCP) to debug:
|
|
115
|
+
curl -s -H "Authorization: Bearer $SWARMPIT_TOKEN" "$SWARMPIT_URL/api/services" | jq .
|
|
116
|
+
|
|
117
|
+
# Re-fetch the swagger from a running Swarmpit (helps when API diverges from local copy):
|
|
118
|
+
curl -s $SWARMPIT_URL/api/swagger.json > swagger.json
|
|
119
|
+
|
|
120
|
+
# List all endpoints from swagger:
|
|
121
|
+
jq -r '.paths | to_entries[] | .key as $p | .value | keys[] | "\(. | ascii_upcase)\t\($p)"' swagger.json | sort
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Style / conventions
|
|
125
|
+
|
|
126
|
+
- Keep commit messages lowercase and succinct (`fix X`, `support $file refs for stack compose`) — no AI/Claude/Anthropic references, no Co-Authored-By.
|
|
127
|
+
- Only push on explicit user request.
|
|
128
|
+
- No code comments for the "what" — names should carry it. Comments only when the "why" is non-obvious (e.g. the base64 encoding workaround, nested `spec.compose` normalisation).
|
package/README.md
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# mcp-swarmpit
|
|
2
|
+
|
|
3
|
+
MCP server for managing [Swarmpit](https://swarmpit.io) Docker Swarm instances from any MCP-compatible client. 100% Swarmpit API coverage (79 endpoints).
|
|
4
|
+
|
|
5
|
+
The server runs locally and holds API tokens — they never enter the LLM conversation context.
|
|
6
|
+
|
|
7
|
+
Works with [opencode](https://opencode.ai) (recommended), [Claude Code](https://claude.ai/code), and any other MCP client.
|
|
8
|
+
|
|
9
|
+
## Configuration
|
|
10
|
+
|
|
11
|
+
### opencode
|
|
12
|
+
|
|
13
|
+
Add to your `.opencode.json`:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"swarmpit-prod": {
|
|
19
|
+
"type": "stdio",
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["github:swarmpit/mcp"],
|
|
22
|
+
"env": {
|
|
23
|
+
"SWARMPIT_URL": "https://swarmpit.example.com",
|
|
24
|
+
"SWARMPIT_TOKEN": "your-api-token",
|
|
25
|
+
"SWARMPIT_REDACT": "sensitive"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Claude Code
|
|
33
|
+
|
|
34
|
+
Add to your `.mcp.json` (project-level) or `~/.claude.json` (global):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"swarmpit-prod": {
|
|
40
|
+
"command": "npx",
|
|
41
|
+
"args": ["github:swarmpit/mcp"],
|
|
42
|
+
"env": {
|
|
43
|
+
"SWARMPIT_URL": "https://swarmpit.example.com",
|
|
44
|
+
"SWARMPIT_TOKEN": "your-api-token",
|
|
45
|
+
"SWARMPIT_REDACT": "sensitive"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Get your API token from Swarmpit UI: Profile → API Access → Generate token.
|
|
53
|
+
|
|
54
|
+
### Multiple servers
|
|
55
|
+
|
|
56
|
+
Register each as a separate MCP server instance:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"swarmpit-prod": {
|
|
62
|
+
"command": "npx",
|
|
63
|
+
"args": ["github:swarmpit/mcp"],
|
|
64
|
+
"env": {
|
|
65
|
+
"SWARMPIT_URL": "https://swarmpit.prod.example.com",
|
|
66
|
+
"SWARMPIT_TOKEN": "prod-token",
|
|
67
|
+
"SWARMPIT_REDACT": "sensitive"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"swarmpit-staging": {
|
|
71
|
+
"command": "npx",
|
|
72
|
+
"args": ["github:swarmpit/mcp"],
|
|
73
|
+
"env": {
|
|
74
|
+
"SWARMPIT_URL": "https://swarmpit.staging.example.com",
|
|
75
|
+
"SWARMPIT_TOKEN": "staging-token",
|
|
76
|
+
"SWARMPIT_REDACT": "sensitive"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Tools appear namespaced: `swarmpit-prod: list_services`, `swarmpit-staging: list_services`.
|
|
84
|
+
|
|
85
|
+
### Environment variables
|
|
86
|
+
|
|
87
|
+
| Variable | Required | Description |
|
|
88
|
+
|----------|----------|-------------|
|
|
89
|
+
| `SWARMPIT_URL` | Yes | Swarmpit instance URL |
|
|
90
|
+
| `SWARMPIT_TOKEN` | One of | API token (with or without `Bearer ` prefix). Avoid putting in `.mcp.json` — see [token handling](#token-handling). |
|
|
91
|
+
| `SWARMPIT_TOKEN_FILE` | One of | Path to a file containing the token. Takes precedence over `SWARMPIT_TOKEN`. **Recommended** for production use. |
|
|
92
|
+
| `SWARMPIT_REDACT` | No | Redaction mode: `all` (default), `sensitive`, or `none` |
|
|
93
|
+
| `SWARMPIT_REDACT_PATTERNS` | No | Comma-separated extra patterns to redact in `sensitive` mode (regex, case-insensitive) |
|
|
94
|
+
|
|
95
|
+
### Redaction modes
|
|
96
|
+
|
|
97
|
+
| Mode | Env vars | Secrets/Configs data |
|
|
98
|
+
|------|----------|---------------------|
|
|
99
|
+
| `all` | All values redacted | Redacted |
|
|
100
|
+
| `sensitive` | Only names matching patterns | Redacted |
|
|
101
|
+
| `none` | No redaction | Not redacted |
|
|
102
|
+
|
|
103
|
+
> **Warning:** `none` mode sends all environment variables, secrets, and config data in full to the LLM provider. Only use this with a local model (e.g. via ollama/opencode) or on servers that definitely do not contain any sensitive environment variables or configs. Never use `none` with cloud-hosted LLM providers on production infrastructure.
|
|
104
|
+
|
|
105
|
+
Built-in sensitive patterns: `pass`, `secret`, `token`, `key`, `auth`, `credential`, `private`, `dsn`, `connection_string`.
|
|
106
|
+
|
|
107
|
+
Add custom patterns via `SWARMPIT_REDACT_PATTERNS`:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
"env": {
|
|
111
|
+
"SWARMPIT_REDACT": "sensitive",
|
|
112
|
+
"SWARMPIT_REDACT_PATTERNS": "GRAFANA,RPC,ENDPOINT,DATABASE"
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Tools
|
|
117
|
+
|
|
118
|
+
### Services
|
|
119
|
+
|
|
120
|
+
| Tool | Description |
|
|
121
|
+
|------|-------------|
|
|
122
|
+
| `list_services` | List all services |
|
|
123
|
+
| `get_service` | Get service details |
|
|
124
|
+
| `service_logs` | Get service logs |
|
|
125
|
+
| `create_service` | Create a service |
|
|
126
|
+
| `update_service` | Update a service |
|
|
127
|
+
| `redeploy_service` | Redeploy (optionally with new tag) |
|
|
128
|
+
| `rollback_service` | Rollback to previous version |
|
|
129
|
+
| `stop_service` | Stop a service |
|
|
130
|
+
| `scale_service` | Scale replicas |
|
|
131
|
+
| `list_service_tasks` | List service tasks/containers |
|
|
132
|
+
| `delete_service` | Delete (requires `confirm: true`) |
|
|
133
|
+
| `update_service_env` | Set/remove env vars (supports `$env` references) |
|
|
134
|
+
| `get_service_env` | Get specific env var values by name |
|
|
135
|
+
| `get_service_compose` | Get compose YAML for a service |
|
|
136
|
+
| `get_service_networks` | Get networks attached to a service |
|
|
137
|
+
|
|
138
|
+
### Stacks
|
|
139
|
+
|
|
140
|
+
| Tool | Description |
|
|
141
|
+
|------|-------------|
|
|
142
|
+
| `list_stacks` | List all stacks |
|
|
143
|
+
| `get_stack` | Get stack services and compose file |
|
|
144
|
+
| `create_stack` | Create from compose YAML |
|
|
145
|
+
| `update_stack` | Update with new compose YAML |
|
|
146
|
+
| `redeploy_stack` | Redeploy all services |
|
|
147
|
+
| `rollback_stack` | Rollback all services |
|
|
148
|
+
| `deactivate_stack` | Stop all services in a stack |
|
|
149
|
+
| `delete_stack` | Delete (requires `confirm: true`) |
|
|
150
|
+
| `get_stack_tasks` | List all tasks in a stack |
|
|
151
|
+
| `get_stack_volumes` | List all volumes in a stack |
|
|
152
|
+
| `get_stack_networks` | List all networks in a stack |
|
|
153
|
+
| `get_stack_compose` | Get generated compose YAML |
|
|
154
|
+
| `get_stack_secrets` | List secrets in a stack |
|
|
155
|
+
| `get_stack_configs` | List configs in a stack |
|
|
156
|
+
| `create_stack_file` | Upload a compose file for a stack |
|
|
157
|
+
| `delete_stack_file` | Delete stack compose file (requires `confirm: true`) |
|
|
158
|
+
|
|
159
|
+
### Networks
|
|
160
|
+
|
|
161
|
+
| Tool | Description |
|
|
162
|
+
|------|-------------|
|
|
163
|
+
| `list_networks` | List all networks |
|
|
164
|
+
| `get_network` | Get network details |
|
|
165
|
+
| `create_network` | Create a network |
|
|
166
|
+
| `delete_network` | Delete (requires `confirm: true`) |
|
|
167
|
+
| `get_network_services` | List services using a network |
|
|
168
|
+
|
|
169
|
+
### Nodes
|
|
170
|
+
|
|
171
|
+
| Tool | Description |
|
|
172
|
+
|------|-------------|
|
|
173
|
+
| `list_nodes` | List all nodes |
|
|
174
|
+
| `get_node` | Get node details |
|
|
175
|
+
| `get_node_tasks` | List tasks running on a node |
|
|
176
|
+
| `edit_node` | Edit node properties |
|
|
177
|
+
| `delete_node` | Remove a node (requires `confirm: true`) |
|
|
178
|
+
|
|
179
|
+
### Tasks
|
|
180
|
+
|
|
181
|
+
| Tool | Description |
|
|
182
|
+
|------|-------------|
|
|
183
|
+
| `list_tasks` | List all tasks |
|
|
184
|
+
| `get_task` | Get task details |
|
|
185
|
+
|
|
186
|
+
### Volumes
|
|
187
|
+
|
|
188
|
+
| Tool | Description |
|
|
189
|
+
|------|-------------|
|
|
190
|
+
| `list_volumes` | List all volumes |
|
|
191
|
+
| `get_volume` | Get volume details |
|
|
192
|
+
| `create_volume` | Create a volume |
|
|
193
|
+
| `delete_volume` | Delete (requires `confirm: true`) |
|
|
194
|
+
| `get_volume_services` | List services using a volume |
|
|
195
|
+
|
|
196
|
+
### Secrets
|
|
197
|
+
|
|
198
|
+
| Tool | Description |
|
|
199
|
+
|------|-------------|
|
|
200
|
+
| `list_secrets` | List all secrets (data redacted) |
|
|
201
|
+
| `get_secret` | Get secret details (data redacted) |
|
|
202
|
+
| `create_secret` | Create a secret (supports `$env` / `$file` references) |
|
|
203
|
+
| `delete_secret` | Delete (requires `confirm: true`) |
|
|
204
|
+
| `get_secret_services` | List services using a secret |
|
|
205
|
+
|
|
206
|
+
### Configs
|
|
207
|
+
|
|
208
|
+
| Tool | Description |
|
|
209
|
+
|------|-------------|
|
|
210
|
+
| `list_configs` | List all configs (data redacted) |
|
|
211
|
+
| `get_config` | Get config details (data redacted) |
|
|
212
|
+
| `create_config` | Create a config (supports `$env` / `$file` references) |
|
|
213
|
+
| `delete_config` | Delete (requires `confirm: true`) |
|
|
214
|
+
| `get_config_services` | List services using a config |
|
|
215
|
+
|
|
216
|
+
### Admin
|
|
217
|
+
|
|
218
|
+
| Tool | Description |
|
|
219
|
+
|------|-------------|
|
|
220
|
+
| `list_users` | List all Swarmpit users |
|
|
221
|
+
| `get_user` | Get user details |
|
|
222
|
+
| `create_user` | Create a user |
|
|
223
|
+
| `edit_user` | Edit user properties |
|
|
224
|
+
| `delete_user` | Delete (requires `confirm: true`) |
|
|
225
|
+
|
|
226
|
+
### Dashboard
|
|
227
|
+
|
|
228
|
+
| Tool | Description |
|
|
229
|
+
|------|-------------|
|
|
230
|
+
| `pin_service_to_dashboard` | Pin a service to the Swarmpit dashboard |
|
|
231
|
+
| `unpin_service_from_dashboard` | Remove a service from the dashboard |
|
|
232
|
+
| `pin_node_to_dashboard` | Pin a node to the dashboard |
|
|
233
|
+
| `unpin_node_from_dashboard` | Remove a node from the dashboard |
|
|
234
|
+
|
|
235
|
+
### Timeseries
|
|
236
|
+
|
|
237
|
+
| Tool | Description |
|
|
238
|
+
|------|-------------|
|
|
239
|
+
| `get_nodes_timeseries` | Node CPU/memory/disk over time |
|
|
240
|
+
| `get_services_cpu_timeseries` | Service CPU usage over time |
|
|
241
|
+
| `get_services_memory_timeseries` | Service memory usage over time |
|
|
242
|
+
| `get_task_timeseries` | Task metrics over time |
|
|
243
|
+
|
|
244
|
+
### Utility
|
|
245
|
+
|
|
246
|
+
| Tool | Description |
|
|
247
|
+
|------|-------------|
|
|
248
|
+
| `swarmpit_info` | Show connected URL and redaction mode |
|
|
249
|
+
|
|
250
|
+
## Token handling
|
|
251
|
+
|
|
252
|
+
Putting `SWARMPIT_TOKEN` directly in `.mcp.json` is convenient but risky: if anything (including Claude / the LLM) reads `.mcp.json`, the token leaks into the conversation and API logs. **Two hardening steps you should take:**
|
|
253
|
+
|
|
254
|
+
### 1. Block MCP config files from being read
|
|
255
|
+
|
|
256
|
+
Add this to your project `.claude/settings.json` so Claude Code refuses to `Read` credential-holding files:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"permissions": {
|
|
261
|
+
"deny": [
|
|
262
|
+
"Read(.mcp.json)",
|
|
263
|
+
"Read(**/.mcp.json)",
|
|
264
|
+
"Read(.env)",
|
|
265
|
+
"Read(.env.*)",
|
|
266
|
+
"Read(**/.env)",
|
|
267
|
+
"Read(**/.env.*)",
|
|
268
|
+
"Read(**/*credentials*)",
|
|
269
|
+
"Read(**/secrets/**)",
|
|
270
|
+
"Read(**/*.pem)",
|
|
271
|
+
"Read(**/*.key)"
|
|
272
|
+
]
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 2. Use `SWARMPIT_TOKEN_FILE` instead of inline
|
|
278
|
+
|
|
279
|
+
Store the token in a file outside the repo and reference it by path. The token never appears in `.mcp.json` itself:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# Write the token to a restricted file
|
|
283
|
+
install -m 600 /dev/stdin ~/.config/swarmpit/lark.token <<<'your-token-here'
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"mcpServers": {
|
|
289
|
+
"swarmpit-lark": {
|
|
290
|
+
"command": "npx",
|
|
291
|
+
"args": ["github:swarmpit/mcp"],
|
|
292
|
+
"env": {
|
|
293
|
+
"SWARMPIT_URL": "https://swarmpit.example.com",
|
|
294
|
+
"SWARMPIT_TOKEN_FILE": "/Users/you/.config/swarmpit/lark.token",
|
|
295
|
+
"SWARMPIT_REDACT": "sensitive"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### 3. Password manager integration
|
|
303
|
+
|
|
304
|
+
For even stronger posture, have the MCP spawn with a secret manager:
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{
|
|
308
|
+
"swarmpit-lark": {
|
|
309
|
+
"command": "op",
|
|
310
|
+
"args": ["run", "--", "npx", "github:swarmpit/mcp"],
|
|
311
|
+
"env": {
|
|
312
|
+
"SWARMPIT_URL": "https://swarmpit.example.com",
|
|
313
|
+
"SWARMPIT_TOKEN": "op://Private/Swarmpit Lark/token",
|
|
314
|
+
"SWARMPIT_REDACT": "sensitive"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
1Password's `op run` substitutes `op://...` references at process start. macOS users can do the equivalent with `security find-generic-password` in a wrapper script.
|
|
321
|
+
|
|
322
|
+
If your token has already been exposed (e.g. you saw it scroll through a log), rotate it in Swarmpit UI → Profile → API Access → regenerate.
|
|
323
|
+
|
|
324
|
+
## Secret handling
|
|
325
|
+
|
|
326
|
+
Secrets in `.mcp.json` `env` are passed to the MCP server process but **never sent to the LLM**.
|
|
327
|
+
|
|
328
|
+
### Reading from local files
|
|
329
|
+
|
|
330
|
+
For large payloads (HTML pages, compose files, certs) use `$file` to have the MCP server read from disk instead of passing content through the LLM context:
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
create_config(configName: "my_dashboard", data: { "$file": "/path/to/index.html" })
|
|
334
|
+
create_secret(secretName: "tls_cert", data: { "$file": "/etc/ssl/server.crt" })
|
|
335
|
+
create_stack(name: "myapp", compose: { "$file": "/path/to/stack.yml" })
|
|
336
|
+
update_stack(name: "myapp", compose: { "$file": "/path/to/stack.yml" })
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Saves context/credits on anything larger than a few hundred bytes. `$env:` references inside the file are still resolved before sending to Swarmpit.
|
|
340
|
+
|
|
341
|
+
### Service env vars
|
|
342
|
+
|
|
343
|
+
Use `$env` references to set secrets without them entering the conversation:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
update_service_env(id: "my-service", set: {
|
|
347
|
+
"NODE_ENV": "production",
|
|
348
|
+
"DB_PASSWORD": { "$env": "MY_DB_PASS" }
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
`MY_DB_PASS` is resolved from the MCP server's environment. Add it to `.mcp.json` env:
|
|
353
|
+
|
|
354
|
+
```json
|
|
355
|
+
"env": {
|
|
356
|
+
"SWARMPIT_URL": "...",
|
|
357
|
+
"SWARMPIT_TOKEN": "...",
|
|
358
|
+
"MY_DB_PASS": "the-actual-password"
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Stack compose files
|
|
363
|
+
|
|
364
|
+
When reading stacks, env var values are redacted according to the redaction mode. When updating, `[REDACTED]` values are automatically preserved from the current stack — only changed values are updated:
|
|
365
|
+
|
|
366
|
+
```yaml
|
|
367
|
+
# Returned by get_stack (sensitive mode):
|
|
368
|
+
environment:
|
|
369
|
+
NODE_ENV: production # visible
|
|
370
|
+
DB_PASSWORD: [REDACTED] # redacted
|
|
371
|
+
|
|
372
|
+
# Sent to update_stack — only NODE_ENV changed:
|
|
373
|
+
environment:
|
|
374
|
+
NODE_ENV: staging # new value
|
|
375
|
+
DB_PASSWORD: [REDACTED] # preserved from current stack
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
Use `$env:VAR_NAME` for new secrets in compose:
|
|
379
|
+
|
|
380
|
+
```yaml
|
|
381
|
+
environment:
|
|
382
|
+
NEW_SECRET: $env:MY_SECRET # resolved locally
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Development
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
git clone https://github.com/swarmpit/mcp
|
|
389
|
+
cd mcp
|
|
390
|
+
npm install
|
|
391
|
+
npm run build # compile TypeScript
|
|
392
|
+
npm run dev # watch mode
|
|
393
|
+
npm test # run tests
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
When developing locally, point `.mcp.json` at your local build:
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"mcpServers": {
|
|
401
|
+
"swarmpit-dev": {
|
|
402
|
+
"command": "node",
|
|
403
|
+
"args": ["/path/to/mcp/dist/index.js"],
|
|
404
|
+
"env": {
|
|
405
|
+
"SWARMPIT_URL": "https://swarmpit.example.com",
|
|
406
|
+
"SWARMPIT_TOKEN": "your-token",
|
|
407
|
+
"SWARMPIT_REDACT": "sensitive"
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## License
|
|
415
|
+
|
|
416
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { SwarmpitService, SwarmpitStack, SwarmpitNetwork, SwarmpitNode, SwarmpitTask, SwarmpitVolume, SwarmpitLogEntry } from "./types.js";
|
|
2
|
+
export declare class SwarmpitApiError extends Error {
|
|
3
|
+
status: number;
|
|
4
|
+
statusText: string;
|
|
5
|
+
body: string;
|
|
6
|
+
constructor(status: number, statusText: string, body: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class SwarmpitClient {
|
|
9
|
+
private baseUrl;
|
|
10
|
+
private token;
|
|
11
|
+
private timeout;
|
|
12
|
+
constructor(baseUrl: string, token: string, timeout?: number);
|
|
13
|
+
private request;
|
|
14
|
+
listServices(): Promise<SwarmpitService[]>;
|
|
15
|
+
getService(id: string): Promise<SwarmpitService>;
|
|
16
|
+
getServiceLogs(id: string, since?: string): Promise<SwarmpitLogEntry[]>;
|
|
17
|
+
createService(spec: Record<string, unknown>): Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
}>;
|
|
20
|
+
updateService(id: string, spec: Record<string, unknown>): Promise<void>;
|
|
21
|
+
redeployService(id: string, tag?: string): Promise<void>;
|
|
22
|
+
rollbackService(id: string): Promise<void>;
|
|
23
|
+
stopService(id: string): Promise<void>;
|
|
24
|
+
deleteService(id: string): Promise<void>;
|
|
25
|
+
getServiceTasks(id: string): Promise<SwarmpitTask[]>;
|
|
26
|
+
getServiceCompose(id: string): Promise<{
|
|
27
|
+
compose: string;
|
|
28
|
+
}>;
|
|
29
|
+
getServiceNetworks(id: string): Promise<SwarmpitNetwork[]>;
|
|
30
|
+
listStacks(): Promise<SwarmpitStack[]>;
|
|
31
|
+
getStackServices(name: string): Promise<SwarmpitService[]>;
|
|
32
|
+
getStackFile(name: string): Promise<{
|
|
33
|
+
compose: string;
|
|
34
|
+
}>;
|
|
35
|
+
createStack(spec: {
|
|
36
|
+
name: string;
|
|
37
|
+
spec: {
|
|
38
|
+
compose: string;
|
|
39
|
+
};
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
updateStack(name: string, compose: string): Promise<void>;
|
|
42
|
+
redeployStack(name: string): Promise<void>;
|
|
43
|
+
rollbackStack(name: string): Promise<void>;
|
|
44
|
+
deactivateStack(name: string): Promise<void>;
|
|
45
|
+
getStackTasks(name: string): Promise<SwarmpitTask[]>;
|
|
46
|
+
getStackVolumes(name: string): Promise<SwarmpitVolume[]>;
|
|
47
|
+
getStackNetworks(name: string): Promise<SwarmpitNetwork[]>;
|
|
48
|
+
getStackCompose(name: string): Promise<{
|
|
49
|
+
compose: string;
|
|
50
|
+
}>;
|
|
51
|
+
getStackSecrets(name: string): Promise<Record<string, unknown>[]>;
|
|
52
|
+
getStackConfigs(name: string): Promise<Record<string, unknown>[]>;
|
|
53
|
+
createStackFile(name: string, compose: string): Promise<void>;
|
|
54
|
+
deleteStackFile(name: string): Promise<void>;
|
|
55
|
+
deleteStack(name: string): Promise<void>;
|
|
56
|
+
listNetworks(): Promise<SwarmpitNetwork[]>;
|
|
57
|
+
getNetwork(id: string): Promise<SwarmpitNetwork>;
|
|
58
|
+
getNetworkServices(id: string): Promise<SwarmpitService[]>;
|
|
59
|
+
createNetwork(spec: Record<string, unknown>): Promise<void>;
|
|
60
|
+
deleteNetwork(id: string): Promise<void>;
|
|
61
|
+
listNodes(): Promise<SwarmpitNode[]>;
|
|
62
|
+
getNode(id: string): Promise<SwarmpitNode>;
|
|
63
|
+
editNode(id: string, spec: Record<string, unknown>): Promise<void>;
|
|
64
|
+
deleteNode(id: string): Promise<void>;
|
|
65
|
+
getNodeTasks(id: string): Promise<SwarmpitTask[]>;
|
|
66
|
+
listTasks(): Promise<SwarmpitTask[]>;
|
|
67
|
+
getTask(id: string): Promise<SwarmpitTask>;
|
|
68
|
+
listVolumes(): Promise<SwarmpitVolume[]>;
|
|
69
|
+
getVolume(name: string): Promise<SwarmpitVolume>;
|
|
70
|
+
getVolumeServices(name: string): Promise<SwarmpitService[]>;
|
|
71
|
+
createVolume(spec: Record<string, unknown>): Promise<void>;
|
|
72
|
+
deleteVolume(name: string): Promise<void>;
|
|
73
|
+
listSecrets(): Promise<Record<string, unknown>[]>;
|
|
74
|
+
getSecret(id: string): Promise<Record<string, unknown>>;
|
|
75
|
+
getSecretServices(id: string): Promise<SwarmpitService[]>;
|
|
76
|
+
createSecret(spec: {
|
|
77
|
+
secretName: string;
|
|
78
|
+
data: string;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
deleteSecret(id: string): Promise<void>;
|
|
81
|
+
listConfigs(): Promise<Record<string, unknown>[]>;
|
|
82
|
+
getConfig(id: string): Promise<Record<string, unknown>>;
|
|
83
|
+
getConfigServices(id: string): Promise<SwarmpitService[]>;
|
|
84
|
+
createConfig(spec: {
|
|
85
|
+
configName: string;
|
|
86
|
+
data: string;
|
|
87
|
+
}): Promise<void>;
|
|
88
|
+
deleteConfig(id: string): Promise<void>;
|
|
89
|
+
listUsers(): Promise<Record<string, unknown>[]>;
|
|
90
|
+
getUser(id: string): Promise<Record<string, unknown>>;
|
|
91
|
+
createUser(spec: {
|
|
92
|
+
username: string;
|
|
93
|
+
password: string;
|
|
94
|
+
role: string;
|
|
95
|
+
email?: string;
|
|
96
|
+
}): Promise<void>;
|
|
97
|
+
editUser(id: string, spec: Record<string, unknown>): Promise<void>;
|
|
98
|
+
deleteUser(id: string): Promise<void>;
|
|
99
|
+
pinNodeToDashboard(id: string): Promise<void>;
|
|
100
|
+
unpinNodeFromDashboard(id: string): Promise<void>;
|
|
101
|
+
pinServiceToDashboard(id: string): Promise<void>;
|
|
102
|
+
unpinServiceFromDashboard(id: string): Promise<void>;
|
|
103
|
+
getNodesTimeseries(): Promise<unknown>;
|
|
104
|
+
getServicesCpuTimeseries(): Promise<unknown>;
|
|
105
|
+
getServicesMemoryTimeseries(): Promise<unknown>;
|
|
106
|
+
getTaskTimeseries(name: string): Promise<unknown>;
|
|
107
|
+
}
|