@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/README.md +95 -41
- package/bin/tailscale-mcp.mjs +45 -0
- package/dist/index.js +939 -265
- package/package.json +7 -7
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(/^[
|
|
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
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
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
|
-
|
|
3170
|
+
return false;
|
|
3154
3171
|
}
|
|
3155
3172
|
return true;
|
|
3156
3173
|
}
|
|
3157
|
-
function
|
|
3158
|
-
let
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
let
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
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
|
-
|
|
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 (
|
|
3197
|
-
if (
|
|
3198
|
-
|
|
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
|
-
|
|
3206
|
-
|
|
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
|
-
|
|
3210
|
-
|
|
3211
|
-
}
|
|
3212
|
-
|
|
3213
|
-
if (
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
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
|
-
|
|
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(
|
|
3357
|
-
output +=
|
|
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
|
-
|
|
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 =
|
|
3562
|
+
let host = component.host;
|
|
3387
3563
|
if (!isIPv42(host)) {
|
|
3388
|
-
|
|
3389
|
-
if (ipV6res.isIPV6
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
3823
|
-
|
|
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 (!
|
|
3842
|
-
|
|
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.
|
|
3853
|
-
parsed.
|
|
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
|
-
|
|
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
|
|
3893
|
-
|
|
3894
|
-
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
4197
|
+
if (typeof uri !== "string" && typeof uri !== "object") {
|
|
4198
|
+
return void 0;
|
|
3895
4199
|
}
|
|
3896
|
-
|
|
3897
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30906
|
-
|
|
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/
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
31110
|
-
if (
|
|
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 =
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
|
31706
|
+
const predictedCostMs = res.status === 429 ? delay : delay + (Date.now() - attemptStartedAt);
|
|
31707
|
+
const nextAttemptBudgetMs = retryCeilingMs - elapsed2 - predictedCostMs;
|
|
31276
31708
|
if (nextAttemptBudgetMs <= 0) {
|
|
31277
|
-
debugLog(
|
|
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(` ->
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
31351
|
-
if (
|
|
31352
|
-
|
|
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
|
-
|
|
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
|
|
31374
|
-
if (
|
|
31375
|
-
console.error(`ACL validation failed: ${
|
|
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(
|
|
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) :
|
|
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
|
|
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(
|
|
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
|
-
|
|
31868
|
-
|
|
31869
|
-
|
|
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(
|
|
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
|
-
|
|
31891
|
-
|
|
31892
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
32027
|
-
name: external_exports.string().describe(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
32118
|
-
|
|
32119
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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. { "
|
|
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
|
-
|
|
32402
|
-
|
|
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
|
|
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
|
-
|
|
32455
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
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(
|
|
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.
|
|
32898
|
-
// mints exactly one auth key per authorization
|
|
32899
|
-
// -- re-authorization is required per device by
|
|
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\
|
|
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(
|
|
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
|
|
32996
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
33349
|
-
|
|
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;
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|