@yawlabs/caddy-mcp 1.0.1 → 1.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/README.md +0 -1
- package/dist/index.js +232 -19
- package/dist/server.js +232 -19
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@yawlabs/caddy-mcp)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://github.com/YawLabs/caddy-mcp/stargazers)
|
|
6
|
-
[](https://github.com/YawLabs/caddy-mcp/actions/workflows/ci.yml) [](https://github.com/YawLabs/caddy-mcp/actions/workflows/release.yml)
|
|
7
6
|
|
|
8
7
|
**Manage Caddy web servers from Claude Code, Cursor, and any MCP client.** 18 tools + 4 resources covering every endpoint of Caddy's admin API — config, routes, reverse proxies, TLS, PKI, metrics, snapshots.
|
|
9
8
|
|
package/dist/index.js
CHANGED
|
@@ -97,7 +97,15 @@ async function attemptRequest(method, path, body, contentType, timeout) {
|
|
|
97
97
|
setEtag(path, etag);
|
|
98
98
|
}
|
|
99
99
|
if (isWrite && res.ok && isConfigPath) {
|
|
100
|
-
|
|
100
|
+
if (method === "PATCH" || method === "PUT") {
|
|
101
|
+
if (etag) {
|
|
102
|
+
setEtag(path, etag);
|
|
103
|
+
} else {
|
|
104
|
+
etagCache.delete(path);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
etagCache.delete(path);
|
|
108
|
+
}
|
|
101
109
|
}
|
|
102
110
|
if (!res.ok) {
|
|
103
111
|
if (res.status === 412) {
|
|
@@ -183,24 +191,34 @@ function getUpstreams() {
|
|
|
183
191
|
return caddyRequest("GET", "/reverse_proxy/upstreams");
|
|
184
192
|
}
|
|
185
193
|
function getPki(ca = "local") {
|
|
194
|
+
const bad = rejectTraversal(ca);
|
|
195
|
+
if (bad) return Promise.resolve(bad);
|
|
186
196
|
return caddyRequest("GET", `/pki/ca/${ca}`);
|
|
187
197
|
}
|
|
188
198
|
function getPkiCertificates(ca = "local") {
|
|
199
|
+
const bad = rejectTraversal(ca);
|
|
200
|
+
if (bad) return Promise.resolve(bad);
|
|
189
201
|
return caddyRequest("GET", `/pki/ca/${ca}/certificates`);
|
|
190
202
|
}
|
|
191
203
|
function configByIdGet(id, subpath = "") {
|
|
204
|
+
const badId = rejectTraversal(id);
|
|
205
|
+
if (badId) return Promise.resolve(badId);
|
|
192
206
|
const bad = rejectTraversal(subpath);
|
|
193
207
|
if (bad) return Promise.resolve(bad);
|
|
194
208
|
const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
|
|
195
209
|
return caddyRequest("GET", path);
|
|
196
210
|
}
|
|
197
211
|
function configByIdSet(id, value, method = "PATCH", subpath = "") {
|
|
212
|
+
const badId = rejectTraversal(id);
|
|
213
|
+
if (badId) return Promise.resolve(badId);
|
|
198
214
|
const bad = rejectTraversal(subpath);
|
|
199
215
|
if (bad) return Promise.resolve(bad);
|
|
200
216
|
const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
|
|
201
217
|
return caddyRequest(method, path, value);
|
|
202
218
|
}
|
|
203
219
|
function configByIdDelete(id, subpath = "") {
|
|
220
|
+
const badId = rejectTraversal(id);
|
|
221
|
+
if (badId) return Promise.resolve(badId);
|
|
204
222
|
const bad = rejectTraversal(subpath);
|
|
205
223
|
if (bad) return Promise.resolve(bad);
|
|
206
224
|
const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
|
|
@@ -517,6 +535,35 @@ function describeServer(raw) {
|
|
|
517
535
|
const listenStr = listen.length > 0 ? listen.map(String).join(", ") : "default";
|
|
518
536
|
return `${routes.length} route(s), listen: ${listenStr}, TLS: ${tls}`;
|
|
519
537
|
}
|
|
538
|
+
var METRICS_DEFAULT_MAX_LINES = 500;
|
|
539
|
+
function metricNameFromLine(line) {
|
|
540
|
+
const trimmed = line.trimStart();
|
|
541
|
+
if (trimmed === "") return void 0;
|
|
542
|
+
if (trimmed.startsWith("#")) {
|
|
543
|
+
const m2 = trimmed.match(/^#\s+(?:HELP|TYPE)\s+([A-Za-z_:][A-Za-z0-9_:]*)/);
|
|
544
|
+
return m2 ? m2[1] : void 0;
|
|
545
|
+
}
|
|
546
|
+
const m = trimmed.match(/^([A-Za-z_:][A-Za-z0-9_:]*)/);
|
|
547
|
+
return m ? m[1] : void 0;
|
|
548
|
+
}
|
|
549
|
+
function applyMetricsControls(raw, filter, maxLines) {
|
|
550
|
+
const lines = raw.split("\n");
|
|
551
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
552
|
+
let filtered;
|
|
553
|
+
if (filter && filter.length > 0) {
|
|
554
|
+
filtered = lines.filter((line) => {
|
|
555
|
+
const name = metricNameFromLine(line);
|
|
556
|
+
return name?.includes(filter) ?? false;
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
filtered = lines;
|
|
560
|
+
}
|
|
561
|
+
if (filtered.length <= maxLines) return filtered.join("\n");
|
|
562
|
+
const dropped = filtered.length - maxLines;
|
|
563
|
+
const kept = filtered.slice(0, maxLines);
|
|
564
|
+
kept.push(`# [truncated, ${dropped} lines omitted -- use filter to narrow]`);
|
|
565
|
+
return kept.join("\n");
|
|
566
|
+
}
|
|
520
567
|
function findAcmeEmail(policies) {
|
|
521
568
|
if (!Array.isArray(policies)) return void 0;
|
|
522
569
|
for (const rawPolicy of policies) {
|
|
@@ -599,10 +646,22 @@ ${lines.join("\n")}` }]
|
|
|
599
646
|
);
|
|
600
647
|
server.tool(
|
|
601
648
|
"caddy_metrics",
|
|
602
|
-
"Get Prometheus metrics from Caddy. Shows request counts, durations, TLS handshake stats, active connections, and more.",
|
|
603
|
-
{
|
|
649
|
+
"Get Prometheus metrics from Caddy. Shows request counts, durations, TLS handshake stats, active connections, and more. Output can be megabytes on busy servers -- use `filter` to keep only metrics whose name contains a substring (e.g. 'http_requests' or 'tls'); HELP/TYPE comment lines for retained metrics are kept. Filter-mode drops blank lines and free-form '# comment' lines (including '# EOF'); only '# HELP'/'# TYPE' lines for matching metrics are kept. Use `max_lines` to cap the response (default 500); a trailing comment reports how many lines were dropped.",
|
|
650
|
+
{
|
|
651
|
+
filter: z3.string().optional().describe(
|
|
652
|
+
"Substring to match against metric names. Keeps sample lines whose metric name contains this substring, plus their `# HELP` and `# TYPE` comment lines. Empty/absent = no filtering."
|
|
653
|
+
),
|
|
654
|
+
max_lines: z3.number().int().positive().optional().describe("Maximum number of output lines (default 500). Excess lines are dropped and a summary is appended.")
|
|
655
|
+
},
|
|
604
656
|
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
605
|
-
async () =>
|
|
657
|
+
async ({ filter, max_lines }) => {
|
|
658
|
+
const res = await getMetrics();
|
|
659
|
+
if (!res.ok) return formatResult(res);
|
|
660
|
+
const raw = typeof res.data === "string" ? res.data : res.data !== void 0 ? String(res.data) : "";
|
|
661
|
+
const limit = max_lines ?? METRICS_DEFAULT_MAX_LINES;
|
|
662
|
+
const text = applyMetricsControls(raw, filter, limit);
|
|
663
|
+
return { content: [{ type: "text", text: text || "OK" }] };
|
|
664
|
+
}
|
|
606
665
|
);
|
|
607
666
|
server.tool(
|
|
608
667
|
"caddy_stop",
|
|
@@ -633,7 +692,10 @@ function parseFrom(from) {
|
|
|
633
692
|
const slashIdx = cleaned.indexOf("/");
|
|
634
693
|
if (slashIdx > 0) {
|
|
635
694
|
match.host = [cleaned.substring(0, slashIdx)];
|
|
636
|
-
|
|
695
|
+
const path = cleaned.substring(slashIdx);
|
|
696
|
+
if (path !== "/") {
|
|
697
|
+
match.path = [path];
|
|
698
|
+
}
|
|
637
699
|
} else if (cleaned.startsWith("/")) {
|
|
638
700
|
match.path = [cleaned];
|
|
639
701
|
} else {
|
|
@@ -644,6 +706,21 @@ function parseFrom(from) {
|
|
|
644
706
|
function cleanUpstreamAddr(addr) {
|
|
645
707
|
return addr.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
646
708
|
}
|
|
709
|
+
function isParentMissing(res) {
|
|
710
|
+
if (res.ok) return false;
|
|
711
|
+
if (res.status === 404) return true;
|
|
712
|
+
return res.error?.includes("key does not exist") ?? false;
|
|
713
|
+
}
|
|
714
|
+
function isUnknownId(res) {
|
|
715
|
+
if (res.ok) return false;
|
|
716
|
+
if (res.status === 404) return true;
|
|
717
|
+
const body = (res.error ?? "").toLowerCase();
|
|
718
|
+
return body.includes("unknown object id") || body.includes("no id found");
|
|
719
|
+
}
|
|
720
|
+
function isRouteShape(obj) {
|
|
721
|
+
if (!obj || typeof obj !== "object") return false;
|
|
722
|
+
return Array.isArray(obj.handle);
|
|
723
|
+
}
|
|
647
724
|
function serverNotFoundError(srv) {
|
|
648
725
|
return {
|
|
649
726
|
isError: true,
|
|
@@ -658,14 +735,17 @@ function serverNotFoundError(srv) {
|
|
|
658
735
|
function registerRouteTools(server) {
|
|
659
736
|
server.tool(
|
|
660
737
|
"caddy_reverse_proxy",
|
|
661
|
-
"Add a reverse proxy route. The most common operation \u2014 just specify where traffic comes from and where it goes. Example: from='api.local' to=['localhost:3000'].",
|
|
738
|
+
"Add a reverse proxy route. The most common operation \u2014 just specify where traffic comes from and where it goes. Example: from='api.local' to=['localhost:3000']. When `id` is OMITTED the route is appended to the server's routes array \u2014 calling the tool twice with the same args produces TWO duplicate routes (non-idempotent). When `id` is SUPPLIED the route is written via PUT under that @id, so repeat calls REPLACE in place (idempotent). Strongly recommended: supply a stable `id` for any route managed from automation or production tooling. Note: @ids are config-global in Caddy (NOT route-scoped). If `id` collides with an @id used by a non-route object (TLS issuer, server, etc.) the call refuses with an error rather than clobbering it. Once an @id is registered to a route under one server, subsequent calls update that route in place regardless of the `server` argument.",
|
|
662
739
|
{
|
|
663
740
|
from: z4.string().describe("Domain, path, or domain/path to match (e.g., 'api.local', '/api/*', 'app.local/ws')"),
|
|
664
741
|
to: z4.array(z4.string()).describe("Upstream addresses (e.g., ['localhost:3000', 'localhost:3001'])"),
|
|
665
|
-
server: z4.string().regex(/^[\w-]{1,128}$/).optional().default("srv0").describe("Caddy server name (default: srv0)")
|
|
742
|
+
server: z4.string().regex(/^[\w-]{1,128}$/).optional().default("srv0").describe("Caddy server name (default: srv0)"),
|
|
743
|
+
id: z4.string().regex(/^[\w-]{1,128}$/).optional().describe(
|
|
744
|
+
"Optional stable @id for the route. When set, repeat calls REPLACE the route in place (idempotent). When omitted, the route is APPENDED \u2014 calling twice with identical args creates a duplicate route. @ids are config-global in Caddy: if this id is already used by a non-route object the call refuses rather than clobbering it."
|
|
745
|
+
)
|
|
666
746
|
},
|
|
667
747
|
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
668
|
-
async ({ from, to, server: srv }) => {
|
|
748
|
+
async ({ from, to, server: srv, id }) => {
|
|
669
749
|
const match = parseFrom(from);
|
|
670
750
|
const cleanedTo = to.map(cleanUpstreamAddr);
|
|
671
751
|
const route = {
|
|
@@ -678,11 +758,58 @@ function registerRouteTools(server) {
|
|
|
678
758
|
],
|
|
679
759
|
terminal: true
|
|
680
760
|
};
|
|
761
|
+
if (id) {
|
|
762
|
+
route["@id"] = id;
|
|
763
|
+
const existing = await configByIdGet(id);
|
|
764
|
+
if (existing.ok) {
|
|
765
|
+
if (!isRouteShape(existing.data)) {
|
|
766
|
+
return {
|
|
767
|
+
isError: true,
|
|
768
|
+
content: [
|
|
769
|
+
{
|
|
770
|
+
type: "text",
|
|
771
|
+
text: `Error: @id "${id}" is already in use by a non-route config object (no top-level "handle" array). @ids are config-global in Caddy, not route-scoped -- pick a different id, or remove the existing object first with caddy_config_by_id { id: "${id}", action: "delete" }.`
|
|
772
|
+
}
|
|
773
|
+
]
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
const putRes = await configByIdSet(id, route, "PUT");
|
|
777
|
+
if (putRes.ok) {
|
|
778
|
+
return {
|
|
779
|
+
content: [
|
|
780
|
+
{
|
|
781
|
+
type: "text",
|
|
782
|
+
text: `Route set @id="${id}": ${from} \u2192 ${cleanedTo.join(", ")}`
|
|
783
|
+
}
|
|
784
|
+
]
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
return formatResult(putRes);
|
|
788
|
+
}
|
|
789
|
+
if (!isUnknownId(existing)) {
|
|
790
|
+
return formatResult(existing);
|
|
791
|
+
}
|
|
792
|
+
const postRes = await configPost(`apps/http/servers/${srv}/routes`, route);
|
|
793
|
+
if (postRes.ok) {
|
|
794
|
+
return {
|
|
795
|
+
content: [
|
|
796
|
+
{
|
|
797
|
+
type: "text",
|
|
798
|
+
text: `Route created @id="${id}": ${from} \u2192 ${cleanedTo.join(", ")}`
|
|
799
|
+
}
|
|
800
|
+
]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (isParentMissing(postRes)) {
|
|
804
|
+
return serverNotFoundError(srv);
|
|
805
|
+
}
|
|
806
|
+
return formatResult(postRes);
|
|
807
|
+
}
|
|
681
808
|
const res = await configPost(`apps/http/servers/${srv}/routes`, route);
|
|
682
809
|
if (res.ok) {
|
|
683
810
|
return { content: [{ type: "text", text: `Route added: ${from} \u2192 ${cleanedTo.join(", ")}` }] };
|
|
684
811
|
}
|
|
685
|
-
if (
|
|
812
|
+
if (isParentMissing(res)) {
|
|
686
813
|
return serverNotFoundError(srv);
|
|
687
814
|
}
|
|
688
815
|
return formatResult(res);
|
|
@@ -701,7 +828,7 @@ function registerRouteTools(server) {
|
|
|
701
828
|
async ({ match, handle, server: srv, terminal }) => {
|
|
702
829
|
const route = { match, handle, terminal };
|
|
703
830
|
const res = await configPost(`apps/http/servers/${srv}/routes`, route);
|
|
704
|
-
if (
|
|
831
|
+
if (isParentMissing(res)) {
|
|
705
832
|
return serverNotFoundError(srv);
|
|
706
833
|
}
|
|
707
834
|
return formatResult(res);
|
|
@@ -914,9 +1041,9 @@ function buildTlsConfig(fields) {
|
|
|
914
1041
|
}
|
|
915
1042
|
};
|
|
916
1043
|
}
|
|
917
|
-
function bothErrors(label, patchRes,
|
|
1044
|
+
function bothErrors(label, patchRes, writeRes, writeLabel) {
|
|
918
1045
|
const patchErr = patchRes.error || `HTTP ${patchRes.status}`;
|
|
919
|
-
const
|
|
1046
|
+
const writeErr = writeRes.error || `HTTP ${writeRes.status}`;
|
|
920
1047
|
return {
|
|
921
1048
|
isError: true,
|
|
922
1049
|
content: [
|
|
@@ -924,11 +1051,97 @@ function bothErrors(label, patchRes, postRes) {
|
|
|
924
1051
|
type: "text",
|
|
925
1052
|
text: `Error: Failed to set ${label}.
|
|
926
1053
|
PATCH attempt: ${patchErr}
|
|
927
|
-
|
|
1054
|
+
${writeLabel} fallback: ${writeErr}`
|
|
928
1055
|
}
|
|
929
1056
|
]
|
|
930
1057
|
};
|
|
931
1058
|
}
|
|
1059
|
+
function isPlainObject(v) {
|
|
1060
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1061
|
+
}
|
|
1062
|
+
function deepClone(v) {
|
|
1063
|
+
return JSON.parse(JSON.stringify(v));
|
|
1064
|
+
}
|
|
1065
|
+
function mergeIssuerFields(existing, fields) {
|
|
1066
|
+
const merged = deepClone(existing);
|
|
1067
|
+
const automation = merged.automation;
|
|
1068
|
+
const issuer = automation.policies[0].issuers[0];
|
|
1069
|
+
if (fields.email !== void 0) issuer.email = fields.email;
|
|
1070
|
+
if (fields.ca !== void 0) issuer.ca = fields.ca;
|
|
1071
|
+
return merged;
|
|
1072
|
+
}
|
|
1073
|
+
function validateIssuerShape(tls) {
|
|
1074
|
+
const automation = tls.automation;
|
|
1075
|
+
if (!isPlainObject(automation)) {
|
|
1076
|
+
return "apps/tls.automation is missing or not an object";
|
|
1077
|
+
}
|
|
1078
|
+
const policies = automation.policies;
|
|
1079
|
+
if (!Array.isArray(policies) || policies.length === 0) {
|
|
1080
|
+
return "apps/tls.automation.policies is missing, not an array, or empty";
|
|
1081
|
+
}
|
|
1082
|
+
const policy0 = policies[0];
|
|
1083
|
+
if (!isPlainObject(policy0)) {
|
|
1084
|
+
return "apps/tls.automation.policies[0] is not an object";
|
|
1085
|
+
}
|
|
1086
|
+
const issuers = policy0.issuers;
|
|
1087
|
+
if (!Array.isArray(issuers) || issuers.length === 0) {
|
|
1088
|
+
return "apps/tls.automation.policies[0].issuers is missing, not an array, or empty";
|
|
1089
|
+
}
|
|
1090
|
+
const issuer0 = issuers[0];
|
|
1091
|
+
if (!isPlainObject(issuer0)) {
|
|
1092
|
+
return "apps/tls.automation.policies[0].issuers[0] is not an object";
|
|
1093
|
+
}
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
async function safeFallback(label, patchRes, fields) {
|
|
1097
|
+
const getRes = await configGet("apps/tls");
|
|
1098
|
+
const absent = !getRes.ok && getRes.status === 404 ? true : getRes.ok && (getRes.data === void 0 || getRes.data === null);
|
|
1099
|
+
if (absent) {
|
|
1100
|
+
const postRes = await configPost("apps/tls", buildTlsConfig(fields));
|
|
1101
|
+
if (postRes.ok) return { kind: "ok" };
|
|
1102
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, postRes, "POST") };
|
|
1103
|
+
}
|
|
1104
|
+
if (!getRes.ok) {
|
|
1105
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, getRes, "GET apps/tls") };
|
|
1106
|
+
}
|
|
1107
|
+
if (!isPlainObject(getRes.data)) {
|
|
1108
|
+
return {
|
|
1109
|
+
kind: "tool-error",
|
|
1110
|
+
result: {
|
|
1111
|
+
isError: true,
|
|
1112
|
+
content: [
|
|
1113
|
+
{
|
|
1114
|
+
type: "text",
|
|
1115
|
+
text: `Error: Failed to set ${label}.
|
|
1116
|
+
PATCH attempt: ${patchRes.error || `HTTP ${patchRes.status}`}
|
|
1117
|
+
Refusing to clobber existing apps/tls: GET returned a non-object value. Use caddy_config_set with an explicit path to update it safely.`
|
|
1118
|
+
}
|
|
1119
|
+
]
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
const shapeError = validateIssuerShape(getRes.data);
|
|
1124
|
+
if (shapeError) {
|
|
1125
|
+
return {
|
|
1126
|
+
kind: "tool-error",
|
|
1127
|
+
result: {
|
|
1128
|
+
isError: true,
|
|
1129
|
+
content: [
|
|
1130
|
+
{
|
|
1131
|
+
type: "text",
|
|
1132
|
+
text: `Error: Failed to set ${label}.
|
|
1133
|
+
PATCH attempt: ${patchRes.error || `HTTP ${patchRes.status}`}
|
|
1134
|
+
Refusing to clobber existing apps/tls: ${shapeError}. Use caddy_config_set with an explicit path (e.g. apps/tls/automation/policies) to update it safely.`
|
|
1135
|
+
}
|
|
1136
|
+
]
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
const merged = mergeIssuerFields(getRes.data, fields);
|
|
1141
|
+
const putRes = await configPut("apps/tls", merged);
|
|
1142
|
+
if (putRes.ok) return { kind: "ok" };
|
|
1143
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, putRes, "PUT") };
|
|
1144
|
+
}
|
|
932
1145
|
function registerTlsTools(server) {
|
|
933
1146
|
server.tool(
|
|
934
1147
|
"caddy_tls",
|
|
@@ -951,9 +1164,9 @@ function registerTlsTools(server) {
|
|
|
951
1164
|
};
|
|
952
1165
|
const patchRes = await configPatch("apps/tls/automation/policies/0/issuers/0/email", email);
|
|
953
1166
|
if (patchRes.ok) return { content: [{ type: "text", text: `ACME email set to: ${email}` }] };
|
|
954
|
-
const
|
|
955
|
-
if (
|
|
956
|
-
return
|
|
1167
|
+
const outcome = await safeFallback("ACME email", patchRes, { email });
|
|
1168
|
+
if (outcome.kind === "ok") return { content: [{ type: "text", text: `ACME email set to: ${email}` }] };
|
|
1169
|
+
return outcome.result;
|
|
957
1170
|
}
|
|
958
1171
|
if (action === "set_acme_ca") {
|
|
959
1172
|
if (!ca)
|
|
@@ -963,9 +1176,9 @@ function registerTlsTools(server) {
|
|
|
963
1176
|
};
|
|
964
1177
|
const patchRes = await configPatch("apps/tls/automation/policies/0/issuers/0/ca", ca);
|
|
965
1178
|
if (patchRes.ok) return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
966
|
-
const
|
|
967
|
-
if (
|
|
968
|
-
return
|
|
1179
|
+
const outcome = await safeFallback("ACME CA", patchRes, { ca });
|
|
1180
|
+
if (outcome.kind === "ok") return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
1181
|
+
return outcome.result;
|
|
969
1182
|
}
|
|
970
1183
|
return { isError: true, content: [{ type: "text", text: `Unknown action: ${action}` }] };
|
|
971
1184
|
}
|
package/dist/server.js
CHANGED
|
@@ -95,7 +95,15 @@ async function attemptRequest(method, path, body, contentType, timeout) {
|
|
|
95
95
|
setEtag(path, etag);
|
|
96
96
|
}
|
|
97
97
|
if (isWrite && res.ok && isConfigPath) {
|
|
98
|
-
|
|
98
|
+
if (method === "PATCH" || method === "PUT") {
|
|
99
|
+
if (etag) {
|
|
100
|
+
setEtag(path, etag);
|
|
101
|
+
} else {
|
|
102
|
+
etagCache.delete(path);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
etagCache.delete(path);
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
108
|
if (!res.ok) {
|
|
101
109
|
if (res.status === 412) {
|
|
@@ -181,24 +189,34 @@ function getUpstreams() {
|
|
|
181
189
|
return caddyRequest("GET", "/reverse_proxy/upstreams");
|
|
182
190
|
}
|
|
183
191
|
function getPki(ca = "local") {
|
|
192
|
+
const bad = rejectTraversal(ca);
|
|
193
|
+
if (bad) return Promise.resolve(bad);
|
|
184
194
|
return caddyRequest("GET", `/pki/ca/${ca}`);
|
|
185
195
|
}
|
|
186
196
|
function getPkiCertificates(ca = "local") {
|
|
197
|
+
const bad = rejectTraversal(ca);
|
|
198
|
+
if (bad) return Promise.resolve(bad);
|
|
187
199
|
return caddyRequest("GET", `/pki/ca/${ca}/certificates`);
|
|
188
200
|
}
|
|
189
201
|
function configByIdGet(id, subpath = "") {
|
|
202
|
+
const badId = rejectTraversal(id);
|
|
203
|
+
if (badId) return Promise.resolve(badId);
|
|
190
204
|
const bad = rejectTraversal(subpath);
|
|
191
205
|
if (bad) return Promise.resolve(bad);
|
|
192
206
|
const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
|
|
193
207
|
return caddyRequest("GET", path);
|
|
194
208
|
}
|
|
195
209
|
function configByIdSet(id, value, method = "PATCH", subpath = "") {
|
|
210
|
+
const badId = rejectTraversal(id);
|
|
211
|
+
if (badId) return Promise.resolve(badId);
|
|
196
212
|
const bad = rejectTraversal(subpath);
|
|
197
213
|
if (bad) return Promise.resolve(bad);
|
|
198
214
|
const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
|
|
199
215
|
return caddyRequest(method, path, value);
|
|
200
216
|
}
|
|
201
217
|
function configByIdDelete(id, subpath = "") {
|
|
218
|
+
const badId = rejectTraversal(id);
|
|
219
|
+
if (badId) return Promise.resolve(badId);
|
|
202
220
|
const bad = rejectTraversal(subpath);
|
|
203
221
|
if (bad) return Promise.resolve(bad);
|
|
204
222
|
const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
|
|
@@ -515,6 +533,35 @@ function describeServer(raw) {
|
|
|
515
533
|
const listenStr = listen.length > 0 ? listen.map(String).join(", ") : "default";
|
|
516
534
|
return `${routes.length} route(s), listen: ${listenStr}, TLS: ${tls}`;
|
|
517
535
|
}
|
|
536
|
+
var METRICS_DEFAULT_MAX_LINES = 500;
|
|
537
|
+
function metricNameFromLine(line) {
|
|
538
|
+
const trimmed = line.trimStart();
|
|
539
|
+
if (trimmed === "") return void 0;
|
|
540
|
+
if (trimmed.startsWith("#")) {
|
|
541
|
+
const m2 = trimmed.match(/^#\s+(?:HELP|TYPE)\s+([A-Za-z_:][A-Za-z0-9_:]*)/);
|
|
542
|
+
return m2 ? m2[1] : void 0;
|
|
543
|
+
}
|
|
544
|
+
const m = trimmed.match(/^([A-Za-z_:][A-Za-z0-9_:]*)/);
|
|
545
|
+
return m ? m[1] : void 0;
|
|
546
|
+
}
|
|
547
|
+
function applyMetricsControls(raw, filter, maxLines) {
|
|
548
|
+
const lines = raw.split("\n");
|
|
549
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
550
|
+
let filtered;
|
|
551
|
+
if (filter && filter.length > 0) {
|
|
552
|
+
filtered = lines.filter((line) => {
|
|
553
|
+
const name = metricNameFromLine(line);
|
|
554
|
+
return name?.includes(filter) ?? false;
|
|
555
|
+
});
|
|
556
|
+
} else {
|
|
557
|
+
filtered = lines;
|
|
558
|
+
}
|
|
559
|
+
if (filtered.length <= maxLines) return filtered.join("\n");
|
|
560
|
+
const dropped = filtered.length - maxLines;
|
|
561
|
+
const kept = filtered.slice(0, maxLines);
|
|
562
|
+
kept.push(`# [truncated, ${dropped} lines omitted -- use filter to narrow]`);
|
|
563
|
+
return kept.join("\n");
|
|
564
|
+
}
|
|
518
565
|
function findAcmeEmail(policies) {
|
|
519
566
|
if (!Array.isArray(policies)) return void 0;
|
|
520
567
|
for (const rawPolicy of policies) {
|
|
@@ -597,10 +644,22 @@ ${lines.join("\n")}` }]
|
|
|
597
644
|
);
|
|
598
645
|
server.tool(
|
|
599
646
|
"caddy_metrics",
|
|
600
|
-
"Get Prometheus metrics from Caddy. Shows request counts, durations, TLS handshake stats, active connections, and more.",
|
|
601
|
-
{
|
|
647
|
+
"Get Prometheus metrics from Caddy. Shows request counts, durations, TLS handshake stats, active connections, and more. Output can be megabytes on busy servers -- use `filter` to keep only metrics whose name contains a substring (e.g. 'http_requests' or 'tls'); HELP/TYPE comment lines for retained metrics are kept. Filter-mode drops blank lines and free-form '# comment' lines (including '# EOF'); only '# HELP'/'# TYPE' lines for matching metrics are kept. Use `max_lines` to cap the response (default 500); a trailing comment reports how many lines were dropped.",
|
|
648
|
+
{
|
|
649
|
+
filter: z3.string().optional().describe(
|
|
650
|
+
"Substring to match against metric names. Keeps sample lines whose metric name contains this substring, plus their `# HELP` and `# TYPE` comment lines. Empty/absent = no filtering."
|
|
651
|
+
),
|
|
652
|
+
max_lines: z3.number().int().positive().optional().describe("Maximum number of output lines (default 500). Excess lines are dropped and a summary is appended.")
|
|
653
|
+
},
|
|
602
654
|
{ readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
603
|
-
async () =>
|
|
655
|
+
async ({ filter, max_lines }) => {
|
|
656
|
+
const res = await getMetrics();
|
|
657
|
+
if (!res.ok) return formatResult(res);
|
|
658
|
+
const raw = typeof res.data === "string" ? res.data : res.data !== void 0 ? String(res.data) : "";
|
|
659
|
+
const limit = max_lines ?? METRICS_DEFAULT_MAX_LINES;
|
|
660
|
+
const text = applyMetricsControls(raw, filter, limit);
|
|
661
|
+
return { content: [{ type: "text", text: text || "OK" }] };
|
|
662
|
+
}
|
|
604
663
|
);
|
|
605
664
|
server.tool(
|
|
606
665
|
"caddy_stop",
|
|
@@ -631,7 +690,10 @@ function parseFrom(from) {
|
|
|
631
690
|
const slashIdx = cleaned.indexOf("/");
|
|
632
691
|
if (slashIdx > 0) {
|
|
633
692
|
match.host = [cleaned.substring(0, slashIdx)];
|
|
634
|
-
|
|
693
|
+
const path = cleaned.substring(slashIdx);
|
|
694
|
+
if (path !== "/") {
|
|
695
|
+
match.path = [path];
|
|
696
|
+
}
|
|
635
697
|
} else if (cleaned.startsWith("/")) {
|
|
636
698
|
match.path = [cleaned];
|
|
637
699
|
} else {
|
|
@@ -642,6 +704,21 @@ function parseFrom(from) {
|
|
|
642
704
|
function cleanUpstreamAddr(addr) {
|
|
643
705
|
return addr.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
644
706
|
}
|
|
707
|
+
function isParentMissing(res) {
|
|
708
|
+
if (res.ok) return false;
|
|
709
|
+
if (res.status === 404) return true;
|
|
710
|
+
return res.error?.includes("key does not exist") ?? false;
|
|
711
|
+
}
|
|
712
|
+
function isUnknownId(res) {
|
|
713
|
+
if (res.ok) return false;
|
|
714
|
+
if (res.status === 404) return true;
|
|
715
|
+
const body = (res.error ?? "").toLowerCase();
|
|
716
|
+
return body.includes("unknown object id") || body.includes("no id found");
|
|
717
|
+
}
|
|
718
|
+
function isRouteShape(obj) {
|
|
719
|
+
if (!obj || typeof obj !== "object") return false;
|
|
720
|
+
return Array.isArray(obj.handle);
|
|
721
|
+
}
|
|
645
722
|
function serverNotFoundError(srv) {
|
|
646
723
|
return {
|
|
647
724
|
isError: true,
|
|
@@ -656,14 +733,17 @@ function serverNotFoundError(srv) {
|
|
|
656
733
|
function registerRouteTools(server) {
|
|
657
734
|
server.tool(
|
|
658
735
|
"caddy_reverse_proxy",
|
|
659
|
-
"Add a reverse proxy route. The most common operation \u2014 just specify where traffic comes from and where it goes. Example: from='api.local' to=['localhost:3000'].",
|
|
736
|
+
"Add a reverse proxy route. The most common operation \u2014 just specify where traffic comes from and where it goes. Example: from='api.local' to=['localhost:3000']. When `id` is OMITTED the route is appended to the server's routes array \u2014 calling the tool twice with the same args produces TWO duplicate routes (non-idempotent). When `id` is SUPPLIED the route is written via PUT under that @id, so repeat calls REPLACE in place (idempotent). Strongly recommended: supply a stable `id` for any route managed from automation or production tooling. Note: @ids are config-global in Caddy (NOT route-scoped). If `id` collides with an @id used by a non-route object (TLS issuer, server, etc.) the call refuses with an error rather than clobbering it. Once an @id is registered to a route under one server, subsequent calls update that route in place regardless of the `server` argument.",
|
|
660
737
|
{
|
|
661
738
|
from: z4.string().describe("Domain, path, or domain/path to match (e.g., 'api.local', '/api/*', 'app.local/ws')"),
|
|
662
739
|
to: z4.array(z4.string()).describe("Upstream addresses (e.g., ['localhost:3000', 'localhost:3001'])"),
|
|
663
|
-
server: z4.string().regex(/^[\w-]{1,128}$/).optional().default("srv0").describe("Caddy server name (default: srv0)")
|
|
740
|
+
server: z4.string().regex(/^[\w-]{1,128}$/).optional().default("srv0").describe("Caddy server name (default: srv0)"),
|
|
741
|
+
id: z4.string().regex(/^[\w-]{1,128}$/).optional().describe(
|
|
742
|
+
"Optional stable @id for the route. When set, repeat calls REPLACE the route in place (idempotent). When omitted, the route is APPENDED \u2014 calling twice with identical args creates a duplicate route. @ids are config-global in Caddy: if this id is already used by a non-route object the call refuses rather than clobbering it."
|
|
743
|
+
)
|
|
664
744
|
},
|
|
665
745
|
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
666
|
-
async ({ from, to, server: srv }) => {
|
|
746
|
+
async ({ from, to, server: srv, id }) => {
|
|
667
747
|
const match = parseFrom(from);
|
|
668
748
|
const cleanedTo = to.map(cleanUpstreamAddr);
|
|
669
749
|
const route = {
|
|
@@ -676,11 +756,58 @@ function registerRouteTools(server) {
|
|
|
676
756
|
],
|
|
677
757
|
terminal: true
|
|
678
758
|
};
|
|
759
|
+
if (id) {
|
|
760
|
+
route["@id"] = id;
|
|
761
|
+
const existing = await configByIdGet(id);
|
|
762
|
+
if (existing.ok) {
|
|
763
|
+
if (!isRouteShape(existing.data)) {
|
|
764
|
+
return {
|
|
765
|
+
isError: true,
|
|
766
|
+
content: [
|
|
767
|
+
{
|
|
768
|
+
type: "text",
|
|
769
|
+
text: `Error: @id "${id}" is already in use by a non-route config object (no top-level "handle" array). @ids are config-global in Caddy, not route-scoped -- pick a different id, or remove the existing object first with caddy_config_by_id { id: "${id}", action: "delete" }.`
|
|
770
|
+
}
|
|
771
|
+
]
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const putRes = await configByIdSet(id, route, "PUT");
|
|
775
|
+
if (putRes.ok) {
|
|
776
|
+
return {
|
|
777
|
+
content: [
|
|
778
|
+
{
|
|
779
|
+
type: "text",
|
|
780
|
+
text: `Route set @id="${id}": ${from} \u2192 ${cleanedTo.join(", ")}`
|
|
781
|
+
}
|
|
782
|
+
]
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
return formatResult(putRes);
|
|
786
|
+
}
|
|
787
|
+
if (!isUnknownId(existing)) {
|
|
788
|
+
return formatResult(existing);
|
|
789
|
+
}
|
|
790
|
+
const postRes = await configPost(`apps/http/servers/${srv}/routes`, route);
|
|
791
|
+
if (postRes.ok) {
|
|
792
|
+
return {
|
|
793
|
+
content: [
|
|
794
|
+
{
|
|
795
|
+
type: "text",
|
|
796
|
+
text: `Route created @id="${id}": ${from} \u2192 ${cleanedTo.join(", ")}`
|
|
797
|
+
}
|
|
798
|
+
]
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
if (isParentMissing(postRes)) {
|
|
802
|
+
return serverNotFoundError(srv);
|
|
803
|
+
}
|
|
804
|
+
return formatResult(postRes);
|
|
805
|
+
}
|
|
679
806
|
const res = await configPost(`apps/http/servers/${srv}/routes`, route);
|
|
680
807
|
if (res.ok) {
|
|
681
808
|
return { content: [{ type: "text", text: `Route added: ${from} \u2192 ${cleanedTo.join(", ")}` }] };
|
|
682
809
|
}
|
|
683
|
-
if (
|
|
810
|
+
if (isParentMissing(res)) {
|
|
684
811
|
return serverNotFoundError(srv);
|
|
685
812
|
}
|
|
686
813
|
return formatResult(res);
|
|
@@ -699,7 +826,7 @@ function registerRouteTools(server) {
|
|
|
699
826
|
async ({ match, handle, server: srv, terminal }) => {
|
|
700
827
|
const route = { match, handle, terminal };
|
|
701
828
|
const res = await configPost(`apps/http/servers/${srv}/routes`, route);
|
|
702
|
-
if (
|
|
829
|
+
if (isParentMissing(res)) {
|
|
703
830
|
return serverNotFoundError(srv);
|
|
704
831
|
}
|
|
705
832
|
return formatResult(res);
|
|
@@ -912,9 +1039,9 @@ function buildTlsConfig(fields) {
|
|
|
912
1039
|
}
|
|
913
1040
|
};
|
|
914
1041
|
}
|
|
915
|
-
function bothErrors(label, patchRes,
|
|
1042
|
+
function bothErrors(label, patchRes, writeRes, writeLabel) {
|
|
916
1043
|
const patchErr = patchRes.error || `HTTP ${patchRes.status}`;
|
|
917
|
-
const
|
|
1044
|
+
const writeErr = writeRes.error || `HTTP ${writeRes.status}`;
|
|
918
1045
|
return {
|
|
919
1046
|
isError: true,
|
|
920
1047
|
content: [
|
|
@@ -922,11 +1049,97 @@ function bothErrors(label, patchRes, postRes) {
|
|
|
922
1049
|
type: "text",
|
|
923
1050
|
text: `Error: Failed to set ${label}.
|
|
924
1051
|
PATCH attempt: ${patchErr}
|
|
925
|
-
|
|
1052
|
+
${writeLabel} fallback: ${writeErr}`
|
|
926
1053
|
}
|
|
927
1054
|
]
|
|
928
1055
|
};
|
|
929
1056
|
}
|
|
1057
|
+
function isPlainObject(v) {
|
|
1058
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1059
|
+
}
|
|
1060
|
+
function deepClone(v) {
|
|
1061
|
+
return JSON.parse(JSON.stringify(v));
|
|
1062
|
+
}
|
|
1063
|
+
function mergeIssuerFields(existing, fields) {
|
|
1064
|
+
const merged = deepClone(existing);
|
|
1065
|
+
const automation = merged.automation;
|
|
1066
|
+
const issuer = automation.policies[0].issuers[0];
|
|
1067
|
+
if (fields.email !== void 0) issuer.email = fields.email;
|
|
1068
|
+
if (fields.ca !== void 0) issuer.ca = fields.ca;
|
|
1069
|
+
return merged;
|
|
1070
|
+
}
|
|
1071
|
+
function validateIssuerShape(tls) {
|
|
1072
|
+
const automation = tls.automation;
|
|
1073
|
+
if (!isPlainObject(automation)) {
|
|
1074
|
+
return "apps/tls.automation is missing or not an object";
|
|
1075
|
+
}
|
|
1076
|
+
const policies = automation.policies;
|
|
1077
|
+
if (!Array.isArray(policies) || policies.length === 0) {
|
|
1078
|
+
return "apps/tls.automation.policies is missing, not an array, or empty";
|
|
1079
|
+
}
|
|
1080
|
+
const policy0 = policies[0];
|
|
1081
|
+
if (!isPlainObject(policy0)) {
|
|
1082
|
+
return "apps/tls.automation.policies[0] is not an object";
|
|
1083
|
+
}
|
|
1084
|
+
const issuers = policy0.issuers;
|
|
1085
|
+
if (!Array.isArray(issuers) || issuers.length === 0) {
|
|
1086
|
+
return "apps/tls.automation.policies[0].issuers is missing, not an array, or empty";
|
|
1087
|
+
}
|
|
1088
|
+
const issuer0 = issuers[0];
|
|
1089
|
+
if (!isPlainObject(issuer0)) {
|
|
1090
|
+
return "apps/tls.automation.policies[0].issuers[0] is not an object";
|
|
1091
|
+
}
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
async function safeFallback(label, patchRes, fields) {
|
|
1095
|
+
const getRes = await configGet("apps/tls");
|
|
1096
|
+
const absent = !getRes.ok && getRes.status === 404 ? true : getRes.ok && (getRes.data === void 0 || getRes.data === null);
|
|
1097
|
+
if (absent) {
|
|
1098
|
+
const postRes = await configPost("apps/tls", buildTlsConfig(fields));
|
|
1099
|
+
if (postRes.ok) return { kind: "ok" };
|
|
1100
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, postRes, "POST") };
|
|
1101
|
+
}
|
|
1102
|
+
if (!getRes.ok) {
|
|
1103
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, getRes, "GET apps/tls") };
|
|
1104
|
+
}
|
|
1105
|
+
if (!isPlainObject(getRes.data)) {
|
|
1106
|
+
return {
|
|
1107
|
+
kind: "tool-error",
|
|
1108
|
+
result: {
|
|
1109
|
+
isError: true,
|
|
1110
|
+
content: [
|
|
1111
|
+
{
|
|
1112
|
+
type: "text",
|
|
1113
|
+
text: `Error: Failed to set ${label}.
|
|
1114
|
+
PATCH attempt: ${patchRes.error || `HTTP ${patchRes.status}`}
|
|
1115
|
+
Refusing to clobber existing apps/tls: GET returned a non-object value. Use caddy_config_set with an explicit path to update it safely.`
|
|
1116
|
+
}
|
|
1117
|
+
]
|
|
1118
|
+
}
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
const shapeError = validateIssuerShape(getRes.data);
|
|
1122
|
+
if (shapeError) {
|
|
1123
|
+
return {
|
|
1124
|
+
kind: "tool-error",
|
|
1125
|
+
result: {
|
|
1126
|
+
isError: true,
|
|
1127
|
+
content: [
|
|
1128
|
+
{
|
|
1129
|
+
type: "text",
|
|
1130
|
+
text: `Error: Failed to set ${label}.
|
|
1131
|
+
PATCH attempt: ${patchRes.error || `HTTP ${patchRes.status}`}
|
|
1132
|
+
Refusing to clobber existing apps/tls: ${shapeError}. Use caddy_config_set with an explicit path (e.g. apps/tls/automation/policies) to update it safely.`
|
|
1133
|
+
}
|
|
1134
|
+
]
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
const merged = mergeIssuerFields(getRes.data, fields);
|
|
1139
|
+
const putRes = await configPut("apps/tls", merged);
|
|
1140
|
+
if (putRes.ok) return { kind: "ok" };
|
|
1141
|
+
return { kind: "tool-error", result: bothErrors(label, patchRes, putRes, "PUT") };
|
|
1142
|
+
}
|
|
930
1143
|
function registerTlsTools(server) {
|
|
931
1144
|
server.tool(
|
|
932
1145
|
"caddy_tls",
|
|
@@ -949,9 +1162,9 @@ function registerTlsTools(server) {
|
|
|
949
1162
|
};
|
|
950
1163
|
const patchRes = await configPatch("apps/tls/automation/policies/0/issuers/0/email", email);
|
|
951
1164
|
if (patchRes.ok) return { content: [{ type: "text", text: `ACME email set to: ${email}` }] };
|
|
952
|
-
const
|
|
953
|
-
if (
|
|
954
|
-
return
|
|
1165
|
+
const outcome = await safeFallback("ACME email", patchRes, { email });
|
|
1166
|
+
if (outcome.kind === "ok") return { content: [{ type: "text", text: `ACME email set to: ${email}` }] };
|
|
1167
|
+
return outcome.result;
|
|
955
1168
|
}
|
|
956
1169
|
if (action === "set_acme_ca") {
|
|
957
1170
|
if (!ca)
|
|
@@ -961,9 +1174,9 @@ function registerTlsTools(server) {
|
|
|
961
1174
|
};
|
|
962
1175
|
const patchRes = await configPatch("apps/tls/automation/policies/0/issuers/0/ca", ca);
|
|
963
1176
|
if (patchRes.ok) return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
964
|
-
const
|
|
965
|
-
if (
|
|
966
|
-
return
|
|
1177
|
+
const outcome = await safeFallback("ACME CA", patchRes, { ca });
|
|
1178
|
+
if (outcome.kind === "ok") return { content: [{ type: "text", text: `ACME CA set to: ${ca}` }] };
|
|
1179
|
+
return outcome.result;
|
|
967
1180
|
}
|
|
968
1181
|
return { isError: true, content: [{ type: "text", text: `Unknown action: ${action}` }] };
|
|
969
1182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/caddy-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server for managing Caddy web servers via the admin API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
},
|
|
41
41
|
"overrides": {
|
|
42
42
|
"hono": "^4.12.14",
|
|
43
|
-
"@hono/node-server": "^1.19.13"
|
|
43
|
+
"@hono/node-server": "^1.19.13",
|
|
44
|
+
"postcss": "^8.5.10"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@biomejs/biome": "^2.4.11",
|