@yawlabs/tailscale-mcp 0.10.5 → 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 +52 -39
  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();
@@ -30210,15 +30210,20 @@ function validateTags(tags) {
30210
30210
  function sanitizeDescription(value) {
30211
30211
  return value.replace(/[/_]/g, "-").replace(/[^a-zA-Z0-9 -]/g, "").replace(/ {2,}/g, " ").trim().slice(0, 50);
30212
30212
  }
30213
- function formatAuthError(apiBody) {
30214
- const usingOAuth = !process.env.TAILSCALE_API_KEY && process.env.TAILSCALE_OAUTH_CLIENT_ID;
30215
- const lines = [
30216
- "Authentication failed (HTTP 401).",
30217
- "",
30218
- "Possible causes:",
30219
- usingOAuth ? " - OAuth client credentials are invalid or lack required scopes" : " - API key has expired or been revoked"
30220
- ];
30221
- 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) {
30222
30227
  lines.push(
30223
30228
  " - On Windows, env vars set in bash/WSL profiles are not visible to MCP servers launched via cmd",
30224
30229
  "",
@@ -30227,7 +30232,8 @@ function formatAuthError(apiBody) {
30227
30232
  " 2. Set TAILSCALE_API_KEY as a Windows user environment variable (System Properties > Environment Variables)"
30228
30233
  );
30229
30234
  }
30230
- 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);
30231
30237
  if (apiBody) {
30232
30238
  lines.push("", `API response: ${apiBody}`);
30233
30239
  }
@@ -30253,28 +30259,32 @@ var concurrencyQueue = [];
30253
30259
  function getConcurrencyLimit() {
30254
30260
  const raw = process.env.TAILSCALE_MAX_CONCURRENT;
30255
30261
  if (!raw) return 0;
30256
- const n = Number.parseInt(raw, 10);
30257
- return Number.isFinite(n) && n > 0 ? n : 0;
30262
+ const n = Number(raw);
30263
+ return Number.isInteger(n) && n > 0 ? n : 0;
30258
30264
  }
30259
30265
  function getRequestBudgetMs() {
30260
30266
  const raw = process.env.TAILSCALE_REQUEST_BUDGET_MS;
30261
30267
  if (!raw) return MAX_REQUEST_BUDGET_MS;
30262
- const n = Number.parseInt(raw, 10);
30263
- 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;
30264
30270
  }
30265
30271
  async function withConcurrencyLimit(fn) {
30266
30272
  const limit = getConcurrencyLimit();
30267
30273
  if (limit === 0) return fn();
30268
30274
  if (inFlight >= limit) {
30269
30275
  await new Promise((resolve) => concurrencyQueue.push(resolve));
30276
+ } else {
30277
+ inFlight++;
30270
30278
  }
30271
- inFlight++;
30272
30279
  try {
30273
30280
  return await fn();
30274
30281
  } finally {
30275
- inFlight--;
30276
30282
  const next = concurrencyQueue.shift();
30277
- if (next) next();
30283
+ if (next) {
30284
+ next();
30285
+ } else {
30286
+ inFlight--;
30287
+ }
30278
30288
  }
30279
30289
  }
30280
30290
  function debugLog(...parts) {
@@ -30350,14 +30360,14 @@ async function apiRequest(method, path, body, options) {
30350
30360
  if (options?.acceptRaw) {
30351
30361
  const rawBody = await response.text();
30352
30362
  if (!response.ok) {
30353
- const error48 = response.status === 401 ? formatAuthError(rawBody) : extractErrorMessage(rawBody);
30363
+ const error48 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, rawBody) : extractErrorMessage(rawBody);
30354
30364
  return { ok: false, status: response.status, error: error48, rawBody, etag };
30355
30365
  }
30356
30366
  return { ok: true, status: response.status, rawBody, etag };
30357
30367
  }
30358
30368
  if (!response.ok) {
30359
30369
  const errorBody = await response.text();
30360
- const error48 = response.status === 401 ? formatAuthError(errorBody) : extractErrorMessage(errorBody);
30370
+ const error48 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, errorBody) : extractErrorMessage(errorBody);
30361
30371
  return { ok: false, status: response.status, error: error48, etag };
30362
30372
  }
30363
30373
  if (response.status === 204 || response.headers.get("content-length") === "0") {
@@ -30731,6 +30741,18 @@ var auditTools = [
30731
30741
  ];
30732
30742
 
30733
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
+ }
30734
30756
  var deviceTools = [
30735
30757
  {
30736
30758
  name: "tailscale_list_devices",
@@ -30894,16 +30916,7 @@ var deviceTools = [
30894
30916
  },
30895
30917
  inputSchema: external_exports3.object({
30896
30918
  deviceId: external_exports3.string().describe("The device ID"),
30897
- routes: external_exports3.array(
30898
- external_exports3.string().refine(
30899
- // Accept v4 (10.0.0.0/24) or v6 (fd7a:115c::/48) CIDRs. Routes can be either.
30900
- // Loose check: must contain a '/' followed by 1-3 digits, and the address part
30901
- // must look like an IPv4 quad-dotted or an IPv6 colon-form. The Tailscale API
30902
- // is the authoritative validator; this just rejects obvious typos client-side.
30903
- (s) => /^([\d.]+|[\da-fA-F:]+)\/\d{1,3}$/.test(s),
30904
- { message: "must be a CIDR (e.g. '10.0.0.0/24' or 'fd7a:115c::/48')" }
30905
- )
30906
- ).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(
30907
30920
  "Full list of CIDR routes to enable (e.g. ['10.0.0.0/24', '192.168.1.0/24']). Replaces existing enabled routes."
30908
30921
  )
30909
30922
  }),
@@ -31607,8 +31620,8 @@ var keyTools = [
31607
31620
  const body = {};
31608
31621
  if (keyType !== "auth") body.keyType = keyType;
31609
31622
  if (input.description !== void 0) {
31610
- const sanitized = sanitizeDescription(input.description);
31611
- if (sanitized.length > 0) body.description = sanitized;
31623
+ const sanitized = validateAndSanitizeDescription(input.description);
31624
+ if (sanitized !== void 0) body.description = sanitized;
31612
31625
  }
31613
31626
  if (keyType === "auth") {
31614
31627
  body.capabilities = {
@@ -31681,8 +31694,8 @@ var keyTools = [
31681
31694
  validateTags(input.tags);
31682
31695
  const body = {};
31683
31696
  if (input.description !== void 0) {
31684
- const sanitized = sanitizeDescription(input.description);
31685
- if (sanitized.length > 0) body.description = sanitized;
31697
+ const sanitized = validateAndSanitizeDescription(input.description);
31698
+ if (sanitized !== void 0) body.description = sanitized;
31686
31699
  }
31687
31700
  if (input.scopes !== void 0) body.scopes = input.scopes;
31688
31701
  if (input.tags !== void 0) body.tags = input.tags;
@@ -32600,7 +32613,7 @@ var webhookTools = [
32600
32613
  ];
32601
32614
 
32602
32615
  // src/index.ts
32603
- var version2 = true ? "0.10.5" : (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;
32604
32617
  var subcommand = process.argv[2];
32605
32618
  if (subcommand === "deploy-acl") {
32606
32619
  const filePath = process.argv[3];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.10.5",
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>",