@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/dist/types.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export interface SwarmpitPort {
|
|
2
|
+
containerPort: number;
|
|
3
|
+
hostPort: number;
|
|
4
|
+
protocol: string;
|
|
5
|
+
mode: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SwarmpitMount {
|
|
8
|
+
containerPath: string;
|
|
9
|
+
host: string;
|
|
10
|
+
type: string;
|
|
11
|
+
readOnly: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface SwarmpitEnvVar {
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SwarmpitLabel {
|
|
18
|
+
name: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SwarmpitSecretRef {
|
|
22
|
+
id: string;
|
|
23
|
+
secretName: string;
|
|
24
|
+
secretTarget: string;
|
|
25
|
+
}
|
|
26
|
+
export interface SwarmpitConfigRef {
|
|
27
|
+
id: string;
|
|
28
|
+
configName: string;
|
|
29
|
+
configTarget: string;
|
|
30
|
+
}
|
|
31
|
+
export interface SwarmpitRepository {
|
|
32
|
+
name: string;
|
|
33
|
+
tag: string;
|
|
34
|
+
image: string;
|
|
35
|
+
imageDigest: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SwarmpitResources {
|
|
38
|
+
cpu: number;
|
|
39
|
+
memory: number;
|
|
40
|
+
}
|
|
41
|
+
export interface SwarmpitServiceStatus {
|
|
42
|
+
tasks: {
|
|
43
|
+
running: number;
|
|
44
|
+
total: number;
|
|
45
|
+
};
|
|
46
|
+
update: string;
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
export interface SwarmpitService {
|
|
50
|
+
id: string;
|
|
51
|
+
version: number;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
updatedAt: string;
|
|
54
|
+
repository: SwarmpitRepository;
|
|
55
|
+
serviceName: string;
|
|
56
|
+
mode: string;
|
|
57
|
+
replicas: number;
|
|
58
|
+
state: string;
|
|
59
|
+
status: SwarmpitServiceStatus;
|
|
60
|
+
ports: SwarmpitPort[];
|
|
61
|
+
mounts: SwarmpitMount[];
|
|
62
|
+
networks: SwarmpitNetwork[];
|
|
63
|
+
secrets: SwarmpitSecretRef[];
|
|
64
|
+
configs: SwarmpitConfigRef[];
|
|
65
|
+
variables: SwarmpitEnvVar[];
|
|
66
|
+
labels: SwarmpitLabel[];
|
|
67
|
+
command: string[] | null;
|
|
68
|
+
stack: string;
|
|
69
|
+
resources: {
|
|
70
|
+
reservation: SwarmpitResources;
|
|
71
|
+
limit: SwarmpitResources;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface SwarmpitStack {
|
|
75
|
+
stackName: string;
|
|
76
|
+
stackFile: string;
|
|
77
|
+
services: SwarmpitService[];
|
|
78
|
+
}
|
|
79
|
+
export interface SwarmpitNetwork {
|
|
80
|
+
id: string;
|
|
81
|
+
networkName: string;
|
|
82
|
+
driver: string;
|
|
83
|
+
scope: string;
|
|
84
|
+
internal: boolean;
|
|
85
|
+
stack: string;
|
|
86
|
+
}
|
|
87
|
+
export interface SwarmpitNode {
|
|
88
|
+
id: string;
|
|
89
|
+
nodeName: string;
|
|
90
|
+
role: string;
|
|
91
|
+
state: string;
|
|
92
|
+
availability: string;
|
|
93
|
+
address: string;
|
|
94
|
+
engine: string;
|
|
95
|
+
}
|
|
96
|
+
export interface SwarmpitTask {
|
|
97
|
+
id: string;
|
|
98
|
+
taskName: string;
|
|
99
|
+
serviceName: string;
|
|
100
|
+
state: string;
|
|
101
|
+
status: string;
|
|
102
|
+
desiredState: string;
|
|
103
|
+
createdAt: string;
|
|
104
|
+
repository: SwarmpitRepository;
|
|
105
|
+
nodeName: string;
|
|
106
|
+
nodeId: string;
|
|
107
|
+
}
|
|
108
|
+
export interface SwarmpitVolume {
|
|
109
|
+
volumeName: string;
|
|
110
|
+
driver: string;
|
|
111
|
+
scope: string;
|
|
112
|
+
stack: string;
|
|
113
|
+
mountpoint: string;
|
|
114
|
+
}
|
|
115
|
+
export interface SwarmpitLogEntry {
|
|
116
|
+
line: string;
|
|
117
|
+
timestamp?: string;
|
|
118
|
+
taskName?: string;
|
|
119
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mseep/mcp-swarmpit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for managing Swarmpit Docker Swarm instances",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-swarmpit": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "node --test dist/test/*.test.js",
|
|
14
|
+
"pretest": "tsc",
|
|
15
|
+
"prepare": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"swarmpit",
|
|
20
|
+
"docker",
|
|
21
|
+
"swarm",
|
|
22
|
+
"claude",
|
|
23
|
+
"mseep",
|
|
24
|
+
"mcp-server"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/swarmpit/mcp"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
|
+
"zod": "^3.24.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.7.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
},
|
|
42
|
+
"publisher": "mseep"
|
|
43
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SwarmpitService,
|
|
3
|
+
SwarmpitStack,
|
|
4
|
+
SwarmpitNetwork,
|
|
5
|
+
SwarmpitNode,
|
|
6
|
+
SwarmpitTask,
|
|
7
|
+
SwarmpitVolume,
|
|
8
|
+
SwarmpitLogEntry,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
|
|
11
|
+
export class SwarmpitApiError extends Error {
|
|
12
|
+
constructor(
|
|
13
|
+
public status: number,
|
|
14
|
+
public statusText: string,
|
|
15
|
+
public body: string
|
|
16
|
+
) {
|
|
17
|
+
super(`Swarmpit API error ${status} ${statusText}: ${body}`);
|
|
18
|
+
this.name = "SwarmpitApiError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SwarmpitClient {
|
|
23
|
+
private baseUrl: string;
|
|
24
|
+
private token: string;
|
|
25
|
+
private timeout: number;
|
|
26
|
+
|
|
27
|
+
constructor(baseUrl: string, token: string, timeout = 30_000) {
|
|
28
|
+
this.baseUrl = baseUrl;
|
|
29
|
+
this.token = token;
|
|
30
|
+
this.timeout = timeout;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async request<T>(
|
|
34
|
+
method: string,
|
|
35
|
+
path: string,
|
|
36
|
+
body?: unknown
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
const url = `${this.baseUrl}${path}`;
|
|
39
|
+
const headers: Record<string, string> = {
|
|
40
|
+
Authorization: this.token.startsWith("Bearer ") ? this.token : `Bearer ${this.token}`,
|
|
41
|
+
Accept: "application/json",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (body !== undefined) {
|
|
45
|
+
headers["Content-Type"] = "application/json";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
method,
|
|
54
|
+
headers,
|
|
55
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
56
|
+
signal: controller.signal,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const text = await response.text().catch(() => "");
|
|
61
|
+
if (response.status === 401) {
|
|
62
|
+
throw new SwarmpitApiError(
|
|
63
|
+
401,
|
|
64
|
+
response.statusText,
|
|
65
|
+
"Token expired or invalid — regenerate the API token in Swarmpit"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
throw new SwarmpitApiError(response.status, response.statusText, text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
if (!text) return undefined as T;
|
|
73
|
+
return JSON.parse(text) as T;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err instanceof SwarmpitApiError) throw err;
|
|
76
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
77
|
+
throw new Error(`Swarmpit API request timed out after ${this.timeout}ms: ${method} ${path}`);
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Services
|
|
86
|
+
async listServices(): Promise<SwarmpitService[]> {
|
|
87
|
+
return this.request<SwarmpitService[]>("GET", "/api/services");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getService(id: string): Promise<SwarmpitService> {
|
|
91
|
+
return this.request<SwarmpitService>("GET", `/api/services/${encodeURIComponent(id)}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async getServiceLogs(id: string, since = "5m"): Promise<SwarmpitLogEntry[]> {
|
|
95
|
+
// Swarmpit accepts Go duration strings: "1s", "1m", "1h", "24h" etc.
|
|
96
|
+
return this.request<SwarmpitLogEntry[]>("GET", `/api/services/${encodeURIComponent(id)}/logs?since=${encodeURIComponent(since)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async createService(spec: Record<string, unknown>): Promise<{ id: string }> {
|
|
100
|
+
return this.request<{ id: string }>("POST", "/api/services", spec);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async updateService(id: string, spec: Record<string, unknown>): Promise<void> {
|
|
104
|
+
await this.request<void>("POST", `/api/services/${encodeURIComponent(id)}`, spec);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async redeployService(id: string, tag?: string): Promise<void> {
|
|
108
|
+
const query = tag ? `?tag=${encodeURIComponent(tag)}` : "";
|
|
109
|
+
await this.request<void>("POST", `/api/services/${encodeURIComponent(id)}/redeploy${query}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async rollbackService(id: string): Promise<void> {
|
|
113
|
+
await this.request<void>("POST", `/api/services/${encodeURIComponent(id)}/rollback`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async stopService(id: string): Promise<void> {
|
|
117
|
+
await this.request<void>("POST", `/api/services/${encodeURIComponent(id)}/stop`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async deleteService(id: string): Promise<void> {
|
|
121
|
+
await this.request<void>("DELETE", `/api/services/${encodeURIComponent(id)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async getServiceTasks(id: string): Promise<SwarmpitTask[]> {
|
|
125
|
+
return this.request<SwarmpitTask[]>("GET", `/api/services/${encodeURIComponent(id)}/tasks`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getServiceCompose(id: string): Promise<{ compose: string }> {
|
|
129
|
+
const result = await this.request<{ spec?: { compose: string }; compose?: string }>(
|
|
130
|
+
"GET", `/api/services/${encodeURIComponent(id)}/compose`
|
|
131
|
+
);
|
|
132
|
+
return { compose: result.spec?.compose ?? result.compose ?? "" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async getServiceNetworks(id: string): Promise<SwarmpitNetwork[]> {
|
|
136
|
+
return this.request<SwarmpitNetwork[]>("GET", `/api/services/${encodeURIComponent(id)}/networks`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Stacks
|
|
140
|
+
async listStacks(): Promise<SwarmpitStack[]> {
|
|
141
|
+
return this.request<SwarmpitStack[]>("GET", "/api/stacks");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async getStackServices(name: string): Promise<SwarmpitService[]> {
|
|
145
|
+
return this.request<SwarmpitService[]>("GET", `/api/stacks/${encodeURIComponent(name)}/services`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async getStackFile(name: string): Promise<{ compose: string }> {
|
|
149
|
+
const result = await this.request<{ spec?: { compose: string }; compose?: string }>(
|
|
150
|
+
"GET", `/api/stacks/${encodeURIComponent(name)}/file`
|
|
151
|
+
);
|
|
152
|
+
// API returns { spec: { compose } } but we normalize to { compose }
|
|
153
|
+
const compose = result.spec?.compose ?? result.compose ?? "";
|
|
154
|
+
return { compose };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async createStack(spec: { name: string; spec: { compose: string } }): Promise<void> {
|
|
158
|
+
await this.request<void>("POST", "/api/stacks", spec);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async updateStack(name: string, compose: string): Promise<void> {
|
|
162
|
+
await this.request<void>("POST", `/api/stacks/${encodeURIComponent(name)}`, {
|
|
163
|
+
name,
|
|
164
|
+
spec: { compose },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async redeployStack(name: string): Promise<void> {
|
|
169
|
+
await this.request<void>("POST", `/api/stacks/${encodeURIComponent(name)}/redeploy`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async rollbackStack(name: string): Promise<void> {
|
|
173
|
+
await this.request<void>("POST", `/api/stacks/${encodeURIComponent(name)}/rollback`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async deactivateStack(name: string): Promise<void> {
|
|
177
|
+
await this.request<void>("POST", `/api/stacks/${encodeURIComponent(name)}/deactivate`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async getStackTasks(name: string): Promise<SwarmpitTask[]> {
|
|
181
|
+
return this.request<SwarmpitTask[]>("GET", `/api/stacks/${encodeURIComponent(name)}/tasks`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getStackVolumes(name: string): Promise<SwarmpitVolume[]> {
|
|
185
|
+
return this.request<SwarmpitVolume[]>("GET", `/api/stacks/${encodeURIComponent(name)}/volumes`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getStackNetworks(name: string): Promise<SwarmpitNetwork[]> {
|
|
189
|
+
return this.request<SwarmpitNetwork[]>("GET", `/api/stacks/${encodeURIComponent(name)}/networks`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async getStackCompose(name: string): Promise<{ compose: string }> {
|
|
193
|
+
const result = await this.request<{ spec?: { compose: string }; compose?: string }>(
|
|
194
|
+
"GET", `/api/stacks/${encodeURIComponent(name)}/compose`
|
|
195
|
+
);
|
|
196
|
+
return { compose: result.spec?.compose ?? result.compose ?? "" };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async getStackSecrets(name: string): Promise<Record<string, unknown>[]> {
|
|
200
|
+
return this.request<Record<string, unknown>[]>("GET", `/api/stacks/${encodeURIComponent(name)}/secrets`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async getStackConfigs(name: string): Promise<Record<string, unknown>[]> {
|
|
204
|
+
return this.request<Record<string, unknown>[]>("GET", `/api/stacks/${encodeURIComponent(name)}/configs`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async createStackFile(name: string, compose: string): Promise<void> {
|
|
208
|
+
await this.request<void>("POST", `/api/stacks/${encodeURIComponent(name)}/file`, { name, spec: { compose } });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async deleteStackFile(name: string): Promise<void> {
|
|
212
|
+
await this.request<void>("DELETE", `/api/stacks/${encodeURIComponent(name)}/file`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async deleteStack(name: string): Promise<void> {
|
|
216
|
+
await this.request<void>("DELETE", `/api/stacks/${encodeURIComponent(name)}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Networks
|
|
220
|
+
async listNetworks(): Promise<SwarmpitNetwork[]> {
|
|
221
|
+
return this.request<SwarmpitNetwork[]>("GET", "/api/networks");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async getNetwork(id: string): Promise<SwarmpitNetwork> {
|
|
225
|
+
return this.request<SwarmpitNetwork>("GET", `/api/networks/${encodeURIComponent(id)}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async getNetworkServices(id: string): Promise<SwarmpitService[]> {
|
|
229
|
+
return this.request<SwarmpitService[]>("GET", `/api/networks/${encodeURIComponent(id)}/services`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async createNetwork(spec: Record<string, unknown>): Promise<void> {
|
|
233
|
+
await this.request<void>("POST", "/api/networks", spec);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async deleteNetwork(id: string): Promise<void> {
|
|
237
|
+
await this.request<void>("DELETE", `/api/networks/${encodeURIComponent(id)}`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Nodes
|
|
241
|
+
async listNodes(): Promise<SwarmpitNode[]> {
|
|
242
|
+
return this.request<SwarmpitNode[]>("GET", "/api/nodes");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async getNode(id: string): Promise<SwarmpitNode> {
|
|
246
|
+
return this.request<SwarmpitNode>("GET", `/api/nodes/${encodeURIComponent(id)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async editNode(id: string, spec: Record<string, unknown>): Promise<void> {
|
|
250
|
+
await this.request<void>("POST", `/api/nodes/${encodeURIComponent(id)}`, spec);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async deleteNode(id: string): Promise<void> {
|
|
254
|
+
await this.request<void>("DELETE", `/api/nodes/${encodeURIComponent(id)}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async getNodeTasks(id: string): Promise<SwarmpitTask[]> {
|
|
258
|
+
return this.request<SwarmpitTask[]>("GET", `/api/nodes/${encodeURIComponent(id)}/tasks`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Tasks
|
|
262
|
+
async listTasks(): Promise<SwarmpitTask[]> {
|
|
263
|
+
return this.request<SwarmpitTask[]>("GET", "/api/tasks");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async getTask(id: string): Promise<SwarmpitTask> {
|
|
267
|
+
return this.request<SwarmpitTask>("GET", `/api/tasks/${encodeURIComponent(id)}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Volumes
|
|
271
|
+
async listVolumes(): Promise<SwarmpitVolume[]> {
|
|
272
|
+
return this.request<SwarmpitVolume[]>("GET", "/api/volumes");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async getVolume(name: string): Promise<SwarmpitVolume> {
|
|
276
|
+
return this.request<SwarmpitVolume>("GET", `/api/volumes/${encodeURIComponent(name)}`);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async getVolumeServices(name: string): Promise<SwarmpitService[]> {
|
|
280
|
+
return this.request<SwarmpitService[]>("GET", `/api/volumes/${encodeURIComponent(name)}/services`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async createVolume(spec: Record<string, unknown>): Promise<void> {
|
|
284
|
+
await this.request<void>("POST", "/api/volumes", spec);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async deleteVolume(name: string): Promise<void> {
|
|
288
|
+
await this.request<void>("DELETE", `/api/volumes/${encodeURIComponent(name)}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Secrets
|
|
292
|
+
async listSecrets(): Promise<Record<string, unknown>[]> {
|
|
293
|
+
return this.request<Record<string, unknown>[]>("GET", "/api/secrets");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async getSecret(id: string): Promise<Record<string, unknown>> {
|
|
297
|
+
return this.request<Record<string, unknown>>("GET", `/api/secrets/${encodeURIComponent(id)}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async getSecretServices(id: string): Promise<SwarmpitService[]> {
|
|
301
|
+
return this.request<SwarmpitService[]>("GET", `/api/secrets/${encodeURIComponent(id)}/services`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async createSecret(spec: { secretName: string; data: string }): Promise<void> {
|
|
305
|
+
// Swarmpit expects base64-encoded data
|
|
306
|
+
const encoded = Buffer.from(spec.data, "utf-8").toString("base64");
|
|
307
|
+
await this.request<void>("POST", "/api/secrets", { ...spec, data: encoded });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async deleteSecret(id: string): Promise<void> {
|
|
311
|
+
await this.request<void>("DELETE", `/api/secrets/${encodeURIComponent(id)}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Configs
|
|
315
|
+
async listConfigs(): Promise<Record<string, unknown>[]> {
|
|
316
|
+
return this.request<Record<string, unknown>[]>("GET", "/api/configs");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async getConfig(id: string): Promise<Record<string, unknown>> {
|
|
320
|
+
return this.request<Record<string, unknown>>("GET", `/api/configs/${encodeURIComponent(id)}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async getConfigServices(id: string): Promise<SwarmpitService[]> {
|
|
324
|
+
return this.request<SwarmpitService[]>("GET", `/api/configs/${encodeURIComponent(id)}/services`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async createConfig(spec: { configName: string; data: string }): Promise<void> {
|
|
328
|
+
// Swarmpit expects base64-encoded data
|
|
329
|
+
const encoded = Buffer.from(spec.data, "utf-8").toString("base64");
|
|
330
|
+
await this.request<void>("POST", "/api/configs", { ...spec, data: encoded });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async deleteConfig(id: string): Promise<void> {
|
|
334
|
+
await this.request<void>("DELETE", `/api/configs/${encodeURIComponent(id)}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Admin
|
|
338
|
+
async listUsers(): Promise<Record<string, unknown>[]> {
|
|
339
|
+
return this.request<Record<string, unknown>[]>("GET", "/api/admin/users");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async getUser(id: string): Promise<Record<string, unknown>> {
|
|
343
|
+
return this.request<Record<string, unknown>>("GET", `/api/admin/users/${encodeURIComponent(id)}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async createUser(spec: { username: string; password: string; role: string; email?: string }): Promise<void> {
|
|
347
|
+
await this.request<void>("POST", "/api/admin/users", spec);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async editUser(id: string, spec: Record<string, unknown>): Promise<void> {
|
|
351
|
+
await this.request<void>("POST", `/api/admin/users/${encodeURIComponent(id)}`, spec);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async deleteUser(id: string): Promise<void> {
|
|
355
|
+
await this.request<void>("DELETE", `/api/admin/users/${encodeURIComponent(id)}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Dashboard
|
|
359
|
+
async pinNodeToDashboard(id: string): Promise<void> {
|
|
360
|
+
await this.request<void>("POST", `/api/nodes/${encodeURIComponent(id)}/dashboard`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async unpinNodeFromDashboard(id: string): Promise<void> {
|
|
364
|
+
await this.request<void>("DELETE", `/api/nodes/${encodeURIComponent(id)}/dashboard`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async pinServiceToDashboard(id: string): Promise<void> {
|
|
368
|
+
await this.request<void>("POST", `/api/services/${encodeURIComponent(id)}/dashboard`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async unpinServiceFromDashboard(id: string): Promise<void> {
|
|
372
|
+
await this.request<void>("DELETE", `/api/services/${encodeURIComponent(id)}/dashboard`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Timeseries
|
|
376
|
+
async getNodesTimeseries(): Promise<unknown> {
|
|
377
|
+
return this.request<unknown>("GET", "/api/nodes/ts");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async getServicesCpuTimeseries(): Promise<unknown> {
|
|
381
|
+
return this.request<unknown>("GET", "/api/services/ts/cpu");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async getServicesMemoryTimeseries(): Promise<unknown> {
|
|
385
|
+
return this.request<unknown>("GET", "/api/services/ts/memory");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async getTaskTimeseries(name: string): Promise<unknown> {
|
|
389
|
+
return this.request<unknown>("GET", `/api/tasks/${encodeURIComponent(name)}/ts`);
|
|
390
|
+
}
|
|
391
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
export type RedactMode = "all" | "sensitive" | "none";
|
|
4
|
+
|
|
5
|
+
export interface SwarmpitConfig {
|
|
6
|
+
url: string;
|
|
7
|
+
token: string;
|
|
8
|
+
redact: RedactMode;
|
|
9
|
+
redactPatterns: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadToken(): string {
|
|
13
|
+
const tokenFile = process.env.SWARMPIT_TOKEN_FILE;
|
|
14
|
+
if (tokenFile) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(tokenFile, "utf-8").trim();
|
|
17
|
+
} catch (err) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`SWARMPIT_TOKEN_FILE "${tokenFile}" could not be read: ${err instanceof Error ? err.message : err}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const token = process.env.SWARMPIT_TOKEN;
|
|
24
|
+
if (!token) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
"Neither SWARMPIT_TOKEN nor SWARMPIT_TOKEN_FILE is set. Use SWARMPIT_TOKEN_FILE to avoid storing the token in MCP client config files."
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadConfig(): SwarmpitConfig {
|
|
33
|
+
const url = process.env.SWARMPIT_URL;
|
|
34
|
+
if (!url) throw new Error("SWARMPIT_URL is not set");
|
|
35
|
+
|
|
36
|
+
const token = loadToken();
|
|
37
|
+
|
|
38
|
+
const redactRaw = process.env.SWARMPIT_REDACT;
|
|
39
|
+
let redact: RedactMode = "all";
|
|
40
|
+
if (redactRaw !== undefined) {
|
|
41
|
+
if (redactRaw !== "all" && redactRaw !== "sensitive" && redactRaw !== "none") {
|
|
42
|
+
throw new Error('SWARMPIT_REDACT must be "all" (default), "sensitive", or "none"');
|
|
43
|
+
}
|
|
44
|
+
redact = redactRaw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const redactPatterns = process.env.SWARMPIT_REDACT_PATTERNS
|
|
48
|
+
? process.env.SWARMPIT_REDACT_PATTERNS.split(",").map((p) => p.trim()).filter(Boolean)
|
|
49
|
+
: [];
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
url: url.replace(/\/+$/, ""),
|
|
53
|
+
token,
|
|
54
|
+
redact,
|
|
55
|
+
redactPatterns,
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { SwarmpitClient } from "./client.js";
|
|
7
|
+
import { setExtraRedactPatterns } from "./sanitize.js";
|
|
8
|
+
import { registerAllTools } from "./tools/register.js";
|
|
9
|
+
|
|
10
|
+
async function checkConnection(url: string, token: string): Promise<void> {
|
|
11
|
+
const client = new SwarmpitClient(url, token, 10_000);
|
|
12
|
+
try {
|
|
13
|
+
await client.listNodes();
|
|
14
|
+
console.error(`mcp-swarmpit: connected to ${url}`);
|
|
15
|
+
} catch (err) {
|
|
16
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
17
|
+
console.error(`mcp-swarmpit: WARNING — cannot reach ${url}: ${msg}`);
|
|
18
|
+
console.error(`mcp-swarmpit: tools will be available but API calls may fail`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
|
|
25
|
+
if (config.redactPatterns.length > 0) {
|
|
26
|
+
setExtraRedactPatterns(config.redactPatterns);
|
|
27
|
+
console.error(`mcp-swarmpit: extra redact patterns: ${config.redactPatterns.join(", ")}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await checkConnection(config.url, config.token);
|
|
31
|
+
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: "mcp-swarmpit",
|
|
34
|
+
version: "0.1.0",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
registerAllTools(server, config);
|
|
38
|
+
|
|
39
|
+
const transport = new StdioServerTransport();
|
|
40
|
+
await server.connect(transport);
|
|
41
|
+
console.error("mcp-swarmpit: running on stdio");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main().catch((err) => {
|
|
45
|
+
console.error("mcp-swarmpit fatal:", err);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
process.on("SIGINT", () => process.exit(0));
|