@yawlabs/caddy-mcp 2.5.0 → 2.5.2

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
@@ -13,6 +13,7 @@ var RETRY_BASE_MS = 100;
13
13
  var RETRY_MAX_DELAY_MS = 2e3;
14
14
  var RETRY_MAX_JITTER_MS = 50;
15
15
  var RETRY_HARD_CAP = 5;
16
+ var ADMIN_RESTART_SETTLE_MS = 250;
16
17
  var etagCache = /* @__PURE__ */ new Map();
17
18
  var MAX_ETAG_CACHE = 256;
18
19
  function setEtag(path, etag) {
@@ -136,45 +137,57 @@ async function caddyRequest(method, path, body, contentType, timeout, rawStringB
136
137
  }
137
138
  const maxRetries = getMaxRetries();
138
139
  let attempt = 0;
139
- let res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
140
- while (isRetryableMethod(method, path) && isTransientFailure(res) && attempt < maxRetries) {
140
+ let { res, refused } = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
141
+ while (isTransientFailure(res) && (refused || isRetryableMethod(method, path)) && attempt < maxRetries) {
141
142
  attempt++;
142
143
  const backoff = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
143
144
  const delay = backoff + Math.random() * RETRY_MAX_JITTER_MS;
144
145
  await sleep(delay);
145
- res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
146
+ ({ res, refused } = await attemptRequest(method, path, body, contentType, timeout, rawStringBody));
146
147
  }
147
148
  return res;
148
149
  }
150
+ function isConnectionRefused(err) {
151
+ let current = err;
152
+ for (let depth = 0; depth < 5 && current !== null && typeof current === "object"; depth++) {
153
+ const e = current;
154
+ if (e.code === "ECONNREFUSED") return true;
155
+ if (Array.isArray(e.errors) && e.errors.length > 0 && e.errors.every((inner) => inner?.code === "ECONNREFUSED")) {
156
+ return true;
157
+ }
158
+ current = e.cause;
159
+ }
160
+ return false;
161
+ }
149
162
  function sendViaUnixSocket(socketPath, path, method, headers, body, timeoutMs) {
150
163
  return new Promise((resolve, reject) => {
151
- const req = httpRequest({ socketPath, path, method, headers, signal: AbortSignal.timeout(timeoutMs) }, (res) => {
152
- const chunks = [];
153
- res.on("data", (chunk) => chunks.push(chunk));
154
- res.on("error", reject);
155
- res.on("end", () => {
156
- const status = res.statusCode ?? 0;
157
- const etag = res.headers.etag;
158
- resolve({
159
- ok: status >= 200 && status < 300,
160
- status,
161
- text: Buffer.concat(chunks).toString("utf8"),
162
- etag: typeof etag === "string" ? etag : void 0
164
+ const req = httpRequest(
165
+ { socketPath, path, method, headers, agent: false, signal: AbortSignal.timeout(timeoutMs) },
166
+ (res) => {
167
+ const chunks = [];
168
+ res.on("data", (chunk) => chunks.push(chunk));
169
+ res.on("error", reject);
170
+ res.on("end", () => {
171
+ const status = res.statusCode ?? 0;
172
+ const etag = res.headers.etag;
173
+ resolve({
174
+ ok: status >= 200 && status < 300,
175
+ status,
176
+ text: Buffer.concat(chunks).toString("utf8"),
177
+ etag: typeof etag === "string" ? etag : void 0
178
+ });
163
179
  });
164
- });
165
- });
180
+ }
181
+ );
166
182
  req.on("error", reject);
167
183
  if (body !== void 0) req.write(body);
168
184
  req.end();
169
185
  });
170
186
  }
171
187
  async function sendViaFetch(url, method, headers, body, timeoutMs) {
172
- const res = await fetch(url, {
173
- method,
174
- headers,
175
- body,
176
- signal: AbortSignal.timeout(timeoutMs)
177
- });
188
+ const pending = fetch(url, { method, headers, body, signal: AbortSignal.timeout(timeoutMs) });
189
+ hookGlobalDispatcher();
190
+ const res = await pending;
178
191
  return {
179
192
  ok: res.ok,
180
193
  status: res.status,
@@ -182,7 +195,59 @@ async function sendViaFetch(url, method, headers, body, timeoutMs) {
182
195
  etag: res.headers.get("ETag") || void 0
183
196
  };
184
197
  }
198
+ var GLOBAL_DISPATCHER_KEY = /* @__PURE__ */ Symbol.for("undici.globalDispatcher.1");
199
+ var liveSockets = /* @__PURE__ */ new Map();
200
+ var socketWaiters = /* @__PURE__ */ new Set();
201
+ var dispatcherHooked = false;
202
+ function hookGlobalDispatcher() {
203
+ if (dispatcherHooked) return;
204
+ const dispatcher = globalThis[GLOBAL_DISPATCHER_KEY];
205
+ if (!dispatcher || typeof dispatcher.on !== "function") return;
206
+ dispatcherHooked = true;
207
+ dispatcher.on("connect", (origin) => {
208
+ const key = originOf(origin);
209
+ if (key) liveSockets.set(key, (liveSockets.get(key) ?? 0) + 1);
210
+ });
211
+ dispatcher.on("disconnect", (origin) => {
212
+ const key = originOf(origin);
213
+ if (!key) return;
214
+ const left = Math.max(0, (liveSockets.get(key) ?? 0) - 1);
215
+ liveSockets.set(key, left);
216
+ if (left === 0) for (const wake of socketWaiters) wake();
217
+ });
218
+ }
219
+ function originOf(origin) {
220
+ try {
221
+ return new URL(String(origin)).origin;
222
+ } catch {
223
+ return void 0;
224
+ }
225
+ }
226
+ function settleAdminRestart(origin) {
227
+ if (!origin || !dispatcherHooked || (liveSockets.get(origin) ?? 0) === 0) return Promise.resolve();
228
+ return new Promise((resolve) => {
229
+ const timer = setTimeout(done, ADMIN_RESTART_SETTLE_MS);
230
+ function check() {
231
+ if ((liveSockets.get(origin) ?? 0) === 0) done();
232
+ }
233
+ function done() {
234
+ clearTimeout(timer);
235
+ socketWaiters.delete(check);
236
+ resolve();
237
+ }
238
+ socketWaiters.add(check);
239
+ });
240
+ }
241
+ function isConfigChange(method, path) {
242
+ if (method === "GET") return false;
243
+ return path === "/load" || path.startsWith("/config/") || path.startsWith("/id/");
244
+ }
185
245
  async function attemptRequest(method, path, body, contentType, timeout, rawStringBody = false) {
246
+ const transport = { refused: false };
247
+ const res = await sendOnce(transport, method, path, body, contentType, timeout, rawStringBody);
248
+ return { res, refused: transport.refused };
249
+ }
250
+ async function sendOnce(transport, method, path, body, contentType, timeout, rawStringBody = false) {
186
251
  const socketPath = getUnixSocketPath();
187
252
  const url = `${getBaseUrl()}${path}`;
188
253
  const effectiveTimeout = timeout ?? getRequestTimeout();
@@ -197,6 +262,7 @@ async function attemptRequest(method, path, body, contentType, timeout, rawStrin
197
262
  }
198
263
  const serializedBody = hasBody ? rawStringBody && typeof body === "string" ? body : JSON.stringify(body) : void 0;
199
264
  const res = socketPath ? await sendViaUnixSocket(socketPath, path, method, headers, serializedBody, effectiveTimeout) : await sendViaFetch(url, method, headers, serializedBody, effectiveTimeout);
265
+ if (!socketPath && res.ok && isConfigChange(method, path)) await settleAdminRestart(getAdminOrigin());
200
266
  const text = res.text;
201
267
  const etag = res.etag;
202
268
  if (method === "GET" && etag && isConfigPath) {
@@ -238,6 +304,7 @@ async function attemptRequest(method, path, body, contentType, timeout, rawStrin
238
304
  return { ok: true, status: res.status, data: text, etag };
239
305
  }
240
306
  } catch (err) {
307
+ transport.refused = isConnectionRefused(err);
241
308
  const msg = err instanceof Error ? err.message : String(err);
242
309
  if (socketPath && msg.includes("ENOENT")) {
243
310
  return {
package/dist/server.js CHANGED
@@ -11,6 +11,7 @@ var RETRY_BASE_MS = 100;
11
11
  var RETRY_MAX_DELAY_MS = 2e3;
12
12
  var RETRY_MAX_JITTER_MS = 50;
13
13
  var RETRY_HARD_CAP = 5;
14
+ var ADMIN_RESTART_SETTLE_MS = 250;
14
15
  var etagCache = /* @__PURE__ */ new Map();
15
16
  var MAX_ETAG_CACHE = 256;
16
17
  function setEtag(path, etag) {
@@ -134,45 +135,57 @@ async function caddyRequest(method, path, body, contentType, timeout, rawStringB
134
135
  }
135
136
  const maxRetries = getMaxRetries();
136
137
  let attempt = 0;
137
- let res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
138
- while (isRetryableMethod(method, path) && isTransientFailure(res) && attempt < maxRetries) {
138
+ let { res, refused } = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
139
+ while (isTransientFailure(res) && (refused || isRetryableMethod(method, path)) && attempt < maxRetries) {
139
140
  attempt++;
140
141
  const backoff = Math.min(RETRY_BASE_MS * 2 ** (attempt - 1), RETRY_MAX_DELAY_MS);
141
142
  const delay = backoff + Math.random() * RETRY_MAX_JITTER_MS;
142
143
  await sleep(delay);
143
- res = await attemptRequest(method, path, body, contentType, timeout, rawStringBody);
144
+ ({ res, refused } = await attemptRequest(method, path, body, contentType, timeout, rawStringBody));
144
145
  }
145
146
  return res;
146
147
  }
148
+ function isConnectionRefused(err) {
149
+ let current = err;
150
+ for (let depth = 0; depth < 5 && current !== null && typeof current === "object"; depth++) {
151
+ const e = current;
152
+ if (e.code === "ECONNREFUSED") return true;
153
+ if (Array.isArray(e.errors) && e.errors.length > 0 && e.errors.every((inner) => inner?.code === "ECONNREFUSED")) {
154
+ return true;
155
+ }
156
+ current = e.cause;
157
+ }
158
+ return false;
159
+ }
147
160
  function sendViaUnixSocket(socketPath, path, method, headers, body, timeoutMs) {
148
161
  return new Promise((resolve, reject) => {
149
- const req = httpRequest({ socketPath, path, method, headers, signal: AbortSignal.timeout(timeoutMs) }, (res) => {
150
- const chunks = [];
151
- res.on("data", (chunk) => chunks.push(chunk));
152
- res.on("error", reject);
153
- res.on("end", () => {
154
- const status = res.statusCode ?? 0;
155
- const etag = res.headers.etag;
156
- resolve({
157
- ok: status >= 200 && status < 300,
158
- status,
159
- text: Buffer.concat(chunks).toString("utf8"),
160
- etag: typeof etag === "string" ? etag : void 0
162
+ const req = httpRequest(
163
+ { socketPath, path, method, headers, agent: false, signal: AbortSignal.timeout(timeoutMs) },
164
+ (res) => {
165
+ const chunks = [];
166
+ res.on("data", (chunk) => chunks.push(chunk));
167
+ res.on("error", reject);
168
+ res.on("end", () => {
169
+ const status = res.statusCode ?? 0;
170
+ const etag = res.headers.etag;
171
+ resolve({
172
+ ok: status >= 200 && status < 300,
173
+ status,
174
+ text: Buffer.concat(chunks).toString("utf8"),
175
+ etag: typeof etag === "string" ? etag : void 0
176
+ });
161
177
  });
162
- });
163
- });
178
+ }
179
+ );
164
180
  req.on("error", reject);
165
181
  if (body !== void 0) req.write(body);
166
182
  req.end();
167
183
  });
168
184
  }
169
185
  async function sendViaFetch(url, method, headers, body, timeoutMs) {
170
- const res = await fetch(url, {
171
- method,
172
- headers,
173
- body,
174
- signal: AbortSignal.timeout(timeoutMs)
175
- });
186
+ const pending = fetch(url, { method, headers, body, signal: AbortSignal.timeout(timeoutMs) });
187
+ hookGlobalDispatcher();
188
+ const res = await pending;
176
189
  return {
177
190
  ok: res.ok,
178
191
  status: res.status,
@@ -180,7 +193,59 @@ async function sendViaFetch(url, method, headers, body, timeoutMs) {
180
193
  etag: res.headers.get("ETag") || void 0
181
194
  };
182
195
  }
196
+ var GLOBAL_DISPATCHER_KEY = /* @__PURE__ */ Symbol.for("undici.globalDispatcher.1");
197
+ var liveSockets = /* @__PURE__ */ new Map();
198
+ var socketWaiters = /* @__PURE__ */ new Set();
199
+ var dispatcherHooked = false;
200
+ function hookGlobalDispatcher() {
201
+ if (dispatcherHooked) return;
202
+ const dispatcher = globalThis[GLOBAL_DISPATCHER_KEY];
203
+ if (!dispatcher || typeof dispatcher.on !== "function") return;
204
+ dispatcherHooked = true;
205
+ dispatcher.on("connect", (origin) => {
206
+ const key = originOf(origin);
207
+ if (key) liveSockets.set(key, (liveSockets.get(key) ?? 0) + 1);
208
+ });
209
+ dispatcher.on("disconnect", (origin) => {
210
+ const key = originOf(origin);
211
+ if (!key) return;
212
+ const left = Math.max(0, (liveSockets.get(key) ?? 0) - 1);
213
+ liveSockets.set(key, left);
214
+ if (left === 0) for (const wake of socketWaiters) wake();
215
+ });
216
+ }
217
+ function originOf(origin) {
218
+ try {
219
+ return new URL(String(origin)).origin;
220
+ } catch {
221
+ return void 0;
222
+ }
223
+ }
224
+ function settleAdminRestart(origin) {
225
+ if (!origin || !dispatcherHooked || (liveSockets.get(origin) ?? 0) === 0) return Promise.resolve();
226
+ return new Promise((resolve) => {
227
+ const timer = setTimeout(done, ADMIN_RESTART_SETTLE_MS);
228
+ function check() {
229
+ if ((liveSockets.get(origin) ?? 0) === 0) done();
230
+ }
231
+ function done() {
232
+ clearTimeout(timer);
233
+ socketWaiters.delete(check);
234
+ resolve();
235
+ }
236
+ socketWaiters.add(check);
237
+ });
238
+ }
239
+ function isConfigChange(method, path) {
240
+ if (method === "GET") return false;
241
+ return path === "/load" || path.startsWith("/config/") || path.startsWith("/id/");
242
+ }
183
243
  async function attemptRequest(method, path, body, contentType, timeout, rawStringBody = false) {
244
+ const transport = { refused: false };
245
+ const res = await sendOnce(transport, method, path, body, contentType, timeout, rawStringBody);
246
+ return { res, refused: transport.refused };
247
+ }
248
+ async function sendOnce(transport, method, path, body, contentType, timeout, rawStringBody = false) {
184
249
  const socketPath = getUnixSocketPath();
185
250
  const url = `${getBaseUrl()}${path}`;
186
251
  const effectiveTimeout = timeout ?? getRequestTimeout();
@@ -195,6 +260,7 @@ async function attemptRequest(method, path, body, contentType, timeout, rawStrin
195
260
  }
196
261
  const serializedBody = hasBody ? rawStringBody && typeof body === "string" ? body : JSON.stringify(body) : void 0;
197
262
  const res = socketPath ? await sendViaUnixSocket(socketPath, path, method, headers, serializedBody, effectiveTimeout) : await sendViaFetch(url, method, headers, serializedBody, effectiveTimeout);
263
+ if (!socketPath && res.ok && isConfigChange(method, path)) await settleAdminRestart(getAdminOrigin());
198
264
  const text = res.text;
199
265
  const etag = res.etag;
200
266
  if (method === "GET" && etag && isConfigPath) {
@@ -236,6 +302,7 @@ async function attemptRequest(method, path, body, contentType, timeout, rawStrin
236
302
  return { ok: true, status: res.status, data: text, etag };
237
303
  }
238
304
  } catch (err) {
305
+ transport.refused = isConnectionRefused(err);
239
306
  const msg = err instanceof Error ? err.message : String(err);
240
307
  if (socketPath && msg.includes("ENOENT")) {
241
308
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/caddy-mcp",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "mcpName": "io.github.YawLabs/caddy-mcp",
5
5
  "description": "Caddy MCP server for Claude Code, Cursor, and any MCP client: admin API, config, routes, reverse proxy, TLS, PKI, metrics",
6
6
  "license": "MIT",
@@ -38,16 +38,16 @@
38
38
  "start": "node dist/index.js"
39
39
  },
40
40
  "dependencies": {
41
- "@modelcontextprotocol/sdk": "^1.29.0",
41
+ "@modelcontextprotocol/sdk": "^1.30.0",
42
42
  "zod": "^4.3.6"
43
43
  },
44
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",
45
+ "hono": "^4.13.5",
46
+ "@hono/node-server": "^1.19.15",
47
+ "postcss": "^8.5.23",
48
+ "ip-address": "^10.3.1",
49
+ "fast-uri": "^3.1.6",
50
+ "qs": "^6.16.0",
51
51
  "esbuild": "^0.28.1"
52
52
  },
53
53
  "devDependencies": {
@@ -80,7 +80,8 @@
80
80
  "claude-code",
81
81
  "cursor",
82
82
  "devtools",
83
- "ai"
83
+ "ai",
84
+ "ai-agents"
84
85
  ],
85
86
  "repository": {
86
87
  "type": "git",