@yawlabs/caddy-mcp 1.0.0 → 1.0.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/dist/index.js CHANGED
@@ -41,6 +41,16 @@ function getHeaders(contentType) {
41
41
  function normalizePath(path) {
42
42
  return path.replace(/^\/?(config(\/|$))?/, "");
43
43
  }
44
+ function rejectTraversal(path) {
45
+ if (/(^|\/)\.\.(\/|$)/.test(path)) {
46
+ return {
47
+ ok: false,
48
+ status: 0,
49
+ error: `Invalid path "${path}": '..' segments are not allowed`
50
+ };
51
+ }
52
+ return null;
53
+ }
44
54
  function sleep(ms) {
45
55
  return new Promise((resolve) => setTimeout(resolve, ms));
46
56
  }
@@ -129,22 +139,32 @@ async function attemptRequest(method, path, body, contentType, timeout) {
129
139
  }
130
140
  function configGet(path = "") {
131
141
  const normalized = normalizePath(path);
142
+ const bad = rejectTraversal(normalized);
143
+ if (bad) return Promise.resolve(bad);
132
144
  return caddyRequest("GET", `/config/${normalized}`);
133
145
  }
134
146
  function configPost(path, value) {
135
147
  const normalized = normalizePath(path);
148
+ const bad = rejectTraversal(normalized);
149
+ if (bad) return Promise.resolve(bad);
136
150
  return caddyRequest("POST", `/config/${normalized}`, value);
137
151
  }
138
152
  function configPut(path, value) {
139
153
  const normalized = normalizePath(path);
154
+ const bad = rejectTraversal(normalized);
155
+ if (bad) return Promise.resolve(bad);
140
156
  return caddyRequest("PUT", `/config/${normalized}`, value);
141
157
  }
142
158
  function configPatch(path, value) {
143
159
  const normalized = normalizePath(path);
160
+ const bad = rejectTraversal(normalized);
161
+ if (bad) return Promise.resolve(bad);
144
162
  return caddyRequest("PATCH", `/config/${normalized}`, value);
145
163
  }
146
164
  function configDelete(path) {
147
165
  const normalized = normalizePath(path);
166
+ const bad = rejectTraversal(normalized);
167
+ if (bad) return Promise.resolve(bad);
148
168
  return caddyRequest("DELETE", `/config/${normalized}`);
149
169
  }
150
170
  var LOAD_TIMEOUT = 6e4;
@@ -169,14 +189,20 @@ function getPkiCertificates(ca = "local") {
169
189
  return caddyRequest("GET", `/pki/ca/${ca}/certificates`);
170
190
  }
171
191
  function configByIdGet(id, subpath = "") {
192
+ const bad = rejectTraversal(subpath);
193
+ if (bad) return Promise.resolve(bad);
172
194
  const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
173
195
  return caddyRequest("GET", path);
174
196
  }
175
197
  function configByIdSet(id, value, method = "PATCH", subpath = "") {
198
+ const bad = rejectTraversal(subpath);
199
+ if (bad) return Promise.resolve(bad);
176
200
  const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
177
201
  return caddyRequest(method, path, value);
178
202
  }
179
203
  function configByIdDelete(id, subpath = "") {
204
+ const bad = rejectTraversal(subpath);
205
+ if (bad) return Promise.resolve(bad);
180
206
  const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
181
207
  return caddyRequest("DELETE", path);
182
208
  }
@@ -399,6 +425,12 @@ ${lines.join("\n")}` }] };
399
425
  if (action === "save") {
400
426
  const current2 = await configGet();
401
427
  if (!current2.ok) return formatResult(current2);
428
+ if (current2.data === void 0) {
429
+ return {
430
+ isError: true,
431
+ content: [{ type: "text", text: "Error: no config loaded to snapshot" }]
432
+ };
433
+ }
402
434
  saveSnapshot(current2.data, "manual");
403
435
  return { content: [{ type: "text", text: "Snapshot saved." }] };
404
436
  }
@@ -809,7 +841,7 @@ function registerRouteTools(server) {
809
841
  );
810
842
  server.tool(
811
843
  "caddy_remove_route",
812
- "Remove a route. Target by @id (preferred \u2014 stable across reorderings) or by array index on a specific server. Uses ETags to prevent concurrent overwrites.",
844
+ "Remove a route. Target by @id (preferred \u2014 stable across reorderings) or by array index on a specific server. Index-based removal is a two-step read-then-delete and can race against concurrent edits; prefer @id when possible.",
813
845
  {
814
846
  id: z4.string().regex(/^[\w-]{1,128}$/).optional().describe("The @id of the route to remove (preferred \u2014 stable even if routes get reordered)"),
815
847
  index: z4.number().int().nonnegative().optional().describe("Zero-based index of the route in the server's routes array (only used if id is not provided)"),
package/dist/server.js CHANGED
@@ -39,6 +39,16 @@ function getHeaders(contentType) {
39
39
  function normalizePath(path) {
40
40
  return path.replace(/^\/?(config(\/|$))?/, "");
41
41
  }
42
+ function rejectTraversal(path) {
43
+ if (/(^|\/)\.\.(\/|$)/.test(path)) {
44
+ return {
45
+ ok: false,
46
+ status: 0,
47
+ error: `Invalid path "${path}": '..' segments are not allowed`
48
+ };
49
+ }
50
+ return null;
51
+ }
42
52
  function sleep(ms) {
43
53
  return new Promise((resolve) => setTimeout(resolve, ms));
44
54
  }
@@ -127,22 +137,32 @@ async function attemptRequest(method, path, body, contentType, timeout) {
127
137
  }
128
138
  function configGet(path = "") {
129
139
  const normalized = normalizePath(path);
140
+ const bad = rejectTraversal(normalized);
141
+ if (bad) return Promise.resolve(bad);
130
142
  return caddyRequest("GET", `/config/${normalized}`);
131
143
  }
132
144
  function configPost(path, value) {
133
145
  const normalized = normalizePath(path);
146
+ const bad = rejectTraversal(normalized);
147
+ if (bad) return Promise.resolve(bad);
134
148
  return caddyRequest("POST", `/config/${normalized}`, value);
135
149
  }
136
150
  function configPut(path, value) {
137
151
  const normalized = normalizePath(path);
152
+ const bad = rejectTraversal(normalized);
153
+ if (bad) return Promise.resolve(bad);
138
154
  return caddyRequest("PUT", `/config/${normalized}`, value);
139
155
  }
140
156
  function configPatch(path, value) {
141
157
  const normalized = normalizePath(path);
158
+ const bad = rejectTraversal(normalized);
159
+ if (bad) return Promise.resolve(bad);
142
160
  return caddyRequest("PATCH", `/config/${normalized}`, value);
143
161
  }
144
162
  function configDelete(path) {
145
163
  const normalized = normalizePath(path);
164
+ const bad = rejectTraversal(normalized);
165
+ if (bad) return Promise.resolve(bad);
146
166
  return caddyRequest("DELETE", `/config/${normalized}`);
147
167
  }
148
168
  var LOAD_TIMEOUT = 6e4;
@@ -167,14 +187,20 @@ function getPkiCertificates(ca = "local") {
167
187
  return caddyRequest("GET", `/pki/ca/${ca}/certificates`);
168
188
  }
169
189
  function configByIdGet(id, subpath = "") {
190
+ const bad = rejectTraversal(subpath);
191
+ if (bad) return Promise.resolve(bad);
170
192
  const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
171
193
  return caddyRequest("GET", path);
172
194
  }
173
195
  function configByIdSet(id, value, method = "PATCH", subpath = "") {
196
+ const bad = rejectTraversal(subpath);
197
+ if (bad) return Promise.resolve(bad);
174
198
  const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
175
199
  return caddyRequest(method, path, value);
176
200
  }
177
201
  function configByIdDelete(id, subpath = "") {
202
+ const bad = rejectTraversal(subpath);
203
+ if (bad) return Promise.resolve(bad);
178
204
  const path = subpath ? `/id/${id}/${subpath}` : `/id/${id}`;
179
205
  return caddyRequest("DELETE", path);
180
206
  }
@@ -397,6 +423,12 @@ ${lines.join("\n")}` }] };
397
423
  if (action === "save") {
398
424
  const current2 = await configGet();
399
425
  if (!current2.ok) return formatResult(current2);
426
+ if (current2.data === void 0) {
427
+ return {
428
+ isError: true,
429
+ content: [{ type: "text", text: "Error: no config loaded to snapshot" }]
430
+ };
431
+ }
400
432
  saveSnapshot(current2.data, "manual");
401
433
  return { content: [{ type: "text", text: "Snapshot saved." }] };
402
434
  }
@@ -807,7 +839,7 @@ function registerRouteTools(server) {
807
839
  );
808
840
  server.tool(
809
841
  "caddy_remove_route",
810
- "Remove a route. Target by @id (preferred \u2014 stable across reorderings) or by array index on a specific server. Uses ETags to prevent concurrent overwrites.",
842
+ "Remove a route. Target by @id (preferred \u2014 stable across reorderings) or by array index on a specific server. Index-based removal is a two-step read-then-delete and can race against concurrent edits; prefer @id when possible.",
811
843
  {
812
844
  id: z4.string().regex(/^[\w-]{1,128}$/).optional().describe("The @id of the route to remove (preferred \u2014 stable even if routes get reordered)"),
813
845
  index: z4.number().int().nonnegative().optional().describe("Zero-based index of the route in the server's routes array (only used if id is not provided)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/caddy-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
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)",