@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 +90 -23
- package/dist/server.js +90 -23
- package/package.json +10 -9
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)
|
|
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(
|
|
152
|
-
|
|
153
|
-
res
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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)
|
|
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(
|
|
150
|
-
|
|
151
|
-
res
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
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.
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
42
42
|
"zod": "^4.3.6"
|
|
43
43
|
},
|
|
44
44
|
"overrides": {
|
|
45
|
-
"hono": "^4.
|
|
46
|
-
"@hono/node-server": "^1.19.
|
|
47
|
-
"postcss": "^8.5.
|
|
48
|
-
"ip-address": "^10.
|
|
49
|
-
"fast-uri": "^3.1.
|
|
50
|
-
"qs": "^6.
|
|
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",
|