@yawlabs/tailscale-mcp 0.20.1 → 0.21.0

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 CHANGED
@@ -3109,9 +3109,28 @@ var require_utils = __commonJS({
3109
3109
  "use strict";
3110
3110
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3111
3111
  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);
3112
+ var isPort = RegExp.prototype.test.bind(/^\d*$/u);
3112
3113
  var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3113
3114
  var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3114
- var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3115
+ var isPathCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/]$/u);
3116
+ var isQueryFragmentCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:@/?]$/u);
3117
+ var isUserinfoCharacter = RegExp.prototype.test.bind(/^[A-Za-z0-9\-._~!$&'()*+,;=:]$/u);
3118
+ var BYTE_HEX = new Array(256);
3119
+ {
3120
+ const HEX_DIGITS = "0123456789ABCDEF";
3121
+ for (let i = 0; i < 256; i++) {
3122
+ BYTE_HEX[i] = "%" + HEX_DIGITS[i >> 4] + HEX_DIGITS[i & 15];
3123
+ }
3124
+ }
3125
+ function percentEncodeNonAscii(cp) {
3126
+ if (cp < 2048) {
3127
+ return BYTE_HEX[192 | cp >> 6] + BYTE_HEX[128 | cp & 63];
3128
+ }
3129
+ if (cp < 65536) {
3130
+ return BYTE_HEX[224 | cp >> 12] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
3131
+ }
3132
+ return BYTE_HEX[240 | cp >> 18] + BYTE_HEX[128 | cp >> 12 & 63] + BYTE_HEX[128 | cp >> 6 & 63] + BYTE_HEX[128 | cp & 63];
3133
+ }
3115
3134
  function stringArrayToHexStripped(input) {
3116
3135
  let acc = "";
3117
3136
  let code = 0;
@@ -3136,91 +3155,105 @@ var require_utils = __commonJS({
3136
3155
  }
3137
3156
  return acc;
3138
3157
  }
3158
+ var isHextet = RegExp.prototype.test.bind(/^[\dA-Fa-f]{1,4}$/);
3159
+ var isIPvFuture = RegExp.prototype.test.bind(/^[vV][\dA-Fa-f]+\.[A-Za-z\d\-._~!$&'()*+,;=:]+$/);
3160
+ var isZoneCharacter = RegExp.prototype.test.bind(/^[A-Za-z\d\-._~]$/);
3139
3161
  var nonSimpleDomain = RegExp.prototype.test.bind(/[^!"$&'()*+,\-.;=_`a-z{}~]/u);
3140
- function consumeIsZone(buffer) {
3141
- buffer.length = 0;
3142
- return true;
3143
- }
3144
- function consumeHextets(buffer, address, output) {
3145
- if (buffer.length) {
3146
- const hex3 = stringArrayToHexStripped(buffer);
3147
- if (hex3 !== "") {
3148
- address.push(hex3);
3149
- } else {
3150
- output.error = true;
3151
- return false;
3162
+ function isZoneIdentifier(zone) {
3163
+ if (zone.length === 0) return false;
3164
+ for (let i = 0; i < zone.length; i++) {
3165
+ if (isZoneCharacter(zone[i])) continue;
3166
+ if (zone[i] === "%" && i + 2 < zone.length && isHexPair(zone.slice(i + 1, i + 3))) {
3167
+ i += 2;
3168
+ continue;
3152
3169
  }
3153
- buffer.length = 0;
3170
+ return false;
3154
3171
  }
3155
3172
  return true;
3156
3173
  }
3157
- function getIPV6(input) {
3158
- let tokenCount = 0;
3159
- const output = { error: false, address: "", zone: "" };
3160
- const address = [];
3161
- const buffer = [];
3162
- let endipv6Encountered = false;
3163
- let endIpv6 = false;
3164
- let consume = consumeHextets;
3165
- for (let i = 0; i < input.length; i++) {
3166
- const cursor = input[i];
3167
- if (cursor === "[" || cursor === "]") {
3168
- continue;
3169
- }
3170
- if (cursor === ":") {
3171
- if (endipv6Encountered === true) {
3172
- endIpv6 = true;
3173
- }
3174
- if (!consume(buffer, address, output)) {
3175
- break;
3174
+ function compressIPv6ZeroRun(hextets) {
3175
+ let bestStart = -1;
3176
+ let bestLength = 0;
3177
+ let runStart = -1;
3178
+ let runLength = 0;
3179
+ for (let i = 0; i < hextets.length; i++) {
3180
+ if (hextets[i] === "0") {
3181
+ if (runStart === -1) runStart = i;
3182
+ runLength++;
3183
+ if (runLength > bestLength) {
3184
+ bestLength = runLength;
3185
+ bestStart = runStart;
3176
3186
  }
3177
- if (++tokenCount > 7) {
3178
- output.error = true;
3179
- break;
3180
- }
3181
- if (i > 0 && input[i - 1] === ":") {
3182
- endipv6Encountered = true;
3183
- }
3184
- address.push(":");
3185
- continue;
3186
- } else if (cursor === "%") {
3187
- if (!consume(buffer, address, output)) {
3188
- break;
3189
- }
3190
- consume = consumeIsZone;
3191
3187
  } else {
3192
- buffer.push(cursor);
3188
+ runStart = -1;
3189
+ runLength = 0;
3190
+ }
3191
+ }
3192
+ if (bestLength < 2) return hextets.join(":");
3193
+ const head = hextets.slice(0, bestStart).join(":");
3194
+ const tail = hextets.slice(bestStart + bestLength).join(":");
3195
+ return head + "::" + tail;
3196
+ }
3197
+ function normalizeIPv6Address(input) {
3198
+ const compression = input.indexOf("::");
3199
+ if (compression !== -1 && input.indexOf("::", compression + 1) !== -1) return void 0;
3200
+ const left = compression === -1 ? input.split(":") : input.slice(0, compression).split(":");
3201
+ const right = compression === -1 ? [] : input.slice(compression + 2).split(":");
3202
+ if (compression !== -1) {
3203
+ if (left.length === 1 && left[0] === "") left.length = 0;
3204
+ if (right.length === 1 && right[0] === "") right.length = 0;
3205
+ }
3206
+ const parts = left.concat(right);
3207
+ let hextetCount = 0;
3208
+ for (let i = 0; i < parts.length; i++) {
3209
+ const part = parts[i];
3210
+ if (part === "") return void 0;
3211
+ if (part.indexOf(".") !== -1) {
3212
+ if (i !== parts.length - 1 || compression !== -1 && right.length === 0 || !isIPv42(part)) return void 0;
3213
+ hextetCount += 2;
3193
3214
  continue;
3194
3215
  }
3216
+ if (!isHextet(part)) return void 0;
3217
+ parts[i] = parseInt(part, 16).toString(16);
3218
+ hextetCount++;
3195
3219
  }
3196
- if (buffer.length) {
3197
- if (consume === consumeIsZone) {
3198
- output.zone = buffer.join("");
3199
- } else if (endIpv6) {
3200
- address.push(buffer.join(""));
3201
- } else {
3202
- address.push(stringArrayToHexStripped(buffer));
3203
- }
3220
+ if (compression === -1) {
3221
+ if (hextetCount !== 8) return void 0;
3222
+ return compressIPv6ZeroRun(parts);
3204
3223
  }
3205
- output.address = address.join("");
3206
- return output;
3224
+ if (hextetCount >= 8) return void 0;
3225
+ const expanded = parts.slice(0, left.length);
3226
+ for (let i = hextetCount; i < 8; i++) expanded.push("0");
3227
+ for (let i = left.length; i < parts.length; i++) expanded.push(parts[i]);
3228
+ return compressIPv6ZeroRun(expanded);
3207
3229
  }
3208
3230
  function normalizeIPv6(host) {
3209
- if (findToken(host, ":") < 2) {
3210
- return { host, isIPV6: false };
3211
- }
3212
- const ipv63 = getIPV6(host);
3213
- if (!ipv63.error) {
3214
- let newHost = ipv63.address;
3215
- let escapedHost = ipv63.address;
3216
- if (ipv63.zone) {
3217
- newHost += "%" + ipv63.zone;
3218
- escapedHost += "%25" + ipv63.zone;
3219
- }
3220
- return { host: newHost, isIPV6: true, escapedHost };
3221
- } else {
3222
- return { host, isIPV6: false };
3223
- }
3231
+ const bracketed = host[0] === "[" && host[host.length - 1] === "]";
3232
+ const hasBracket = host[0] === "[" || host[host.length - 1] === "]";
3233
+ if (hasBracket && !bracketed) return { host, isIPV6: false, error: true };
3234
+ let input = bracketed ? host.slice(1, -1) : host;
3235
+ if (bracketed && isIPvFuture(input)) {
3236
+ input = input.toLowerCase();
3237
+ return { host: `[${input}]`, escapedHost: input, isIPV6: false, isIPVFuture: true };
3238
+ }
3239
+ if (findToken(input, ":") < 2) {
3240
+ return { host, isIPV6: false, error: bracketed };
3241
+ }
3242
+ let zoneIdentifier = "";
3243
+ const zoneSeparator = input.indexOf("%");
3244
+ if (zoneSeparator !== -1) {
3245
+ const separatorLength = input.slice(zoneSeparator, zoneSeparator + 3).toLowerCase() === "%25" ? 3 : 1;
3246
+ zoneIdentifier = input.slice(zoneSeparator + separatorLength);
3247
+ if (!isZoneIdentifier(zoneIdentifier)) return { host, isIPV6: false, error: true };
3248
+ input = input.slice(0, zoneSeparator);
3249
+ }
3250
+ const address = normalizeIPv6Address(input);
3251
+ if (address === void 0) return { host, isIPV6: false, error: true };
3252
+ return {
3253
+ host: address + (zoneIdentifier ? "%" + zoneIdentifier : ""),
3254
+ escapedHost: address + (zoneIdentifier ? "%25" + zoneIdentifier : ""),
3255
+ isIPV6: true
3256
+ };
3224
3257
  }
3225
3258
  function findToken(str, token) {
3226
3259
  let ind = 0;
@@ -3339,7 +3372,8 @@ var require_utils = __commonJS({
3339
3372
  function normalizePathEncoding(input) {
3340
3373
  let output = "";
3341
3374
  for (let i = 0; i < input.length; i++) {
3342
- if (input[i] === "%" && i + 2 < input.length) {
3375
+ const ch = input[i];
3376
+ if (ch === "%" && i + 2 < input.length) {
3343
3377
  const hex3 = input.slice(i + 1, i + 3);
3344
3378
  if (isHexPair(hex3)) {
3345
3379
  const normalizedHex = hex3.toUpperCase();
@@ -3353,10 +3387,152 @@ var require_utils = __commonJS({
3353
3387
  continue;
3354
3388
  }
3355
3389
  }
3356
- if (isPathCharacter(input[i])) {
3357
- output += input[i];
3390
+ if (isPathCharacter(ch)) {
3391
+ output += ch;
3392
+ } else {
3393
+ const code = input.charCodeAt(i);
3394
+ if (code < 128) {
3395
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3396
+ } else if (code < 55296 || code > 57343) {
3397
+ output += percentEncodeNonAscii(code);
3398
+ } else if (code <= 56319 && i + 1 < input.length) {
3399
+ const low = input.charCodeAt(i + 1);
3400
+ if (low >= 56320 && low <= 57343) {
3401
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3402
+ i++;
3403
+ } else {
3404
+ output += percentEncodeNonAscii(65533);
3405
+ }
3406
+ } else {
3407
+ output += percentEncodeNonAscii(65533);
3408
+ }
3409
+ }
3410
+ }
3411
+ return output;
3412
+ }
3413
+ function serializePathEncoding(input, pathNoScheme = false) {
3414
+ let output = "";
3415
+ let firstSegment = pathNoScheme && input[0] !== "/";
3416
+ for (let i = 0; i < input.length; i++) {
3417
+ const ch = input[i];
3418
+ if (ch === "%" && i + 2 < input.length) {
3419
+ const hex3 = input.slice(i + 1, i + 3);
3420
+ if (isHexPair(hex3)) {
3421
+ output += "%" + hex3.toUpperCase();
3422
+ i += 2;
3423
+ continue;
3424
+ }
3425
+ }
3426
+ if (ch === "/") {
3427
+ firstSegment = false;
3428
+ }
3429
+ if (isPathCharacter(ch) && (ch !== ":" || !firstSegment)) {
3430
+ output += ch;
3358
3431
  } else {
3359
- output += escape(input[i]);
3432
+ const code = input.charCodeAt(i);
3433
+ if (code < 128) {
3434
+ output += BYTE_HEX[code];
3435
+ } else if (code < 55296 || code > 57343) {
3436
+ output += percentEncodeNonAscii(code);
3437
+ } else if (code <= 56319 && i + 1 < input.length) {
3438
+ const low = input.charCodeAt(i + 1);
3439
+ if (low >= 56320 && low <= 57343) {
3440
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3441
+ i++;
3442
+ } else {
3443
+ output += percentEncodeNonAscii(65533);
3444
+ }
3445
+ } else {
3446
+ output += percentEncodeNonAscii(65533);
3447
+ }
3448
+ }
3449
+ }
3450
+ return output;
3451
+ }
3452
+ function encodeComponent(input, isAllowed) {
3453
+ let output = "";
3454
+ for (let i = 0; i < input.length; i++) {
3455
+ const ch = input[i];
3456
+ if (ch === "%" && i + 2 < input.length) {
3457
+ const hex3 = input.slice(i + 1, i + 3);
3458
+ if (isHexPair(hex3)) {
3459
+ output += "%" + hex3.toUpperCase();
3460
+ i += 2;
3461
+ continue;
3462
+ }
3463
+ }
3464
+ if (isAllowed(ch)) {
3465
+ output += ch;
3466
+ } else {
3467
+ const code = input.charCodeAt(i);
3468
+ if (code < 128) {
3469
+ output += BYTE_HEX[code];
3470
+ } else if (code < 55296 || code > 57343) {
3471
+ output += percentEncodeNonAscii(code);
3472
+ } else if (code <= 56319 && i + 1 < input.length) {
3473
+ const low = input.charCodeAt(i + 1);
3474
+ if (low >= 56320 && low <= 57343) {
3475
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3476
+ i++;
3477
+ } else {
3478
+ output += percentEncodeNonAscii(65533);
3479
+ }
3480
+ } else {
3481
+ output += percentEncodeNonAscii(65533);
3482
+ }
3483
+ }
3484
+ }
3485
+ return output;
3486
+ }
3487
+ function encodeUserinfo(input) {
3488
+ return encodeComponent(input, isUserinfoCharacter);
3489
+ }
3490
+ function encodeQuery(input) {
3491
+ return encodeComponent(input, isQueryFragmentCharacter);
3492
+ }
3493
+ function encodeFragment(input) {
3494
+ return encodeComponent(input, isQueryFragmentCharacter);
3495
+ }
3496
+ function isEscapeSafe(cp) {
3497
+ return cp >= 48 && cp <= 57 || cp >= 65 && cp <= 90 || cp >= 97 && cp <= 122 || cp === 42 || cp === 43 || cp === 45 || cp === 46 || cp === 47 || cp === 64 || cp === 95;
3498
+ }
3499
+ function normalizeQueryFragmentEncoding(input) {
3500
+ let output = "";
3501
+ for (let i = 0; i < input.length; i++) {
3502
+ const ch = input[i];
3503
+ if (ch === "%" && i + 2 < input.length) {
3504
+ const hex3 = input.slice(i + 1, i + 3);
3505
+ if (isHexPair(hex3)) {
3506
+ const normalizedHex = hex3.toUpperCase();
3507
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3508
+ if (isUnreserved(decoded)) {
3509
+ output += decoded;
3510
+ } else {
3511
+ output += "%" + normalizedHex;
3512
+ }
3513
+ i += 2;
3514
+ continue;
3515
+ }
3516
+ }
3517
+ if (isQueryFragmentCharacter(ch)) {
3518
+ output += ch;
3519
+ } else {
3520
+ const code = input.charCodeAt(i);
3521
+ if (code < 128) {
3522
+ output += isEscapeSafe(code) ? ch : BYTE_HEX[code];
3523
+ } else if (code < 55296 || code > 57343) {
3524
+ output += percentEncodeNonAscii(code);
3525
+ } else if (code <= 56319 && i + 1 < input.length) {
3526
+ const low = input.charCodeAt(i + 1);
3527
+ if (low >= 56320 && low <= 57343) {
3528
+ output += percentEncodeNonAscii(65536 + (code - 55296 << 10) + (low - 56320));
3529
+ i++;
3530
+ } else {
3531
+ output += percentEncodeNonAscii(65533);
3532
+ }
3533
+ } else {
3534
+ output += percentEncodeNonAscii(65533);
3535
+ }
3360
3536
  }
3361
3537
  }
3362
3538
  return output;
@@ -3379,14 +3555,18 @@ var require_utils = __commonJS({
3379
3555
  function recomposeAuthority(component) {
3380
3556
  const uriTokens = [];
3381
3557
  if (component.userinfo !== void 0) {
3382
- uriTokens.push(component.userinfo);
3558
+ uriTokens.push(encodeUserinfo(component.userinfo));
3383
3559
  uriTokens.push("@");
3384
3560
  }
3385
3561
  if (component.host !== void 0) {
3386
- let host = unescape(component.host);
3562
+ let host = component.host;
3387
3563
  if (!isIPv42(host)) {
3388
- const ipV6res = normalizeIPv6(host);
3389
- if (ipV6res.isIPV6 === true) {
3564
+ let ipV6res = normalizeIPv6(host);
3565
+ if (ipV6res.isIPV6 !== true && ipV6res.isIPVFuture !== true) {
3566
+ host = normalizePercentEncoding(host, true);
3567
+ ipV6res = normalizeIPv6(host);
3568
+ }
3569
+ if (ipV6res.isIPV6 === true || ipV6res.isIPVFuture === true) {
3390
3570
  host = `[${ipV6res.escapedHost}]`;
3391
3571
  } else {
3392
3572
  host = reescapeHostDelimiters(host, false);
@@ -3395,8 +3575,12 @@ var require_utils = __commonJS({
3395
3575
  uriTokens.push(host);
3396
3576
  }
3397
3577
  if (typeof component.port === "number" || typeof component.port === "string") {
3578
+ const port = String(component.port);
3579
+ if (!isPort(port)) {
3580
+ throw new TypeError("URI port is malformed.");
3581
+ }
3398
3582
  uriTokens.push(":");
3399
- uriTokens.push(String(component.port));
3583
+ uriTokens.push(port);
3400
3584
  }
3401
3585
  return uriTokens.length ? uriTokens.join("") : void 0;
3402
3586
  }
@@ -3406,6 +3590,11 @@ var require_utils = __commonJS({
3406
3590
  reescapeHostDelimiters,
3407
3591
  normalizePercentEncoding,
3408
3592
  normalizePathEncoding,
3593
+ serializePathEncoding,
3594
+ normalizeQueryFragmentEncoding,
3595
+ encodeUserinfo,
3596
+ encodeQuery,
3597
+ encodeFragment,
3409
3598
  escapePreservingEscapes,
3410
3599
  removeDotSegments,
3411
3600
  isIPv4: isIPv42,
@@ -3421,7 +3610,7 @@ var require_schemes = __commonJS({
3421
3610
  "node_modules/fast-uri/lib/schemes.js"(exports, module) {
3422
3611
  "use strict";
3423
3612
  var { isUUID } = require_utils();
3424
- var URN_REG = /([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-.:;=@]|%[\da-f]{2})+)/iu;
3613
+ var URN_REG = /^([\da-z][\d\-a-z]{0,31}):((?:[\w!$'()*+,\-./:;=@]|%[\da-f]{2})+)$/iu;
3425
3614
  var supportedSchemeNames = (
3426
3615
  /** @type {const} */
3427
3616
  [
@@ -3482,9 +3671,10 @@ var require_schemes = __commonJS({
3482
3671
  wsComponent.secure = void 0;
3483
3672
  }
3484
3673
  if (wsComponent.resourceName) {
3485
- const [path, query] = wsComponent.resourceName.split("?");
3674
+ const queryIndex = wsComponent.resourceName.indexOf("?");
3675
+ const path = queryIndex === -1 ? wsComponent.resourceName : wsComponent.resourceName.slice(0, queryIndex);
3486
3676
  wsComponent.path = path && path !== "/" ? path : void 0;
3487
- wsComponent.query = query;
3677
+ wsComponent.query = queryIndex === -1 ? void 0 : wsComponent.resourceName.slice(queryIndex + 1);
3488
3678
  wsComponent.resourceName = void 0;
3489
3679
  }
3490
3680
  wsComponent.fragment = void 0;
@@ -3496,7 +3686,7 @@ var require_schemes = __commonJS({
3496
3686
  return urnComponent;
3497
3687
  }
3498
3688
  const matches = urnComponent.path.match(URN_REG);
3499
- if (matches) {
3689
+ if (matches && matches[0] === urnComponent.path) {
3500
3690
  const scheme = options.scheme || urnComponent.scheme || "urn";
3501
3691
  urnComponent.nid = matches[1].toLowerCase();
3502
3692
  urnComponent.nss = matches[2];
@@ -3630,8 +3820,17 @@ var require_schemes = __commonJS({
3630
3820
  var require_fast_uri = __commonJS({
3631
3821
  "node_modules/fast-uri/index.js"(exports, module) {
3632
3822
  "use strict";
3633
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4: isIPv42, nonSimpleDomain } = require_utils();
3823
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, serializePathEncoding, normalizeQueryFragmentEncoding, encodeQuery, encodeFragment, reescapeHostDelimiters, isIPv4: isIPv42, nonSimpleDomain } = require_utils();
3634
3824
  var { SCHEMES, getSchemeHandler } = require_schemes();
3825
+ var VALID_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
3826
+ var MALFORMED_SCHEME_ERROR = "URI scheme is malformed.";
3827
+ function decodeValidScheme(scheme) {
3828
+ const decodedScheme = unescape(String(scheme));
3829
+ if (!VALID_SCHEME.test(decodedScheme)) {
3830
+ throw new TypeError(MALFORMED_SCHEME_ERROR);
3831
+ }
3832
+ return decodedScheme;
3833
+ }
3635
3834
  function normalize(uri, options) {
3636
3835
  if (typeof uri === "string") {
3637
3836
  uri = /** @type {T} */
@@ -3644,7 +3843,34 @@ var require_fast_uri = __commonJS({
3644
3843
  }
3645
3844
  function resolve(baseURI, relativeURI, options) {
3646
3845
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3647
- const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3846
+ const {
3847
+ parsed: baseParsed,
3848
+ malformedAuthorityOrPort: baseMalformed,
3849
+ malformedPercentEncoding: baseMalformedPercentEncoding,
3850
+ malformedSchemeSpecific: baseMalformedSchemeSpecific,
3851
+ malformedHost: baseMalformedHost,
3852
+ malformedScheme: baseMalformedScheme
3853
+ } = parseWithStatus(baseURI, schemelessOptions);
3854
+ const {
3855
+ parsed: relativeParsed,
3856
+ malformedAuthorityOrPort: relativeMalformed,
3857
+ malformedPercentEncoding: relativeMalformedPercentEncoding,
3858
+ malformedSchemeSpecific: relativeMalformedSchemeSpecific,
3859
+ malformedHost: relativeMalformedHost,
3860
+ malformedScheme: relativeMalformedScheme
3861
+ } = parseWithStatus(relativeURI, schemelessOptions);
3862
+ if (baseMalformed || relativeMalformed || baseMalformedPercentEncoding || relativeMalformedPercentEncoding || baseMalformedSchemeSpecific || relativeMalformedSchemeSpecific || baseMalformedHost || relativeMalformedHost || baseMalformedScheme || relativeMalformedScheme) {
3863
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
3864
+ }
3865
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
3866
+ const resolvedSchemeHandler = getSchemeHandler(options && options.scheme || resolved.scheme);
3867
+ const resolvedHost = resolved.host;
3868
+ const resolvedHostIsIP = resolvedHost !== void 0 && resolvedHost !== "" && (isIPv42(resolvedHost) || normalizeIPv6(resolvedHost).isIPV6);
3869
+ canonicalizeHost(resolved, options || {}, resolvedSchemeHandler, resolvedHostIsIP);
3870
+ const encodedASCIIHost = resolvedHost && resolvedHost.indexOf("%") !== -1 && !new RegExp("\\P{ASCII}", "u").test(resolvedHost);
3871
+ if (resolved.error && !encodedASCIIHost) {
3872
+ throw new Error(resolved.error);
3873
+ }
3648
3874
  schemelessOptions.skipEscape = true;
3649
3875
  return serialize(resolved, schemelessOptions);
3650
3876
  }
@@ -3704,7 +3930,7 @@ var require_fast_uri = __commonJS({
3704
3930
  function equal(uriA, uriB, options) {
3705
3931
  const normalizedA = normalizeComparableURI(uriA, options);
3706
3932
  const normalizedB = normalizeComparableURI(uriB, options);
3707
- return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3933
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA === normalizedB;
3708
3934
  }
3709
3935
  function serialize(cmpts, opts) {
3710
3936
  const component = {
@@ -3725,19 +3951,22 @@ var require_fast_uri = __commonJS({
3725
3951
  };
3726
3952
  const options = Object.assign({}, opts);
3727
3953
  const uriTokens = [];
3954
+ if (component.scheme) {
3955
+ component.scheme = decodeValidScheme(component.scheme);
3956
+ }
3728
3957
  const schemeHandler = getSchemeHandler(options.scheme || component.scheme);
3729
3958
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
3959
+ const hasAuthority = component.userinfo !== void 0 || component.host !== void 0 || component.port !== void 0;
3960
+ const pathNoScheme = !options.skipEscape && component.scheme === void 0 && !hasAuthority;
3730
3961
  if (component.path !== void 0) {
3731
3962
  if (!options.skipEscape) {
3732
- component.path = escapePreservingEscapes(component.path);
3733
- if (component.scheme !== void 0) {
3734
- component.path = component.path.split("%3A").join(":");
3735
- }
3963
+ component.path = serializePathEncoding(component.path, pathNoScheme);
3736
3964
  } else {
3737
3965
  component.path = normalizePercentEncoding(component.path);
3738
3966
  }
3739
3967
  }
3740
3968
  if (options.reference !== "suffix" && component.scheme) {
3969
+ component.scheme = decodeValidScheme(component.scheme);
3741
3970
  uriTokens.push(component.scheme, ":");
3742
3971
  }
3743
3972
  const authority = recomposeAuthority(component);
@@ -3755,20 +3984,25 @@ var require_fast_uri = __commonJS({
3755
3984
  if (!options.absolutePath && (!schemeHandler || !schemeHandler.absolutePath)) {
3756
3985
  s = removeDotSegments(s);
3757
3986
  }
3987
+ if (pathNoScheme) {
3988
+ s = serializePathEncoding(s, true);
3989
+ }
3758
3990
  if (authority === void 0 && s[0] === "/" && s[1] === "/") {
3759
3991
  s = "/%2F" + s.slice(2);
3760
3992
  }
3761
3993
  uriTokens.push(s);
3762
3994
  }
3763
3995
  if (component.query !== void 0) {
3764
- uriTokens.push("?", component.query);
3996
+ uriTokens.push("?", encodeQuery(component.query));
3765
3997
  }
3766
3998
  if (component.fragment !== void 0) {
3767
- uriTokens.push("#", component.fragment);
3999
+ uriTokens.push("#", encodeFragment(component.fragment));
3768
4000
  }
3769
4001
  return uriTokens.join("");
3770
4002
  }
3771
4003
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
4004
+ var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
4005
+ var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
3772
4006
  function getParseError(parsed, matches) {
3773
4007
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3774
4008
  return 'URI path must start with "/" when authority is present.';
@@ -3778,6 +4012,35 @@ var require_fast_uri = __commonJS({
3778
4012
  }
3779
4013
  return void 0;
3780
4014
  }
4015
+ function hasMalformedPercentEncoding(component) {
4016
+ if (component === void 0) return false;
4017
+ let percent = component.indexOf("%");
4018
+ while (percent !== -1) {
4019
+ if (percent + 2 >= component.length || !/^[\da-f]{2}$/iu.test(component.slice(percent + 1, percent + 3))) {
4020
+ return true;
4021
+ }
4022
+ percent = component.indexOf("%", percent + 3);
4023
+ }
4024
+ return false;
4025
+ }
4026
+ function isIPLiteral(host) {
4027
+ return host[0] === "[" && host[host.length - 1] === "]";
4028
+ }
4029
+ function hasMalformedComponentPercentEncoding(matches) {
4030
+ const host = matches[4];
4031
+ return hasMalformedPercentEncoding(matches[3]) || host !== void 0 && !isIPLiteral(host) && hasMalformedPercentEncoding(host) || hasMalformedPercentEncoding(matches[6]) || hasMalformedPercentEncoding(matches[7]) || hasMalformedPercentEncoding(matches[8]);
4032
+ }
4033
+ function canonicalizeHost(parsed, options, schemeHandler, isIP2) {
4034
+ if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport) && parsed.host && !isIPLiteral(parsed.host) && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP2 === false && nonSimpleDomain(parsed.host)) {
4035
+ try {
4036
+ parsed.host = new URL("http://" + parsed.host).hostname;
4037
+ } catch (e) {
4038
+ parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
4039
+ return true;
4040
+ }
4041
+ }
4042
+ return false;
4043
+ }
3781
4044
  function parseWithStatus(uri, opts) {
3782
4045
  const options = Object.assign({}, opts);
3783
4046
  const parsed = {
@@ -3790,6 +4053,11 @@ var require_fast_uri = __commonJS({
3790
4053
  fragment: void 0
3791
4054
  };
3792
4055
  let malformedAuthorityOrPort = false;
4056
+ let malformedPercentEncoding = false;
4057
+ let malformedSchemeSpecific = false;
4058
+ let malformedHost = false;
4059
+ let malformedIPLiteral = false;
4060
+ let malformedScheme = false;
3793
4061
  let isIP2 = false;
3794
4062
  if (options.reference === "suffix") {
3795
4063
  if (options.scheme) {
@@ -3798,6 +4066,25 @@ var require_fast_uri = __commonJS({
3798
4066
  uri = "//" + uri;
3799
4067
  }
3800
4068
  }
4069
+ const authorityMatch = uri.match(AUTHORITY_PREFIX);
4070
+ if (authorityMatch !== null && authorityMatch[1].indexOf("\\") !== -1) {
4071
+ parsed.error = "URI authority must not contain a literal backslash.";
4072
+ malformedAuthorityOrPort = true;
4073
+ }
4074
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
4075
+ if (introducerMatch !== null) {
4076
+ const region = introducerMatch[1];
4077
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
4078
+ if (normalizedRegion.length >= 2) {
4079
+ if (normalizedRegion.slice(0, 2) !== "//") {
4080
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
4081
+ malformedAuthorityOrPort = true;
4082
+ } else if (region.length !== normalizedRegion.length) {
4083
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
4084
+ malformedAuthorityOrPort = true;
4085
+ }
4086
+ }
4087
+ }
3801
4088
  const matches = uri.match(URI_PARSE);
3802
4089
  if (matches) {
3803
4090
  parsed.scheme = matches[1];
@@ -3807,6 +4094,19 @@ var require_fast_uri = __commonJS({
3807
4094
  parsed.path = matches[6] || "";
3808
4095
  parsed.query = matches[7];
3809
4096
  parsed.fragment = matches[8];
4097
+ if (parsed.scheme !== void 0) {
4098
+ const decodedScheme = unescape(parsed.scheme);
4099
+ if (VALID_SCHEME.test(decodedScheme)) {
4100
+ parsed.scheme = decodedScheme.toLowerCase();
4101
+ } else {
4102
+ parsed.error = parsed.error || MALFORMED_SCHEME_ERROR;
4103
+ malformedScheme = true;
4104
+ }
4105
+ }
4106
+ malformedPercentEncoding = hasMalformedComponentPercentEncoding(matches);
4107
+ if (malformedPercentEncoding) {
4108
+ parsed.error = parsed.error || "URI contains malformed percent-encoding.";
4109
+ }
3810
4110
  if (isNaN(parsed.port)) {
3811
4111
  parsed.port = matches[5];
3812
4112
  }
@@ -3818,9 +4118,16 @@ var require_fast_uri = __commonJS({
3818
4118
  if (parsed.host) {
3819
4119
  const ipv4result = isIPv42(parsed.host);
3820
4120
  if (ipv4result === false) {
4121
+ const bracketedIPLiteral = isIPLiteral(parsed.host);
4122
+ const hasIPLiteralBracket = parsed.host.indexOf("[") !== -1 || parsed.host.indexOf("]") !== -1;
3821
4123
  const ipv6result = normalizeIPv6(parsed.host);
3822
- parsed.host = ipv6result.host.toLowerCase();
3823
- isIP2 = ipv6result.isIPV6;
4124
+ isIP2 = ipv6result.isIPV6 || ipv6result.isIPVFuture === true;
4125
+ malformedIPLiteral = hasIPLiteralBracket && (!bracketedIPLiteral || ipv6result.error === true);
4126
+ parsed.host = isIP2 ? ipv6result.host : ipv6result.host.toLowerCase();
4127
+ if (malformedIPLiteral) {
4128
+ parsed.error = parsed.error || "URI host is malformed.";
4129
+ malformedAuthorityOrPort = true;
4130
+ }
3824
4131
  } else {
3825
4132
  isIP2 = true;
3826
4133
  }
@@ -3838,42 +4145,36 @@ var require_fast_uri = __commonJS({
3838
4145
  parsed.error = parsed.error || "URI is not a " + options.reference + " reference.";
3839
4146
  }
3840
4147
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
3841
- if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
3842
- if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP2 === false && nonSimpleDomain(parsed.host)) {
3843
- try {
3844
- parsed.host = URL.domainToASCII(parsed.host.toLowerCase());
3845
- } catch (e) {
3846
- parsed.error = parsed.error || "Host's domain name can not be converted to ASCII: " + e;
3847
- }
3848
- }
4148
+ if (!malformedIPLiteral) {
4149
+ malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP2);
3849
4150
  }
3850
4151
  if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
3851
4152
  if (uri.indexOf("%") !== -1) {
3852
- if (parsed.scheme !== void 0) {
3853
- parsed.scheme = unescape(parsed.scheme);
3854
- }
3855
- if (parsed.host !== void 0) {
3856
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP2);
4153
+ if (parsed.host !== void 0 && !malformedIPLiteral) {
4154
+ const host = isIP2 ? parsed.host : normalizePercentEncoding(parsed.host, true);
4155
+ parsed.host = reescapeHostDelimiters(host, isIP2);
3857
4156
  }
3858
4157
  }
3859
4158
  if (parsed.path) {
3860
4159
  parsed.path = normalizePathEncoding(parsed.path);
3861
4160
  }
4161
+ if (parsed.query) {
4162
+ parsed.query = normalizeQueryFragmentEncoding(parsed.query);
4163
+ }
3862
4164
  if (parsed.fragment) {
3863
- try {
3864
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3865
- } catch {
3866
- parsed.error = parsed.error || "URI malformed";
3867
- }
4165
+ parsed.fragment = normalizeQueryFragmentEncoding(parsed.fragment);
3868
4166
  }
3869
4167
  }
3870
4168
  if (schemeHandler && schemeHandler.parse) {
3871
4169
  schemeHandler.parse(parsed, options);
4170
+ if (schemeHandler === SCHEMES.urn && parsed.nid === void 0) {
4171
+ malformedSchemeSpecific = true;
4172
+ }
3872
4173
  }
3873
4174
  } else {
3874
4175
  parsed.error = parsed.error || "URI can not be parsed.";
3875
4176
  }
3876
- return { parsed, malformedAuthorityOrPort };
4177
+ return { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme };
3877
4178
  }
3878
4179
  function parse3(uri, opts) {
3879
4180
  return parseWithStatus(uri, opts).parsed;
@@ -3882,20 +4183,28 @@ var require_fast_uri = __commonJS({
3882
4183
  return normalizeStringWithStatus(uri, opts).normalized;
3883
4184
  }
3884
4185
  function normalizeStringWithStatus(uri, opts) {
3885
- const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
4186
+ const { parsed, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = parseWithStatus(uri, opts);
3886
4187
  return {
3887
- normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3888
- malformedAuthorityOrPort
4188
+ normalized: malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? uri : serialize(parsed, opts),
4189
+ malformedAuthorityOrPort,
4190
+ malformedPercentEncoding,
4191
+ malformedSchemeSpecific,
4192
+ malformedHost,
4193
+ malformedScheme
3889
4194
  };
3890
4195
  }
3891
4196
  function normalizeComparableURI(uri, opts) {
3892
- if (typeof uri === "string") {
3893
- const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3894
- return malformedAuthorityOrPort ? void 0 : normalized;
4197
+ if (typeof uri !== "string" && typeof uri !== "object") {
4198
+ return void 0;
3895
4199
  }
3896
- if (typeof uri === "object") {
3897
- return serialize(uri, opts);
4200
+ let value;
4201
+ try {
4202
+ value = typeof uri === "string" ? uri : serialize(uri, opts);
4203
+ } catch {
4204
+ return void 0;
3898
4205
  }
4206
+ const { normalized, malformedAuthorityOrPort, malformedPercentEncoding, malformedSchemeSpecific, malformedHost, malformedScheme } = normalizeStringWithStatus(value, opts);
4207
+ return malformedAuthorityOrPort || malformedPercentEncoding || malformedSchemeSpecific || malformedHost || malformedScheme ? void 0 : normalized;
3899
4208
  }
3900
4209
  var fastUri = {
3901
4210
  SCHEMES,
@@ -23045,17 +23354,33 @@ function normalizeObjectSchema(schema) {
23045
23354
  }
23046
23355
  return void 0;
23047
23356
  }
23357
+ function getDotPath(path) {
23358
+ if (path.length === 0) {
23359
+ return "object root";
23360
+ }
23361
+ return path.reduce((acc, seg, index) => {
23362
+ if (index === 0) {
23363
+ return String(seg);
23364
+ }
23365
+ if (typeof seg === "number") {
23366
+ return `${acc}[${seg}]`;
23367
+ }
23368
+ return `${acc}.${seg}`;
23369
+ }, "");
23370
+ }
23048
23371
  function getParseErrorMessage(error51) {
23049
23372
  if (error51 && typeof error51 === "object") {
23373
+ if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
23374
+ return error51.issues.map((i) => {
23375
+ if (!i.path?.length) {
23376
+ return i.message;
23377
+ }
23378
+ return `${i.message} at ${getDotPath(i.path)}`;
23379
+ }).join("\n");
23380
+ }
23050
23381
  if ("message" in error51 && typeof error51.message === "string") {
23051
23382
  return error51.message;
23052
23383
  }
23053
- if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
23054
- const firstIssue = error51.issues[0];
23055
- if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
23056
- return String(firstIssue.message);
23057
- }
23058
- }
23059
23384
  try {
23060
23385
  return JSON.stringify(error51);
23061
23386
  } catch {
@@ -29670,16 +29995,7 @@ var Server = class extends Protocol {
29670
29995
  if (!methodSchema) {
29671
29996
  throw new Error("Schema is missing a method literal");
29672
29997
  }
29673
- let methodValue;
29674
- if (isZ4Schema(methodSchema)) {
29675
- const v4Schema = methodSchema;
29676
- const v4Def = v4Schema._zod?.def;
29677
- methodValue = v4Def?.value ?? v4Schema.value;
29678
- } else {
29679
- const v3Schema = methodSchema;
29680
- const legacyDef = v3Schema._def;
29681
- methodValue = legacyDef?.value ?? v3Schema.value;
29682
- }
29998
+ const methodValue = getLiteralValue(methodSchema);
29683
29999
  if (typeof methodValue !== "string") {
29684
30000
  throw new Error("Schema method literal must be a string");
29685
30001
  }
@@ -30867,8 +31183,17 @@ var EMPTY_COMPLETION_RESULT = {
30867
31183
  import process3 from "node:process";
30868
31184
 
30869
31185
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
31186
+ var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
30870
31187
  var ReadBuffer = class {
31188
+ constructor(options) {
31189
+ this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
31190
+ }
30871
31191
  append(chunk) {
31192
+ const newSize = (this._buffer?.length ?? 0) + chunk.length;
31193
+ if (newSize > this._maxBufferSize) {
31194
+ this.clear();
31195
+ throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
31196
+ }
30872
31197
  this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
30873
31198
  }
30874
31199
  readMessage() {
@@ -30896,18 +31221,24 @@ function serializeMessage(message) {
30896
31221
 
30897
31222
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
30898
31223
  var StdioServerTransport = class {
30899
- constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
31224
+ constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
30900
31225
  this._stdin = _stdin;
30901
31226
  this._stdout = _stdout;
30902
- this._readBuffer = new ReadBuffer();
30903
31227
  this._started = false;
30904
31228
  this._ondata = (chunk) => {
30905
- this._readBuffer.append(chunk);
30906
- this.processReadBuffer();
31229
+ try {
31230
+ this._readBuffer.append(chunk);
31231
+ this.processReadBuffer();
31232
+ } catch (error51) {
31233
+ this.onerror?.(error51);
31234
+ this.close().catch(() => {
31235
+ });
31236
+ }
30907
31237
  };
30908
31238
  this._onerror = (error51) => {
30909
31239
  this.onerror?.(error51);
30910
31240
  };
31241
+ this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
30911
31242
  }
30912
31243
  /**
30913
31244
  * Starts listening for messages on stdin.
@@ -30966,7 +31297,9 @@ var DEFAULT_429_DELAY_MS = 1e3;
30966
31297
  var MAX_429_DELAY_MS = 3e4;
30967
31298
  var MAX_429_JITTER_MS = 250;
30968
31299
  var MAX_REQUEST_BUDGET_MS = 9e4;
31300
+ var GATEWAY_5XX_BUDGET_FRACTION = 0.5;
30969
31301
  var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
31302
+ var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
30970
31303
  var oauthToken = null;
30971
31304
  var oauthRefreshPromise = null;
30972
31305
  function getAuthConfig() {
@@ -31020,7 +31353,7 @@ async function getOAuthAccessToken(clientId, clientSecret) {
31020
31353
  });
31021
31354
  if (!res.ok) {
31022
31355
  const body = await res.text();
31023
- const guidance = res.status === 401 || res.status === 403 ? " Verify TAILSCALE_OAUTH_CLIENT_ID and TAILSCALE_OAUTH_CLIENT_SECRET, and that the client has the scopes your tools need (https://console.tailscale.com/admin/settings/oauth)." + (oauthTailnet ? ` Targeting tailnet "${oauthTailnet}" via TAILSCALE_OAUTH_TAILNET -- that requires an OAuth client from the CREATING tailnet with the 'all' scope.` : "") : "";
31356
+ const guidance = res.status === 401 || res.status === 403 ? " Verify TAILSCALE_OAUTH_CLIENT_ID and TAILSCALE_OAUTH_CLIENT_SECRET, and that the client has the scopes your tools need (https://console.tailscale.com/admin/settings/trust-credentials)." + (oauthTailnet ? ` Targeting tailnet "${oauthTailnet}" via TAILSCALE_OAUTH_TAILNET -- that requires an OAuth client from the CREATING tailnet with the 'all' scope.` : "") : "";
31024
31357
  throw new Error(`OAuth token exchange failed (${res.status}): ${body}.${guidance}`);
31025
31358
  }
31026
31359
  const data = await res.json();
@@ -31054,6 +31387,7 @@ function getTailnet() {
31054
31387
  function encPath(segment) {
31055
31388
  return encodeURIComponent(segment);
31056
31389
  }
31390
+ var DEVICE_ID_HINT = "nodeId from tailscale_list_devices (e.g. nPM2KNuedB21DEVEL); numeric id ok; not the nodeKey.";
31057
31391
  function validateTags(tags) {
31058
31392
  if (!tags || tags.length === 0) return;
31059
31393
  const invalid = tags.filter((t) => !t.startsWith("tag:"));
@@ -31072,6 +31406,7 @@ function validateAndSanitizeDescription(value) {
31072
31406
  `description ${JSON.stringify(value)} contains no valid characters after sanitization. Allowed characters: alphanumeric, spaces, and hyphens (max 50 chars).`
31073
31407
  );
31074
31408
  }
31409
+ var OAUTH_SCOPE_TABLE_HEADING = "OAuth scopes by tool group";
31075
31410
  function formatAuthError(status, apiBody) {
31076
31411
  let usingOAuth = false;
31077
31412
  try {
@@ -31080,7 +31415,7 @@ function formatAuthError(status, apiBody) {
31080
31415
  usingOAuth = false;
31081
31416
  }
31082
31417
  const headline = status === 401 ? "Authentication failed (HTTP 401)." : "Authorization failed (HTTP 403): the request was authenticated but not permitted for this resource.";
31083
- 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";
31418
+ 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 (scopes per tool group: README, "${OAUTH_SCOPE_TABLE_HEADING}")` : " - API key lacks the permission required for this endpoint";
31084
31419
  const lines = [headline, "", "Possible causes:", cause];
31085
31420
  if (status === 401 && process.platform === "win32" && !usingOAuth) {
31086
31421
  lines.push(
@@ -31091,13 +31426,89 @@ function formatAuthError(status, apiBody) {
31091
31426
  " 2. Set TAILSCALE_API_KEY as a Windows user environment variable (System Properties > Environment Variables)"
31092
31427
  );
31093
31428
  }
31094
- const link = status === 401 ? "Generate a new key at: https://console.tailscale.com/admin/settings/keys" : usingOAuth ? "Adjust the OAuth client scopes at: https://console.tailscale.com/admin/settings/oauth" : "Adjust the API key permissions at: https://console.tailscale.com/admin/settings/keys";
31429
+ const link = status === 401 ? "Generate a new key at: https://console.tailscale.com/admin/settings/keys" : usingOAuth ? "Adjust the credential's scopes at: https://console.tailscale.com/admin/settings/trust-credentials" : "Adjust the API key permissions at: https://console.tailscale.com/admin/settings/keys";
31095
31430
  lines.push("", link);
31096
31431
  if (apiBody) {
31097
31432
  lines.push("", `API response: ${apiBody}`);
31098
31433
  }
31099
31434
  return lines.join("\n");
31100
31435
  }
31436
+ var ERROR_DATA_MAX_LINES = 100;
31437
+ var ERROR_DATA_MAX_CHARS = 8e3;
31438
+ var ERROR_DATA_MAX_JSON_CHARS = 1e3;
31439
+ var SENSITIVE_DATA_KEY = /secret|token|password|credential|key/i;
31440
+ function redactSensitive(value) {
31441
+ if (Array.isArray(value)) return value.map(redactSensitive);
31442
+ if (value === null || typeof value !== "object") return value;
31443
+ const out = {};
31444
+ for (const [key, inner] of Object.entries(value)) {
31445
+ out[key] = SENSITIVE_DATA_KEY.test(key) ? "[redacted]" : redactSensitive(inner);
31446
+ }
31447
+ return out;
31448
+ }
31449
+ function asStringArray(value) {
31450
+ return Array.isArray(value) && value.every((v) => typeof v === "string") ? value : void 0;
31451
+ }
31452
+ function renderJsonEntry(value) {
31453
+ const json2 = JSON.stringify(redactSensitive(value)) ?? String(value);
31454
+ if (json2.length <= ERROR_DATA_MAX_JSON_CHARS) return json2;
31455
+ return `${json2.slice(0, ERROR_DATA_MAX_JSON_CHARS)}... (${json2.length - ERROR_DATA_MAX_JSON_CHARS} more characters)`;
31456
+ }
31457
+ function joinErrorDataBlocks(blocks) {
31458
+ const out = [];
31459
+ let lines = 0;
31460
+ let chars = 0;
31461
+ let shown = 0;
31462
+ for (const block of blocks) {
31463
+ const blockChars = block.reduce((n, line) => n + line.length + 1, 0);
31464
+ if (lines + block.length <= ERROR_DATA_MAX_LINES && chars + blockChars <= ERROR_DATA_MAX_CHARS) {
31465
+ out.push(...block);
31466
+ lines += block.length;
31467
+ chars += blockChars;
31468
+ shown++;
31469
+ continue;
31470
+ }
31471
+ if (shown === 0) {
31472
+ const kept = block.slice(0, ERROR_DATA_MAX_LINES);
31473
+ out.push(...kept);
31474
+ if (kept.length < block.length) out.push(`... and ${block.length - kept.length} more lines in this entry`);
31475
+ shown++;
31476
+ }
31477
+ break;
31478
+ }
31479
+ const dropped = blocks.length - shown;
31480
+ if (dropped > 0) out.push(`... and ${dropped} more ${dropped === 1 ? "entry" : "entries"}`);
31481
+ return out.join("\n");
31482
+ }
31483
+ function formatApiErrorData(data) {
31484
+ if (!Array.isArray(data) || data.length === 0) return "";
31485
+ const blocks = [];
31486
+ for (const entry of data) {
31487
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
31488
+ blocks.push([renderJsonEntry(entry)]);
31489
+ continue;
31490
+ }
31491
+ const obj = entry;
31492
+ const consumed = /* @__PURE__ */ new Set();
31493
+ const rendered = [];
31494
+ if (typeof obj.user === "string") {
31495
+ consumed.add("user");
31496
+ if (obj.user.length > 0) rendered.push(`For user ${obj.user}:`);
31497
+ }
31498
+ for (const [key, heading] of [
31499
+ ["errors", "Errors found:"],
31500
+ ["warnings", "Warnings found:"]
31501
+ ]) {
31502
+ const items = asStringArray(obj[key]);
31503
+ if (!items) continue;
31504
+ consumed.add(key);
31505
+ if (items.length > 0) rendered.push(heading, ...items.map((item) => `- ${item}`));
31506
+ }
31507
+ if (rendered.length > 0 && Object.keys(obj).every((key) => consumed.has(key))) blocks.push(rendered);
31508
+ else blocks.push([renderJsonEntry(obj)]);
31509
+ }
31510
+ return joinErrorDataBlocks(blocks);
31511
+ }
31101
31512
  function extractErrorMessage(body) {
31102
31513
  if (!body) return body;
31103
31514
  const trimmed = body.trim();
@@ -31106,8 +31517,12 @@ function extractErrorMessage(body) {
31106
31517
  const parsed = JSON.parse(trimmed);
31107
31518
  if (parsed && typeof parsed === "object") {
31108
31519
  const obj = parsed;
31109
- if (typeof obj.message === "string" && obj.message.length > 0) return obj.message;
31110
- if (typeof obj.error === "string" && obj.error.length > 0) return obj.error;
31520
+ const message = typeof obj.message === "string" && obj.message.length > 0 ? obj.message : typeof obj.error === "string" && obj.error.length > 0 ? obj.error : void 0;
31521
+ if (message !== void 0) {
31522
+ const details = formatApiErrorData(obj.data);
31523
+ return details ? `${message}
31524
+ ${details}` : message;
31525
+ }
31111
31526
  }
31112
31527
  } catch {
31113
31528
  }
@@ -31205,6 +31620,15 @@ function describeBudgetExhaustion(budgetMs, queuedForMs, lastTransportError) {
31205
31620
  }
31206
31621
  return `Request budget of ${budgetMs}ms exhausted before attempt could begin.`;
31207
31622
  }
31623
+ function annotateAmbiguousDelete(error51, status, method, priorGatewayStatus, priorTransportError) {
31624
+ if (status !== 404 || method.toUpperCase() !== "DELETE") return error51;
31625
+ const causes = [];
31626
+ if (priorGatewayStatus !== void 0) causes.push(`returned HTTP ${priorGatewayStatus}`);
31627
+ if (priorTransportError !== void 0) causes.push(`never returned a response (${priorTransportError})`);
31628
+ if (causes.length === 0) return error51;
31629
+ const subject = causes.length > 1 ? "earlier attempts" : "an earlier attempt";
31630
+ return `${error51 || `HTTP ${status}`} (${subject} ${causes.join(" and ")}; the delete may already have succeeded)`;
31631
+ }
31208
31632
  async function apiRequest(method, path, body, options) {
31209
31633
  const headers = {};
31210
31634
  if (options?.accept) {
@@ -31238,16 +31662,20 @@ async function apiRequest(method, path, body, options) {
31238
31662
  headers.Authorization = await getAuthHeader();
31239
31663
  let res;
31240
31664
  let lastTransportError;
31665
+ let priorGatewayStatus;
31666
+ let priorTransportError;
31667
+ let budgetMs = requestBudgetMs;
31241
31668
  for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
31242
- const remaining = requestBudgetMs - (Date.now() - startedAt);
31669
+ const remaining = budgetMs - (Date.now() - startedAt);
31243
31670
  if (remaining <= 0) {
31244
31671
  return {
31245
31672
  ok: false,
31246
31673
  status: 0,
31247
- error: describeBudgetExhaustion(requestBudgetMs, queuedForMs, lastTransportError)
31674
+ error: describeBudgetExhaustion(budgetMs, queuedForMs, lastTransportError)
31248
31675
  };
31249
31676
  }
31250
31677
  const attemptTimeoutMs = Math.min(REQUEST_TIMEOUT_MS, remaining);
31678
+ const attemptStartedAt = Date.now();
31251
31679
  let attemptRes;
31252
31680
  try {
31253
31681
  attemptRes = await executeFetch(method, url2, headers, fetchBody, attemptTimeoutMs);
@@ -31259,25 +31687,35 @@ async function apiRequest(method, path, body, options) {
31259
31687
  }
31260
31688
  const delay2 = compute429DelayMs(null, attempt);
31261
31689
  const elapsed3 = Date.now() - startedAt;
31262
- if (requestBudgetMs - elapsed3 - delay2 <= 0) {
31690
+ if (budgetMs - elapsed3 - delay2 <= 0) {
31263
31691
  return { ok: false, status: 0, error: `${desc}; request budget exhausted before retry.` };
31264
31692
  }
31265
31693
  debugLog(
31266
31694
  ` -> transport error (attempt ${attempt + 1}/${MAX_429_RETRIES + 1}): ${desc}, retrying in ${delay2}ms`
31267
31695
  );
31696
+ priorTransportError = desc;
31268
31697
  await new Promise((r) => setTimeout(r, delay2));
31269
31698
  continue;
31270
31699
  }
31271
31700
  res = attemptRes;
31272
- if (res.status !== 429 || attempt === MAX_429_RETRIES || !isRetryable) break;
31701
+ if (!RETRYABLE_STATUSES.has(res.status) || attempt === MAX_429_RETRIES || !isRetryable) break;
31273
31702
  const delay = compute429DelayMs(res.headers.get("retry-after"), attempt);
31703
+ const gatewayCeilingMs = Math.min(budgetMs, Math.floor(requestBudgetMs * GATEWAY_5XX_BUDGET_FRACTION));
31704
+ const retryCeilingMs = res.status === 429 ? budgetMs : gatewayCeilingMs;
31274
31705
  const elapsed2 = Date.now() - startedAt;
31275
- const nextAttemptBudgetMs = requestBudgetMs - elapsed2 - delay;
31706
+ const predictedCostMs = res.status === 429 ? delay : delay + (Date.now() - attemptStartedAt);
31707
+ const nextAttemptBudgetMs = retryCeilingMs - elapsed2 - predictedCostMs;
31276
31708
  if (nextAttemptBudgetMs <= 0) {
31277
- debugLog(` -> 429 (attempt ${attempt + 1}), giving up: budget exhausted (${elapsed2}ms + ${delay}ms)`);
31709
+ debugLog(
31710
+ ` -> ${res.status} (attempt ${attempt + 1}), giving up: budget exhausted (${elapsed2}ms + ${predictedCostMs}ms of ${retryCeilingMs}ms)`
31711
+ );
31278
31712
  break;
31279
31713
  }
31280
- debugLog(` -> 429 (attempt ${attempt + 1}/${MAX_429_RETRIES + 1}), retrying in ${delay}ms`);
31714
+ debugLog(` -> ${res.status} (attempt ${attempt + 1}/${MAX_429_RETRIES + 1}), retrying in ${delay}ms`);
31715
+ if (res.status !== 429) {
31716
+ priorGatewayStatus = res.status;
31717
+ budgetMs = retryCeilingMs;
31718
+ }
31281
31719
  await res.text().catch(() => void 0);
31282
31720
  await new Promise((r) => setTimeout(r, delay));
31283
31721
  }
@@ -31293,14 +31731,25 @@ async function apiRequest(method, path, body, options) {
31293
31731
  const rawBody = await response.text();
31294
31732
  if (!response.ok) {
31295
31733
  const error51 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, rawBody) : extractErrorMessage(rawBody);
31296
- return { ok: false, status: response.status, error: error51, rawBody, etag };
31734
+ return {
31735
+ ok: false,
31736
+ status: response.status,
31737
+ error: annotateAmbiguousDelete(error51, response.status, method, priorGatewayStatus, priorTransportError),
31738
+ rawBody,
31739
+ etag
31740
+ };
31297
31741
  }
31298
31742
  return { ok: true, status: response.status, rawBody, etag };
31299
31743
  }
31300
31744
  if (!response.ok) {
31301
31745
  const errorBody = await response.text();
31302
31746
  const error51 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, errorBody) : extractErrorMessage(errorBody);
31303
- return { ok: false, status: response.status, error: error51, etag };
31747
+ return {
31748
+ ok: false,
31749
+ status: response.status,
31750
+ error: annotateAmbiguousDelete(error51, response.status, method, priorGatewayStatus, priorTransportError),
31751
+ etag
31752
+ };
31304
31753
  }
31305
31754
  if (response.status === 204 || response.headers.get("content-length") === "0") {
31306
31755
  return { ok: true, status: response.status, etag };
@@ -31334,26 +31783,43 @@ async function apiDelete(path, options) {
31334
31783
  }
31335
31784
 
31336
31785
  // src/cli.ts
31786
+ function isWarningsOnly(message, data) {
31787
+ if (!/^warning/i.test(message)) return false;
31788
+ if (!Array.isArray(data) || data.length === 0) return false;
31789
+ return data.every((entry) => {
31790
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
31791
+ const obj = entry;
31792
+ if (!Object.keys(obj).every((key) => key === "user" || key === "warnings")) return false;
31793
+ if (obj.user !== void 0 && typeof obj.user !== "string") return false;
31794
+ return Array.isArray(obj.warnings) && obj.warnings.length > 0 && obj.warnings.every((w) => typeof w === "string");
31795
+ });
31796
+ }
31337
31797
  function parseValidationError(rawBody) {
31338
31798
  const trimmed = rawBody?.trim();
31339
- if (!trimmed) return void 0;
31799
+ if (!trimmed) return { kind: "valid" };
31340
31800
  let parsed;
31341
31801
  try {
31342
31802
  parsed = JSON.parse(trimmed);
31343
31803
  } catch {
31344
- return trimmed;
31804
+ return { kind: "failure", message: trimmed };
31345
31805
  }
31346
31806
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
31347
- return trimmed;
31807
+ return { kind: "failure", message: trimmed };
31348
31808
  }
31349
31809
  const obj = parsed;
31350
- if (typeof obj.message === "string" && obj.message.length > 0) return obj.message;
31351
- if (typeof obj.error === "string" && obj.error.length > 0) return obj.error;
31352
- return void 0;
31810
+ const message = typeof obj.message === "string" && obj.message.length > 0 ? obj.message : typeof obj.error === "string" && obj.error.length > 0 ? obj.error : void 0;
31811
+ if (message === void 0) return { kind: "valid" };
31812
+ const details = formatApiErrorData(obj.data);
31813
+ return {
31814
+ kind: isWarningsOnly(message, obj.data) ? "warnings" : "failure",
31815
+ message,
31816
+ ...details ? { details } : {}
31817
+ };
31353
31818
  }
31354
31819
  function readPolicyFile(filePath) {
31355
31820
  try {
31356
- return readFileSync(filePath, "utf-8");
31821
+ const text = readFileSync(filePath, "utf-8");
31822
+ return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
31357
31823
  } catch (err) {
31358
31824
  console.error(`Failed to read ${filePath}: ${err instanceof Error ? err.message : err}`);
31359
31825
  process.exit(1);
@@ -31370,9 +31836,10 @@ async function validatePolicy(policy) {
31370
31836
  console.error(`ACL validation failed: ${validateRes.error}`);
31371
31837
  process.exit(1);
31372
31838
  }
31373
- const validationError = parseValidationError(validateRes.rawBody);
31374
- if (validationError) {
31375
- console.error(`ACL validation failed: ${validationError}`);
31839
+ const validation = parseValidationError(validateRes.rawBody);
31840
+ if (validation.kind !== "valid") {
31841
+ console.error(`ACL validation failed: ${validation.message}${validation.details ? `
31842
+ ${validation.details}` : ""}`);
31376
31843
  process.exit(1);
31377
31844
  }
31378
31845
  }
@@ -31542,6 +32009,17 @@ function stripEtagFooter(body) {
31542
32009
  }
31543
32010
  return lines.slice(0, cut).join("\n");
31544
32011
  }
32012
+ function normalizeIfMatch(etag) {
32013
+ const trimmed = etag.trim();
32014
+ if (trimmed.startsWith("W/")) return trimmed;
32015
+ const inner = trimmed.replace(/^"+|"+$/g, "").trim();
32016
+ if (!inner) {
32017
+ throw new Error(
32018
+ "etag is empty once its quotes are removed -- a value with nothing inside its quotes cannot match any ETag the tailnet holds, so this overwrite would come back 412."
32019
+ );
32020
+ }
32021
+ return `"${inner}"`;
32022
+ }
31545
32023
  var aclTools = [
31546
32024
  {
31547
32025
  name: "tailscale_get_acl",
@@ -31574,7 +32052,7 @@ var aclTools = [
31574
32052
  },
31575
32053
  {
31576
32054
  name: "tailscale_update_acl",
31577
- description: "Update the ACL policy for your tailnet. Accepts the full policy as a string to preserve formatting, comments, and trailing commas (HuJSON). You MUST pass the ETag from tailscale_get_acl to prevent overwriting concurrent changes. Always get the current ACL first, make targeted edits to the text, and pass the full modified text back.",
32055
+ description: "Update the ACL policy for your tailnet. Accepts the full policy as a string to preserve formatting, comments, and trailing commas (HuJSON). You MUST pass the ETag from tailscale_get_acl to prevent overwriting concurrent changes, or `ts-default` for the first write to a tailnet nobody has edited yet. Always get the current ACL first, make targeted edits to the text, and pass the full modified text back.",
31578
32056
  annotations: {
31579
32057
  title: "Update ACL policy",
31580
32058
  readOnlyHint: false,
@@ -31589,7 +32067,9 @@ var aclTools = [
31589
32067
  policy: external_exports.string().describe(
31590
32068
  "The full ACL policy text. Preserve existing formatting, comments, and structure. Only modify the specific parts that need to change."
31591
32069
  ),
31592
- etag: external_exports.string().trim().min(1, "etag must not be empty -- an empty ETag would send this overwrite with no concurrency guard.").describe("The ETag from tailscale_get_acl. Required to prevent concurrent edit conflicts.")
32070
+ etag: external_exports.string().trim().min(1, "etag must not be empty -- an empty ETag would send this overwrite with no concurrency guard.").describe(
32071
+ "The ETag from tailscale_get_acl (quotes optional -- they are normalized). Required to prevent concurrent edit conflicts. For the FIRST write to a fresh tailnet you may pass `ts-default` instead: the update then succeeds only if the policy file is still Tailscale's untouched default."
32072
+ )
31593
32073
  }),
31594
32074
  // `.trim().min(1)`, not a bare `z.string()`: apiRequest sets If-Match behind
31595
32075
  // `if (options?.ifMatch)`, so an empty etag is falsy there and the header is
@@ -31599,11 +32079,15 @@ var aclTools = [
31599
32079
  // on tailnets.ts's ids: a bare `.min(1)` accepts " ", which is truthy, so the
31600
32080
  // header goes out carrying a precondition that cannot match any real ETag --
31601
32081
  // a confusing 412 instead of a local validation error naming the field.
32082
+ // Quote normalization lives in the handler rather than in the schema because
32083
+ // this is the code that builds the header, and because the handlers are what
32084
+ // the tests call directly -- a transform on the schema would be invisible to
32085
+ // every assertion made at the header.
31602
32086
  handler: async (input) => {
31603
32087
  return apiPost(`/tailnet/${getTailnet()}/acl`, void 0, {
31604
32088
  rawBody: input.policy,
31605
32089
  contentType: "application/hujson",
31606
- ifMatch: input.etag,
32090
+ ifMatch: normalizeIfMatch(input.etag),
31607
32091
  acceptRaw: true,
31608
32092
  accept: "application/hujson"
31609
32093
  });
@@ -31834,9 +32318,9 @@ function assertRFC3339(value, label) {
31834
32318
  }
31835
32319
  }
31836
32320
  var MAX_LOG_RANGE_MS = 30 * 24 * 60 * 60 * 1e3;
31837
- function assertLogRange(start, end, label) {
32321
+ function assertLogRange(start, end, label, nowMs = Date.now()) {
31838
32322
  const startMs = Date.parse(start);
31839
- const endMs = end ? Date.parse(end) : Date.now();
32323
+ const endMs = end ? Date.parse(end) : nowMs;
31840
32324
  if (endMs < startMs) {
31841
32325
  throw new Error(`${label}: end must be >= start. start=${start} end=${end ?? "<now>"}`);
31842
32326
  }
@@ -31846,10 +32330,14 @@ function assertLogRange(start, end, label) {
31846
32330
  );
31847
32331
  }
31848
32332
  }
32333
+ function isoSecond(now) {
32334
+ const pad = (value, width = 2) => String(value).padStart(width, "0");
32335
+ return `${pad(now.getUTCFullYear(), 4)}-${pad(now.getUTCMonth() + 1)}-${pad(now.getUTCDate())}T${pad(now.getUTCHours())}:${pad(now.getUTCMinutes())}:${pad(now.getUTCSeconds())}Z`;
32336
+ }
31849
32337
  var auditTools = [
31850
32338
  {
31851
32339
  name: "tailscale_get_audit_log",
31852
- description: "Get the tailnet audit/configuration log. Shows who changed what and when \u2014 useful for troubleshooting and compliance.",
32340
+ description: "Get the tailnet audit/configuration log. Shows who changed what and when -- useful for troubleshooting and compliance. Optional actor, target and event filters narrow the query server-side, so a targeted question doesn't have to pull the whole window.",
31853
32341
  annotations: {
31854
32342
  title: "Get audit log",
31855
32343
  readOnlyHint: true,
@@ -31857,16 +32345,46 @@ var auditTools = [
31857
32345
  idempotentHint: true,
31858
32346
  openWorldHint: true
31859
32347
  },
32348
+ // The three filters are free strings rather than the spec's 138-value event
32349
+ // enum: that list is still growing (the PAM_* entries are recent), and a
32350
+ // hard enum would make a new event type unqueryable rather than merely
32351
+ // unvalidated. `.trim().min(1)` is the house idiom -- a bare min(1) admits
32352
+ // " ", which goes on the wire as `event=+...` and comes back empty with no
32353
+ // error.
32354
+ //
32355
+ // `.max(1)` for now. The spec declares no style/explode on these array
32356
+ // parameters, so repeated keys are inferred from the OpenAPI default and
32357
+ // nothing upstream corroborates it: neither the Go client nor Terraform
32358
+ // implements this endpoint, and the KB page documents only start and end. A
32359
+ // single value is wire-identical whether the server wants repeated keys or a
32360
+ // comma-joined list, so it cannot under-report; two values under the wrong
32361
+ // guess would return a subset with no error, which in a compliance query
32362
+ // reads as "that change never happened". Lift the cap once a live call
32363
+ // settles it.
31860
32364
  inputSchema: external_exports.object({
31861
32365
  start: external_exports.string().describe("Start time in RFC3339 format (e.g. '2026-04-01T00:00:00Z'). Required."),
31862
- end: external_exports.string().optional().describe("End time in RFC3339 format. Defaults to now.")
32366
+ end: external_exports.string().optional().describe(
32367
+ "End time in RFC3339 format. Optional: when omitted the tool sends the current time, which Tailscale's API requires."
32368
+ ),
32369
+ actor: external_exports.array(external_exports.string().trim().min(1)).max(1).optional().describe(
32370
+ "Server-side filter: one exact actor ID, or '~text' to wildcard-match a login or display name (e.g. '~bob'). One value per call -- how the API reads a repeated filter key is not verified yet."
32371
+ ),
32372
+ target: external_exports.array(external_exports.string().trim().min(1)).max(1).optional().describe(
32373
+ "Server-side filter: one string, matched against any part of any of an entry's targets (ID or name). One value per call, as for actor."
32374
+ ),
32375
+ event: external_exports.array(external_exports.string().trim().min(1)).max(1).optional().describe(
32376
+ "Server-side filter: one event type from Tailscale's audit event list, e.g. 'TAILNET.UPDATE.ACL', 'TAILNET.UPDATE.DNS_CONFIG', 'NODE.CREATE', 'NODE.DELETE', 'API_KEY.CREATE', 'USER.UPDATE.USER_ROLE', 'WEBHOOK_ENDPOINT.CREATE'. Not a closed set -- the list keeps growing. One value per call, as for actor."
32377
+ )
31863
32378
  }),
31864
32379
  handler: async (input) => {
31865
32380
  assertRFC3339(input.start, "start");
31866
32381
  if (input.end) assertRFC3339(input.end, "end");
31867
- assertLogRange(input.start, input.end, "tailscale_get_audit_log");
31868
- const params = new URLSearchParams({ start: input.start });
31869
- if (input.end) params.set("end", input.end);
32382
+ const wireEnd = input.end ? input.end : isoSecond(/* @__PURE__ */ new Date());
32383
+ assertLogRange(input.start, input.end, "tailscale_get_audit_log", Date.parse(wireEnd));
32384
+ const params = new URLSearchParams({ start: input.start, end: wireEnd });
32385
+ for (const actor of input.actor ?? []) params.append("actor", actor);
32386
+ for (const target of input.target ?? []) params.append("target", target);
32387
+ for (const event of input.event ?? []) params.append("event", event);
31870
32388
  return apiGet(`/tailnet/${getTailnet()}/logging/configuration?${params}`);
31871
32389
  }
31872
32390
  },
@@ -31880,16 +32398,20 @@ var auditTools = [
31880
32398
  idempotentHint: true,
31881
32399
  openWorldHint: true
31882
32400
  },
32401
+ // The spec gives this endpoint no actor/target/event filters -- they are on
32402
+ // the configuration log only.
31883
32403
  inputSchema: external_exports.object({
31884
32404
  start: external_exports.string().describe("Start time in RFC3339 format (e.g. '2026-04-01T00:00:00Z'). Required."),
31885
- end: external_exports.string().optional().describe("End time in RFC3339 format. Defaults to now.")
32405
+ end: external_exports.string().optional().describe(
32406
+ "End time in RFC3339 format. Optional: when omitted the tool sends the current time, which Tailscale's API requires."
32407
+ )
31886
32408
  }),
31887
32409
  handler: async (input) => {
31888
32410
  assertRFC3339(input.start, "start");
31889
32411
  if (input.end) assertRFC3339(input.end, "end");
31890
- assertLogRange(input.start, input.end, "tailscale_get_network_flow_logs");
31891
- const params = new URLSearchParams({ start: input.start });
31892
- if (input.end) params.set("end", input.end);
32412
+ const wireEnd = input.end ? input.end : isoSecond(/* @__PURE__ */ new Date());
32413
+ assertLogRange(input.start, input.end, "tailscale_get_network_flow_logs", Date.parse(wireEnd));
32414
+ const params = new URLSearchParams({ start: input.start, end: wireEnd });
31893
32415
  return apiGet(`/tailnet/${getTailnet()}/logging/network?${params}`);
31894
32416
  }
31895
32417
  }
@@ -31908,10 +32430,12 @@ function isCidr(s) {
31908
32430
  if (net.isIPv6(addr)) return prefixN <= 128;
31909
32431
  return false;
31910
32432
  }
32433
+ var DEVICE_FIELDS_DESC = "Which device fields to return. Tailscale documents exactly two values. 'default' (also what you get when this is omitted) is the limited set: addresses, id, nodeId, user, name, hostname, clientVersion, updateAvailable, os, created, connectedToControl, lastSeen, keyExpiryDisabled, expires, authorized, isExternal, machineKey, nodeKey, blocksIncomingConnections, tailnetLockKey, tailnetLockError, tags, isEphemeral. 'all' adds advertisedRoutes, enabledRoutes, clientConnectivity (endpoints, DERP latency), sshEnabled, distro, multipleConnections and postureIdentity (serial numbers and, where a posture integration collects them, hardware/MAC addresses). Omitting it does NOT return everything.";
32434
+ var DEVICE_LAST_SEEN_NOTE = " 'lastSeen' is omitted while a device is connected (connectedToControl: true) and for devices that have never been online -- on a connected device a missing lastSeen means online now, not never seen.";
31911
32435
  var deviceTools = [
31912
32436
  {
31913
32437
  name: "tailscale_list_devices",
31914
- description: "List all devices in your tailnet with their status, IP addresses, OS, and last seen time.",
32438
+ description: `List all devices in your tailnet with their status, IP addresses, OS, and last seen time.${DEVICE_LAST_SEEN_NOTE}`,
31915
32439
  annotations: {
31916
32440
  title: "List devices",
31917
32441
  readOnlyHint: true,
@@ -31920,11 +32444,9 @@ var deviceTools = [
31920
32444
  openWorldHint: true
31921
32445
  },
31922
32446
  inputSchema: external_exports.object({
31923
- fields: external_exports.string().optional().describe(
31924
- "Comma-separated list of fields to include. Omit for all fields. Valid fields: addresses, advertisedRoutes, authorized, blocksIncomingConnections, clientConnectivity, clientVersion, connectedToControl, created, distro, enabledRoutes, expires, hostname, id, isExternal, keyExpiryDisabled, lastSeen, machineKey, name, nodeId, nodeKey, os, sshEnabled, tags, tailnetLockError, tailnetLockKey, updateAvailable, user. Use 'all' for every field."
31925
- ),
31926
- filters: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
31927
- "Server-side filters as key-value pairs. Filter by any top-level device property (e.g. { isEphemeral: 'true', os: 'linux', tags: 'tag:prod' }). Multiple filters are ANDed together."
32447
+ fields: external_exports.string().optional().describe(`${DEVICE_FIELDS_DESC} Any other value is forwarded unvalidated; Tailscale documents none.`),
32448
+ filters: external_exports.record(external_exports.string(), external_exports.union([external_exports.string(), external_exports.array(external_exports.string()).min(1)])).optional().describe(
32449
+ "Server-side filters on top-level device properties, exact match only (e.g. { isEphemeral: 'true', os: 'linux' }). All filters are ANDed. Pass an array to repeat a key: { tags: ['tag:prod', 'tag:subnetrouter'] } sends tags=..&tags=.. and matches devices whose tags contain BOTH. Properties that are complex objects (e.g. clientConnectivity) cannot be filtered; repeating a key on a non-list property is undocumented upstream."
31928
32450
  )
31929
32451
  }),
31930
32452
  handler: async (input) => {
@@ -31937,7 +32459,7 @@ var deviceTools = [
31937
32459
  "filters.fields is not allowed -- use the top-level 'fields' parameter to select which device fields to return."
31938
32460
  );
31939
32461
  }
31940
- params.set(key, value);
32462
+ for (const one of Array.isArray(value) ? value : [value]) params.append(key, one);
31941
32463
  }
31942
32464
  }
31943
32465
  const qs = params.toString();
@@ -31946,7 +32468,7 @@ var deviceTools = [
31946
32468
  },
31947
32469
  {
31948
32470
  name: "tailscale_get_device",
31949
- description: "Get detailed information about a specific device by its ID.",
32471
+ description: `Get detailed information about a specific device by its ID. Returns the default field subset unless fields: 'all'.${DEVICE_LAST_SEEN_NOTE}`,
31950
32472
  annotations: {
31951
32473
  title: "Get device",
31952
32474
  readOnlyHint: true,
@@ -31955,10 +32477,14 @@ var deviceTools = [
31955
32477
  openWorldHint: true
31956
32478
  },
31957
32479
  inputSchema: external_exports.object({
31958
- deviceId: external_exports.string().describe("The device ID (numeric id or nodeId, NOT the nodeKey)")
32480
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
32481
+ fields: external_exports.enum(["all", "default"]).optional().describe(DEVICE_FIELDS_DESC)
31959
32482
  }),
31960
32483
  handler: async (input) => {
31961
- return apiGet(`/device/${encPath(input.deviceId)}`);
32484
+ const params = new URLSearchParams();
32485
+ if (input.fields) params.set("fields", input.fields);
32486
+ const qs = params.toString();
32487
+ return apiGet(`/device/${encPath(input.deviceId)}${qs ? `?${qs}` : ""}`);
31962
32488
  }
31963
32489
  },
31964
32490
  {
@@ -31972,7 +32498,7 @@ var deviceTools = [
31972
32498
  openWorldHint: true
31973
32499
  },
31974
32500
  inputSchema: external_exports.object({
31975
- deviceId: external_exports.string().describe("The device ID to authorize")
32501
+ deviceId: external_exports.string().describe(`The device ID to authorize. ${DEVICE_ID_HINT}`)
31976
32502
  }),
31977
32503
  handler: async (input) => {
31978
32504
  return apiPost(`/device/${encPath(input.deviceId)}/authorized`, { authorized: true });
@@ -31989,7 +32515,7 @@ var deviceTools = [
31989
32515
  openWorldHint: true
31990
32516
  },
31991
32517
  inputSchema: external_exports.object({
31992
- deviceId: external_exports.string().describe("The device ID to deauthorize")
32518
+ deviceId: external_exports.string().describe(`The device ID to deauthorize. ${DEVICE_ID_HINT}`)
31993
32519
  }),
31994
32520
  handler: async (input) => {
31995
32521
  return apiPost(`/device/${encPath(input.deviceId)}/authorized`, { authorized: false });
@@ -32006,7 +32532,7 @@ var deviceTools = [
32006
32532
  openWorldHint: true
32007
32533
  },
32008
32534
  inputSchema: external_exports.object({
32009
- deviceId: external_exports.string().describe("The device ID to delete")
32535
+ deviceId: external_exports.string().describe(`The device ID to delete. ${DEVICE_ID_HINT}`)
32010
32536
  }),
32011
32537
  handler: async (input) => {
32012
32538
  return apiDelete(`/device/${encPath(input.deviceId)}`);
@@ -32014,7 +32540,7 @@ var deviceTools = [
32014
32540
  },
32015
32541
  {
32016
32542
  name: "tailscale_rename_device",
32017
- description: "Set the name of a device in the tailnet.",
32543
+ description: "Set the name of a device in the tailnet, or reset it to its OS hostname.",
32018
32544
  annotations: {
32019
32545
  title: "Rename device",
32020
32546
  readOnlyHint: false,
@@ -32023,8 +32549,10 @@ var deviceTools = [
32023
32549
  openWorldHint: true
32024
32550
  },
32025
32551
  inputSchema: external_exports.object({
32026
- deviceId: external_exports.string().describe("The device ID to rename"),
32027
- name: external_exports.string().describe("The new name for the device (FQDN within your tailnet)")
32552
+ deviceId: external_exports.string().describe(`The device ID to rename. ${DEVICE_ID_HINT}`),
32553
+ name: external_exports.string().describe(
32554
+ "New device name: the FQDN (e.g. 'nodename.your-tailnet.ts.net') or just the base name (e.g. 'nodename'). Pass an empty string to reset the name to one generated from the OS hostname (per Tailscale's API spec)."
32555
+ )
32028
32556
  }),
32029
32557
  handler: async (input) => {
32030
32558
  return apiPost(`/device/${encPath(input.deviceId)}/name`, { name: input.name });
@@ -32041,7 +32569,7 @@ var deviceTools = [
32041
32569
  openWorldHint: true
32042
32570
  },
32043
32571
  inputSchema: external_exports.object({
32044
- deviceId: external_exports.string().describe("The device ID to expire")
32572
+ deviceId: external_exports.string().describe(`The device ID to expire. ${DEVICE_ID_HINT}`)
32045
32573
  }),
32046
32574
  handler: async (input) => {
32047
32575
  return apiPost(`/device/${encPath(input.deviceId)}/expire`);
@@ -32058,7 +32586,7 @@ var deviceTools = [
32058
32586
  openWorldHint: true
32059
32587
  },
32060
32588
  inputSchema: external_exports.object({
32061
- deviceId: external_exports.string().describe("The device ID")
32589
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`)
32062
32590
  }),
32063
32591
  handler: async (input) => {
32064
32592
  return apiGet(`/device/${encPath(input.deviceId)}/routes`);
@@ -32077,7 +32605,7 @@ var deviceTools = [
32077
32605
  openWorldHint: true
32078
32606
  },
32079
32607
  inputSchema: external_exports.object({
32080
- deviceId: external_exports.string().describe("The device ID"),
32608
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
32081
32609
  routes: external_exports.array(external_exports.string().refine(isCidr, { message: "must be a CIDR (e.g. '10.0.0.0/24' or 'fd7a:115c::/48')" })).describe(
32082
32610
  "Full list of CIDR routes to enable (e.g. ['10.0.0.0/24', '192.168.1.0/24']). Replaces existing enabled routes."
32083
32611
  )
@@ -32097,7 +32625,7 @@ var deviceTools = [
32097
32625
  openWorldHint: true
32098
32626
  },
32099
32627
  inputSchema: external_exports.object({
32100
- deviceId: external_exports.string().describe("The device ID")
32628
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`)
32101
32629
  }),
32102
32630
  handler: async (input) => {
32103
32631
  return apiGet(`/device/${encPath(input.deviceId)}/attributes`);
@@ -32114,12 +32642,22 @@ var deviceTools = [
32114
32642
  openWorldHint: true
32115
32643
  },
32116
32644
  inputSchema: external_exports.object({
32117
- deviceId: external_exports.string().describe("The device ID"),
32118
- attributeKey: external_exports.string().describe("The attribute key (must start with 'custom:', e.g. 'custom:lastAuditDate')"),
32119
- value: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]).describe("The attribute value (string, number, or boolean)"),
32645
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
32646
+ // The length and charset rules below are Tailscale's, and Tailscale is
32647
+ // the one that enforces them -- only the 'custom:' prefix is checked
32648
+ // client-side, because that one is a namespace choice an agent gets
32649
+ // wrong by accident. A local regex for the rest would be one more thing
32650
+ // to drift out of step with the API's own error.
32651
+ attributeKey: external_exports.string().describe(
32652
+ "The attribute key (must start with 'custom:', e.g. 'custom:lastAuditDate'). Max 128 characters including the prefix; letters, numbers, underscores and colons only. Keys are case-sensitive but are checked for uniqueness case-insensitively, so 'custom:MyAttribute' and 'custom:myattribute' cannot both exist in one tailnet."
32653
+ ),
32654
+ value: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]).describe(
32655
+ "The attribute value: a string (max 50 characters, letters, numbers, underscores and periods only), an integer number (JSON-safe, up to 2^53-1), or a boolean. The type is fixed by the first value written for a key -- every device's value for that key must then be the same type."
32656
+ ),
32120
32657
  expiry: external_exports.string().optional().describe(
32121
32658
  "Optional expiry time in RFC3339 format (e.g. '2026-12-01T00:00:00Z'). Attribute is automatically removed after expiry."
32122
- )
32659
+ ),
32660
+ comment: external_exports.string().max(200).optional().describe("Optional comment added to the audit log explaining why the attribute is being set (max 200 chars)")
32123
32661
  }),
32124
32662
  handler: async (input) => {
32125
32663
  if (!input.attributeKey.startsWith("custom:")) {
@@ -32127,6 +32665,7 @@ var deviceTools = [
32127
32665
  }
32128
32666
  const body = { value: input.value };
32129
32667
  if (input.expiry !== void 0) body.expiry = input.expiry;
32668
+ if (input.comment !== void 0) body.comment = input.comment;
32130
32669
  return apiPost(`/device/${encPath(input.deviceId)}/attributes/${encPath(input.attributeKey)}`, body);
32131
32670
  }
32132
32671
  },
@@ -32141,7 +32680,7 @@ var deviceTools = [
32141
32680
  openWorldHint: true
32142
32681
  },
32143
32682
  inputSchema: external_exports.object({
32144
- deviceId: external_exports.string().describe("The device ID"),
32683
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
32145
32684
  attributeKey: external_exports.string().describe("The attribute key to delete (e.g. 'custom:lastAuditDate')")
32146
32685
  }),
32147
32686
  handler: async (input) => {
@@ -32164,7 +32703,7 @@ var deviceTools = [
32164
32703
  openWorldHint: true
32165
32704
  },
32166
32705
  inputSchema: external_exports.object({
32167
- deviceId: external_exports.string().describe("The device ID"),
32706
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
32168
32707
  tags: external_exports.array(external_exports.string()).describe("Full list of ACL tags (e.g. ['tag:server', 'tag:production']). Replaces all existing tags.")
32169
32708
  }),
32170
32709
  handler: async (input) => {
@@ -32183,7 +32722,7 @@ var deviceTools = [
32183
32722
  openWorldHint: true
32184
32723
  },
32185
32724
  inputSchema: external_exports.object({
32186
- deviceId: external_exports.string().describe("The device ID"),
32725
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
32187
32726
  ipv4: external_exports.ipv4().describe("The new Tailscale IPv4 address for the device (e.g. '100.64.0.1')")
32188
32727
  }),
32189
32728
  handler: async (input) => {
@@ -32201,7 +32740,7 @@ var deviceTools = [
32201
32740
  openWorldHint: true
32202
32741
  },
32203
32742
  inputSchema: external_exports.object({
32204
- deviceId: external_exports.string().describe("The device ID"),
32743
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
32205
32744
  keyExpiryDisabled: external_exports.boolean().describe("Whether to disable key expiry for this device")
32206
32745
  }),
32207
32746
  handler: async (input) => {
@@ -32223,7 +32762,7 @@ var deviceTools = [
32223
32762
  openWorldHint: true
32224
32763
  },
32225
32764
  inputSchema: external_exports.object({
32226
- deviceIds: external_exports.array(external_exports.string().min(1)).min(1).describe("Device IDs to update"),
32765
+ deviceIds: external_exports.array(external_exports.string().min(1)).min(1).describe("Device IDs to update (nodeIds preferred; legacy numeric ids also work)"),
32227
32766
  authorized: external_exports.boolean().describe("true to authorize, false to deauthorize")
32228
32767
  }),
32229
32768
  handler: async (input) => {
@@ -32276,7 +32815,7 @@ var deviceTools = [
32276
32815
  ])
32277
32816
  )
32278
32817
  ).describe(
32279
- 'Map of device ID to attribute config map (e.g. { "12345": { "custom:compliant": { "value": "true" } }, "67890": { "custom:compliant": { "value": false, "expiry": "2026-12-01T00:00:00Z" } } }). Pass null as the config to delete an attribute.'
32818
+ 'Map of device ID to attribute config map (e.g. { "nPM2KNuedB21DEVEL": { "custom:compliant": { "value": "true" } }, "nPpz3VEKzX11DEVEL": { "custom:compliant": { "value": false, "expiry": "2026-12-01T00:00:00Z" } } }). Keys are device IDs, nodeIds preferred. Pass null as the config to delete an attribute.'
32280
32819
  ),
32281
32820
  comment: external_exports.string().max(200).optional().describe("Optional comment added to the audit log explaining why attributes are being set (max 200 chars)")
32282
32821
  }),
@@ -32318,7 +32857,7 @@ var dnsTools = [
32318
32857
  },
32319
32858
  {
32320
32859
  name: "tailscale_set_nameservers",
32321
- description: "Set the DNS nameservers for your tailnet. Replaces all existing nameservers.",
32860
+ description: "Set the DNS nameservers for your tailnet. Replaces all existing nameservers. Removing every nameserver may also change MagicDNS: the API reference says it is switched off, while Tailscale's current MagicDNS docs say a nameserver is no longer required -- check `magicDNS` in the response.",
32322
32861
  annotations: {
32323
32862
  title: "Set nameservers",
32324
32863
  readOnlyHint: false,
@@ -32387,7 +32926,7 @@ var dnsTools = [
32387
32926
  },
32388
32927
  {
32389
32928
  name: "tailscale_set_split_dns",
32390
- description: "Set split DNS configuration. Maps domains to specific nameservers. Replaces the entire split DNS configuration.",
32929
+ description: "Set split DNS configuration. Maps domains to specific nameservers. Replaces the entire split DNS configuration: a domain you leave out is removed, and an empty object clears every domain. Per the API reference, setting a domain to null clears that domain's nameservers.",
32391
32930
  annotations: {
32392
32931
  title: "Set split DNS",
32393
32932
  readOnlyHint: false,
@@ -32398,8 +32937,11 @@ var dnsTools = [
32398
32937
  openWorldHint: true
32399
32938
  },
32400
32939
  inputSchema: external_exports.object({
32401
- splitDns: external_exports.record(external_exports.string(), external_exports.array(external_exports.string())).describe(
32402
- 'Map of domain to nameserver list (e.g. { "corp.example.com": ["10.0.0.1"], "internal.dev": ["10.0.0.2"] })'
32940
+ // `.nullable()` per the spec's SplitDns schema, which types each value as
32941
+ // an array OR null. Forwarded verbatim below, so null reaches the API as
32942
+ // JSON null rather than being pruned.
32943
+ splitDns: external_exports.record(external_exports.string(), external_exports.array(external_exports.string()).nullable()).describe(
32944
+ 'Map of domain to nameserver list, or to null to clear that domain (e.g. { "corp.example.com": ["10.0.0.1"], "old.example.com": null })'
32403
32945
  )
32404
32946
  }),
32405
32947
  handler: async (input) => {
@@ -32423,7 +32965,7 @@ var dnsTools = [
32423
32965
  },
32424
32966
  {
32425
32967
  name: "tailscale_set_dns_preferences",
32426
- description: "Set DNS preferences for your tailnet, such as enabling or disabling MagicDNS.",
32968
+ description: "Set DNS preferences for your tailnet, such as enabling or disabling MagicDNS. The API reference says enabling can fail when the tailnet has no nameservers; if it does, add one with tailscale_set_nameservers first.",
32427
32969
  annotations: {
32428
32970
  title: "Set DNS preferences",
32429
32971
  readOnlyHint: false,
@@ -32442,7 +32984,7 @@ var dnsTools = [
32442
32984
  },
32443
32985
  {
32444
32986
  name: "tailscale_update_split_dns",
32445
- description: "Partially update split DNS configuration. Merges the provided domains with the existing config \u2014 only the specified domains are changed, others are untouched. Set a domain's nameservers to an empty array to remove it.",
32987
+ description: "Partially update split DNS configuration. Merges the provided domains with the existing config -- only the specified domains are changed, others are untouched. To remove a domain, set it to null: that is the idiom the API reference documents. An empty array is also accepted and forwarded as-is -- it is what Tailscale's Terraform provider sends.",
32446
32988
  annotations: {
32447
32989
  title: "Update split DNS (partial)",
32448
32990
  readOnlyHint: false,
@@ -32451,8 +32993,11 @@ var dnsTools = [
32451
32993
  openWorldHint: true
32452
32994
  },
32453
32995
  inputSchema: external_exports.object({
32454
- splitDns: external_exports.record(external_exports.string(), external_exports.array(external_exports.string())).describe(
32455
- 'Map of domain to nameserver list to merge (e.g. { "new.example.com": ["10.0.0.3"] }). Only specified domains are changed.'
32996
+ // Nullable for the same reason as the PUT sibling above: the spec's
32997
+ // SplitDns body types each value as an array OR null, and null is how it
32998
+ // documents clearing a domain.
32999
+ splitDns: external_exports.record(external_exports.string(), external_exports.array(external_exports.string()).nullable()).describe(
33000
+ 'Map of domain to nameserver list to merge, or to null to remove that domain (e.g. { "new.example.com": ["10.0.0.3"], "old.example.com": null }). Only specified domains are changed.'
32456
33001
  )
32457
33002
  }),
32458
33003
  handler: async (input) => {
@@ -32519,7 +33064,7 @@ var inviteTools = [
32519
33064
  openWorldHint: true
32520
33065
  },
32521
33066
  inputSchema: external_exports.object({
32522
- deviceId: external_exports.string().describe("The device ID to list invites for")
33067
+ deviceId: external_exports.string().describe(`The device ID to list invites for. ${DEVICE_ID_HINT}`)
32523
33068
  }),
32524
33069
  handler: async (input) => {
32525
33070
  return apiGet(`/device/${encPath(input.deviceId)}/device-invites`);
@@ -32536,7 +33081,7 @@ var inviteTools = [
32536
33081
  openWorldHint: true
32537
33082
  },
32538
33083
  inputSchema: external_exports.object({
32539
- deviceId: external_exports.string().describe("The device ID to create an invite for"),
33084
+ deviceId: external_exports.string().describe(`The device ID to create an invite for. ${DEVICE_ID_HINT}`),
32540
33085
  multiUse: external_exports.boolean().optional().describe("Whether the invite can be used more than once (default: false)"),
32541
33086
  allowExitNode: external_exports.boolean().optional().describe("Whether the invited device can be used as an exit node (default: false)"),
32542
33087
  email: external_exports.email().optional().describe("Email address to send the invite to")
@@ -32603,7 +33148,7 @@ var inviteTools = [
32603
33148
  // --- User Invites ---
32604
33149
  {
32605
33150
  name: "tailscale_list_user_invites",
32606
- description: "List all user invites for your tailnet.",
33151
+ description: "List the open (not yet accepted) user invites for your tailnet. Accepted invites are not returned.",
32607
33152
  annotations: {
32608
33153
  title: "List user invites",
32609
33154
  readOnlyHint: true,
@@ -32713,7 +33258,7 @@ var inviteTools = [
32713
33258
  var keyTools = [
32714
33259
  {
32715
33260
  name: "tailscale_list_keys",
32716
- description: "List keys in your tailnet. By default lists auth keys only. Set 'all' to true to include OAuth clients and federated identities.",
33261
+ description: "List keys in your tailnet: auth keys, API access tokens, OAuth clients and federated identities. Without 'all', what comes back depends on the credential this server runs on -- a user-owned API key sees only that user's keys (including the API access token the server itself is using, keyType 'api'); an OAuth-client token sees the tailnet's OAuth clients; a federated-identity token sees its federated identities. Set 'all' to true for the tailnet-wide list (needs the matching :read scopes; only 'all:read' and 'all' return every API access token).",
32717
33262
  annotations: {
32718
33263
  title: "List keys",
32719
33264
  readOnlyHint: true,
@@ -32722,7 +33267,7 @@ var keyTools = [
32722
33267
  openWorldHint: true
32723
33268
  },
32724
33269
  inputSchema: external_exports.object({
32725
- all: external_exports.boolean().optional().describe("When true, returns all key types (auth keys, OAuth clients, federated identities). Default: false")
33270
+ all: external_exports.boolean().optional().describe("When true, list keys tailnet-wide instead of the credential-dependent default set. Default: false")
32726
33271
  }),
32727
33272
  handler: async (input) => {
32728
33273
  const qs = input.all ? "?all=true" : "";
@@ -32731,7 +33276,7 @@ var keyTools = [
32731
33276
  },
32732
33277
  {
32733
33278
  name: "tailscale_get_key",
32734
- description: "Get details for a specific key (auth key, OAuth client, or federated identity).",
33279
+ description: "Get details for a specific key (auth key, API access token, OAuth client, or federated identity). A revoked or expired key is still returned, with `invalid: true`.",
32735
33280
  annotations: {
32736
33281
  title: "Get key",
32737
33282
  readOnlyHint: true,
@@ -32740,7 +33285,7 @@ var keyTools = [
32740
33285
  openWorldHint: true
32741
33286
  },
32742
33287
  inputSchema: external_exports.object({
32743
- keyId: external_exports.string().describe("The key ID (auth key, OAuth client, or federated identity)")
33288
+ keyId: external_exports.string().describe("The key ID (auth key, API access token, OAuth client, or federated identity)")
32744
33289
  }),
32745
33290
  handler: async (input) => {
32746
33291
  return apiGet(`/tailnet/${getTailnet()}/keys/${encPath(input.keyId)}`);
@@ -32748,7 +33293,7 @@ var keyTools = [
32748
33293
  },
32749
33294
  {
32750
33295
  name: "tailscale_create_key",
32751
- description: "Create a new key in your tailnet. Supports auth keys (for adding devices), OAuth clients (for programmatic API access), and federated identities (for OIDC-based CI/CD access). Returns the key value \u2014 save it immediately, as it cannot be retrieved again.\n\nSECURITY: the response body contains a long-lived credential verbatim. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive (do not commit it, avoid re-sharing it in unrelated chat history).\n\nExamples:\n- Auth key: {keyType:'auth', reusable:true, tags:['tag:ci']}\n- OAuth client: {keyType:'client', scopes:['devices:read','dns']}\n- Federated (GitHub Actions): {keyType:'federated', scopes:['devices:read'], issuer:'https://token.actions.githubusercontent.com', subject:'repo:my-org/my-repo:*'}",
33296
+ description: "Create a new key in your tailnet. Supports auth keys (for adding devices), OAuth clients (for programmatic API access), and federated identities (for OIDC-based CI/CD access). Returns the key value -- save it immediately, as it cannot be retrieved again.\n\nSECURITY: the response body contains a long-lived credential verbatim. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive (do not commit it, avoid re-sharing it in unrelated chat history).\n\nExamples:\n- Auth key: {keyType:'auth', reusable:true, tags:['tag:ci']}\n- OAuth client: {keyType:'client', scopes:['devices:core:read','dns:read']}\n- Federated (GitHub Actions): {keyType:'federated', scopes:['devices:core:read'], issuer:'https://token.actions.githubusercontent.com', subject:'repo:my-org/my-repo:*'}",
32752
33297
  annotations: {
32753
33298
  title: "Create key",
32754
33299
  readOnlyHint: false,
@@ -32771,7 +33316,9 @@ var keyTools = [
32771
33316
  "ACL tags (must start with 'tag:'). Required for client/federated if scopes include 'devices:core' or 'auth_keys'"
32772
33317
  ),
32773
33318
  // Client + Federated fields
32774
- scopes: external_exports.array(external_exports.string()).optional().describe("(client/federated) OAuth scopes to grant (e.g. ['devices:read', 'dns', 'acl'])"),
33319
+ scopes: external_exports.array(external_exports.string()).optional().describe(
33320
+ "(client/federated) OAuth scopes to grant (e.g. ['devices:core:read', 'dns:read']). Use the current scope names listed at https://tailscale.com/kb/1623/trust-credentials#scopes; the pre-2024 names such as 'devices:read' and 'acl' are legacy."
33321
+ ),
32775
33322
  // Federated-only fields
32776
33323
  issuer: external_exports.string().optional().describe("(federated only) OIDC issuer URL (e.g. 'https://token.actions.githubusercontent.com')"),
32777
33324
  subject: external_exports.string().optional().describe("(federated only) Expected subject claim, supports * wildcards"),
@@ -32834,7 +33381,7 @@ var keyTools = [
32834
33381
  },
32835
33382
  {
32836
33383
  name: "tailscale_delete_key",
32837
- description: "Delete a key (auth key, OAuth client, or federated identity). This is irreversible. For auth keys, devices already authenticated are unaffected but no new devices can use it. For OAuth clients and federated identities, any integrations using them lose access immediately.",
33384
+ description: "Delete a key (auth key, API access token, OAuth client, or federated identity). This is irreversible. For auth keys, devices already authenticated are unaffected but no new devices can use it. For OAuth clients and federated identities, any integrations using them lose access immediately. API access tokens are deletable here too: if keyId is the token this server authenticates with -- it shows up in tailscale_list_keys under API-key auth -- the server revokes its own credential and every later call fails with 401 until it is reconfigured.",
32838
33385
  annotations: {
32839
33386
  title: "Delete key",
32840
33387
  readOnlyHint: false,
@@ -32843,7 +33390,7 @@ var keyTools = [
32843
33390
  openWorldHint: true
32844
33391
  },
32845
33392
  inputSchema: external_exports.object({
32846
- keyId: external_exports.string().describe("The key ID to delete (auth key, OAuth client, or federated identity)")
33393
+ keyId: external_exports.string().describe("The key ID to delete (auth key, API access token, OAuth client, or federated identity)")
32847
33394
  }),
32848
33395
  handler: async (input) => {
32849
33396
  return apiDelete(`/tailnet/${getTailnet()}/keys/${encPath(input.keyId)}`);
@@ -32894,12 +33441,15 @@ var keyTools = [
32894
33441
  // tailscale_create_key above: an OAuth client is a machine credential you
32895
33442
  // hold, whereas an OAuth App is a three-legged authorization-code app that
32896
33443
  // lets a THIRD PARTY enroll one device into your tailnet after a user
32897
- // consents. The only scope it takes today is `auth_keys:create:once`, which
32898
- // mints exactly one auth key per authorization and returns no refresh token
32899
- // -- re-authorization is required per device by design.
33444
+ // consents. Tailscale's device-provisioning guide documents one scope,
33445
+ // `auth_keys:create:once`, which mints exactly one auth key per authorization
33446
+ // and returns no refresh token -- re-authorization is required per device by
33447
+ // design. The API reference's own example shows the bare `auth_keys:create`
33448
+ // instead, and the schema restricts neither, so the description names both
33449
+ // sources rather than picking one.
32900
33450
  {
32901
33451
  name: "tailscale_create_oauth_app",
32902
- description: "Create an OAuth App for device provisioning (Tailscale alpha). Lets a third-party application enroll a device into your tailnet via the authorization-code flow, after a user consents. Returns the app's client secret -- save it immediately, it cannot be retrieved again.\n\nSECURITY: the response body contains a long-lived credential verbatim. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive.\n\nThe supported scope is 'auth_keys:create:once' (one auth key per authorization, no refresh token). Distinct from tailscale_create_key with keyType='client', which mints a machine-to-machine OAuth client instead.",
33452
+ description: "Create an OAuth App for device provisioning (Tailscale alpha). Lets a third-party application enroll a device into your tailnet via the authorization-code flow, after a user consents. Returns the app's client secret -- save it immediately, it cannot be retrieved again.\n\nSECURITY: the response body contains a long-lived credential verbatim. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive.\n\nUse scope 'auth_keys:create:once' (one auth key per authorization, no refresh token) -- the scope Tailscale's device-provisioning guide documents. The API reference's example shows 'auth_keys:create'; this tool does not restrict the value. Distinct from tailscale_create_key with keyType='client', which mints a machine-to-machine OAuth client instead.",
32903
33453
  annotations: {
32904
33454
  title: "Create OAuth app",
32905
33455
  readOnlyHint: false,
@@ -32910,7 +33460,9 @@ var keyTools = [
32910
33460
  inputSchema: external_exports.object({
32911
33461
  name: external_exports.string().min(1).describe("Human-readable name for the OAuth app, shown on the consent screen"),
32912
33462
  redirectUris: external_exports.array(external_exports.url()).min(1).describe("Allowed redirect URIs for the authorization-code flow (e.g. ['https://example.com/callback'])"),
32913
- scopes: external_exports.array(external_exports.string()).min(1).describe("Scopes to grant. Currently 'auth_keys:create:once' is the supported value."),
33463
+ scopes: external_exports.array(external_exports.string()).min(1).describe(
33464
+ "Scopes to grant. Use 'auth_keys:create:once', the scope the device-provisioning guide documents; the API reference's example shows 'auth_keys:create'. Not restricted here."
33465
+ ),
32914
33466
  allowedNodeAttributes: external_exports.array(external_exports.string()).optional().describe("Optional node attributes the app may request when provisioning a device")
32915
33467
  }),
32916
33468
  handler: async (input) => {
@@ -32989,14 +33541,57 @@ import * as net2 from "node:net";
32989
33541
 
32990
33542
  // src/local-cli.ts
32991
33543
  import { execFile as execFileCb } from "node:child_process";
33544
+ import { existsSync, readFileSync as readFileSync2 } from "node:fs";
32992
33545
  var DEFAULT_TIMEOUT_MS = 3e4;
32993
33546
  var MAX_BUFFER_BYTES = 10 * 1024 * 1024;
33547
+ var DARWIN_CANDIDATES = [
33548
+ "/Applications/Tailscale.app/Contents/MacOS/Tailscale",
33549
+ "/opt/homebrew/bin/tailscale",
33550
+ "/usr/local/bin/tailscale"
33551
+ ];
33552
+ var LINUX_CANDIDATES = ["/usr/bin/tailscale", "/snap/bin/tailscale"];
32994
33553
  var execFileImpl = execFileCb;
32995
- function getBinaryPath() {
32996
- return process.env.TAILSCALE_BINARY || "tailscale";
33554
+ function binaryCandidates(platform) {
33555
+ if (platform === "darwin") return DARWIN_CANDIDATES;
33556
+ if (platform === "linux") return LINUX_CANDIDATES;
33557
+ return [];
33558
+ }
33559
+ function resolveBinary(platform = process.platform, exists = existsSync) {
33560
+ const override = process.env.TAILSCALE_BINARY;
33561
+ if (override) return override;
33562
+ for (const candidate of binaryCandidates(platform)) {
33563
+ if (exists(candidate)) return candidate;
33564
+ }
33565
+ return "tailscale";
33566
+ }
33567
+ function looksLikeWsl(platform, procVersion = readProcVersion) {
33568
+ if (platform !== "linux") return false;
33569
+ if (process.env.WSL_DISTRO_NAME) return true;
33570
+ return /microsoft/i.test(procVersion());
33571
+ }
33572
+ function readProcVersion() {
33573
+ try {
33574
+ return readFileSync2("/proc/version", "utf-8");
33575
+ } catch {
33576
+ return "";
33577
+ }
33578
+ }
33579
+ function describeMissingBinary(binary, fromEnv, platform, wsl) {
33580
+ if (fromEnv) {
33581
+ const pathNote = platform === "win32" ? `On Windows the value must be a Windows path (C:/Program Files/Tailscale/tailscale.exe), not an MSYS one (/c/...) -- Git Bash rewrites /c/... when you type it at the prompt, but a value read from an MCP client's JSON config or a .env file arrives untranslated.` : `It must be the absolute path of an executable file -- not a directory, and not a shell alias or function.`;
33582
+ return `Could not find the 'tailscale' binary at '${binary}', which is where TAILSCALE_BINARY points. PATH was never consulted, so nothing here is a PATH problem. ${pathNote}`;
33583
+ }
33584
+ if (platform === "darwin") {
33585
+ return `Could not find the 'tailscale' binary in PATH, or at ${DARWIN_CANDIDATES.join(", ")}. A default macOS install keeps the CLI inside the app bundle and puts nothing on PATH, and an MCP client launched from the Dock or Spotlight sees a minimal PATH rather than your shell's. Install Tailscale (https://tailscale.com/download) or set TAILSCALE_BINARY to its absolute path, usually /Applications/Tailscale.app/Contents/MacOS/Tailscale.`;
33586
+ }
33587
+ if (wsl) {
33588
+ return `Could not find the 'tailscale' binary in PATH, or at ${LINUX_CANDIDATES.join(", ")}. This looks like WSL, where the only tailscale in reach is usually the Windows one: a Linux process cannot exec tailscale.exe, and pointing TAILSCALE_BINARY at it would report the WINDOWS host's tailnet rather than this machine's, which is what these tools describe. Install Tailscale inside the distro (https://tailscale.com/download/linux) and run tailscaled there.`;
33589
+ }
33590
+ return `Could not find the 'tailscale' binary in PATH. Install Tailscale (https://tailscale.com/download) or set TAILSCALE_BINARY to its absolute path.`;
32997
33591
  }
32998
33592
  async function runTailscaleCli(args, options = {}) {
32999
- const binary = getBinaryPath();
33593
+ const binary = resolveBinary();
33594
+ const fromEnv = Boolean(process.env.TAILSCALE_BINARY);
33000
33595
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33001
33596
  return new Promise((resolve) => {
33002
33597
  execFileImpl(binary, args, { timeout: timeoutMs, maxBuffer: MAX_BUFFER_BYTES }, (err, stdout, stderr) => {
@@ -33007,7 +33602,7 @@ async function runTailscaleCli(args, options = {}) {
33007
33602
  if (errno.code === "ENOENT") {
33008
33603
  resolve({
33009
33604
  ok: false,
33010
- error: `Could not find the 'tailscale' binary in PATH. Install Tailscale (https://tailscale.com/download) or set TAILSCALE_BINARY to its absolute path.`
33605
+ error: describeMissingBinary(binary, fromEnv, process.platform, looksLikeWsl(process.platform))
33011
33606
  });
33012
33607
  return;
33013
33608
  }
@@ -33063,7 +33658,7 @@ function isValidPingTarget(s) {
33063
33658
  var localCliTools = [
33064
33659
  {
33065
33660
  name: "tailscale_local_status",
33066
- description: "Get this machine's view of its tailnet -- own connection state, peers it can see, DERP region, MagicDNS suffix, etc. Shells out to the local `tailscale` binary; distinct from `tailscale_status`, which queries the admin API for tailnet-wide info. Requires the tailscale CLI installed locally and TAILSCALE_LOCAL_CLI=1.",
33661
+ description: "Get this machine's view of its tailnet -- own connection state, peers it can see, DERP region, MagicDNS suffix, etc. Shells out to the local `tailscale` binary; distinct from `tailscale_status`, which queries the admin API for tailnet-wide info. Read a peer's path in decision order: direct when `CurAddr` is set, peer-relayed when `PeerRelay` is set, otherwise DERP via the region named in `Relay`. `Relay` is the peer's home DERP region and is populated either way, so a non-empty `Relay` on its own does not mean the traffic is relayed. The `Peer` map is what makes this response scale with the tailnet rather than with the request; `peers` and `activeOnly` narrow it. Requires the tailscale CLI installed locally and TAILSCALE_LOCAL_CLI=1.",
33067
33662
  annotations: {
33068
33663
  title: "Local tailscale status",
33069
33664
  readOnlyHint: true,
@@ -33071,8 +33666,30 @@ var localCliTools = [
33071
33666
  idempotentHint: true,
33072
33667
  openWorldHint: true
33073
33668
  },
33074
- inputSchema: external_exports.object({}),
33075
- handler: async () => runTailscaleCli(["status", "--json"], { parseJson: true })
33669
+ inputSchema: external_exports.object({
33670
+ peers: external_exports.boolean().optional().describe(
33671
+ "Set false to omit the `Peer` map (`--peers=false`), leaving this node's own state -- `Self`, `BackendState`, `Health`, `CurrentTailnet` and the rest of the top level. Omit it for the CLI's default, which includes peers."
33672
+ ),
33673
+ activeOnly: external_exports.boolean().optional().describe(
33674
+ "Set true to keep only peers with an active session (`--active`) -- upstream defines that as a packet sent to the peer in roughly the last two minutes. Omit it to get every peer."
33675
+ )
33676
+ }),
33677
+ handler: async (input = {}) => {
33678
+ const args = ["status", "--json"];
33679
+ if (input.peers === false) args.push("--peers=false");
33680
+ if (input.activeOnly) args.push("--active");
33681
+ const result = await runTailscaleCli(args, { parseJson: true });
33682
+ if (!result.ok && result.error?.includes("output limit")) {
33683
+ return { ...result, error: `${result.error} Retry with peers:false or activeOnly:true.` };
33684
+ }
33685
+ if (!result.ok && result.error?.includes("timed out after")) {
33686
+ return {
33687
+ ...result,
33688
+ error: `${result.error}. On a large tailnet this is usually the peer map -- retry with peers:false.`
33689
+ };
33690
+ }
33691
+ return result;
33692
+ }
33076
33693
  },
33077
33694
  {
33078
33695
  name: "tailscale_ping",
@@ -33336,17 +33953,30 @@ var logStreamingTools = [
33336
33953
  },
33337
33954
  {
33338
33955
  name: "tailscale_create_aws_external_id",
33339
- description: "Create or get an AWS external ID for your tailnet. Used when configuring log streaming to S3 \u2014 the external ID is included in the IAM role trust policy.",
33956
+ description: "Create or get the AWS external ID Tailscale presents when assuming your IAM role for S3 log streaming. Put it in the role trust policy's sts:ExternalId condition, then check it with tailscale_validate_aws_trust_policy.",
33340
33957
  annotations: {
33341
33958
  title: "Create AWS external ID",
33342
33959
  readOnlyHint: false,
33343
33960
  destructiveHint: false,
33344
- idempotentHint: true,
33961
+ // Not idempotent: a static hint has to hold for every accepted input, and
33962
+ // reusable:false mints a distinct ID by design. Even reusable:true mints
33963
+ // a new one once the previous ID has been linked to an AWS account.
33964
+ idempotentHint: false,
33345
33965
  openWorldHint: true
33346
33966
  },
33347
- inputSchema: external_exports.object({}),
33348
- handler: async () => {
33349
- return apiPost(`/tailnet/${getTailnet()}/aws-external-id`);
33967
+ inputSchema: external_exports.object({
33968
+ reusable: external_exports.boolean().optional().describe(
33969
+ "Default true: Tailscale returns the SAME external ID on repeat calls until that ID has been linked to an AWS account, so asking again does not invalidate the ID already pasted into an IAM trust policy. Set false to force a fresh ID (what Tailscale's Terraform provider does, one ID per resource)."
33970
+ )
33971
+ }),
33972
+ // The flag was previously never sent at all, so the server applied whatever
33973
+ // default it applies to an absent body -- undocumented either way. It now
33974
+ // goes on the wire explicitly, as the Go client always does. The default
33975
+ // lives here rather than in a Zod `.default()` because handlers are called
33976
+ // with the client's raw input, and `??` rather than `||` because false is a
33977
+ // meaningful value.
33978
+ handler: async (input) => {
33979
+ return apiPost(`/tailnet/${getTailnet()}/aws-external-id`, { reusable: input?.reusable ?? true });
33350
33980
  }
33351
33981
  },
33352
33982
  {
@@ -33445,14 +34075,16 @@ var postureTools = [
33445
34075
  "The posture provider slug: falcon (CrowdStrike Falcon), fleet, huntress, intune (Microsoft Intune), jamfpro (Jamf Pro), kandji (Iru, formerly Kandji), kolide (1Password XAM, formerly Kolide), sentinelone"
33446
34076
  ),
33447
34077
  clientId: external_exports.string().optional().describe(
33448
- "Client ID for the provider (Intune: application UUID; Falcon/Jamf Pro: client id; Fleet/Huntress/Kandji/Kolide/Sentinel One: leave blank)"
34078
+ "Client ID for the provider (Intune: application UUID; Falcon/Jamf Pro: client id; Kandji/Kolide/Sentinel One: leave blank). Fleet and Huntress: Tailscale does not document how their credentials map onto these API fields -- the admin console asks for a Fleet URL + API token (Fleet) and an API key + API secret, plus an optional organization ID (Huntress). Do not assume this can be left blank; confirm the mapping first."
33449
34079
  ),
33450
34080
  clientSecret: external_exports.string().describe(
33451
34081
  "The secret (auth key, token, etc.) used to authenticate with the provider. SENSITIVE: passed straight to Tailscale and not echoed back, but MCP clients may log the input value you supply."
33452
34082
  ),
33453
- tenantId: external_exports.string().optional().describe("Microsoft Intune directory (tenant) ID. Other providers leave blank."),
34083
+ tenantId: external_exports.string().optional().describe(
34084
+ "Microsoft Intune directory (tenant) ID. Other providers leave blank. Fleet/Huntress: undocumented upstream (see clientId)."
34085
+ ),
33454
34086
  cloudId: external_exports.string().optional().describe(
33455
- "Identifies which of the provider's clouds to integrate with. Falcon: us-1|us-2|eu-1|us-gov; Intune: global|us-gov; Jamf Pro/Kandji/Sentinel One: FQDN of your subdomain; Kolide: leave blank."
34087
+ "Identifies which of the provider's clouds to integrate with. Falcon: us-1|us-2|eu-1|us-gov; Intune: global|us-gov; Jamf Pro/Kandji/Sentinel One: FQDN of your subdomain; Kolide: leave blank. Fleet/Huntress: undocumented upstream (see clientId)."
33456
34088
  )
33457
34089
  }),
33458
34090
  handler: async (input) => {
@@ -33630,7 +34262,7 @@ var serviceTools = [
33630
34262
  },
33631
34263
  inputSchema: external_exports.object({
33632
34264
  serviceName: external_exports.string().describe("The service name"),
33633
- deviceId: external_exports.string().describe("The device ID")
34265
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`)
33634
34266
  }),
33635
34267
  handler: async (input) => {
33636
34268
  return apiGet(
@@ -33650,7 +34282,7 @@ var serviceTools = [
33650
34282
  },
33651
34283
  inputSchema: external_exports.object({
33652
34284
  serviceName: external_exports.string().describe("The service name"),
33653
- deviceId: external_exports.string().describe("The device ID"),
34285
+ deviceId: external_exports.string().describe(`The device ID. ${DEVICE_ID_HINT}`),
33654
34286
  approved: external_exports.boolean().describe("Whether to approve (true) or reject (false) the device")
33655
34287
  }),
33656
34288
  handler: async (input) => {
@@ -33871,17 +34503,24 @@ var tailnetsTools = [
33871
34503
  },
33872
34504
  {
33873
34505
  name: "tailscale_create_org_tailnet",
33874
- description: "Create a new API-only tailnet in your organization. Returns the tailnet (id, displayName, orgId, dnsName, createdAt) AND a freshly-minted OAuth client for it.\n\nSECURITY: the response body contains that OAuth client's secret verbatim, and it cannot be retrieved again. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive.\n\nRequires an OAuth client with the 'tailnets' scope -- an API key will not work. To then operate on the new tailnet, set TAILSCALE_OAUTH_TAILNET to its id and use an OAuth client with the 'all' scope.",
34506
+ description: "Create a new API-only tailnet in your organization. Returns the tailnet (id, displayName, orgId, dnsName, createdAt) AND a freshly-minted OAuth client for it.\n\nSECURITY: the response body contains that OAuth client's secret verbatim, and it cannot be retrieved again. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive.\n\nRequires an OAuth client with the 'tailnets' scope -- an API key will not work. To then operate on the new tailnet, set TAILSCALE_OAUTH_TAILNET to its id and use an OAuth client with the 'all' scope.\n\nOrganizations are limited to 10 tailnets including the original unless Tailscale sales has raised the limit.\n\nThe response may include `alreadyExists: true`; Tailscale's spec ALSO documents a 400 for a name already in use and neither has been observed, so after a timeout call tailscale_list_org_tailnets before retrying rather than assuming either.",
33875
34507
  annotations: {
33876
34508
  title: "Create organization tailnet",
33877
34509
  readOnlyHint: false,
33878
34510
  destructiveHint: false,
33879
- // Each call creates a distinct tailnet; there is no idempotency key.
34511
+ // No idempotency key. The spec's `alreadyExists` field hints that a
34512
+ // duplicate displayName may return the existing tailnet, but the same
34513
+ // spec documents a 400 for that case and neither is observed --
34514
+ // idempotentHint stays false until one is.
33880
34515
  idempotentHint: false,
33881
34516
  openWorldHint: true
33882
34517
  },
33883
34518
  inputSchema: external_exports.object({
33884
- displayName: external_exports.string().trim().min(1).describe("Human-readable name for the new tailnet"),
34519
+ // No charset regex: the rule below is Tailscale's and Tailscale enforces
34520
+ // it, so a local copy would only drift out of step with its own error.
34521
+ displayName: external_exports.string().trim().min(1).describe(
34522
+ "Human-readable name for the new tailnet. May contain letters, numbers, spaces, apostrophes and hyphens, and must be unique within the organization."
34523
+ ),
33885
34524
  organization: organizationSchema
33886
34525
  }),
33887
34526
  handler: async (input) => {
@@ -34082,11 +34721,12 @@ var STATIC_WEBHOOK_EVENT_TYPES = [
34082
34721
  "subnetIPForwardingNotEnabled",
34083
34722
  "exitNodeIPForwardingNotEnabled"
34084
34723
  ];
34724
+ var WEBHOOK_CATEGORY_SUBSCRIPTIONS = ["categoryTailnetManagement", "categoryDeviceMisconfigurations"];
34085
34725
  function getAllowedWebhookEvents() {
34086
34726
  const raw = process.env.TAILSCALE_EXTRA_WEBHOOK_EVENTS;
34087
- if (!raw) return new Set(STATIC_WEBHOOK_EVENT_TYPES);
34727
+ if (!raw) return /* @__PURE__ */ new Set([...STATIC_WEBHOOK_EVENT_TYPES, ...WEBHOOK_CATEGORY_SUBSCRIPTIONS]);
34088
34728
  const extras = raw.split(",").map((s) => s.trim()).filter(Boolean);
34089
- return /* @__PURE__ */ new Set([...STATIC_WEBHOOK_EVENT_TYPES, ...extras]);
34729
+ return /* @__PURE__ */ new Set([...STATIC_WEBHOOK_EVENT_TYPES, ...WEBHOOK_CATEGORY_SUBSCRIPTIONS, ...extras]);
34090
34730
  }
34091
34731
  var endpointUrlSchema = external_exports.url().refine((u) => u.startsWith("https://"), "endpointUrl must use https://");
34092
34732
  var webhookSubscriptionsSchema = external_exports.array(external_exports.string().meta({ enum: [...getAllowedWebhookEvents()].sort() })).min(1).superRefine((arr, ctx) => {
@@ -34143,7 +34783,7 @@ var webhookTools = [
34143
34783
  },
34144
34784
  {
34145
34785
  name: "tailscale_create_webhook",
34146
- description: "Create a new webhook. The response includes the webhook's signing secret -- this is the only opportunity to capture it; save it immediately.\n\nSECURITY: the response body contains the secret verbatim. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive.",
34786
+ description: "Create a new webhook. The response includes the webhook's signing secret -- this is the only opportunity to capture it; save it immediately. Set providerType when the endpoint is a Slack, Mattermost, Google Chat or Discord incoming-webhook URL, so the events arrive in the format that provider renders.\n\nSECURITY: the response body contains the secret verbatim. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive.",
34147
34787
  annotations: {
34148
34788
  title: "Create webhook",
34149
34789
  readOnlyHint: false,
@@ -34153,13 +34793,20 @@ var webhookTools = [
34153
34793
  },
34154
34794
  inputSchema: external_exports.object({
34155
34795
  endpointUrl: endpointUrlSchema.describe("The HTTPS URL to send webhook events to"),
34156
- subscriptions: webhookSubscriptionsSchema.describe("Event types to subscribe to (at least one)")
34796
+ providerType: external_exports.enum(["slack", "mattermost", "googlechat", "discord"]).optional().describe(
34797
+ "Format deliveries for a chat provider's incoming-webhook URL. Omit for raw Tailscale JSON -- the default, and what a custom receiver verifying signatures wants. Set once: it cannot be changed after creation."
34798
+ ),
34799
+ subscriptions: webhookSubscriptionsSchema.describe(
34800
+ "Event types to subscribe to (at least one). 'categoryTailnetManagement' and 'categoryDeviceMisconfigurations' subscribe to a whole category, including events Tailscale adds to it later."
34801
+ )
34157
34802
  }),
34158
34803
  handler: async (input) => {
34159
- return apiPost(`/tailnet/${getTailnet()}/webhooks`, {
34804
+ const body = {
34160
34805
  endpointUrl: input.endpointUrl,
34161
34806
  subscriptions: input.subscriptions
34162
- });
34807
+ };
34808
+ if (input.providerType !== void 0) body.providerType = input.providerType;
34809
+ return apiPost(`/tailnet/${getTailnet()}/webhooks`, body);
34163
34810
  }
34164
34811
  },
34165
34812
  {
@@ -34175,7 +34822,9 @@ var webhookTools = [
34175
34822
  inputSchema: external_exports.object({
34176
34823
  webhookId: external_exports.string().describe("The webhook ID to update"),
34177
34824
  endpointUrl: endpointUrlSchema.optional().describe("New HTTPS URL to send webhook events to"),
34178
- subscriptions: webhookSubscriptionsSchema.optional().describe("Updated list of event types to subscribe to (at least one)")
34825
+ subscriptions: webhookSubscriptionsSchema.optional().describe(
34826
+ "Updated list of event types to subscribe to (at least one). 'categoryTailnetManagement' and 'categoryDeviceMisconfigurations' subscribe to a whole category, including events Tailscale adds to it later."
34827
+ )
34179
34828
  }),
34180
34829
  handler: async (input) => {
34181
34830
  const body = {};
@@ -34270,7 +34919,13 @@ var LARGE_RESULT_TOOLS = [
34270
34919
  "tailscale_get_audit_log",
34271
34920
  "tailscale_get_network_flow_logs",
34272
34921
  "tailscale_list_devices",
34273
- "tailscale_list_users"
34922
+ "tailscale_list_users",
34923
+ // The one opt-in entry: it registers only under TAILSCALE_LOCAL_CLI=1, so a
34924
+ // test comparing this list against a session's `_meta` has to filter it to
34925
+ // the names that session registered. Its payload is the local client's whole
34926
+ // peer map, which scales with the tailnet exactly as the admin reads above
34927
+ // do -- the tool takes `peers` and `activeOnly` for callers who want less.
34928
+ "tailscale_local_status"
34274
34929
  ];
34275
34930
  function buildToolMeta(toolName, options) {
34276
34931
  const meta3 = {};
@@ -34551,7 +35206,26 @@ function buildMetaTools(state) {
34551
35206
  }
34552
35207
 
34553
35208
  // src/index.ts
34554
- var version2 = true ? "0.20.1" : resolveVersionFallback();
35209
+ var NODE_MIN = [20, 11, 0];
35210
+ function belowNodeFloor(reported) {
35211
+ const found = /(\d+)\.(\d+)\.(\d+)/.exec(reported);
35212
+ if (!found) return false;
35213
+ for (const [i, min] of NODE_MIN.entries()) {
35214
+ const part = Number(found[i + 1]);
35215
+ if (part > min) return false;
35216
+ if (part < min) return true;
35217
+ }
35218
+ return false;
35219
+ }
35220
+ if (process.versions.oam === void 0 && belowNodeFloor(process.versions.node)) {
35221
+ process.stderr.write(
35222
+ `tailscale-mcp: needs Node ${NODE_MIN.join(".")} or newer, found ${process.versions.node}.
35223
+ Install a newer Node (https://nodejs.org/en/download), or point your MCP client's "command" at one.
35224
+ `
35225
+ );
35226
+ process.exit(1);
35227
+ }
35228
+ var version2 = true ? "0.21.0" : resolveVersionFallback();
34555
35229
  var subcommand = process.argv[2];
34556
35230
  var USAGE = `Usage: tailscale-mcp [command]
34557
35231