@yawlabs/tailscale-mcp 0.10.4 → 0.10.6

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 +139 -121
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3104,7 +3104,7 @@ var require_utils = __commonJS({
3104
3104
  "node_modules/fast-uri/lib/utils.js"(exports, module) {
3105
3105
  "use strict";
3106
3106
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3107
- var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3107
+ var isIPv42 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3108
3108
  function stringArrayToHexStripped(input) {
3109
3109
  let acc = "";
3110
3110
  let code = 0;
@@ -3327,7 +3327,7 @@ var require_utils = __commonJS({
3327
3327
  }
3328
3328
  if (component.host !== void 0) {
3329
3329
  let host = unescape(component.host);
3330
- if (!isIPv4(host)) {
3330
+ if (!isIPv42(host)) {
3331
3331
  const ipV6res = normalizeIPv6(host);
3332
3332
  if (ipV6res.isIPV6 === true) {
3333
3333
  host = `[${ipV6res.escapedHost}]`;
@@ -3348,7 +3348,7 @@ var require_utils = __commonJS({
3348
3348
  recomposeAuthority,
3349
3349
  normalizeComponentEncoding,
3350
3350
  removeDotSegments,
3351
- isIPv4,
3351
+ isIPv4: isIPv42,
3352
3352
  isUUID,
3353
3353
  normalizeIPv6,
3354
3354
  stringArrayToHexStripped
@@ -3570,7 +3570,7 @@ var require_schemes = __commonJS({
3570
3570
  var require_fast_uri = __commonJS({
3571
3571
  "node_modules/fast-uri/index.js"(exports, module) {
3572
3572
  "use strict";
3573
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
3573
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4: isIPv42, nonSimpleDomain } = require_utils();
3574
3574
  var { SCHEMES, getSchemeHandler } = require_schemes();
3575
3575
  function normalize(uri, options) {
3576
3576
  if (typeof uri === "string") {
@@ -3751,7 +3751,7 @@ var require_fast_uri = __commonJS({
3751
3751
  parsed.port = matches[5];
3752
3752
  }
3753
3753
  if (parsed.host) {
3754
- const ipv4result = isIPv4(parsed.host);
3754
+ const ipv4result = isIPv42(parsed.host);
3755
3755
  if (ipv4result === false) {
3756
3756
  const ipv6result = normalizeIPv6(parsed.host);
3757
3757
  parsed.host = ipv6result.host.toLowerCase();
@@ -30113,6 +30113,9 @@ var StdioServerTransport = class {
30113
30113
  }
30114
30114
  };
30115
30115
 
30116
+ // src/cli.ts
30117
+ import { readFileSync } from "node:fs";
30118
+
30116
30119
  // src/api.ts
30117
30120
  var BASE_URL = "https://api.tailscale.com/api/v2";
30118
30121
  var REQUEST_TIMEOUT_MS = 3e4;
@@ -30207,15 +30210,20 @@ function validateTags(tags) {
30207
30210
  function sanitizeDescription(value) {
30208
30211
  return value.replace(/[/_]/g, "-").replace(/[^a-zA-Z0-9 -]/g, "").replace(/ {2,}/g, " ").trim().slice(0, 50);
30209
30212
  }
30210
- function formatAuthError(apiBody) {
30211
- const usingOAuth = !process.env.TAILSCALE_API_KEY && process.env.TAILSCALE_OAUTH_CLIENT_ID;
30212
- const lines = [
30213
- "Authentication failed (HTTP 401).",
30214
- "",
30215
- "Possible causes:",
30216
- usingOAuth ? " - OAuth client credentials are invalid or lack required scopes" : " - API key has expired or been revoked"
30217
- ];
30218
- if (process.platform === "win32" && !usingOAuth) {
30213
+ function validateAndSanitizeDescription(value) {
30214
+ const sanitized = sanitizeDescription(value);
30215
+ if (sanitized.length > 0) return sanitized;
30216
+ if (value.trim().length === 0) return void 0;
30217
+ throw new Error(
30218
+ `description ${JSON.stringify(value)} contains no valid characters after sanitization. Allowed characters: alphanumeric, spaces, and hyphens (max 50 chars).`
30219
+ );
30220
+ }
30221
+ function formatAuthError(status, apiBody) {
30222
+ const usingOAuth = !process.env.TAILSCALE_API_KEY && !!process.env.TAILSCALE_OAUTH_CLIENT_ID;
30223
+ const headline = status === 401 ? "Authentication failed (HTTP 401)." : "Authorization failed (HTTP 403): the request was authenticated but not permitted for this resource.";
30224
+ const cause = status === 401 ? usingOAuth ? " - OAuth client credentials are invalid or lack required scopes" : " - API key has expired or been revoked" : usingOAuth ? " - OAuth client is missing a scope required for this endpoint" : " - API key lacks the permission required for this endpoint";
30225
+ const lines = [headline, "", "Possible causes:", cause];
30226
+ if (status === 401 && process.platform === "win32" && !usingOAuth) {
30219
30227
  lines.push(
30220
30228
  " - On Windows, env vars set in bash/WSL profiles are not visible to MCP servers launched via cmd",
30221
30229
  "",
@@ -30224,7 +30232,8 @@ function formatAuthError(apiBody) {
30224
30232
  " 2. Set TAILSCALE_API_KEY as a Windows user environment variable (System Properties > Environment Variables)"
30225
30233
  );
30226
30234
  }
30227
- lines.push("", "Generate a new key at: https://login.tailscale.com/admin/settings/keys");
30235
+ const link = status === 401 ? "Generate a new key at: https://login.tailscale.com/admin/settings/keys" : usingOAuth ? "Adjust the OAuth client scopes at: https://login.tailscale.com/admin/settings/oauth" : "Adjust the API key permissions at: https://login.tailscale.com/admin/settings/keys";
30236
+ lines.push("", link);
30228
30237
  if (apiBody) {
30229
30238
  lines.push("", `API response: ${apiBody}`);
30230
30239
  }
@@ -30250,28 +30259,32 @@ var concurrencyQueue = [];
30250
30259
  function getConcurrencyLimit() {
30251
30260
  const raw = process.env.TAILSCALE_MAX_CONCURRENT;
30252
30261
  if (!raw) return 0;
30253
- const n = Number.parseInt(raw, 10);
30254
- return Number.isFinite(n) && n > 0 ? n : 0;
30262
+ const n = Number(raw);
30263
+ return Number.isInteger(n) && n > 0 ? n : 0;
30255
30264
  }
30256
30265
  function getRequestBudgetMs() {
30257
30266
  const raw = process.env.TAILSCALE_REQUEST_BUDGET_MS;
30258
30267
  if (!raw) return MAX_REQUEST_BUDGET_MS;
30259
- const n = Number.parseInt(raw, 10);
30260
- return Number.isFinite(n) && n > 0 ? n : MAX_REQUEST_BUDGET_MS;
30268
+ const n = Number(raw);
30269
+ return Number.isInteger(n) && n > 0 ? n : MAX_REQUEST_BUDGET_MS;
30261
30270
  }
30262
30271
  async function withConcurrencyLimit(fn) {
30263
30272
  const limit = getConcurrencyLimit();
30264
30273
  if (limit === 0) return fn();
30265
30274
  if (inFlight >= limit) {
30266
30275
  await new Promise((resolve) => concurrencyQueue.push(resolve));
30276
+ } else {
30277
+ inFlight++;
30267
30278
  }
30268
- inFlight++;
30269
30279
  try {
30270
30280
  return await fn();
30271
30281
  } finally {
30272
- inFlight--;
30273
30282
  const next = concurrencyQueue.shift();
30274
- if (next) next();
30283
+ if (next) {
30284
+ next();
30285
+ } else {
30286
+ inFlight--;
30287
+ }
30275
30288
  }
30276
30289
  }
30277
30290
  function debugLog(...parts) {
@@ -30347,14 +30360,14 @@ async function apiRequest(method, path, body, options) {
30347
30360
  if (options?.acceptRaw) {
30348
30361
  const rawBody = await response.text();
30349
30362
  if (!response.ok) {
30350
- const error48 = response.status === 401 ? formatAuthError(rawBody) : extractErrorMessage(rawBody);
30363
+ const error48 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, rawBody) : extractErrorMessage(rawBody);
30351
30364
  return { ok: false, status: response.status, error: error48, rawBody, etag };
30352
30365
  }
30353
30366
  return { ok: true, status: response.status, rawBody, etag };
30354
30367
  }
30355
30368
  if (!response.ok) {
30356
30369
  const errorBody = await response.text();
30357
- const error48 = response.status === 401 ? formatAuthError(errorBody) : extractErrorMessage(errorBody);
30370
+ const error48 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, errorBody) : extractErrorMessage(errorBody);
30358
30371
  return { ok: false, status: response.status, error: error48, etag };
30359
30372
  }
30360
30373
  if (response.status === 204 || response.headers.get("content-length") === "0") {
@@ -30381,7 +30394,6 @@ async function apiDelete(path, options) {
30381
30394
  }
30382
30395
 
30383
30396
  // src/cli.ts
30384
- import { readFileSync } from "node:fs";
30385
30397
  async function deployAcl(filePath) {
30386
30398
  let policy;
30387
30399
  try {
@@ -30463,6 +30475,85 @@ function filterTools(groups, options) {
30463
30475
  return result;
30464
30476
  }
30465
30477
 
30478
+ // src/server-wiring.ts
30479
+ function wrapToolHandler(tool) {
30480
+ return async (input) => {
30481
+ try {
30482
+ const result = await tool.handler(input);
30483
+ const response = result;
30484
+ if (!response.ok) {
30485
+ return {
30486
+ content: [
30487
+ {
30488
+ type: "text",
30489
+ text: `Error: ${response.error || "Unknown error"}`
30490
+ }
30491
+ ],
30492
+ isError: true
30493
+ };
30494
+ }
30495
+ const text = response.rawBody ?? JSON.stringify(response.data ?? { success: true }, null, 2);
30496
+ return {
30497
+ content: [{ type: "text", text }]
30498
+ };
30499
+ } catch (err) {
30500
+ const message = err instanceof Error ? err.message : String(err);
30501
+ return {
30502
+ content: [{ type: "text", text: `Error: ${message}` }],
30503
+ isError: true
30504
+ };
30505
+ }
30506
+ };
30507
+ }
30508
+ async function tailnetStatusResource(uri) {
30509
+ const [devicesRes, settingsRes] = await Promise.all([
30510
+ apiGet(`/tailnet/${getTailnet()}/devices?fields=id`),
30511
+ apiGet(`/tailnet/${getTailnet()}/settings`)
30512
+ ]);
30513
+ const data = {
30514
+ tailnet: getTailnet(),
30515
+ deviceCount: devicesRes.ok ? devicesRes.data?.devices?.length ?? 0 : null,
30516
+ settings: settingsRes.ok ? settingsRes.data : null
30517
+ };
30518
+ const errors = {};
30519
+ if (!devicesRes.ok) errors.devices = devicesRes.error ?? `HTTP ${devicesRes.status}`;
30520
+ if (!settingsRes.ok) errors.settings = settingsRes.error ?? `HTTP ${settingsRes.status}`;
30521
+ if (Object.keys(errors).length > 0) data.errors = errors;
30522
+ return { contents: [{ uri: uri.href, text: JSON.stringify(data, null, 2), mimeType: "application/json" }] };
30523
+ }
30524
+ async function tailnetDevicesResource(uri) {
30525
+ const res = await apiGet(`/tailnet/${getTailnet()}/devices`);
30526
+ const text = res.ok ? JSON.stringify(res.data, null, 2) : JSON.stringify({ error: res.error ?? `HTTP ${res.status}` }, null, 2);
30527
+ return { contents: [{ uri: uri.href, text, mimeType: "application/json" }] };
30528
+ }
30529
+ async function tailnetAclResource(uri) {
30530
+ const res = await apiGet(`/tailnet/${getTailnet()}/acl`, { acceptRaw: true, accept: "application/hujson" });
30531
+ const text = res.ok ? res.rawBody ?? "" : `// Error: ${res.error ?? `HTTP ${res.status}`}
30532
+ `;
30533
+ return { contents: [{ uri: uri.href, text, mimeType: "application/hujson" }] };
30534
+ }
30535
+ async function tailnetDnsResource(uri) {
30536
+ const [nameservers, searchPaths, splitDns, preferences] = await Promise.all([
30537
+ apiGet(`/tailnet/${getTailnet()}/dns/nameservers`),
30538
+ apiGet(`/tailnet/${getTailnet()}/dns/searchpaths`),
30539
+ apiGet(`/tailnet/${getTailnet()}/dns/split-dns`),
30540
+ apiGet(`/tailnet/${getTailnet()}/dns/preferences`)
30541
+ ]);
30542
+ const data = {
30543
+ nameservers: nameservers.ok ? nameservers.data : null,
30544
+ searchPaths: searchPaths.ok ? searchPaths.data : null,
30545
+ splitDns: splitDns.ok ? splitDns.data : null,
30546
+ preferences: preferences.ok ? preferences.data : null
30547
+ };
30548
+ const errors = {};
30549
+ if (!nameservers.ok) errors.nameservers = nameservers.error ?? `HTTP ${nameservers.status}`;
30550
+ if (!searchPaths.ok) errors.searchPaths = searchPaths.error ?? `HTTP ${searchPaths.status}`;
30551
+ if (!splitDns.ok) errors.splitDns = splitDns.error ?? `HTTP ${splitDns.status}`;
30552
+ if (!preferences.ok) errors.preferences = preferences.error ?? `HTTP ${preferences.status}`;
30553
+ if (Object.keys(errors).length > 0) data.errors = errors;
30554
+ return { contents: [{ uri: uri.href, text: JSON.stringify(data, null, 2), mimeType: "application/json" }] };
30555
+ }
30556
+
30466
30557
  // src/tools/acl.ts
30467
30558
  var aclTools = [
30468
30559
  {
@@ -30650,6 +30741,18 @@ var auditTools = [
30650
30741
  ];
30651
30742
 
30652
30743
  // src/tools/devices.ts
30744
+ import * as net from "node:net";
30745
+ function isCidr(s) {
30746
+ const slash = s.indexOf("/");
30747
+ if (slash < 0) return false;
30748
+ const addr = s.slice(0, slash);
30749
+ const prefix = s.slice(slash + 1);
30750
+ const prefixN = Number(prefix);
30751
+ if (!Number.isInteger(prefixN) || prefixN < 0) return false;
30752
+ if (net.isIPv4(addr)) return prefixN <= 32;
30753
+ if (net.isIPv6(addr)) return prefixN <= 128;
30754
+ return false;
30755
+ }
30653
30756
  var deviceTools = [
30654
30757
  {
30655
30758
  name: "tailscale_list_devices",
@@ -30813,16 +30916,7 @@ var deviceTools = [
30813
30916
  },
30814
30917
  inputSchema: external_exports3.object({
30815
30918
  deviceId: external_exports3.string().describe("The device ID"),
30816
- routes: external_exports3.array(
30817
- external_exports3.string().refine(
30818
- // Accept v4 (10.0.0.0/24) or v6 (fd7a:115c::/48) CIDRs. Routes can be either.
30819
- // Loose check: must contain a '/' followed by 1-3 digits, and the address part
30820
- // must look like an IPv4 quad-dotted or an IPv6 colon-form. The Tailscale API
30821
- // is the authoritative validator; this just rejects obvious typos client-side.
30822
- (s) => /^([\d.]+|[\da-fA-F:]+)\/\d{1,3}$/.test(s),
30823
- { message: "must be a CIDR (e.g. '10.0.0.0/24' or 'fd7a:115c::/48')" }
30824
- )
30825
- ).describe(
30919
+ routes: external_exports3.array(external_exports3.string().refine(isCidr, { message: "must be a CIDR (e.g. '10.0.0.0/24' or 'fd7a:115c::/48')" })).describe(
30826
30920
  "Full list of CIDR routes to enable (e.g. ['10.0.0.0/24', '192.168.1.0/24']). Replaces existing enabled routes."
30827
30921
  )
30828
30922
  }),
@@ -31526,8 +31620,8 @@ var keyTools = [
31526
31620
  const body = {};
31527
31621
  if (keyType !== "auth") body.keyType = keyType;
31528
31622
  if (input.description !== void 0) {
31529
- const sanitized = sanitizeDescription(input.description);
31530
- if (sanitized.length > 0) body.description = sanitized;
31623
+ const sanitized = validateAndSanitizeDescription(input.description);
31624
+ if (sanitized !== void 0) body.description = sanitized;
31531
31625
  }
31532
31626
  if (keyType === "auth") {
31533
31627
  body.capabilities = {
@@ -31600,8 +31694,8 @@ var keyTools = [
31600
31694
  validateTags(input.tags);
31601
31695
  const body = {};
31602
31696
  if (input.description !== void 0) {
31603
- const sanitized = sanitizeDescription(input.description);
31604
- if (sanitized.length > 0) body.description = sanitized;
31697
+ const sanitized = validateAndSanitizeDescription(input.description);
31698
+ if (sanitized !== void 0) body.description = sanitized;
31605
31699
  }
31606
31700
  if (input.scopes !== void 0) body.scopes = input.scopes;
31607
31701
  if (input.tags !== void 0) body.tags = input.tags;
@@ -32519,7 +32613,7 @@ var webhookTools = [
32519
32613
  ];
32520
32614
 
32521
32615
  // src/index.ts
32522
- var version2 = true ? "0.10.4" : (await null).createRequire(import.meta.url)("../package.json").version;
32616
+ var version2 = true ? "0.10.6" : (await null).createRequire(import.meta.url)("../package.json").version;
32523
32617
  var subcommand = process.argv[2];
32524
32618
  if (subcommand === "deploy-acl") {
32525
32619
  const filePath = process.argv[3];
@@ -32576,81 +32670,25 @@ var server = new McpServer({
32576
32670
  version: version2
32577
32671
  });
32578
32672
  for (const tool of allTools) {
32579
- server.tool(
32580
- tool.name,
32581
- tool.description,
32582
- tool.inputSchema.shape,
32583
- tool.annotations,
32584
- async (input) => {
32585
- try {
32586
- const result = await tool.handler(input);
32587
- const response = result;
32588
- if (!response.ok) {
32589
- return {
32590
- content: [
32591
- {
32592
- type: "text",
32593
- text: `Error: ${response.error || "Unknown error"}`
32594
- }
32595
- ],
32596
- isError: true
32597
- };
32598
- }
32599
- const text = response.rawBody ?? JSON.stringify(response.data ?? { success: true }, null, 2);
32600
- return {
32601
- content: [{ type: "text", text }]
32602
- };
32603
- } catch (err) {
32604
- const message = err instanceof Error ? err.message : String(err);
32605
- return {
32606
- content: [{ type: "text", text: `Error: ${message}` }],
32607
- isError: true
32608
- };
32609
- }
32610
- }
32611
- );
32673
+ server.tool(tool.name, tool.description, tool.inputSchema.shape, tool.annotations, wrapToolHandler(tool));
32612
32674
  }
32613
32675
  server.resource(
32614
32676
  "tailnet-status",
32615
32677
  "tailscale://tailnet/status",
32616
32678
  { description: "Current tailnet status including device count and settings", mimeType: "application/json" },
32617
- async (uri) => {
32618
- const [devicesRes, settingsRes] = await Promise.all([
32619
- apiGet(`/tailnet/${getTailnet()}/devices?fields=id`),
32620
- apiGet(`/tailnet/${getTailnet()}/settings`)
32621
- ]);
32622
- const data = {
32623
- tailnet: getTailnet(),
32624
- deviceCount: devicesRes.ok ? devicesRes.data?.devices?.length ?? 0 : null,
32625
- settings: settingsRes.ok ? settingsRes.data : null
32626
- };
32627
- const errors = {};
32628
- if (!devicesRes.ok) errors.devices = devicesRes.error ?? `HTTP ${devicesRes.status}`;
32629
- if (!settingsRes.ok) errors.settings = settingsRes.error ?? `HTTP ${settingsRes.status}`;
32630
- if (Object.keys(errors).length > 0) data.errors = errors;
32631
- return { contents: [{ uri: uri.href, text: JSON.stringify(data, null, 2), mimeType: "application/json" }] };
32632
- }
32679
+ tailnetStatusResource
32633
32680
  );
32634
32681
  server.resource(
32635
32682
  "tailnet-devices",
32636
32683
  "tailscale://tailnet/devices",
32637
32684
  { description: "List of all devices in the tailnet with their status", mimeType: "application/json" },
32638
- async (uri) => {
32639
- const res = await apiGet(`/tailnet/${getTailnet()}/devices`);
32640
- const text = res.ok ? JSON.stringify(res.data, null, 2) : JSON.stringify({ error: res.error ?? `HTTP ${res.status}` }, null, 2);
32641
- return { contents: [{ uri: uri.href, text, mimeType: "application/json" }] };
32642
- }
32685
+ tailnetDevicesResource
32643
32686
  );
32644
32687
  server.resource(
32645
32688
  "tailnet-acl",
32646
32689
  "tailscale://tailnet/acl",
32647
32690
  { description: "Current ACL policy (HuJSON with comments preserved)", mimeType: "application/hujson" },
32648
- async (uri) => {
32649
- const res = await apiGet(`/tailnet/${getTailnet()}/acl`, { acceptRaw: true, accept: "application/hujson" });
32650
- const text = res.ok ? res.rawBody ?? "" : `// Error: ${res.error ?? `HTTP ${res.status}`}
32651
- `;
32652
- return { contents: [{ uri: uri.href, text, mimeType: "application/hujson" }] };
32653
- }
32691
+ tailnetAclResource
32654
32692
  );
32655
32693
  server.resource(
32656
32694
  "tailnet-dns",
@@ -32659,27 +32697,7 @@ server.resource(
32659
32697
  description: "DNS configuration including nameservers, search paths, split DNS, and MagicDNS status",
32660
32698
  mimeType: "application/json"
32661
32699
  },
32662
- async (uri) => {
32663
- const [nameservers, searchPaths, splitDns, preferences] = await Promise.all([
32664
- apiGet(`/tailnet/${getTailnet()}/dns/nameservers`),
32665
- apiGet(`/tailnet/${getTailnet()}/dns/searchpaths`),
32666
- apiGet(`/tailnet/${getTailnet()}/dns/split-dns`),
32667
- apiGet(`/tailnet/${getTailnet()}/dns/preferences`)
32668
- ]);
32669
- const data = {
32670
- nameservers: nameservers.ok ? nameservers.data : null,
32671
- searchPaths: searchPaths.ok ? searchPaths.data : null,
32672
- splitDns: splitDns.ok ? splitDns.data : null,
32673
- preferences: preferences.ok ? preferences.data : null
32674
- };
32675
- const errors = {};
32676
- if (!nameservers.ok) errors.nameservers = nameservers.error ?? `HTTP ${nameservers.status}`;
32677
- if (!searchPaths.ok) errors.searchPaths = searchPaths.error ?? `HTTP ${searchPaths.status}`;
32678
- if (!splitDns.ok) errors.splitDns = splitDns.error ?? `HTTP ${splitDns.status}`;
32679
- if (!preferences.ok) errors.preferences = preferences.error ?? `HTTP ${preferences.status}`;
32680
- if (Object.keys(errors).length > 0) data.errors = errors;
32681
- return { contents: [{ uri: uri.href, text: JSON.stringify(data, null, 2), mimeType: "application/json" }] };
32682
- }
32700
+ tailnetDnsResource
32683
32701
  );
32684
32702
  var transport = new StdioServerTransport();
32685
32703
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
4
4
  "description": "Tailscale MCP server for managing your tailnet from AI assistants",
5
5
  "license": "MIT",
6
6
  "author": "YawLabs <contact@yaw.sh>",