@yawlabs/caddy-mcp 2.2.0 → 2.3.1
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/README.md +276 -246
- package/bin/caddy-mcp.mjs +334 -175
- package/dist/index.js +196 -54
- package/dist/server.js +196 -54
- package/dist/snapshots.d.ts +10 -0
- package/package.json +82 -82
package/dist/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
6
|
// src/api.ts
|
|
7
|
+
import { request as httpRequest } from "http";
|
|
7
8
|
var DEFAULT_URL = "http://localhost:2019";
|
|
8
9
|
var TIMEOUT = 1e4;
|
|
9
10
|
var RETRY_BASE_MS = 100;
|
|
@@ -43,6 +44,15 @@ function invalidateRelated(path) {
|
|
|
43
44
|
function getBaseUrl() {
|
|
44
45
|
return (process.env.CADDY_ADMIN_URL || DEFAULT_URL).replace(/\/+$/, "");
|
|
45
46
|
}
|
|
47
|
+
function getUnixSocketPath() {
|
|
48
|
+
const raw = (process.env.CADDY_ADMIN_URL || "").trim();
|
|
49
|
+
if (!raw) return void 0;
|
|
50
|
+
const urlForm = /^unix:\/\/(\/.*)$/.exec(raw);
|
|
51
|
+
if (urlForm) return urlForm[1];
|
|
52
|
+
const caddyForm = /^unix\/(\/.*)$/.exec(raw);
|
|
53
|
+
if (caddyForm) return caddyForm[1];
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
46
56
|
var warnedRetryClamp = false;
|
|
47
57
|
function getMaxRetries() {
|
|
48
58
|
const raw = process.env.CADDY_MAX_RETRIES;
|
|
@@ -63,11 +73,12 @@ function getAdminOrigin() {
|
|
|
63
73
|
return void 0;
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
|
-
function getHeaders(contentType) {
|
|
76
|
+
function getHeaders(contentType, overUnixSocket = false) {
|
|
67
77
|
const headers = {};
|
|
68
78
|
if (contentType) headers["Content-Type"] = contentType;
|
|
69
79
|
const token = process.env.CADDY_API_TOKEN;
|
|
70
80
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
81
|
+
if (overUnixSocket) return headers;
|
|
71
82
|
const origin = getAdminOrigin();
|
|
72
83
|
if (origin) headers.Origin = origin;
|
|
73
84
|
return headers;
|
|
@@ -100,39 +111,85 @@ function isRetryableMethod(method, path) {
|
|
|
100
111
|
if (method !== "POST") return true;
|
|
101
112
|
return !path.startsWith("/config/") && !path.startsWith("/id/");
|
|
102
113
|
}
|
|
103
|
-
|
|
114
|
+
function getMalformedUnixUrl() {
|
|
115
|
+
const raw = (process.env.CADDY_ADMIN_URL || "").trim();
|
|
116
|
+
if (!raw || !/^unix[:/]/i.test(raw)) return void 0;
|
|
117
|
+
return getUnixSocketPath() === void 0 ? raw : void 0;
|
|
118
|
+
}
|
|
119
|
+
async function caddyRequest(method, path, body, contentType, timeout, rawStringBody = false) {
|
|
120
|
+
const malformed = getMalformedUnixUrl();
|
|
121
|
+
if (malformed) {
|
|
122
|
+
return {
|
|
123
|
+
ok: false,
|
|
124
|
+
status: 0,
|
|
125
|
+
error: `CADDY_ADMIN_URL="${malformed}" looks like a unix socket address but is not a recognized form. Use "unix:///absolute/path.sock" (URL form) or "unix//absolute/path.sock" (Caddy's own spelling). A single slash after "unix:", or a relative path, will not parse.`
|
|
126
|
+
};
|
|
127
|
+
}
|
|
104
128
|
const maxRetries = getMaxRetries();
|
|
105
129
|
let attempt = 0;
|
|
106
|
-
let res = await attemptRequest(method, path, body, contentType, timeout);
|
|
130
|
+
let res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
|
|
107
131
|
while (isRetryableMethod(method, path) && isTransientFailure(res) && attempt < maxRetries) {
|
|
108
132
|
attempt++;
|
|
109
133
|
const backoff = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
|
|
110
134
|
const delay = backoff + Math.random() * RETRY_MAX_JITTER_MS;
|
|
111
135
|
await sleep(delay);
|
|
112
|
-
res = await attemptRequest(method, path, body, contentType, timeout);
|
|
136
|
+
res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
|
|
113
137
|
}
|
|
114
138
|
return res;
|
|
115
139
|
}
|
|
116
|
-
|
|
140
|
+
function sendViaUnixSocket(socketPath, path, method, headers, body, timeoutMs) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const req = httpRequest({ socketPath, path, method, headers, signal: AbortSignal.timeout(timeoutMs) }, (res) => {
|
|
143
|
+
const chunks = [];
|
|
144
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
145
|
+
res.on("error", reject);
|
|
146
|
+
res.on("end", () => {
|
|
147
|
+
const status = res.statusCode ?? 0;
|
|
148
|
+
const etag = res.headers.etag;
|
|
149
|
+
resolve({
|
|
150
|
+
ok: status >= 200 && status < 300,
|
|
151
|
+
status,
|
|
152
|
+
text: Buffer.concat(chunks).toString("utf8"),
|
|
153
|
+
etag: typeof etag === "string" ? etag : void 0
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
req.on("error", reject);
|
|
158
|
+
if (body !== void 0) req.write(body);
|
|
159
|
+
req.end();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function sendViaFetch(url, method, headers, body, timeoutMs) {
|
|
163
|
+
const res = await fetch(url, {
|
|
164
|
+
method,
|
|
165
|
+
headers,
|
|
166
|
+
body,
|
|
167
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
ok: res.ok,
|
|
171
|
+
status: res.status,
|
|
172
|
+
text: await res.text(),
|
|
173
|
+
etag: res.headers.get("ETag") || void 0
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function attemptRequest(method, path, body, contentType, timeout, rawStringBody = false) {
|
|
177
|
+
const socketPath = getUnixSocketPath();
|
|
117
178
|
const url = `${getBaseUrl()}${path}`;
|
|
118
179
|
const effectiveTimeout = timeout ?? getRequestTimeout();
|
|
119
180
|
try {
|
|
120
181
|
const hasBody = body !== void 0;
|
|
121
|
-
const headers = getHeaders(hasBody ? contentType || "application/json" : void 0);
|
|
182
|
+
const headers = getHeaders(hasBody ? contentType || "application/json" : void 0, socketPath !== void 0);
|
|
122
183
|
const isConfigPath = path.startsWith("/config/") || path.startsWith("/id/");
|
|
123
184
|
const isWrite = method !== "GET";
|
|
124
185
|
if (isWrite && isConfigPath) {
|
|
125
186
|
const cachedEtag = etagCache.get(path);
|
|
126
187
|
if (cachedEtag) headers["If-Match"] = cachedEtag;
|
|
127
188
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
signal: AbortSignal.timeout(effectiveTimeout)
|
|
133
|
-
});
|
|
134
|
-
const text = await res.text();
|
|
135
|
-
const etag = res.headers.get("ETag") || void 0;
|
|
189
|
+
const serializedBody = hasBody ? rawStringBody && typeof body === "string" ? body : JSON.stringify(body) : void 0;
|
|
190
|
+
const res = socketPath ? await sendViaUnixSocket(socketPath, path, method, headers, serializedBody, effectiveTimeout) : await sendViaFetch(url, method, headers, serializedBody, effectiveTimeout);
|
|
191
|
+
const text = res.text;
|
|
192
|
+
const etag = res.etag;
|
|
136
193
|
if (method === "GET" && etag && isConfigPath) {
|
|
137
194
|
setEtag(path, etag);
|
|
138
195
|
}
|
|
@@ -156,10 +213,11 @@ async function attemptRequest(method, path, body, contentType, timeout) {
|
|
|
156
213
|
return { ok: false, status: res.status, error: `HTTP ${res.status}${hint}` };
|
|
157
214
|
}
|
|
158
215
|
if (res.status === 403 && /origin/i.test(text)) {
|
|
216
|
+
const hint = socketPath ? `Over a unix socket caddy-mcp deliberately sends no Origin header, because Caddy builds no default origin allowlist for a unix listener (sending one would fail against an empty list). Reaching here means admin.enforce_origin is enabled -- disable it, or move the admin endpoint to a TCP address.` : `Set CADDY_ADMIN_URL to the exact origin Caddy allows (default http://localhost:2019), or add this origin to the admin.origins list in Caddy's config.`;
|
|
159
217
|
return {
|
|
160
218
|
ok: false,
|
|
161
219
|
status: 403,
|
|
162
|
-
error: `${text.trim()} -- Caddy's admin API rejected this client's Origin.
|
|
220
|
+
error: `${text.trim()} -- Caddy's admin API rejected this client's Origin. ${hint}`
|
|
163
221
|
};
|
|
164
222
|
}
|
|
165
223
|
return { ok: false, status: res.status, error: text };
|
|
@@ -172,17 +230,25 @@ async function attemptRequest(method, path, body, contentType, timeout) {
|
|
|
172
230
|
}
|
|
173
231
|
} catch (err) {
|
|
174
232
|
const msg = err instanceof Error ? err.message : String(err);
|
|
233
|
+
if (socketPath && msg.includes("ENOENT")) {
|
|
234
|
+
return {
|
|
235
|
+
ok: false,
|
|
236
|
+
status: 0,
|
|
237
|
+
error: `No socket at ${socketPath} \u2014 check the path in CADDY_ADMIN_URL and that Caddy's admin endpoint is configured to listen on it.`
|
|
238
|
+
};
|
|
239
|
+
}
|
|
175
240
|
if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
241
|
+
let target = socketPath ?? getBaseUrl();
|
|
242
|
+
if (!socketPath) {
|
|
243
|
+
try {
|
|
244
|
+
target = new URL(target).origin;
|
|
245
|
+
} catch {
|
|
246
|
+
}
|
|
181
247
|
}
|
|
182
248
|
return {
|
|
183
249
|
ok: false,
|
|
184
250
|
status: 0,
|
|
185
|
-
error: `Cannot connect to Caddy admin API at ${
|
|
251
|
+
error: `Cannot connect to Caddy admin API at ${target} \u2014 is Caddy running?`
|
|
186
252
|
};
|
|
187
253
|
}
|
|
188
254
|
if (msg.includes("abort") || msg.includes("timeout")) {
|
|
@@ -240,12 +306,12 @@ function getLoadTimeout() {
|
|
|
240
306
|
return floored;
|
|
241
307
|
}
|
|
242
308
|
async function loadConfig(config, contentType) {
|
|
243
|
-
const res = await caddyRequest("POST", "/load", config, contentType, getLoadTimeout());
|
|
309
|
+
const res = await caddyRequest("POST", "/load", config, contentType, getLoadTimeout(), true);
|
|
244
310
|
if (res.ok) etagCache.clear();
|
|
245
311
|
return res;
|
|
246
312
|
}
|
|
247
313
|
function adapt(config, adapter = "caddyfile") {
|
|
248
|
-
return caddyRequest("POST", "/adapt", config, `text/${adapter}
|
|
314
|
+
return caddyRequest("POST", "/adapt", config, `text/${adapter}`, void 0, true);
|
|
249
315
|
}
|
|
250
316
|
function stop() {
|
|
251
317
|
return caddyRequest("POST", "/stop");
|
|
@@ -592,25 +658,83 @@ ${warnLines.join("\n")}` });
|
|
|
592
658
|
import { z as z3 } from "zod";
|
|
593
659
|
|
|
594
660
|
// src/snapshots.ts
|
|
661
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
662
|
+
import { join } from "path";
|
|
595
663
|
var MAX_SNAPSHOTS = 10;
|
|
596
664
|
var store = [];
|
|
665
|
+
function isSnapshotableConfig(data) {
|
|
666
|
+
return data !== null && typeof data === "object" && !Array.isArray(data);
|
|
667
|
+
}
|
|
668
|
+
var SNAPSHOT_FILE_RE = /^snapshot-(\d+)-([\w-]+)\.json$/;
|
|
669
|
+
function snapshotDir() {
|
|
670
|
+
const dir = process.env.CADDY_MCP_SNAPSHOT_DIR?.trim();
|
|
671
|
+
return dir ? dir : void 0;
|
|
672
|
+
}
|
|
673
|
+
var hydrated = false;
|
|
674
|
+
function hydrate() {
|
|
675
|
+
if (hydrated) return;
|
|
676
|
+
hydrated = true;
|
|
677
|
+
const dir = snapshotDir();
|
|
678
|
+
if (!dir || !existsSync(dir)) return;
|
|
679
|
+
try {
|
|
680
|
+
const names = readdirSync(dir).filter((n) => SNAPSHOT_FILE_RE.test(n)).sort().reverse().slice(0, MAX_SNAPSHOTS);
|
|
681
|
+
const loaded = [];
|
|
682
|
+
for (const name of names) {
|
|
683
|
+
const m = SNAPSHOT_FILE_RE.exec(name);
|
|
684
|
+
if (!m) continue;
|
|
685
|
+
try {
|
|
686
|
+
const config = JSON.parse(readFileSync(join(dir, name), "utf-8"));
|
|
687
|
+
if (!isSnapshotableConfig(config)) continue;
|
|
688
|
+
loaded.push({ config, timestamp: Number(m[1]), trigger: m[2] });
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
loaded.sort((a, b) => b.timestamp - a.timestamp);
|
|
693
|
+
store.push(...loaded);
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function persist(snap) {
|
|
698
|
+
const dir = snapshotDir();
|
|
699
|
+
if (!dir) return;
|
|
700
|
+
try {
|
|
701
|
+
mkdirSync(dir, { recursive: true });
|
|
702
|
+
const safeTrigger = snap.trigger.replace(/[^\w-]/g, "_");
|
|
703
|
+
writeFileSync(
|
|
704
|
+
join(dir, `snapshot-${snap.timestamp}-${safeTrigger}.json`),
|
|
705
|
+
JSON.stringify(snap.config, null, 2),
|
|
706
|
+
"utf-8"
|
|
707
|
+
);
|
|
708
|
+
const files = readdirSync(dir).filter((n) => SNAPSHOT_FILE_RE.test(n)).sort().reverse();
|
|
709
|
+
for (const stale of files.slice(MAX_SNAPSHOTS)) {
|
|
710
|
+
rmSync(join(dir, stale), { force: true });
|
|
711
|
+
}
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
}
|
|
597
715
|
function saveSnapshot(config, trigger) {
|
|
598
|
-
|
|
716
|
+
hydrate();
|
|
717
|
+
let timestamp = Date.now();
|
|
718
|
+
if (store.length > 0 && timestamp <= store[0].timestamp) {
|
|
719
|
+
timestamp = store[0].timestamp + 1;
|
|
720
|
+
}
|
|
721
|
+
const snap = { config, timestamp, trigger };
|
|
722
|
+
store.unshift(snap);
|
|
599
723
|
if (store.length > MAX_SNAPSHOTS) {
|
|
600
724
|
store.length = MAX_SNAPSHOTS;
|
|
601
725
|
}
|
|
726
|
+
persist(snap);
|
|
602
727
|
}
|
|
603
728
|
function listSnapshots() {
|
|
729
|
+
hydrate();
|
|
604
730
|
return store;
|
|
605
731
|
}
|
|
606
732
|
function getSnapshot(index) {
|
|
733
|
+
hydrate();
|
|
607
734
|
return store[index];
|
|
608
735
|
}
|
|
609
736
|
|
|
610
737
|
// src/tools/config.ts
|
|
611
|
-
function isSnapshotableConfig(data) {
|
|
612
|
-
return data !== null && typeof data === "object" && !Array.isArray(data);
|
|
613
|
-
}
|
|
614
738
|
function registerConfigTools(server) {
|
|
615
739
|
server.tool(
|
|
616
740
|
"caddy_config_get",
|
|
@@ -690,7 +814,7 @@ function registerConfigTools(server) {
|
|
|
690
814
|
);
|
|
691
815
|
server.tool(
|
|
692
816
|
"caddy_revert",
|
|
693
|
-
"Manage config snapshots for rollback. Snapshots are auto-captured before caddy_load
|
|
817
|
+
"Manage config snapshots for rollback. Snapshots are auto-captured before caddy_load (last 10). By default they live in memory only and are LOST when this server restarts -- set CADDY_MCP_SNAPSHOT_DIR to a writable directory to persist them across restarts (they contain full Caddy configs, so pick the location deliberately). Actions: 'list' shows snapshots with timestamps, 'save' manually captures the current config, 'apply' restores a snapshot (requires confirm=true).",
|
|
694
818
|
{
|
|
695
819
|
action: z3.enum(["list", "save", "apply"]).describe("Action to perform"),
|
|
696
820
|
index: z3.number().int().nonnegative().optional().default(0).describe("Snapshot index for 'apply' (0 = most recent, default)"),
|
|
@@ -1233,6 +1357,7 @@ function buildTlsConfig(fields) {
|
|
|
1233
1357
|
const issuer = { module: "acme" };
|
|
1234
1358
|
if (fields.email) issuer.email = fields.email;
|
|
1235
1359
|
if (fields.ca) issuer.ca = fields.ca;
|
|
1360
|
+
if (fields.profile) issuer.profile = fields.profile;
|
|
1236
1361
|
return {
|
|
1237
1362
|
automation: {
|
|
1238
1363
|
policies: [{ issuers: [issuer] }]
|
|
@@ -1266,6 +1391,7 @@ function mergeIssuerFields(existing, fields) {
|
|
|
1266
1391
|
const issuer = automation.policies[0].issuers[0];
|
|
1267
1392
|
if (fields.email !== void 0) issuer.email = fields.email;
|
|
1268
1393
|
if (fields.ca !== void 0) issuer.ca = fields.ca;
|
|
1394
|
+
if (fields.profile !== void 0) issuer.profile = fields.profile;
|
|
1269
1395
|
return merged;
|
|
1270
1396
|
}
|
|
1271
1397
|
function validateIssuerShape(tls) {
|
|
@@ -1336,47 +1462,63 @@ async function safeFallback(label, patchRes, fields) {
|
|
|
1336
1462
|
};
|
|
1337
1463
|
}
|
|
1338
1464
|
const merged = mergeIssuerFields(getRes.data, fields);
|
|
1339
|
-
const
|
|
1340
|
-
if (
|
|
1341
|
-
return { kind: "tool-error", result: bothErrors(label, patchRes,
|
|
1465
|
+
const mergeRes = await configPatch("apps/tls", merged);
|
|
1466
|
+
if (mergeRes.ok) return { kind: "ok" };
|
|
1467
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, mergeRes, "PATCH apps/tls") };
|
|
1468
|
+
}
|
|
1469
|
+
async function setIssuerField(field, value, label) {
|
|
1470
|
+
const patchRes = await configPatch(`apps/tls/automation/policies/0/issuers/0/${field}`, value);
|
|
1471
|
+
const ok = { content: [{ type: "text", text: `${label} set to: ${value}` }] };
|
|
1472
|
+
if (patchRes.ok) return ok;
|
|
1473
|
+
const outcome = await safeFallback(label, patchRes, { [field]: value });
|
|
1474
|
+
return outcome.kind === "ok" ? ok : outcome.result;
|
|
1475
|
+
}
|
|
1476
|
+
function missingArgError(text) {
|
|
1477
|
+
return { isError: true, content: [{ type: "text", text: `Error: ${text}` }] };
|
|
1342
1478
|
}
|
|
1343
1479
|
function registerTlsTools(server) {
|
|
1344
1480
|
server.tool(
|
|
1345
1481
|
"caddy_tls",
|
|
1346
|
-
"Get or configure TLS/HTTPS settings. Actions: 'status' shows current TLS config, 'set_email' sets the ACME email, 'set_acme_ca' sets the ACME CA URL. Works on both fresh and existing Caddy instances.",
|
|
1482
|
+
"Get or configure TLS/HTTPS settings. Actions: 'status' shows current TLS config, 'set_email' sets the ACME email, 'set_acme_ca' sets the ACME CA URL, 'set_acme_profile' sets the ACME profile (Caddy 2.10+), 'ech_status' reads the Encrypted ClientHello config (Caddy 2.10+, read-only here). Works on both fresh and existing Caddy instances. Writes target policies[0].issuers[0] only -- on a multi-policy TLS config, edit the intended policy with caddy_config_set instead.",
|
|
1347
1483
|
{
|
|
1348
|
-
action: z5.enum(["status", "set_email", "set_acme_ca"]).describe("Action to perform"),
|
|
1484
|
+
action: z5.enum(["status", "set_email", "set_acme_ca", "set_acme_profile", "ech_status"]).describe("Action to perform"),
|
|
1349
1485
|
email: z5.string().optional().describe("ACME email address (for 'set_email' action)"),
|
|
1350
|
-
ca: z5.string().optional().describe("ACME CA URL (for 'set_acme_ca' action)")
|
|
1486
|
+
ca: z5.string().optional().describe("ACME CA URL (for 'set_acme_ca' action)"),
|
|
1487
|
+
profile: z5.string().optional().describe(
|
|
1488
|
+
"ACME profile name (for 'set_acme_profile'). Requires Caddy 2.10+ and a CA that offers profiles; Let's Encrypt uses 'shortlived' for 6-day certificates. Valid names are defined by the CA, not by Caddy."
|
|
1489
|
+
)
|
|
1351
1490
|
},
|
|
1352
1491
|
{ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
1353
|
-
async ({ action, email, ca }) => {
|
|
1492
|
+
async ({ action, email, ca, profile }) => {
|
|
1354
1493
|
if (action === "status") {
|
|
1355
1494
|
return formatResult(await configGet("apps/tls"));
|
|
1356
1495
|
}
|
|
1357
|
-
if (action === "
|
|
1358
|
-
|
|
1496
|
+
if (action === "ech_status") {
|
|
1497
|
+
const echRes = await configGet("apps/tls/ech");
|
|
1498
|
+
const absent = !echRes.ok && echRes.status === 404 || echRes.ok && (echRes.data === void 0 || echRes.data === null);
|
|
1499
|
+
if (absent) {
|
|
1359
1500
|
return {
|
|
1360
|
-
|
|
1361
|
-
|
|
1501
|
+
content: [
|
|
1502
|
+
{
|
|
1503
|
+
type: "text",
|
|
1504
|
+
text: "ECH (Encrypted ClientHello) is not configured on this instance. Requires Caddy 2.10+; enable it by applying a config with apps/tls/ech via caddy_load."
|
|
1505
|
+
}
|
|
1506
|
+
]
|
|
1362
1507
|
};
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
return
|
|
1508
|
+
}
|
|
1509
|
+
return formatResult(echRes);
|
|
1510
|
+
}
|
|
1511
|
+
if (action === "set_email") {
|
|
1512
|
+
if (!email) return missingArgError("email is required for set_email action");
|
|
1513
|
+
return setIssuerField("email", email, "ACME email");
|
|
1368
1514
|
}
|
|
1369
1515
|
if (action === "set_acme_ca") {
|
|
1370
|
-
if (!ca)
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
if (patchRes.ok) return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
1377
|
-
const outcome = await safeFallback("ACME CA", patchRes, { ca });
|
|
1378
|
-
if (outcome.kind === "ok") return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
1379
|
-
return outcome.result;
|
|
1516
|
+
if (!ca) return missingArgError("ca is required for set_acme_ca action");
|
|
1517
|
+
return setIssuerField("ca", ca, "ACME CA");
|
|
1518
|
+
}
|
|
1519
|
+
if (action === "set_acme_profile") {
|
|
1520
|
+
if (!profile) return missingArgError("profile is required for set_acme_profile action");
|
|
1521
|
+
return setIssuerField("profile", profile, "ACME profile");
|
|
1380
1522
|
}
|
|
1381
1523
|
return { isError: true, content: [{ type: "text", text: `Unknown action: ${action}` }] };
|
|
1382
1524
|
}
|
package/dist/snapshots.d.ts
CHANGED
|
@@ -3,6 +3,16 @@ export interface Snapshot {
|
|
|
3
3
|
timestamp: number;
|
|
4
4
|
trigger: string;
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* A snapshot must be re-applyable via /load, which requires a JSON object root.
|
|
8
|
+
* Strings, arrays, null, and primitives are not valid Caddy configs -- refuse to
|
|
9
|
+
* capture them so a later `caddy_revert apply` can't replay garbage.
|
|
10
|
+
*
|
|
11
|
+
* Exported because both entry points into the ring have to agree: the in-memory
|
|
12
|
+
* save path in tools/config.ts, and the rehydration path below. A file on disk
|
|
13
|
+
* that merely parses as JSON is not automatically a config.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isSnapshotableConfig(data: unknown): data is Record<string, unknown>;
|
|
6
16
|
export declare function saveSnapshot(config: unknown, trigger: string): void;
|
|
7
17
|
export declare function listSnapshots(): readonly Snapshot[];
|
|
8
18
|
export declare function getSnapshot(index: number): Snapshot | undefined;
|
package/package.json
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@yawlabs/caddy-mcp",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"mcpName": "io.github.YawLabs/caddy-mcp",
|
|
5
|
-
"description": "MCP server for managing Caddy web servers via the admin API",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",
|
|
8
|
-
"type": "module",
|
|
9
|
-
"exports": {
|
|
10
|
-
".": {
|
|
11
|
-
"import": "./dist/server.js",
|
|
12
|
-
"types": "./dist/server.d.ts"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"main": "./dist/server.js",
|
|
16
|
-
"types": "./dist/server.d.ts",
|
|
17
|
-
"bin": {
|
|
18
|
-
"caddy-mcp": "bin/caddy-mcp.mjs"
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"bin/caddy-mcp.mjs",
|
|
22
|
-
"dist",
|
|
23
|
-
"!dist/**/*.test.*",
|
|
24
|
-
"README.md",
|
|
25
|
-
"LICENSE"
|
|
26
|
-
],
|
|
27
|
-
"scripts": {
|
|
28
|
-
"build": "tsup && tsc -p tsconfig.build.json",
|
|
29
|
-
"dev": "tsup --watch",
|
|
30
|
-
"test": "vitest run",
|
|
31
|
-
"lint": "biome check src/",
|
|
32
|
-
"lint:fix": "biome check --write src/",
|
|
33
|
-
"typecheck": "node scripts/typecheck.mjs",
|
|
34
|
-
"typecheck:tsc": "tsc --noEmit",
|
|
35
|
-
"test:ci": "npm run build && npm test",
|
|
36
|
-
"prepublishOnly": "npm run build",
|
|
37
|
-
"prepare": "git config core.hooksPath .githooks 2>/dev/null || true",
|
|
38
|
-
"start": "node dist/index.js"
|
|
39
|
-
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
-
"zod": "^4.3.6"
|
|
43
|
-
},
|
|
44
|
-
"overrides": {
|
|
45
|
-
"hono": "^4.12.21",
|
|
46
|
-
"@hono/node-server": "^1.19.13",
|
|
47
|
-
"postcss": "^8.5.10",
|
|
48
|
-
"ip-address": "^10.1.1",
|
|
49
|
-
"fast-uri": "^3.1.2",
|
|
50
|
-
"qs": "^6.15.2",
|
|
51
|
-
"esbuild": "^0.28.1"
|
|
52
|
-
},
|
|
53
|
-
"devDependencies": {
|
|
54
|
-
"@biomejs/biome": "~2.4.11",
|
|
55
|
-
"@types/node": "^26.0.0",
|
|
56
|
-
"postject": "^1.0.0-alpha.6",
|
|
57
|
-
"tsup": "^8.4.0",
|
|
58
|
-
"typescript": "^7.0.2",
|
|
59
|
-
"vitest": "^4.1.4"
|
|
60
|
-
},
|
|
61
|
-
"engines": {
|
|
62
|
-
"node": ">=20"
|
|
63
|
-
},
|
|
64
|
-
"keywords": [
|
|
65
|
-
"mcp",
|
|
66
|
-
"model-context-protocol",
|
|
67
|
-
"caddy",
|
|
68
|
-
"reverse-proxy",
|
|
69
|
-
"web-server",
|
|
70
|
-
"mcp-server",
|
|
71
|
-
"devtools",
|
|
72
|
-
"ai"
|
|
73
|
-
],
|
|
74
|
-
"repository": {
|
|
75
|
-
"type": "git",
|
|
76
|
-
"url": "git+https://github.com/YawLabs/caddy-mcp.git"
|
|
77
|
-
},
|
|
78
|
-
"bugs": {
|
|
79
|
-
"url": "https://github.com/YawLabs/caddy-mcp/issues"
|
|
80
|
-
},
|
|
81
|
-
"homepage": "https://github.com/YawLabs/caddy-mcp"
|
|
82
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@yawlabs/caddy-mcp",
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"mcpName": "io.github.YawLabs/caddy-mcp",
|
|
5
|
+
"description": "MCP server for managing Caddy web servers via the admin API",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/server.js",
|
|
12
|
+
"types": "./dist/server.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/server.js",
|
|
16
|
+
"types": "./dist/server.d.ts",
|
|
17
|
+
"bin": {
|
|
18
|
+
"caddy-mcp": "bin/caddy-mcp.mjs"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"bin/caddy-mcp.mjs",
|
|
22
|
+
"dist",
|
|
23
|
+
"!dist/**/*.test.*",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"lint": "biome check src/",
|
|
32
|
+
"lint:fix": "biome check --write src/",
|
|
33
|
+
"typecheck": "node scripts/typecheck.mjs",
|
|
34
|
+
"typecheck:tsc": "tsc --noEmit",
|
|
35
|
+
"test:ci": "npm run build && npm test",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"prepare": "git config core.hooksPath .githooks 2>/dev/null || true",
|
|
38
|
+
"start": "node dist/index.js"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
+
"zod": "^4.3.6"
|
|
43
|
+
},
|
|
44
|
+
"overrides": {
|
|
45
|
+
"hono": "^4.12.21",
|
|
46
|
+
"@hono/node-server": "^1.19.13",
|
|
47
|
+
"postcss": "^8.5.10",
|
|
48
|
+
"ip-address": "^10.1.1",
|
|
49
|
+
"fast-uri": "^3.1.2",
|
|
50
|
+
"qs": "^6.15.2",
|
|
51
|
+
"esbuild": "^0.28.1"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "~2.4.11",
|
|
55
|
+
"@types/node": "^26.0.0",
|
|
56
|
+
"postject": "^1.0.0-alpha.6",
|
|
57
|
+
"tsup": "^8.4.0",
|
|
58
|
+
"typescript": "^7.0.2",
|
|
59
|
+
"vitest": "^4.1.4"
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=20"
|
|
63
|
+
},
|
|
64
|
+
"keywords": [
|
|
65
|
+
"mcp",
|
|
66
|
+
"model-context-protocol",
|
|
67
|
+
"caddy",
|
|
68
|
+
"reverse-proxy",
|
|
69
|
+
"web-server",
|
|
70
|
+
"mcp-server",
|
|
71
|
+
"devtools",
|
|
72
|
+
"ai"
|
|
73
|
+
],
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "git+https://github.com/YawLabs/caddy-mcp.git"
|
|
77
|
+
},
|
|
78
|
+
"bugs": {
|
|
79
|
+
"url": "https://github.com/YawLabs/caddy-mcp/issues"
|
|
80
|
+
},
|
|
81
|
+
"homepage": "https://github.com/YawLabs/caddy-mcp"
|
|
82
|
+
}
|