@yawlabs/tailscale-mcp 0.12.2 → 0.12.4
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 +58 -62
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30958,7 +30958,7 @@ var MAX_429_RETRIES = 3;
|
|
|
30958
30958
|
var DEFAULT_429_DELAY_MS = 1e3;
|
|
30959
30959
|
var MAX_429_DELAY_MS = 3e4;
|
|
30960
30960
|
var MAX_REQUEST_BUDGET_MS = 9e4;
|
|
30961
|
-
var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET", "
|
|
30961
|
+
var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
|
|
30962
30962
|
var oauthToken = null;
|
|
30963
30963
|
var oauthRefreshPromise = null;
|
|
30964
30964
|
function getAuthConfig() {
|
|
@@ -31188,7 +31188,8 @@ async function apiRequest(method, path, body, options) {
|
|
|
31188
31188
|
if (res.status !== 429 || attempt === MAX_429_RETRIES || !isRetryable) break;
|
|
31189
31189
|
const delay = compute429DelayMs(res.headers.get("retry-after"), attempt);
|
|
31190
31190
|
const elapsed2 = Date.now() - startedAt;
|
|
31191
|
-
|
|
31191
|
+
const nextAttemptBudgetMs = requestBudgetMs - elapsed2 - delay;
|
|
31192
|
+
if (nextAttemptBudgetMs <= 0) {
|
|
31192
31193
|
debugLog(` -> 429 (attempt ${attempt + 1}), giving up: budget exhausted (${elapsed2}ms + ${delay}ms)`);
|
|
31193
31194
|
break;
|
|
31194
31195
|
}
|
|
@@ -31285,6 +31286,9 @@ var PROFILES = {
|
|
|
31285
31286
|
full: []
|
|
31286
31287
|
// empty = all groups
|
|
31287
31288
|
};
|
|
31289
|
+
function parseReadonlyFlag(value) {
|
|
31290
|
+
return value === "1" || value === "true";
|
|
31291
|
+
}
|
|
31288
31292
|
function filterTools(groups, options) {
|
|
31289
31293
|
const validNames = new Set(Object.keys(groups));
|
|
31290
31294
|
let profileGroups;
|
|
@@ -31305,7 +31309,7 @@ function filterTools(groups, options) {
|
|
|
31305
31309
|
const effectiveGroups = explicitTools2 ?? profileGroups ?? null;
|
|
31306
31310
|
const enabledGroups = effectiveGroups ? new Set(effectiveGroups) : null;
|
|
31307
31311
|
const unknownGroups2 = enabledGroups ? [...enabledGroups].filter((g) => !validNames.has(g)) : [];
|
|
31308
|
-
const readonly2 = options.readonly
|
|
31312
|
+
const readonly2 = parseReadonlyFlag(options.readonly);
|
|
31309
31313
|
const out = [];
|
|
31310
31314
|
for (const [name, tools] of Object.entries(groups)) {
|
|
31311
31315
|
if (enabledGroups && !enabledGroups.has(name)) continue;
|
|
@@ -31322,6 +31326,49 @@ function filterTools(groups, options) {
|
|
|
31322
31326
|
return result;
|
|
31323
31327
|
}
|
|
31324
31328
|
|
|
31329
|
+
// src/tools/status.ts
|
|
31330
|
+
function composeTailnetStatusData(devicesRes, settingsRes, extras = {}) {
|
|
31331
|
+
const { deviceCount: _deviceCount, settings: _settings, errors: _errors, ...safeExtras } = extras;
|
|
31332
|
+
const data = {
|
|
31333
|
+
...safeExtras,
|
|
31334
|
+
deviceCount: devicesRes.ok ? devicesRes.data?.devices?.length ?? null : null,
|
|
31335
|
+
settings: settingsRes.ok ? settingsRes.data : null
|
|
31336
|
+
};
|
|
31337
|
+
const errors = {};
|
|
31338
|
+
if (!devicesRes.ok) errors.devices = devicesRes.error ?? `HTTP ${devicesRes.status}`;
|
|
31339
|
+
if (!settingsRes.ok) errors.settings = settingsRes.error ?? `HTTP ${settingsRes.status}`;
|
|
31340
|
+
if (Object.keys(errors).length > 0) data.errors = errors;
|
|
31341
|
+
return data;
|
|
31342
|
+
}
|
|
31343
|
+
var statusTools = [
|
|
31344
|
+
{
|
|
31345
|
+
name: "tailscale_status",
|
|
31346
|
+
description: "Check that the Tailscale API connection is working. Returns your tailnet name, device count, and confirms authentication is valid. Use this to verify setup.",
|
|
31347
|
+
annotations: {
|
|
31348
|
+
title: "Check API status",
|
|
31349
|
+
readOnlyHint: true,
|
|
31350
|
+
destructiveHint: false,
|
|
31351
|
+
idempotentHint: true,
|
|
31352
|
+
openWorldHint: true
|
|
31353
|
+
},
|
|
31354
|
+
inputSchema: external_exports.object({}),
|
|
31355
|
+
handler: async () => {
|
|
31356
|
+
const [devicesRes, settingsRes] = await Promise.all([
|
|
31357
|
+
apiGet(`/tailnet/${getTailnet()}/devices?fields=id`),
|
|
31358
|
+
apiGet(`/tailnet/${getTailnet()}/settings`)
|
|
31359
|
+
]);
|
|
31360
|
+
if (!devicesRes.ok && !settingsRes.ok) {
|
|
31361
|
+
return devicesRes;
|
|
31362
|
+
}
|
|
31363
|
+
const data = composeTailnetStatusData(devicesRes, settingsRes, {
|
|
31364
|
+
connected: true,
|
|
31365
|
+
tailnet: getTailnet()
|
|
31366
|
+
});
|
|
31367
|
+
return { ok: true, status: 200, data };
|
|
31368
|
+
}
|
|
31369
|
+
}
|
|
31370
|
+
];
|
|
31371
|
+
|
|
31325
31372
|
// src/server-wiring.ts
|
|
31326
31373
|
function isLocalCliEnabled(env) {
|
|
31327
31374
|
return env.TAILSCALE_LOCAL_CLI === "1" || env.TAILSCALE_LOCAL_CLI === "true";
|
|
@@ -31371,19 +31418,7 @@ async function tailnetStatusResource(uri) {
|
|
|
31371
31418
|
apiGet(`/tailnet/${getTailnet()}/devices?fields=id`),
|
|
31372
31419
|
apiGet(`/tailnet/${getTailnet()}/settings`)
|
|
31373
31420
|
]);
|
|
31374
|
-
const data = {
|
|
31375
|
-
tailnet: getTailnet(),
|
|
31376
|
-
// `?? null` (not `?? 0`): the request succeeded but the body was missing
|
|
31377
|
-
// a `devices` array (204 / empty content-length / unexpected shape).
|
|
31378
|
-
// Reporting `0` in that case would be confidently wrong; null signals
|
|
31379
|
-
// "unknown" so the caller doesn't conflate it with an actually-empty tailnet.
|
|
31380
|
-
deviceCount: devicesRes.ok ? devicesRes.data?.devices?.length ?? null : null,
|
|
31381
|
-
settings: settingsRes.ok ? settingsRes.data : null
|
|
31382
|
-
};
|
|
31383
|
-
const errors = {};
|
|
31384
|
-
if (!devicesRes.ok) errors.devices = devicesRes.error ?? `HTTP ${devicesRes.status}`;
|
|
31385
|
-
if (!settingsRes.ok) errors.settings = settingsRes.error ?? `HTTP ${settingsRes.status}`;
|
|
31386
|
-
if (Object.keys(errors).length > 0) data.errors = errors;
|
|
31421
|
+
const data = composeTailnetStatusData(devicesRes, settingsRes, { tailnet: getTailnet() });
|
|
31387
31422
|
return { contents: [{ uri: uri.href, text: JSON.stringify(data, null, 2), mimeType: "application/json" }] };
|
|
31388
31423
|
}
|
|
31389
31424
|
async function tailnetDevicesResource(uri) {
|
|
@@ -31616,8 +31651,8 @@ function isCidr(s) {
|
|
|
31616
31651
|
if (slash < 0) return false;
|
|
31617
31652
|
const addr = s.slice(0, slash);
|
|
31618
31653
|
const prefix = s.slice(slash + 1);
|
|
31654
|
+
if (!/^\d+$/.test(prefix)) return false;
|
|
31619
31655
|
const prefixN = Number(prefix);
|
|
31620
|
-
if (!Number.isInteger(prefixN) || prefixN < 0) return false;
|
|
31621
31656
|
if (net.isIPv4(addr)) return prefixN <= 32;
|
|
31622
31657
|
if (net.isIPv6(addr)) return prefixN <= 128;
|
|
31623
31658
|
return false;
|
|
@@ -32655,7 +32690,7 @@ async function runTailscaleCli(args, options = {}) {
|
|
|
32655
32690
|
}
|
|
32656
32691
|
|
|
32657
32692
|
// src/tools/local-cli.ts
|
|
32658
|
-
var HOSTNAME_LABEL = /^[a-zA-Z0-
|
|
32693
|
+
var HOSTNAME_LABEL = /^[a-zA-Z0-9]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;
|
|
32659
32694
|
function isValidPingTarget(s) {
|
|
32660
32695
|
if (s.length === 0 || s.length > 253) return false;
|
|
32661
32696
|
if (net2.isIP(s)) return true;
|
|
@@ -33197,46 +33232,6 @@ var serviceTools = [
|
|
|
33197
33232
|
}
|
|
33198
33233
|
];
|
|
33199
33234
|
|
|
33200
|
-
// src/tools/status.ts
|
|
33201
|
-
var statusTools = [
|
|
33202
|
-
{
|
|
33203
|
-
name: "tailscale_status",
|
|
33204
|
-
description: "Check that the Tailscale API connection is working. Returns your tailnet name, device count, and confirms authentication is valid. Use this to verify setup.",
|
|
33205
|
-
annotations: {
|
|
33206
|
-
title: "Check API status",
|
|
33207
|
-
readOnlyHint: true,
|
|
33208
|
-
destructiveHint: false,
|
|
33209
|
-
idempotentHint: true,
|
|
33210
|
-
openWorldHint: true
|
|
33211
|
-
},
|
|
33212
|
-
inputSchema: external_exports.object({}),
|
|
33213
|
-
handler: async () => {
|
|
33214
|
-
const [devicesRes, settingsRes] = await Promise.all([
|
|
33215
|
-
apiGet(`/tailnet/${getTailnet()}/devices?fields=id`),
|
|
33216
|
-
apiGet(`/tailnet/${getTailnet()}/settings`)
|
|
33217
|
-
]);
|
|
33218
|
-
if (!devicesRes.ok && !settingsRes.ok) {
|
|
33219
|
-
return devicesRes;
|
|
33220
|
-
}
|
|
33221
|
-
const data = {
|
|
33222
|
-
connected: true,
|
|
33223
|
-
tailnet: getTailnet(),
|
|
33224
|
-
// `?? null` (not `?? 0`): the request succeeded but the body was missing
|
|
33225
|
-
// a `devices` array (204 / empty content-length / unexpected shape).
|
|
33226
|
-
// Reporting `0` in that case would be confidently wrong; null signals
|
|
33227
|
-
// "unknown" so the caller doesn't conflate it with an actually-empty tailnet.
|
|
33228
|
-
deviceCount: devicesRes.ok ? devicesRes.data?.devices?.length ?? null : null,
|
|
33229
|
-
settings: settingsRes.ok ? settingsRes.data : null
|
|
33230
|
-
};
|
|
33231
|
-
const errors = {};
|
|
33232
|
-
if (!devicesRes.ok) errors.devices = devicesRes.error ?? `HTTP ${devicesRes.status}`;
|
|
33233
|
-
if (!settingsRes.ok) errors.settings = settingsRes.error ?? `HTTP ${settingsRes.status}`;
|
|
33234
|
-
if (Object.keys(errors).length > 0) data.errors = errors;
|
|
33235
|
-
return { ok: true, status: 200, data };
|
|
33236
|
-
}
|
|
33237
|
-
}
|
|
33238
|
-
];
|
|
33239
|
-
|
|
33240
33235
|
// src/tools/tailnet.ts
|
|
33241
33236
|
var tailnetTools = [
|
|
33242
33237
|
{
|
|
@@ -33526,6 +33521,7 @@ function getAllowedWebhookEvents() {
|
|
|
33526
33521
|
const extras = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
33527
33522
|
return /* @__PURE__ */ new Set([...STATIC_WEBHOOK_EVENT_TYPES, ...extras]);
|
|
33528
33523
|
}
|
|
33524
|
+
var endpointUrlSchema = external_exports.string().url().refine((u) => u.startsWith("https://"), "endpointUrl must use https://");
|
|
33529
33525
|
var webhookSubscriptionsSchema = external_exports.array(external_exports.string()).min(1).superRefine((arr, ctx) => {
|
|
33530
33526
|
const allowed = getAllowedWebhookEvents();
|
|
33531
33527
|
let knownEventsList = null;
|
|
@@ -33589,7 +33585,7 @@ var webhookTools = [
|
|
|
33589
33585
|
openWorldHint: true
|
|
33590
33586
|
},
|
|
33591
33587
|
inputSchema: external_exports.object({
|
|
33592
|
-
endpointUrl:
|
|
33588
|
+
endpointUrl: endpointUrlSchema.describe("The HTTPS URL to send webhook events to"),
|
|
33593
33589
|
subscriptions: webhookSubscriptionsSchema.describe("Event types to subscribe to (at least one)")
|
|
33594
33590
|
}),
|
|
33595
33591
|
handler: async (input) => {
|
|
@@ -33611,7 +33607,7 @@ var webhookTools = [
|
|
|
33611
33607
|
},
|
|
33612
33608
|
inputSchema: external_exports.object({
|
|
33613
33609
|
webhookId: external_exports.string().describe("The webhook ID to update"),
|
|
33614
|
-
endpointUrl:
|
|
33610
|
+
endpointUrl: endpointUrlSchema.optional().describe("New HTTPS URL to send webhook events to"),
|
|
33615
33611
|
subscriptions: webhookSubscriptionsSchema.optional().describe("Updated list of event types to subscribe to (at least one)")
|
|
33616
33612
|
}),
|
|
33617
33613
|
handler: async (input) => {
|
|
@@ -33680,7 +33676,7 @@ var webhookTools = [
|
|
|
33680
33676
|
];
|
|
33681
33677
|
|
|
33682
33678
|
// src/index.ts
|
|
33683
|
-
var version2 = true ? "0.12.
|
|
33679
|
+
var version2 = true ? "0.12.4" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
33684
33680
|
var subcommand = process.argv[2];
|
|
33685
33681
|
if (subcommand === "deploy-acl") {
|
|
33686
33682
|
const filePath = process.argv[3];
|
|
@@ -33774,7 +33770,7 @@ server.resource(
|
|
|
33774
33770
|
);
|
|
33775
33771
|
var transport = new StdioServerTransport();
|
|
33776
33772
|
await server.connect(transport);
|
|
33777
|
-
var readonlyMode = process.env.TAILSCALE_READONLY
|
|
33773
|
+
var readonlyMode = parseReadonlyFlag(process.env.TAILSCALE_READONLY);
|
|
33778
33774
|
var filterSuffix = formatBannerFilterSuffix({
|
|
33779
33775
|
unknownProfile,
|
|
33780
33776
|
explicitTools,
|