@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.
Files changed (98) hide show
  1. package/CLAUDE.md +128 -0
  2. package/README.md +416 -0
  3. package/dist/client.d.ts +107 -0
  4. package/dist/client.js +297 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/config.d.ts +8 -0
  7. package/dist/config.js +41 -0
  8. package/dist/config.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +41 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/sanitize.d.ts +41 -0
  13. package/dist/sanitize.js +165 -0
  14. package/dist/sanitize.js.map +1 -0
  15. package/dist/test/config.test.d.ts +1 -0
  16. package/dist/test/config.test.js +103 -0
  17. package/dist/test/config.test.js.map +1 -0
  18. package/dist/test/file-ref.test.d.ts +1 -0
  19. package/dist/test/file-ref.test.js +163 -0
  20. package/dist/test/file-ref.test.js.map +1 -0
  21. package/dist/test/helpers.test.d.ts +1 -0
  22. package/dist/test/helpers.test.js +133 -0
  23. package/dist/test/helpers.test.js.map +1 -0
  24. package/dist/test/sanitize.test.d.ts +1 -0
  25. package/dist/test/sanitize.test.js +207 -0
  26. package/dist/test/sanitize.test.js.map +1 -0
  27. package/dist/tools/admin.d.ts +3 -0
  28. package/dist/tools/admin.js +64 -0
  29. package/dist/tools/admin.js.map +1 -0
  30. package/dist/tools/configs.d.ts +4 -0
  31. package/dist/tools/configs.js +70 -0
  32. package/dist/tools/configs.js.map +1 -0
  33. package/dist/tools/dashboard.d.ts +3 -0
  34. package/dist/tools/dashboard.js +41 -0
  35. package/dist/tools/dashboard.js.map +1 -0
  36. package/dist/tools/helpers.d.ts +16 -0
  37. package/dist/tools/helpers.js +74 -0
  38. package/dist/tools/helpers.js.map +1 -0
  39. package/dist/tools/networks.d.ts +3 -0
  40. package/dist/tools/networks.js +70 -0
  41. package/dist/tools/networks.js.map +1 -0
  42. package/dist/tools/nodes.d.ts +3 -0
  43. package/dist/tools/nodes.js +59 -0
  44. package/dist/tools/nodes.js.map +1 -0
  45. package/dist/tools/register.d.ts +3 -0
  46. package/dist/tools/register.js +30 -0
  47. package/dist/tools/register.js.map +1 -0
  48. package/dist/tools/secrets.d.ts +4 -0
  49. package/dist/tools/secrets.js +70 -0
  50. package/dist/tools/secrets.js.map +1 -0
  51. package/dist/tools/services.d.ts +4 -0
  52. package/dist/tools/services.js +198 -0
  53. package/dist/tools/services.js.map +1 -0
  54. package/dist/tools/stacks.d.ts +4 -0
  55. package/dist/tools/stacks.js +196 -0
  56. package/dist/tools/stacks.js.map +1 -0
  57. package/dist/tools/tasks.d.ts +3 -0
  58. package/dist/tools/tasks.js +23 -0
  59. package/dist/tools/tasks.js.map +1 -0
  60. package/dist/tools/timeseries.d.ts +3 -0
  61. package/dist/tools/timeseries.js +41 -0
  62. package/dist/tools/timeseries.js.map +1 -0
  63. package/dist/tools/util.d.ts +3 -0
  64. package/dist/tools/util.js +10 -0
  65. package/dist/tools/util.js.map +1 -0
  66. package/dist/tools/volumes.d.ts +3 -0
  67. package/dist/tools/volumes.js +59 -0
  68. package/dist/tools/volumes.js.map +1 -0
  69. package/dist/types.d.ts +119 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/package.json +43 -0
  73. package/src/client.ts +391 -0
  74. package/src/config.ts +57 -0
  75. package/src/index.ts +49 -0
  76. package/src/sanitize.ts +218 -0
  77. package/src/test/config.test.ts +118 -0
  78. package/src/test/file-ref.test.ts +191 -0
  79. package/src/test/helpers.test.ts +147 -0
  80. package/src/test/sanitize.test.ts +234 -0
  81. package/src/tools/admin.ts +93 -0
  82. package/src/tools/configs.ts +101 -0
  83. package/src/tools/dashboard.ts +65 -0
  84. package/src/tools/helpers.ts +91 -0
  85. package/src/tools/networks.ts +99 -0
  86. package/src/tools/nodes.ts +88 -0
  87. package/src/tools/register.ts +36 -0
  88. package/src/tools/secrets.ts +101 -0
  89. package/src/tools/services.ts +283 -0
  90. package/src/tools/stacks.ts +282 -0
  91. package/src/tools/tasks.ts +37 -0
  92. package/src/tools/timeseries.ts +65 -0
  93. package/src/tools/util.ts +20 -0
  94. package/src/tools/volumes.ts +88 -0
  95. package/src/types.ts +131 -0
  96. package/swagger.json +1 -0
  97. package/swarmpit-config.example.json +9 -0
  98. 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
@@ -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
+ }