@clawops/cli 0.2.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/LICENSE +373 -0
- package/README.md +335 -0
- package/dist/apply-GTVALBAY.js +12 -0
- package/dist/automation-JG2YBEL7.js +30 -0
- package/dist/aws-A3323GNM.js +285 -0
- package/dist/azure-LNWBK2ZI.js +295 -0
- package/dist/bootstrap-NRIDVS5F.js +91 -0
- package/dist/chunk-5XEZAU7V.js +254 -0
- package/dist/chunk-ALSUDYA7.js +89 -0
- package/dist/chunk-ENQY5OW2.js +113 -0
- package/dist/chunk-LT24GUUO.js +39 -0
- package/dist/chunk-PERSDQMT.js +114 -0
- package/dist/chunk-PRYLTCS4.js +37 -0
- package/dist/chunk-RZE35FQF.js +56 -0
- package/dist/chunk-YTH4L2GN.js +41 -0
- package/dist/chunk-ZSE4QRKE.js +44 -0
- package/dist/cli.js +1752 -0
- package/dist/context-T52JWL3P.js +10 -0
- package/dist/firewall-YYDOWDDP.js +36 -0
- package/dist/gcp-OHWCTFLL.js +198 -0
- package/dist/generate-Y6O465VB.js +14 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +0 -0
- package/dist/local-DXBEVZ5C.js +47 -0
- package/dist/outputs-6DAVEEAZ.js +8 -0
- package/dist/package-YZFYSSQM.js +109 -0
- package/dist/pool-D4JCUA2V.js +10 -0
- package/dist/providers-2OABPW2E.js +24 -0
- package/dist/server-TKDURITQ.js +1349 -0
- package/dist/store-ARJ2EO6L.js +16 -0
- package/dist/validate-T5M5EHSJ.js +9 -0
- package/package.json +77 -0
|
@@ -0,0 +1,1349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
acquireSession,
|
|
4
|
+
drainPool
|
|
5
|
+
} from "./chunk-5XEZAU7V.js";
|
|
6
|
+
import {
|
|
7
|
+
extractBaseOutputs
|
|
8
|
+
} from "./chunk-LT24GUUO.js";
|
|
9
|
+
import {
|
|
10
|
+
generatePlan
|
|
11
|
+
} from "./chunk-ENQY5OW2.js";
|
|
12
|
+
import {
|
|
13
|
+
applyPlan
|
|
14
|
+
} from "./chunk-RZE35FQF.js";
|
|
15
|
+
import {
|
|
16
|
+
buildContext
|
|
17
|
+
} from "./chunk-PERSDQMT.js";
|
|
18
|
+
import "./chunk-PRYLTCS4.js";
|
|
19
|
+
import {
|
|
20
|
+
validatePlan
|
|
21
|
+
} from "./chunk-YTH4L2GN.js";
|
|
22
|
+
import {
|
|
23
|
+
getConfig,
|
|
24
|
+
getConfigDir
|
|
25
|
+
} from "./chunk-ALSUDYA7.js";
|
|
26
|
+
import {
|
|
27
|
+
StateError,
|
|
28
|
+
UsageError
|
|
29
|
+
} from "./chunk-ZSE4QRKE.js";
|
|
30
|
+
|
|
31
|
+
// src/mcp/server.ts
|
|
32
|
+
import { createServer } from "http";
|
|
33
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
34
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
35
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
36
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
37
|
+
|
|
38
|
+
// src/mcp/audit.ts
|
|
39
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
40
|
+
import { randomUUID } from "crypto";
|
|
41
|
+
import path from "path";
|
|
42
|
+
import process2 from "process";
|
|
43
|
+
var _sessionId = null;
|
|
44
|
+
function getSessionId() {
|
|
45
|
+
if (!_sessionId) _sessionId = randomUUID();
|
|
46
|
+
return _sessionId;
|
|
47
|
+
}
|
|
48
|
+
var SENSITIVE_KEYS = ["token", "secret", "password", "connectionstring", "authorization"];
|
|
49
|
+
var EXEMPT_KEYS = ["keyname", "keypath", "privateKeyPath", "knownHostsPath"];
|
|
50
|
+
var ARN_RE = /arn:aws:[a-z0-9\-*]+:[a-z0-9\-]*:[0-9]*:[^\s,'"]+/gi;
|
|
51
|
+
function sanitize(args) {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const [k, v] of Object.entries(args)) {
|
|
54
|
+
const kl = k.toLowerCase();
|
|
55
|
+
const exempt = EXEMPT_KEYS.some((e) => kl === e.toLowerCase());
|
|
56
|
+
const sensitive = !exempt && SENSITIVE_KEYS.some((s) => kl.includes(s));
|
|
57
|
+
if (sensitive) {
|
|
58
|
+
result[k] = "[REDACTED]";
|
|
59
|
+
} else if (typeof v === "string") {
|
|
60
|
+
result[k] = v.replace(ARN_RE, "arn:aws:***:<region>:<account>:***");
|
|
61
|
+
} else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
62
|
+
result[k] = sanitize(v);
|
|
63
|
+
} else {
|
|
64
|
+
result[k] = v;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function auditLog(entry) {
|
|
70
|
+
const line = JSON.stringify(entry) + "\n";
|
|
71
|
+
process2.stderr.write(line);
|
|
72
|
+
try {
|
|
73
|
+
const logPath = getAuditLogPath();
|
|
74
|
+
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
75
|
+
appendFileSync(logPath, line, { encoding: "utf-8" });
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function getAuditLogPath() {
|
|
80
|
+
const cfg = getConfig();
|
|
81
|
+
return cfg?.mcp?.auditLogPath ?? path.join(getConfigDir(), "mcp-audit.log");
|
|
82
|
+
}
|
|
83
|
+
function withAudit(toolName, handler) {
|
|
84
|
+
return async (input) => {
|
|
85
|
+
const start = Date.now();
|
|
86
|
+
try {
|
|
87
|
+
const result = await handler(input);
|
|
88
|
+
auditLog({
|
|
89
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
90
|
+
sessionId: getSessionId(),
|
|
91
|
+
tool: toolName,
|
|
92
|
+
args: sanitize(input),
|
|
93
|
+
durationMs: Date.now() - start,
|
|
94
|
+
result: "ok"
|
|
95
|
+
});
|
|
96
|
+
return result;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
auditLog({
|
|
99
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
100
|
+
sessionId: getSessionId(),
|
|
101
|
+
tool: toolName,
|
|
102
|
+
args: sanitize(input),
|
|
103
|
+
durationMs: Date.now() - start,
|
|
104
|
+
result: "error"
|
|
105
|
+
});
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/mcp/tools/_generated.ts
|
|
112
|
+
import { z } from "zod";
|
|
113
|
+
var clawops_statusSchema = z.object({
|
|
114
|
+
stackName: z.string().optional()
|
|
115
|
+
});
|
|
116
|
+
var clawops_statusAnnotations = {
|
|
117
|
+
title: "Get Stack Status",
|
|
118
|
+
readOnlyHint: true,
|
|
119
|
+
destructiveHint: false,
|
|
120
|
+
idempotentHint: true,
|
|
121
|
+
openWorldHint: true,
|
|
122
|
+
toolsets: ["cli", "read"]
|
|
123
|
+
};
|
|
124
|
+
var clawops_logs_tailSchema = z.object({
|
|
125
|
+
stackName: z.string().optional(),
|
|
126
|
+
tailLines: z.number().int().default(100),
|
|
127
|
+
sinceMin: z.number().int().optional()
|
|
128
|
+
});
|
|
129
|
+
var clawops_logs_tailAnnotations = {
|
|
130
|
+
title: "Tail Gateway Logs",
|
|
131
|
+
readOnlyHint: true,
|
|
132
|
+
destructiveHint: false,
|
|
133
|
+
idempotentHint: true,
|
|
134
|
+
openWorldHint: true,
|
|
135
|
+
toolsets: ["cli", "read"]
|
|
136
|
+
};
|
|
137
|
+
var clawops_stacks_listSchema = z.object({});
|
|
138
|
+
var clawops_stacks_listAnnotations = {
|
|
139
|
+
title: "List Stacks",
|
|
140
|
+
readOnlyHint: true,
|
|
141
|
+
destructiveHint: false,
|
|
142
|
+
idempotentHint: true,
|
|
143
|
+
openWorldHint: false,
|
|
144
|
+
toolsets: ["admin", "read"]
|
|
145
|
+
};
|
|
146
|
+
var clawops_config_getSchema = z.object({
|
|
147
|
+
stackName: z.string().optional(),
|
|
148
|
+
key: z.string()
|
|
149
|
+
});
|
|
150
|
+
var clawops_config_getAnnotations = {
|
|
151
|
+
title: "Get OpenClaw Config Value",
|
|
152
|
+
readOnlyHint: true,
|
|
153
|
+
destructiveHint: false,
|
|
154
|
+
idempotentHint: true,
|
|
155
|
+
openWorldHint: true,
|
|
156
|
+
toolsets: ["cli", "read"]
|
|
157
|
+
};
|
|
158
|
+
var clawops_agents_listSchema = z.object({
|
|
159
|
+
stackName: z.string().optional()
|
|
160
|
+
});
|
|
161
|
+
var clawops_agents_listAnnotations = {
|
|
162
|
+
title: "List OpenClaw Agents",
|
|
163
|
+
readOnlyHint: true,
|
|
164
|
+
destructiveHint: false,
|
|
165
|
+
idempotentHint: true,
|
|
166
|
+
openWorldHint: true,
|
|
167
|
+
toolsets: ["cli", "read"]
|
|
168
|
+
};
|
|
169
|
+
var clawops_upSchema = z.object({
|
|
170
|
+
stackName: z.string().optional(),
|
|
171
|
+
provider: z.enum(["aws", "gcp", "azure", "local"]).optional(),
|
|
172
|
+
region: z.string().optional(),
|
|
173
|
+
instanceType: z.enum(["micro", "small", "medium", "large", "gpu"]).default("small"),
|
|
174
|
+
openclawVersion: z.string().optional(),
|
|
175
|
+
dryRun: z.boolean().default(false)
|
|
176
|
+
});
|
|
177
|
+
var clawops_upAnnotations = {
|
|
178
|
+
title: "Provision and Deploy Stack",
|
|
179
|
+
readOnlyHint: false,
|
|
180
|
+
destructiveHint: true,
|
|
181
|
+
idempotentHint: true,
|
|
182
|
+
openWorldHint: true,
|
|
183
|
+
toolsets: ["cli"]
|
|
184
|
+
};
|
|
185
|
+
var clawops_destroySchema = z.object({
|
|
186
|
+
stackName: z.string(),
|
|
187
|
+
yes: z.boolean().default(false)
|
|
188
|
+
});
|
|
189
|
+
var clawops_destroyAnnotations = {
|
|
190
|
+
title: "Destroy Stack (DESTRUCTIVE)",
|
|
191
|
+
readOnlyHint: false,
|
|
192
|
+
destructiveHint: true,
|
|
193
|
+
idempotentHint: true,
|
|
194
|
+
openWorldHint: true,
|
|
195
|
+
toolsets: ["cli"]
|
|
196
|
+
};
|
|
197
|
+
var clawops_applySchema = z.object({
|
|
198
|
+
planPath: z.string(),
|
|
199
|
+
yes: z.boolean().default(false)
|
|
200
|
+
});
|
|
201
|
+
var clawops_applyAnnotations = {
|
|
202
|
+
title: "Apply Maker Plan",
|
|
203
|
+
readOnlyHint: false,
|
|
204
|
+
destructiveHint: true,
|
|
205
|
+
idempotentHint: false,
|
|
206
|
+
openWorldHint: true,
|
|
207
|
+
toolsets: ["cli"]
|
|
208
|
+
};
|
|
209
|
+
var clawops_planSchema = z.object({
|
|
210
|
+
stackName: z.string().optional(),
|
|
211
|
+
provider: z.enum(["aws", "gcp", "azure", "local"]).optional(),
|
|
212
|
+
region: z.string().optional(),
|
|
213
|
+
instanceType: z.enum(["micro", "small", "medium", "large", "gpu"]).optional(),
|
|
214
|
+
outPath: z.string().optional()
|
|
215
|
+
});
|
|
216
|
+
var clawops_planAnnotations = {
|
|
217
|
+
title: "Generate Deploy Plan",
|
|
218
|
+
readOnlyHint: true,
|
|
219
|
+
destructiveHint: false,
|
|
220
|
+
idempotentHint: true,
|
|
221
|
+
openWorldHint: true,
|
|
222
|
+
toolsets: ["cli", "read"]
|
|
223
|
+
};
|
|
224
|
+
var clawops_config_setSchema = z.object({
|
|
225
|
+
stackName: z.string().optional(),
|
|
226
|
+
key: z.string(),
|
|
227
|
+
value: z.string(),
|
|
228
|
+
restart: z.boolean().default(false)
|
|
229
|
+
});
|
|
230
|
+
var clawops_config_setAnnotations = {
|
|
231
|
+
title: "Set OpenClaw Config Value",
|
|
232
|
+
readOnlyHint: false,
|
|
233
|
+
destructiveHint: true,
|
|
234
|
+
idempotentHint: true,
|
|
235
|
+
openWorldHint: true,
|
|
236
|
+
toolsets: ["cli"]
|
|
237
|
+
};
|
|
238
|
+
var clawops_agents_restartSchema = z.object({
|
|
239
|
+
stackName: z.string().optional(),
|
|
240
|
+
agentId: z.string()
|
|
241
|
+
});
|
|
242
|
+
var clawops_agents_restartAnnotations = {
|
|
243
|
+
title: "Restart OpenClaw Agent",
|
|
244
|
+
readOnlyHint: false,
|
|
245
|
+
destructiveHint: true,
|
|
246
|
+
idempotentHint: true,
|
|
247
|
+
openWorldHint: true,
|
|
248
|
+
toolsets: ["cli"]
|
|
249
|
+
};
|
|
250
|
+
var clawops_gateway_restartSchema = z.object({
|
|
251
|
+
stackName: z.string().optional()
|
|
252
|
+
});
|
|
253
|
+
var clawops_gateway_restartAnnotations = {
|
|
254
|
+
title: "Restart Gateway Daemon",
|
|
255
|
+
readOnlyHint: false,
|
|
256
|
+
destructiveHint: true,
|
|
257
|
+
idempotentHint: true,
|
|
258
|
+
openWorldHint: true,
|
|
259
|
+
toolsets: ["cli"]
|
|
260
|
+
};
|
|
261
|
+
var clawops_workflow_deploy_appSchema = z.object({
|
|
262
|
+
provider: z.enum(["aws", "gcp", "azure", "local"]),
|
|
263
|
+
region: z.string().optional(),
|
|
264
|
+
stackName: z.string().default("default"),
|
|
265
|
+
instanceType: z.enum(["micro", "small", "medium", "large", "gpu"]).default("small")
|
|
266
|
+
});
|
|
267
|
+
var clawops_workflow_deploy_appAnnotations = {
|
|
268
|
+
title: "Deploy OpenClaw (End-to-End Workflow)",
|
|
269
|
+
readOnlyHint: false,
|
|
270
|
+
destructiveHint: true,
|
|
271
|
+
idempotentHint: true,
|
|
272
|
+
openWorldHint: true,
|
|
273
|
+
toolsets: ["workflow"]
|
|
274
|
+
};
|
|
275
|
+
var clawops_workflow_recoverSchema = z.object({
|
|
276
|
+
stackName: z.string().optional()
|
|
277
|
+
});
|
|
278
|
+
var clawops_workflow_recoverAnnotations = {
|
|
279
|
+
title: "Recover/Diagnose Stack",
|
|
280
|
+
readOnlyHint: true,
|
|
281
|
+
destructiveHint: false,
|
|
282
|
+
idempotentHint: true,
|
|
283
|
+
openWorldHint: true,
|
|
284
|
+
toolsets: ["workflow", "read"]
|
|
285
|
+
};
|
|
286
|
+
var clawops_task_statusSchema = z.object({
|
|
287
|
+
taskId: z.string()
|
|
288
|
+
});
|
|
289
|
+
var clawops_task_statusAnnotations = {
|
|
290
|
+
title: "Get Task Status",
|
|
291
|
+
readOnlyHint: true,
|
|
292
|
+
destructiveHint: false,
|
|
293
|
+
idempotentHint: true,
|
|
294
|
+
openWorldHint: false,
|
|
295
|
+
toolsets: ["cli", "read"]
|
|
296
|
+
};
|
|
297
|
+
var TOOLSETS = {
|
|
298
|
+
cli: [
|
|
299
|
+
"clawops_status",
|
|
300
|
+
"clawops_logs_tail",
|
|
301
|
+
"clawops_config_get",
|
|
302
|
+
"clawops_agents_list",
|
|
303
|
+
"clawops_up",
|
|
304
|
+
"clawops_destroy",
|
|
305
|
+
"clawops_apply",
|
|
306
|
+
"clawops_plan",
|
|
307
|
+
"clawops_config_set",
|
|
308
|
+
"clawops_agents_restart",
|
|
309
|
+
"clawops_gateway_restart",
|
|
310
|
+
"clawops_task_status"
|
|
311
|
+
],
|
|
312
|
+
workflow: [
|
|
313
|
+
"clawops_workflow_deploy_app",
|
|
314
|
+
"clawops_workflow_recover"
|
|
315
|
+
],
|
|
316
|
+
read: [
|
|
317
|
+
"clawops_status",
|
|
318
|
+
"clawops_logs_tail",
|
|
319
|
+
"clawops_stacks_list",
|
|
320
|
+
"clawops_config_get",
|
|
321
|
+
"clawops_agents_list",
|
|
322
|
+
"clawops_plan",
|
|
323
|
+
"clawops_workflow_recover",
|
|
324
|
+
"clawops_task_status"
|
|
325
|
+
],
|
|
326
|
+
admin: [
|
|
327
|
+
"clawops_stacks_list"
|
|
328
|
+
]
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/mcp/tools/cli/status.ts
|
|
332
|
+
async function handleStatus(input, _server) {
|
|
333
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
334
|
+
if (ctx.adapter.name === "local") {
|
|
335
|
+
const state = ctx.localState;
|
|
336
|
+
if (!state) {
|
|
337
|
+
return text(JSON.stringify({ stack: ctx.stackName, status: "not bootstrapped" }, null, 2));
|
|
338
|
+
}
|
|
339
|
+
return text(JSON.stringify({ stack: ctx.stackName, ...state }, null, 2));
|
|
340
|
+
}
|
|
341
|
+
const stack = await ctx.getStack();
|
|
342
|
+
const outputMap = await stack.outputs();
|
|
343
|
+
const outputs = Object.fromEntries(
|
|
344
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
345
|
+
);
|
|
346
|
+
if (!outputs["publicIp"]) {
|
|
347
|
+
return text(JSON.stringify({ stack: ctx.stackName, status: "not deployed" }, null, 2));
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const base = extractBaseOutputs(outputs);
|
|
351
|
+
return text(JSON.stringify({ stack: ctx.stackName, ...base }, null, 2));
|
|
352
|
+
} catch (err) {
|
|
353
|
+
if (err instanceof StateError) {
|
|
354
|
+
return text(JSON.stringify({ stack: ctx.stackName, status: "not deployed", detail: err.message }, null, 2));
|
|
355
|
+
}
|
|
356
|
+
throw err;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function text(t) {
|
|
360
|
+
return { content: [{ type: "text", text: t }] };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/mcp/tools/_conn.ts
|
|
364
|
+
async function resolveConn(ctx) {
|
|
365
|
+
if (ctx.adapter.name === "local") {
|
|
366
|
+
const state = ctx.localState;
|
|
367
|
+
if (!state) throw new StateError("Stack has no local state \u2014 run `clawops up` first.");
|
|
368
|
+
return {
|
|
369
|
+
host: state.sshHost,
|
|
370
|
+
port: state.sshPort,
|
|
371
|
+
user: state.sshUser,
|
|
372
|
+
privateKeyPath: state.privateKeyPath,
|
|
373
|
+
knownHostsPath: state.knownHostsPath
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const { extractBaseOutputs: extractBaseOutputs2 } = await import("./outputs-6DAVEEAZ.js");
|
|
377
|
+
const stack = await ctx.getStack();
|
|
378
|
+
const outputMap = await stack.outputs();
|
|
379
|
+
const outputs = Object.fromEntries(
|
|
380
|
+
Object.entries(outputMap).map(([k, v]) => [k, v.value])
|
|
381
|
+
);
|
|
382
|
+
if (!outputs["publicIp"]) {
|
|
383
|
+
throw new StateError("Stack has no outputs \u2014 run `clawops up` first.");
|
|
384
|
+
}
|
|
385
|
+
const base = extractBaseOutputs2(outputs);
|
|
386
|
+
return ctx.adapter.getConnectionInfo({
|
|
387
|
+
...base,
|
|
388
|
+
privateKeyPath: ctx.config.ssh.keyPath,
|
|
389
|
+
knownHostsPath: ctx.config.ssh.knownHostsPath
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function errText(message) {
|
|
393
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
394
|
+
}
|
|
395
|
+
function okText(t) {
|
|
396
|
+
return { content: [{ type: "text", text: t }] };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// src/mcp/tools/cli/logs.ts
|
|
400
|
+
var MAX_BYTES = 8 * 1024;
|
|
401
|
+
async function handleLogsTail(input, _server) {
|
|
402
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
403
|
+
const conn = await resolveConn(ctx);
|
|
404
|
+
const { session, release } = await acquireSession(conn);
|
|
405
|
+
try {
|
|
406
|
+
const tailLines = input.tailLines ?? 100;
|
|
407
|
+
const sinceFlag = input.sinceMin ? `--since "${input.sinceMin} min ago"` : "";
|
|
408
|
+
const command = [
|
|
409
|
+
"journalctl -u openclaw",
|
|
410
|
+
`-n ${tailLines}`,
|
|
411
|
+
sinceFlag,
|
|
412
|
+
"2>/dev/null",
|
|
413
|
+
"|| docker logs openclaw",
|
|
414
|
+
`-n ${tailLines}`
|
|
415
|
+
].filter(Boolean).join(" ");
|
|
416
|
+
const result = await session.exec(command);
|
|
417
|
+
if (result.code !== 0 && !result.stdout) {
|
|
418
|
+
return errText(`Failed to fetch logs: ${result.stderr}`);
|
|
419
|
+
}
|
|
420
|
+
let output = result.stdout;
|
|
421
|
+
if (Buffer.byteLength(output) > MAX_BYTES) {
|
|
422
|
+
output = output.slice(0, MAX_BYTES) + "\n\n[output truncated at 8KB]";
|
|
423
|
+
}
|
|
424
|
+
return okText(output || "(no log output)");
|
|
425
|
+
} finally {
|
|
426
|
+
release();
|
|
427
|
+
drainPool();
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/mcp/tools/cli/stacks.ts
|
|
432
|
+
async function handleStacksList(_input, _server) {
|
|
433
|
+
const config = getConfig();
|
|
434
|
+
if (!config) {
|
|
435
|
+
return { content: [{ type: "text", text: JSON.stringify({ stacks: [] }) }] };
|
|
436
|
+
}
|
|
437
|
+
const stacks = Object.entries(config.stacks).map(([name, cfg]) => ({
|
|
438
|
+
name,
|
|
439
|
+
provider: cfg.provider,
|
|
440
|
+
region: cfg.region,
|
|
441
|
+
stateUrl: cfg.stateUrl,
|
|
442
|
+
isDefault: name === config.defaults.stack
|
|
443
|
+
}));
|
|
444
|
+
return { content: [{ type: "text", text: JSON.stringify({ stacks }, null, 2) }] };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/mcp/tools/cli/config.ts
|
|
448
|
+
var OPENCLAW_CONFIG = "/home/clawops/openclaw.json";
|
|
449
|
+
var OPENCLAW_TMP = "/tmp/clawops-config.json.tmp";
|
|
450
|
+
async function handleConfigGet(input, _server) {
|
|
451
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
452
|
+
const conn = await resolveConn(ctx);
|
|
453
|
+
const { session, release } = await acquireSession(conn);
|
|
454
|
+
try {
|
|
455
|
+
const result = await session.exec(`cat ${OPENCLAW_CONFIG}`);
|
|
456
|
+
let cfg;
|
|
457
|
+
try {
|
|
458
|
+
cfg = JSON.parse(result.stdout);
|
|
459
|
+
} catch {
|
|
460
|
+
return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${result.stderr || result.stdout}`);
|
|
461
|
+
}
|
|
462
|
+
const value = input.key ? getPath(cfg, input.key) : cfg;
|
|
463
|
+
return okText(JSON.stringify(value, null, 2));
|
|
464
|
+
} finally {
|
|
465
|
+
release();
|
|
466
|
+
drainPool();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function handleConfigSet(input, server) {
|
|
470
|
+
const elicit = await server.server.elicitInput({
|
|
471
|
+
message: `Set ${input.key} = ${input.value} on stack "${input.stackName ?? "default"}"?`,
|
|
472
|
+
requestedSchema: {
|
|
473
|
+
type: "object",
|
|
474
|
+
properties: { confirmed: { type: "boolean", title: "Confirm config change" } },
|
|
475
|
+
required: ["confirmed"]
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
|
|
479
|
+
return okText("Config change cancelled.");
|
|
480
|
+
}
|
|
481
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
482
|
+
const conn = await resolveConn(ctx);
|
|
483
|
+
const { session, release } = await acquireSession(conn);
|
|
484
|
+
try {
|
|
485
|
+
const readResult = await session.exec(`cat ${OPENCLAW_CONFIG}`);
|
|
486
|
+
let cfg;
|
|
487
|
+
try {
|
|
488
|
+
cfg = JSON.parse(readResult.stdout);
|
|
489
|
+
} catch {
|
|
490
|
+
return errText(`Cannot parse ${OPENCLAW_CONFIG}: ${readResult.stderr}`);
|
|
491
|
+
}
|
|
492
|
+
let parsedValue = input.value;
|
|
493
|
+
try {
|
|
494
|
+
parsedValue = JSON.parse(input.value);
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
setPath(cfg, input.key, parsedValue);
|
|
498
|
+
const json = JSON.stringify(cfg, null, 2);
|
|
499
|
+
const b64 = Buffer.from(json, "utf-8").toString("base64");
|
|
500
|
+
const writeCmd = `echo '${b64}' | base64 -d > ${OPENCLAW_TMP} && mv ${OPENCLAW_TMP} ${OPENCLAW_CONFIG} && chown clawops:clawops ${OPENCLAW_CONFIG}`;
|
|
501
|
+
const writeResult = await session.exec(writeCmd);
|
|
502
|
+
if (writeResult.code !== 0) {
|
|
503
|
+
return errText(`Failed to write config: ${writeResult.stderr}`);
|
|
504
|
+
}
|
|
505
|
+
let note = "";
|
|
506
|
+
if (input.restart) {
|
|
507
|
+
const imgResult = await session.exec(
|
|
508
|
+
`docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo 'ghcr.io/openclaw/openclaw:stable'`
|
|
509
|
+
);
|
|
510
|
+
const image = imgResult.stdout.trim();
|
|
511
|
+
const restartCmd = [
|
|
512
|
+
"docker stop openclaw 2>/dev/null || true",
|
|
513
|
+
"docker rm openclaw 2>/dev/null || true",
|
|
514
|
+
`docker run -d --name openclaw --restart unless-stopped -p 18789:18789 -v ${OPENCLAW_CONFIG}:/app/config.json:ro ${image}`
|
|
515
|
+
].join(" && ");
|
|
516
|
+
await session.exec(restartCmd);
|
|
517
|
+
note = " (gateway restarted)";
|
|
518
|
+
}
|
|
519
|
+
return okText(`Config set: ${input.key}${note}`);
|
|
520
|
+
} finally {
|
|
521
|
+
release();
|
|
522
|
+
drainPool();
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function getPath(obj, dotKey) {
|
|
526
|
+
return dotKey.split(".").reduce((cur, k) => {
|
|
527
|
+
if (cur !== null && typeof cur === "object") return cur[k];
|
|
528
|
+
return void 0;
|
|
529
|
+
}, obj);
|
|
530
|
+
}
|
|
531
|
+
function setPath(obj, dotKey, value) {
|
|
532
|
+
const keys = dotKey.split(".");
|
|
533
|
+
let cur = obj;
|
|
534
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
535
|
+
const k = keys[i];
|
|
536
|
+
if (typeof cur[k] !== "object" || cur[k] === null) cur[k] = {};
|
|
537
|
+
cur = cur[k];
|
|
538
|
+
}
|
|
539
|
+
cur[keys[keys.length - 1]] = value;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/mcp/tools/cli/agents.ts
|
|
543
|
+
async function handleAgentsList(input, _server) {
|
|
544
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
545
|
+
const conn = await resolveConn(ctx);
|
|
546
|
+
const { session, release } = await acquireSession(conn);
|
|
547
|
+
try {
|
|
548
|
+
const result = await session.exec('docker exec openclaw openclaw-ctl agents list --json 2>&1 || echo "[]"');
|
|
549
|
+
return okText(result.stdout.trim());
|
|
550
|
+
} finally {
|
|
551
|
+
release();
|
|
552
|
+
drainPool();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function handleAgentsRestart(input, server) {
|
|
556
|
+
const elicit = await server.server.elicitInput({
|
|
557
|
+
message: `Restart agent "${input.agentId}" on stack "${input.stackName ?? "default"}"?`,
|
|
558
|
+
requestedSchema: {
|
|
559
|
+
type: "object",
|
|
560
|
+
properties: { confirmed: { type: "boolean", title: "Confirm restart" } },
|
|
561
|
+
required: ["confirmed"]
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
|
|
565
|
+
return okText("Agent restart cancelled.");
|
|
566
|
+
}
|
|
567
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
568
|
+
const conn = await resolveConn(ctx);
|
|
569
|
+
const { session, release } = await acquireSession(conn);
|
|
570
|
+
try {
|
|
571
|
+
const result = await session.exec(
|
|
572
|
+
`docker exec openclaw openclaw-ctl agents restart ${input.agentId} 2>&1`
|
|
573
|
+
);
|
|
574
|
+
if (result.code !== 0) {
|
|
575
|
+
return errText(`Failed to restart agent: ${result.stderr || result.stdout}`);
|
|
576
|
+
}
|
|
577
|
+
return okText(`Agent "${input.agentId}" restarted.`);
|
|
578
|
+
} finally {
|
|
579
|
+
release();
|
|
580
|
+
drainPool();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// src/mcp/tools/cli/gateway.ts
|
|
585
|
+
async function handleGatewayRestart(input, server) {
|
|
586
|
+
const elicit = await server.server.elicitInput({
|
|
587
|
+
message: `Restart the OpenClaw gateway on stack "${input.stackName ?? "default"}"? This will briefly interrupt connections.`,
|
|
588
|
+
requestedSchema: {
|
|
589
|
+
type: "object",
|
|
590
|
+
properties: { confirmed: { type: "boolean", title: "Confirm restart" } },
|
|
591
|
+
required: ["confirmed"]
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
|
|
595
|
+
return okText("Gateway restart cancelled.");
|
|
596
|
+
}
|
|
597
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
598
|
+
const conn = await resolveConn(ctx);
|
|
599
|
+
const { session, release } = await acquireSession(conn);
|
|
600
|
+
try {
|
|
601
|
+
const imgResult = await session.exec(
|
|
602
|
+
`docker inspect openclaw --format '{{.Config.Image}}' 2>/dev/null || echo 'ghcr.io/openclaw/openclaw:stable'`
|
|
603
|
+
);
|
|
604
|
+
const image = imgResult.stdout.trim();
|
|
605
|
+
const cmd = [
|
|
606
|
+
"docker stop openclaw 2>/dev/null || true",
|
|
607
|
+
"docker rm openclaw 2>/dev/null || true",
|
|
608
|
+
`docker run -d --name openclaw --restart unless-stopped -p 18789:18789 ${image}`
|
|
609
|
+
].join(" && ");
|
|
610
|
+
const result = await session.exec(cmd);
|
|
611
|
+
if (result.code !== 0) {
|
|
612
|
+
return errText(`Gateway restart failed: ${result.stderr}`);
|
|
613
|
+
}
|
|
614
|
+
return okText("Gateway restarted successfully.");
|
|
615
|
+
} finally {
|
|
616
|
+
release();
|
|
617
|
+
drainPool();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/mcp/tools/cli/up.ts
|
|
622
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
623
|
+
|
|
624
|
+
// src/mcp/progress.ts
|
|
625
|
+
var taskMap = /* @__PURE__ */ new Map();
|
|
626
|
+
function startTask(id, description) {
|
|
627
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
628
|
+
const record = { id, description, status: "running", startedAt: now, updatedAt: now };
|
|
629
|
+
taskMap.set(id, record);
|
|
630
|
+
return record;
|
|
631
|
+
}
|
|
632
|
+
function updateTask(id, status, result, error) {
|
|
633
|
+
const record = taskMap.get(id);
|
|
634
|
+
if (!record) return;
|
|
635
|
+
record.status = status;
|
|
636
|
+
record.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
637
|
+
if (result !== void 0) record.result = result;
|
|
638
|
+
if (error !== void 0) record.error = error;
|
|
639
|
+
}
|
|
640
|
+
function getTask(id) {
|
|
641
|
+
return taskMap.get(id);
|
|
642
|
+
}
|
|
643
|
+
function makeProgressEmitter(server, progressToken) {
|
|
644
|
+
if (!progressToken) return () => {
|
|
645
|
+
};
|
|
646
|
+
return (message) => {
|
|
647
|
+
server.server.notification({
|
|
648
|
+
method: "notifications/progress",
|
|
649
|
+
params: { progressToken, progress: 0, message }
|
|
650
|
+
}).catch(() => {
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/mcp/tools/_trim.ts
|
|
656
|
+
import { writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
657
|
+
import path2 from "path";
|
|
658
|
+
var MAX_BYTES2 = 8 * 1024;
|
|
659
|
+
function trimForMcp(output, stackName) {
|
|
660
|
+
try {
|
|
661
|
+
const dir = path2.join(getConfigDir(), "state");
|
|
662
|
+
mkdirSync2(dir, { recursive: true });
|
|
663
|
+
writeFileSync(path2.join(dir, `${stackName}.last-run.json`), JSON.stringify({ output }), "utf-8");
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
if (Buffer.byteLength(output) <= MAX_BYTES2) {
|
|
667
|
+
return { content: output, truncated: false };
|
|
668
|
+
}
|
|
669
|
+
const trimmed = output.slice(0, MAX_BYTES2);
|
|
670
|
+
return {
|
|
671
|
+
content: trimmed + `
|
|
672
|
+
|
|
673
|
+
[Output truncated at 8KB. Full output available at clawops://stacks/${stackName}/last-run]`,
|
|
674
|
+
truncated: true
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// src/mcp/tools/cli/up.ts
|
|
679
|
+
var VALID_INSTANCE_TYPES = ["micro", "small", "medium", "large", "gpu"];
|
|
680
|
+
async function handleUp(input, server) {
|
|
681
|
+
if (!input.dryRun) {
|
|
682
|
+
const elicit = await server.server.elicitInput({
|
|
683
|
+
message: `Deploy stack "${input.stackName ?? "default"}" (provider: ${input.provider ?? "from config"}, instance: ${input.instanceType})? This will provision cloud resources.`,
|
|
684
|
+
requestedSchema: {
|
|
685
|
+
type: "object",
|
|
686
|
+
properties: { confirmed: { type: "boolean", title: "Confirm deployment" } },
|
|
687
|
+
required: ["confirmed"]
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
|
|
691
|
+
return okText("Deployment cancelled.");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const ctx = buildContext({ stack: input.stackName, provider: input.provider });
|
|
695
|
+
const openclawVersion = input.openclawVersion ?? "stable";
|
|
696
|
+
if (ctx.adapter.name === "local") {
|
|
697
|
+
const stackConfig = ctx.config.stacks[ctx.stackName];
|
|
698
|
+
if (!stackConfig?.localOpts) {
|
|
699
|
+
throw new UsageError(
|
|
700
|
+
`Stack "${ctx.stackName}" has no localOpts. Run \`clawops init --provider local --host <HOST>\` first.`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
const { localOpts } = stackConfig;
|
|
704
|
+
const { localBootstrap } = await import("./bootstrap-NRIDVS5F.js");
|
|
705
|
+
const ac = new AbortController();
|
|
706
|
+
const state = await localBootstrap({
|
|
707
|
+
host: localOpts.host,
|
|
708
|
+
port: localOpts.sshPort,
|
|
709
|
+
user: localOpts.sshUser,
|
|
710
|
+
privateKeyPath: localOpts.sshKeyPath,
|
|
711
|
+
knownHostsPath: ctx.config.ssh.knownHostsPath,
|
|
712
|
+
openclawVersion,
|
|
713
|
+
stackName: ctx.stackName,
|
|
714
|
+
noWait: false,
|
|
715
|
+
signal: ac.signal
|
|
716
|
+
});
|
|
717
|
+
return okText(JSON.stringify({ stack: ctx.stackName, ...state }, null, 2));
|
|
718
|
+
}
|
|
719
|
+
const instanceAlias = input.instanceType ?? "small";
|
|
720
|
+
if (!VALID_INSTANCE_TYPES.includes(instanceAlias)) {
|
|
721
|
+
throw new UsageError(`Invalid instanceType: ${instanceAlias}`);
|
|
722
|
+
}
|
|
723
|
+
const validation = await ctx.adapter.validateConfig();
|
|
724
|
+
if (!validation.ok) {
|
|
725
|
+
return errText(`Provider config invalid: ${validation.errors.join("; ")}`);
|
|
726
|
+
}
|
|
727
|
+
const taskId = randomUUID2();
|
|
728
|
+
const progressToken = void 0;
|
|
729
|
+
const emit = makeProgressEmitter(server, progressToken);
|
|
730
|
+
startTask(taskId, `clawops_up stack=${ctx.stackName}`);
|
|
731
|
+
const stack = await ctx.getStack();
|
|
732
|
+
const region = input.region ?? ctx.adapter.defaultRegion();
|
|
733
|
+
const instanceType = ctx.adapter.normalizeInstanceType(instanceAlias);
|
|
734
|
+
await stack.setConfig("region", { value: region });
|
|
735
|
+
await stack.setConfig("instanceType", { value: instanceType });
|
|
736
|
+
await stack.setConfig("openclawVersion", { value: openclawVersion });
|
|
737
|
+
if (input.dryRun) {
|
|
738
|
+
const lines = [];
|
|
739
|
+
await stack.preview({ onOutput: (o) => {
|
|
740
|
+
emit(o.trim());
|
|
741
|
+
lines.push(o);
|
|
742
|
+
} });
|
|
743
|
+
const output = lines.join("");
|
|
744
|
+
updateTask(taskId, "completed", output);
|
|
745
|
+
const { content } = trimForMcp(output, ctx.stackName);
|
|
746
|
+
return okText(content);
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
const lines = [];
|
|
750
|
+
const result = await stack.up({
|
|
751
|
+
onOutput: (o) => {
|
|
752
|
+
emit(o.trim());
|
|
753
|
+
lines.push(o);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
const summary = `Stack "${ctx.stackName}" deployed.
|
|
757
|
+
` + (result.outputs["publicIp"] ? `Public IP: ${result.outputs["publicIp"].value}
|
|
758
|
+
` : "") + (result.outputs["gatewayUrl"] ? `Gateway URL: ${result.outputs["gatewayUrl"].value}
|
|
759
|
+
` : "");
|
|
760
|
+
updateTask(taskId, "completed", summary);
|
|
761
|
+
const fullOutput = lines.join("");
|
|
762
|
+
trimForMcp(fullOutput, ctx.stackName);
|
|
763
|
+
return okText(summary);
|
|
764
|
+
} catch (err) {
|
|
765
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
766
|
+
updateTask(taskId, "failed", void 0, msg);
|
|
767
|
+
throw err;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// src/mcp/tools/cli/destroy.ts
|
|
772
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
773
|
+
async function handleDestroy(input, server) {
|
|
774
|
+
if (!input.yes) {
|
|
775
|
+
const elicit = await server.server.elicitInput({
|
|
776
|
+
message: `DESTROY stack "${input.stackName}"? This permanently deletes all provisioned resources and cannot be undone.`,
|
|
777
|
+
requestedSchema: {
|
|
778
|
+
type: "object",
|
|
779
|
+
properties: { confirmed: { type: "boolean", title: "Confirm destruction (irreversible)" } },
|
|
780
|
+
required: ["confirmed"]
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
|
|
784
|
+
return okText("Destruction cancelled.");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const ctx = buildContext({ stack: input.stackName });
|
|
788
|
+
if (ctx.adapter.name === "local") {
|
|
789
|
+
return errText("Local provider stacks are destroyed with `clawops down --destroy` via SSH. Use clawops_up with the local provider instead.");
|
|
790
|
+
}
|
|
791
|
+
const taskId = randomUUID3();
|
|
792
|
+
const emit = makeProgressEmitter(server, void 0);
|
|
793
|
+
startTask(taskId, `clawops_destroy stack=${input.stackName}`);
|
|
794
|
+
try {
|
|
795
|
+
const stack = await ctx.getStack();
|
|
796
|
+
const lines = [];
|
|
797
|
+
await stack.destroy({ onOutput: (o) => {
|
|
798
|
+
emit(o.trim());
|
|
799
|
+
lines.push(o);
|
|
800
|
+
} });
|
|
801
|
+
const fullOutput = lines.join("");
|
|
802
|
+
updateTask(taskId, "completed", `Stack "${input.stackName}" destroyed.`);
|
|
803
|
+
const { content } = trimForMcp(fullOutput, input.stackName);
|
|
804
|
+
return okText(`Stack "${input.stackName}" destroyed.
|
|
805
|
+
|
|
806
|
+
${content}`);
|
|
807
|
+
} catch (err) {
|
|
808
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
809
|
+
updateTask(taskId, "failed", void 0, msg);
|
|
810
|
+
throw err;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/mcp/tools/cli/plan.ts
|
|
815
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
816
|
+
async function handlePlan(input, _server) {
|
|
817
|
+
const ctx = buildContext({
|
|
818
|
+
stack: input.stackName,
|
|
819
|
+
provider: input.provider
|
|
820
|
+
});
|
|
821
|
+
if (ctx.adapter.name === "local") {
|
|
822
|
+
return errText("plan/apply is not supported for the local provider. Use clawops_up directly.");
|
|
823
|
+
}
|
|
824
|
+
const provider = input.provider ?? ctx.adapter.name;
|
|
825
|
+
const stackName = ctx.stackName;
|
|
826
|
+
let plan;
|
|
827
|
+
try {
|
|
828
|
+
plan = await generatePlan({
|
|
829
|
+
stackName,
|
|
830
|
+
provider,
|
|
831
|
+
region: input.region,
|
|
832
|
+
instanceType: input.instanceType
|
|
833
|
+
});
|
|
834
|
+
} catch (err) {
|
|
835
|
+
return errText(`Plan generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
836
|
+
}
|
|
837
|
+
const planJson = JSON.stringify(plan, null, 2);
|
|
838
|
+
if (input.outPath) {
|
|
839
|
+
try {
|
|
840
|
+
writeFileSync2(input.outPath, planJson + "\n", "utf-8");
|
|
841
|
+
return { content: [{ type: "text", text: `Plan written to ${input.outPath}` }] };
|
|
842
|
+
} catch (err) {
|
|
843
|
+
return errText(`Failed to write plan to ${input.outPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const { content, truncated } = trimForMcp(planJson, stackName);
|
|
847
|
+
return {
|
|
848
|
+
content: [{ type: "text", text: truncated ? content : planJson }]
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/mcp/tools/cli/apply.ts
|
|
853
|
+
import { readFileSync } from "fs";
|
|
854
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
855
|
+
async function handleApply(input, server) {
|
|
856
|
+
let plan;
|
|
857
|
+
try {
|
|
858
|
+
const raw = readFileSync(input.planPath, "utf-8");
|
|
859
|
+
plan = JSON.parse(raw);
|
|
860
|
+
} catch (err) {
|
|
861
|
+
return errText(`Cannot read plan file "${input.planPath}": ${err instanceof Error ? err.message : String(err)}`);
|
|
862
|
+
}
|
|
863
|
+
const validation = validatePlan(plan);
|
|
864
|
+
if (!validation.ok) {
|
|
865
|
+
return errText(`Invalid plan:
|
|
866
|
+
${validation.errors.join("\n")}`);
|
|
867
|
+
}
|
|
868
|
+
if (plan.spec.provider === "local") {
|
|
869
|
+
return errText("plan/apply is not supported for the local provider. Use clawops_up directly.");
|
|
870
|
+
}
|
|
871
|
+
if (!input.yes) {
|
|
872
|
+
const diffLine = plan.diff ? `${plan.diff.create.length} to create, ${plan.diff.update.length} to update, ${plan.diff.delete.length} to delete` : "diff unavailable";
|
|
873
|
+
const elicit = await server.server.elicitInput({
|
|
874
|
+
message: `Apply plan for stack "${plan.spec.stackName}" (${plan.spec.provider})?
|
|
875
|
+
Changes: ${diffLine}.
|
|
876
|
+
This will provision cloud resources and may incur costs.`,
|
|
877
|
+
requestedSchema: {
|
|
878
|
+
type: "object",
|
|
879
|
+
properties: { confirmed: { type: "boolean", title: "Confirm plan apply" } },
|
|
880
|
+
required: ["confirmed"]
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
|
|
884
|
+
return okText("Apply cancelled.");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
const taskId = randomUUID4();
|
|
888
|
+
const emit = makeProgressEmitter(server, void 0);
|
|
889
|
+
startTask(taskId, `clawops_apply stack=${plan.spec.stackName}`);
|
|
890
|
+
try {
|
|
891
|
+
const result = await applyPlan(plan, {
|
|
892
|
+
onOutput: (line) => emit(line.trim())
|
|
893
|
+
});
|
|
894
|
+
const summaryLines = [`Stack "${plan.spec.stackName}" applied.`];
|
|
895
|
+
const changed = Object.entries(result.changeSummary).filter(([, n]) => n > 0);
|
|
896
|
+
if (changed.length > 0) {
|
|
897
|
+
summaryLines.push(changed.map(([op, n]) => ` ${op}: ${n}`).join("\n"));
|
|
898
|
+
}
|
|
899
|
+
if (result.outputs["gatewayUrl"]) summaryLines.push(`Gateway URL: ${result.outputs["gatewayUrl"]}`);
|
|
900
|
+
if (result.outputs["publicIp"]) summaryLines.push(`Public IP: ${result.outputs["publicIp"]}`);
|
|
901
|
+
summaryLines.push(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s`);
|
|
902
|
+
const summary = summaryLines.join("\n");
|
|
903
|
+
updateTask(taskId, "completed", summary);
|
|
904
|
+
const { content } = trimForMcp(summary, plan.spec.stackName);
|
|
905
|
+
return okText(content);
|
|
906
|
+
} catch (err) {
|
|
907
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
908
|
+
updateTask(taskId, "failed", void 0, msg);
|
|
909
|
+
throw err;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/mcp/tools/cli/task.ts
|
|
914
|
+
async function handleTaskStatus(input, _server) {
|
|
915
|
+
const record = getTask(input.taskId);
|
|
916
|
+
if (!record) {
|
|
917
|
+
return {
|
|
918
|
+
content: [{ type: "text", text: JSON.stringify({ taskId: input.taskId, status: "not_found" }) }],
|
|
919
|
+
isError: true
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
return { content: [{ type: "text", text: JSON.stringify(record, null, 2) }] };
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/mcp/tools/workflow/deploy_app.ts
|
|
926
|
+
async function handleWorkflowDeployApp(input, server) {
|
|
927
|
+
const parts = [`## Deployment Workflow \u2014 ${input.provider} / ${input.stackName}
|
|
928
|
+
`];
|
|
929
|
+
if (input.provider === "local") {
|
|
930
|
+
return handleUp(
|
|
931
|
+
{ stackName: input.stackName, provider: input.provider, region: input.region, instanceType: input.instanceType, dryRun: false },
|
|
932
|
+
server
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
const provider = input.provider;
|
|
936
|
+
parts.push("### Step 1: Plan");
|
|
937
|
+
let plan;
|
|
938
|
+
try {
|
|
939
|
+
plan = await generatePlan({
|
|
940
|
+
stackName: input.stackName,
|
|
941
|
+
provider,
|
|
942
|
+
region: input.region,
|
|
943
|
+
instanceType: input.instanceType
|
|
944
|
+
});
|
|
945
|
+
const diffSummary = plan.diff ? `${plan.diff.create.length} to create, ${plan.diff.update.length} to update, ${plan.diff.delete.length} to delete` : "diff unavailable";
|
|
946
|
+
parts.push(`Plan generated. Changes: ${diffSummary}.`);
|
|
947
|
+
} catch (err) {
|
|
948
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
949
|
+
parts.push(`Plan generation failed: ${msg}
|
|
950
|
+
Falling back to direct deployment.`);
|
|
951
|
+
return handleUp(
|
|
952
|
+
{ stackName: input.stackName, provider: input.provider, region: input.region, instanceType: input.instanceType, dryRun: false },
|
|
953
|
+
server
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
const diffLine = plan.diff ? `${plan.diff.create.length} to create, ${plan.diff.update.length} to update, ${plan.diff.delete.length} to delete` : "? changes";
|
|
957
|
+
const elicit = await server.server.elicitInput({
|
|
958
|
+
message: `Deploy OpenClaw on ${provider} (stack: "${input.stackName}", instance: ${input.instanceType}${input.region ? `, region: ${input.region}` : ""})?
|
|
959
|
+
Changes: ${diffLine}. This will provision cloud resources and may incur costs.`,
|
|
960
|
+
requestedSchema: {
|
|
961
|
+
type: "object",
|
|
962
|
+
properties: { confirmed: { type: "boolean", title: "Confirm end-to-end deployment" } },
|
|
963
|
+
required: ["confirmed"]
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
if (elicit.action !== "accept" || !elicit.content?.["confirmed"]) {
|
|
967
|
+
return okText("Deployment workflow cancelled.");
|
|
968
|
+
}
|
|
969
|
+
parts.push("### Step 2: Apply");
|
|
970
|
+
const emit = makeProgressEmitter(server, void 0);
|
|
971
|
+
try {
|
|
972
|
+
const result = await applyPlan(plan, { onOutput: (line) => emit(line.trim()) });
|
|
973
|
+
const changed = Object.entries(result.changeSummary).filter(([, n]) => n > 0);
|
|
974
|
+
const applySummary = changed.length > 0 ? changed.map(([op, n]) => ` ${op}: ${n}`).join("\n") : "No changes.";
|
|
975
|
+
parts.push(applySummary);
|
|
976
|
+
if (result.outputs["gatewayUrl"]) parts.push(`Gateway URL: ${result.outputs["gatewayUrl"]}`);
|
|
977
|
+
if (result.outputs["publicIp"]) parts.push(`Public IP: ${result.outputs["publicIp"]}`);
|
|
978
|
+
} catch (err) {
|
|
979
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
980
|
+
parts.push(`Apply failed: ${msg}`);
|
|
981
|
+
return { content: [{ type: "text", text: parts.join("\n\n") }], isError: true };
|
|
982
|
+
}
|
|
983
|
+
parts.push("### Step 3: Status");
|
|
984
|
+
try {
|
|
985
|
+
const statusResult = await handleStatus({ stackName: input.stackName }, server);
|
|
986
|
+
const statusText = statusResult.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
987
|
+
parts.push(statusText);
|
|
988
|
+
} catch {
|
|
989
|
+
parts.push("Status check skipped (stack may still be starting up).");
|
|
990
|
+
}
|
|
991
|
+
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/mcp/tools/workflow/recover.ts
|
|
995
|
+
async function handleWorkflowRecover(input, server) {
|
|
996
|
+
const parts = [`## Diagnostic Report \u2014 stack: ${input.stackName ?? "default"}
|
|
997
|
+
`];
|
|
998
|
+
try {
|
|
999
|
+
const statusResult = await handleStatus({ stackName: input.stackName }, server);
|
|
1000
|
+
const statusText = statusResult.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
1001
|
+
parts.push(`### Status
|
|
1002
|
+
\`\`\`json
|
|
1003
|
+
${statusText}
|
|
1004
|
+
\`\`\``);
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
parts.push(`### Status
|
|
1007
|
+
Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1008
|
+
}
|
|
1009
|
+
try {
|
|
1010
|
+
const logsResult = await handleLogsTail({ stackName: input.stackName, tailLines: 50 }, server);
|
|
1011
|
+
const logsText = logsResult.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
1012
|
+
parts.push(`### Recent Logs (last 50 lines)
|
|
1013
|
+
\`\`\`
|
|
1014
|
+
${logsText}
|
|
1015
|
+
\`\`\``);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
parts.push(`### Recent Logs
|
|
1018
|
+
Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1019
|
+
}
|
|
1020
|
+
parts.push(
|
|
1021
|
+
`### Next Steps
|
|
1022
|
+
- If gateway is not running: try \`clawops_gateway_restart\`
|
|
1023
|
+
- If bootstrap failed: run \`clawops_up\` again (idempotent)
|
|
1024
|
+
- If stack is not deployed: run \`clawops_workflow_deploy_app\`
|
|
1025
|
+
- For Pulumi drift: run \`clawops refresh\` from the CLI`
|
|
1026
|
+
);
|
|
1027
|
+
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/mcp/tools/registry.ts
|
|
1031
|
+
function makeEntry(schema, annotations, handler) {
|
|
1032
|
+
return { schema, annotations, handler };
|
|
1033
|
+
}
|
|
1034
|
+
var TOOL_REGISTRY = {
|
|
1035
|
+
clawops_status: makeEntry(clawops_statusSchema, clawops_statusAnnotations, handleStatus),
|
|
1036
|
+
clawops_logs_tail: makeEntry(clawops_logs_tailSchema, clawops_logs_tailAnnotations, handleLogsTail),
|
|
1037
|
+
clawops_stacks_list: makeEntry(clawops_stacks_listSchema, clawops_stacks_listAnnotations, handleStacksList),
|
|
1038
|
+
clawops_config_get: makeEntry(clawops_config_getSchema, clawops_config_getAnnotations, handleConfigGet),
|
|
1039
|
+
clawops_agents_list: makeEntry(clawops_agents_listSchema, clawops_agents_listAnnotations, handleAgentsList),
|
|
1040
|
+
clawops_up: makeEntry(clawops_upSchema, clawops_upAnnotations, handleUp),
|
|
1041
|
+
clawops_destroy: makeEntry(clawops_destroySchema, clawops_destroyAnnotations, handleDestroy),
|
|
1042
|
+
clawops_apply: makeEntry(clawops_applySchema, clawops_applyAnnotations, handleApply),
|
|
1043
|
+
clawops_plan: makeEntry(clawops_planSchema, clawops_planAnnotations, handlePlan),
|
|
1044
|
+
clawops_config_set: makeEntry(clawops_config_setSchema, clawops_config_setAnnotations, handleConfigSet),
|
|
1045
|
+
clawops_agents_restart: makeEntry(clawops_agents_restartSchema, clawops_agents_restartAnnotations, handleAgentsRestart),
|
|
1046
|
+
clawops_gateway_restart: makeEntry(clawops_gateway_restartSchema, clawops_gateway_restartAnnotations, handleGatewayRestart),
|
|
1047
|
+
clawops_workflow_deploy_app: makeEntry(clawops_workflow_deploy_appSchema, clawops_workflow_deploy_appAnnotations, handleWorkflowDeployApp),
|
|
1048
|
+
clawops_workflow_recover: makeEntry(clawops_workflow_recoverSchema, clawops_workflow_recoverAnnotations, handleWorkflowRecover),
|
|
1049
|
+
clawops_task_status: makeEntry(clawops_task_statusSchema, clawops_task_statusAnnotations, handleTaskStatus)
|
|
1050
|
+
};
|
|
1051
|
+
function resolveEnabledTools(opts) {
|
|
1052
|
+
let names;
|
|
1053
|
+
if (opts.readOnly) {
|
|
1054
|
+
names = [...TOOLSETS.read];
|
|
1055
|
+
} else if (opts.toolsets && opts.toolsets.length > 0) {
|
|
1056
|
+
names = opts.toolsets.flatMap((ts) => TOOLSETS[ts] ?? []);
|
|
1057
|
+
} else {
|
|
1058
|
+
names = [
|
|
1059
|
+
...TOOLSETS.cli,
|
|
1060
|
+
...TOOLSETS.workflow,
|
|
1061
|
+
...TOOLSETS.admin
|
|
1062
|
+
];
|
|
1063
|
+
}
|
|
1064
|
+
if (opts.noDestructive) {
|
|
1065
|
+
names = names.filter((n) => {
|
|
1066
|
+
const entry = TOOL_REGISTRY[n];
|
|
1067
|
+
return entry ? !entry.annotations.destructiveHint : true;
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
return [...new Set(names)];
|
|
1071
|
+
}
|
|
1072
|
+
function registerTools(server, opts) {
|
|
1073
|
+
const enabled = resolveEnabledTools(opts);
|
|
1074
|
+
for (const toolName of enabled) {
|
|
1075
|
+
const entry = TOOL_REGISTRY[toolName];
|
|
1076
|
+
if (!entry) continue;
|
|
1077
|
+
const { schema, annotations, handler } = entry;
|
|
1078
|
+
const { title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint } = annotations;
|
|
1079
|
+
const audited = withAudit(toolName, (input) => handler(input, server));
|
|
1080
|
+
server.registerTool(
|
|
1081
|
+
toolName,
|
|
1082
|
+
{
|
|
1083
|
+
title,
|
|
1084
|
+
inputSchema: schema,
|
|
1085
|
+
annotations: { title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint }
|
|
1086
|
+
},
|
|
1087
|
+
audited
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// src/mcp/resources.ts
|
|
1093
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1094
|
+
import path3 from "path";
|
|
1095
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1096
|
+
function registerResources(server) {
|
|
1097
|
+
server.registerResource(
|
|
1098
|
+
"current-context",
|
|
1099
|
+
"clawops://current-context",
|
|
1100
|
+
{ description: "Active stack name, provider, and region" },
|
|
1101
|
+
async () => {
|
|
1102
|
+
const config = getConfig();
|
|
1103
|
+
const stackName = config?.defaults.stack ?? "unknown";
|
|
1104
|
+
const stack = config?.stacks[stackName];
|
|
1105
|
+
const data = {
|
|
1106
|
+
stackName,
|
|
1107
|
+
provider: stack?.provider ?? config?.defaults.provider ?? "unknown",
|
|
1108
|
+
region: stack?.region ?? "unknown"
|
|
1109
|
+
};
|
|
1110
|
+
return {
|
|
1111
|
+
contents: [{
|
|
1112
|
+
uri: "clawops://current-context",
|
|
1113
|
+
mimeType: "application/json",
|
|
1114
|
+
text: JSON.stringify(data, null, 2)
|
|
1115
|
+
}]
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
);
|
|
1119
|
+
server.registerResource(
|
|
1120
|
+
"stacks",
|
|
1121
|
+
"clawops://stacks",
|
|
1122
|
+
{ description: "All configured stacks" },
|
|
1123
|
+
async () => {
|
|
1124
|
+
const config = getConfig();
|
|
1125
|
+
const stacks = config ? Object.entries(config.stacks).map(([name, cfg]) => ({
|
|
1126
|
+
name,
|
|
1127
|
+
provider: cfg.provider,
|
|
1128
|
+
region: cfg.region,
|
|
1129
|
+
isDefault: name === config.defaults.stack
|
|
1130
|
+
})) : [];
|
|
1131
|
+
return {
|
|
1132
|
+
contents: [{
|
|
1133
|
+
uri: "clawops://stacks",
|
|
1134
|
+
mimeType: "application/json",
|
|
1135
|
+
text: JSON.stringify({ stacks }, null, 2)
|
|
1136
|
+
}]
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
);
|
|
1140
|
+
server.registerResource(
|
|
1141
|
+
"stack-last-run",
|
|
1142
|
+
new ResourceTemplate("clawops://stacks/{name}/last-run", { list: void 0 }),
|
|
1143
|
+
{ description: "Full Pulumi output from the last up/destroy run for a stack" },
|
|
1144
|
+
async (uri, { name }) => {
|
|
1145
|
+
const stackName = Array.isArray(name) ? name[0] : name;
|
|
1146
|
+
const filePath = path3.join(getConfigDir(), "state", `${stackName}.last-run.json`);
|
|
1147
|
+
let text2;
|
|
1148
|
+
try {
|
|
1149
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
1150
|
+
const parsed = JSON.parse(raw);
|
|
1151
|
+
text2 = parsed.output ?? "(no output recorded)";
|
|
1152
|
+
} catch {
|
|
1153
|
+
text2 = `No last-run output found for stack "${stackName}".`;
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
contents: [{
|
|
1157
|
+
uri: uri.href,
|
|
1158
|
+
mimeType: "text/plain",
|
|
1159
|
+
text: text2
|
|
1160
|
+
}]
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/mcp/prompts.ts
|
|
1167
|
+
import { z as z2 } from "zod";
|
|
1168
|
+
function registerPrompts(server) {
|
|
1169
|
+
server.registerPrompt(
|
|
1170
|
+
"deploy-to-aws",
|
|
1171
|
+
{
|
|
1172
|
+
description: "Step-by-step guide to deploy OpenClaw on AWS",
|
|
1173
|
+
argsSchema: {
|
|
1174
|
+
region: z2.string().optional().describe("AWS region (e.g. us-east-1)"),
|
|
1175
|
+
instanceType: z2.string().optional().describe("EC2 instance type (e.g. t3.medium)")
|
|
1176
|
+
}
|
|
1177
|
+
},
|
|
1178
|
+
({ region, instanceType }) => {
|
|
1179
|
+
const r = region ?? "us-east-1";
|
|
1180
|
+
const it = instanceType ?? "t3.medium";
|
|
1181
|
+
return {
|
|
1182
|
+
messages: [
|
|
1183
|
+
{
|
|
1184
|
+
role: "user",
|
|
1185
|
+
content: {
|
|
1186
|
+
type: "text",
|
|
1187
|
+
text: `# Deploy OpenClaw on AWS
|
|
1188
|
+
|
|
1189
|
+
Follow these steps to provision an OpenClaw instance on AWS in region **${r}** using instance type **${it}**.
|
|
1190
|
+
|
|
1191
|
+
## Prerequisites
|
|
1192
|
+
|
|
1193
|
+
1. AWS CLI configured: \`aws configure\` or set \`AWS_PROFILE\`.
|
|
1194
|
+
2. clawops installed: \`npm install -g clawops\`.
|
|
1195
|
+
3. SSH key pair available locally.
|
|
1196
|
+
|
|
1197
|
+
## Steps
|
|
1198
|
+
|
|
1199
|
+
### 1. Initialise a stack
|
|
1200
|
+
\`\`\`
|
|
1201
|
+
clawops init --provider aws --region ${r} --stack my-stack
|
|
1202
|
+
\`\`\`
|
|
1203
|
+
|
|
1204
|
+
### 2. Preview the deployment (dry run)
|
|
1205
|
+
\`\`\`
|
|
1206
|
+
clawops up --stack my-stack --dry-run
|
|
1207
|
+
\`\`\`
|
|
1208
|
+
Review the resource plan \u2014 it will show EC2 instance, security groups, and IAM role.
|
|
1209
|
+
|
|
1210
|
+
### 3. Deploy
|
|
1211
|
+
Use the MCP tool \`clawops_up\` or the CLI:
|
|
1212
|
+
\`\`\`
|
|
1213
|
+
clawops up --stack my-stack
|
|
1214
|
+
\`\`\`
|
|
1215
|
+
Or via MCP:
|
|
1216
|
+
\`\`\`
|
|
1217
|
+
clawops_up({ stackName: "my-stack", provider: "aws", region: "${r}", instanceType: "${it}" })
|
|
1218
|
+
\`\`\`
|
|
1219
|
+
|
|
1220
|
+
### 4. Verify
|
|
1221
|
+
\`\`\`
|
|
1222
|
+
clawops status --stack my-stack
|
|
1223
|
+
\`\`\`
|
|
1224
|
+
Confirm \`status: running\` and note the public IP.
|
|
1225
|
+
|
|
1226
|
+
### 5. Connect
|
|
1227
|
+
The stack outputs a \`gatewayUrl\` \u2014 open it in your browser to reach the OpenClaw gateway.
|
|
1228
|
+
|
|
1229
|
+
## Troubleshooting
|
|
1230
|
+
|
|
1231
|
+
- Use \`clawops_workflow_recover\` to collect diagnostic information.
|
|
1232
|
+
- Logs: \`clawops logs --stack my-stack --tail 100\`
|
|
1233
|
+
- If deployment fails mid-way, re-run \`clawops up\` \u2014 Pulumi is idempotent.
|
|
1234
|
+
`
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
]
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
);
|
|
1241
|
+
server.registerPrompt(
|
|
1242
|
+
"recover-failed-stack",
|
|
1243
|
+
{
|
|
1244
|
+
description: "Diagnostic playbook for a failed or unhealthy clawops stack",
|
|
1245
|
+
argsSchema: {
|
|
1246
|
+
stackName: z2.string().optional().describe("Stack name to diagnose")
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
({ stackName }) => {
|
|
1250
|
+
const s = stackName ?? "default";
|
|
1251
|
+
return {
|
|
1252
|
+
messages: [
|
|
1253
|
+
{
|
|
1254
|
+
role: "user",
|
|
1255
|
+
content: {
|
|
1256
|
+
type: "text",
|
|
1257
|
+
text: `# Recover Failed Stack: ${s}
|
|
1258
|
+
|
|
1259
|
+
## Step 1 \u2014 Gather diagnostics
|
|
1260
|
+
Run the recovery workflow:
|
|
1261
|
+
\`\`\`
|
|
1262
|
+
clawops_workflow_recover({ stackName: "${s}" })
|
|
1263
|
+
\`\`\`
|
|
1264
|
+
This returns current status + last 50 log lines.
|
|
1265
|
+
|
|
1266
|
+
## Step 2 \u2014 Common failure modes
|
|
1267
|
+
|
|
1268
|
+
### Gateway not responding
|
|
1269
|
+
\`\`\`
|
|
1270
|
+
clawops_gateway_restart({ stackName: "${s}" })
|
|
1271
|
+
\`\`\`
|
|
1272
|
+
|
|
1273
|
+
### Agents not running
|
|
1274
|
+
\`\`\`
|
|
1275
|
+
clawops_agents_list({ stackName: "${s}" })
|
|
1276
|
+
clawops_agents_restart({ stackName: "${s}" })
|
|
1277
|
+
\`\`\`
|
|
1278
|
+
|
|
1279
|
+
### Bootstrap failed or incomplete
|
|
1280
|
+
Re-run (Pulumi is idempotent):
|
|
1281
|
+
\`\`\`
|
|
1282
|
+
clawops_up({ stackName: "${s}", provider: "aws" })
|
|
1283
|
+
\`\`\`
|
|
1284
|
+
|
|
1285
|
+
### Pulumi state drift
|
|
1286
|
+
From the CLI:
|
|
1287
|
+
\`\`\`
|
|
1288
|
+
clawops refresh --stack ${s}
|
|
1289
|
+
\`\`\`
|
|
1290
|
+
|
|
1291
|
+
### Stack never deployed
|
|
1292
|
+
\`\`\`
|
|
1293
|
+
clawops_workflow_deploy_app({ stackName: "${s}", provider: "aws" })
|
|
1294
|
+
\`\`\`
|
|
1295
|
+
|
|
1296
|
+
## Step 3 \u2014 Review last run output
|
|
1297
|
+
|
|
1298
|
+
Read the full Pulumi output:
|
|
1299
|
+
\`\`\`
|
|
1300
|
+
clawops://stacks/${s}/last-run
|
|
1301
|
+
\`\`\`
|
|
1302
|
+
|
|
1303
|
+
## Step 4 \u2014 Escalation
|
|
1304
|
+
|
|
1305
|
+
If none of the above resolves the issue:
|
|
1306
|
+
1. Check AWS/GCP/Azure console for resource state.
|
|
1307
|
+
2. Look for IAM permission errors in logs.
|
|
1308
|
+
3. Verify SSH key is present and matches the deployed instance.
|
|
1309
|
+
`
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
]
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// src/mcp/server.ts
|
|
1319
|
+
async function serveMcp(opts) {
|
|
1320
|
+
const { version } = await import("./package-YZFYSSQM.js");
|
|
1321
|
+
const server = new McpServer({ name: "clawops", version });
|
|
1322
|
+
registerTools(server, opts);
|
|
1323
|
+
registerResources(server);
|
|
1324
|
+
registerPrompts(server);
|
|
1325
|
+
if (opts.port) {
|
|
1326
|
+
const bind = opts.bind ?? "127.0.0.1";
|
|
1327
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID5() });
|
|
1328
|
+
const httpServer = createServer((req, res) => {
|
|
1329
|
+
void transport.handleRequest(req, res);
|
|
1330
|
+
});
|
|
1331
|
+
await new Promise((resolve, reject) => {
|
|
1332
|
+
httpServer.once("error", reject);
|
|
1333
|
+
httpServer.listen(opts.port, bind, resolve);
|
|
1334
|
+
});
|
|
1335
|
+
process.stderr.write(`[clawops] MCP HTTP server listening on ${bind}:${opts.port}
|
|
1336
|
+
`);
|
|
1337
|
+
await server.connect(transport);
|
|
1338
|
+
await new Promise((resolve) => httpServer.once("close", resolve));
|
|
1339
|
+
} else {
|
|
1340
|
+
const transport = new StdioServerTransport();
|
|
1341
|
+
await server.connect(transport);
|
|
1342
|
+
await new Promise((resolve) => {
|
|
1343
|
+
server.server.onclose = resolve;
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
export {
|
|
1348
|
+
serveMcp
|
|
1349
|
+
};
|