@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.
Files changed (2) hide show
  1. package/dist/index.js +58 -62
  2. 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", "HEAD", "PUT", "DELETE"]);
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
- if (elapsed2 + delay + REQUEST_TIMEOUT_MS > requestBudgetMs) {
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 === "1" || options.readonly === "true";
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-9_]([a-zA-Z0-9_-]*[a-zA-Z0-9_])?$/;
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: external_exports.string().url().refine((u) => u.startsWith("https://"), "endpointUrl must use https://").describe("The HTTPS URL to send webhook events to"),
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: external_exports.string().url().refine((u) => u.startsWith("https://"), "endpointUrl must use https://").optional().describe("New HTTPS URL to send webhook events to"),
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.2" : (await null).createRequire(import.meta.url)("../package.json").version;
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 === "1" || process.env.TAILSCALE_READONLY === "true";
33773
+ var readonlyMode = parseReadonlyFlag(process.env.TAILSCALE_READONLY);
33778
33774
  var filterSuffix = formatBannerFilterSuffix({
33779
33775
  unknownProfile,
33780
33776
  explicitTools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "mcpName": "io.github.YawLabs/tailscale-mcp",
5
5
  "description": "Tailscale MCP server for managing your tailnet from AI assistants",
6
6
  "license": "MIT",