@yawlabs/tailscale-mcp 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +214 -142
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __commonJS = (cb, mod) => function __require() {
|
|
9
|
-
|
|
9
|
+
try {
|
|
10
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw mod = 0, e;
|
|
13
|
+
}
|
|
10
14
|
};
|
|
11
15
|
var __export = (target, all) => {
|
|
12
16
|
for (var name in all)
|
|
@@ -28506,11 +28510,11 @@ var Protocol = class {
|
|
|
28506
28510
|
*
|
|
28507
28511
|
* The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
28508
28512
|
*/
|
|
28509
|
-
async connect(
|
|
28513
|
+
async connect(transport) {
|
|
28510
28514
|
if (this._transport) {
|
|
28511
28515
|
throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
|
|
28512
28516
|
}
|
|
28513
|
-
this._transport =
|
|
28517
|
+
this._transport = transport;
|
|
28514
28518
|
const _onclose = this.transport?.onclose;
|
|
28515
28519
|
this._transport.onclose = () => {
|
|
28516
28520
|
_onclose?.();
|
|
@@ -30100,8 +30104,8 @@ var McpServer = class {
|
|
|
30100
30104
|
*
|
|
30101
30105
|
* The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
|
|
30102
30106
|
*/
|
|
30103
|
-
async connect(
|
|
30104
|
-
return await this.server.connect(
|
|
30107
|
+
async connect(transport) {
|
|
30108
|
+
return await this.server.connect(transport);
|
|
30105
30109
|
}
|
|
30106
30110
|
/**
|
|
30107
30111
|
* Closes the connection.
|
|
@@ -31158,6 +31162,19 @@ async function executeFetch(method, url2, headers, body, timeoutMs) {
|
|
|
31158
31162
|
signal: AbortSignal.timeout(timeoutMs)
|
|
31159
31163
|
});
|
|
31160
31164
|
}
|
|
31165
|
+
function describeTransportError(err, method, attemptTimeoutMs) {
|
|
31166
|
+
if (err instanceof Error) {
|
|
31167
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
31168
|
+
return `${method} request timed out after ${attemptTimeoutMs}ms`;
|
|
31169
|
+
}
|
|
31170
|
+
const cause = err.cause;
|
|
31171
|
+
if (cause instanceof Error && cause.message) {
|
|
31172
|
+
return `${method} request failed: ${err.message} (${cause.message})`;
|
|
31173
|
+
}
|
|
31174
|
+
return `${method} request failed: ${err.message}`;
|
|
31175
|
+
}
|
|
31176
|
+
return `${method} request failed: ${String(err)}`;
|
|
31177
|
+
}
|
|
31161
31178
|
async function apiRequest(method, path, body, options) {
|
|
31162
31179
|
const headers = {};
|
|
31163
31180
|
if (options?.accept) {
|
|
@@ -31175,6 +31192,13 @@ async function apiRequest(method, path, body, options) {
|
|
|
31175
31192
|
fetchBody = JSON.stringify(body);
|
|
31176
31193
|
}
|
|
31177
31194
|
const isAbsolute = path.startsWith("http://") || path.startsWith("https://");
|
|
31195
|
+
if (isAbsolute && !path.startsWith("https://api.tailscale.com/")) {
|
|
31196
|
+
return {
|
|
31197
|
+
ok: false,
|
|
31198
|
+
status: 0,
|
|
31199
|
+
error: `Absolute URL ${JSON.stringify(path)} is not on api.tailscale.com -- refusing to send an authenticated request elsewhere.`
|
|
31200
|
+
};
|
|
31201
|
+
}
|
|
31178
31202
|
const url2 = isAbsolute ? path : `${BASE_URL}${path}`;
|
|
31179
31203
|
const startedAt = Date.now();
|
|
31180
31204
|
debugLog(`${method} ${url2}`);
|
|
@@ -31183,17 +31207,38 @@ async function apiRequest(method, path, body, options) {
|
|
|
31183
31207
|
return withConcurrencyLimit(async () => {
|
|
31184
31208
|
headers.Authorization = await getAuthHeader();
|
|
31185
31209
|
let res;
|
|
31210
|
+
let lastTransportError;
|
|
31186
31211
|
for (let attempt = 0; attempt <= MAX_429_RETRIES; attempt++) {
|
|
31187
31212
|
const remaining = requestBudgetMs - (Date.now() - startedAt);
|
|
31188
31213
|
if (remaining <= 0) {
|
|
31189
31214
|
return {
|
|
31190
31215
|
ok: false,
|
|
31191
31216
|
status: 0,
|
|
31192
|
-
error: `Request budget of ${requestBudgetMs}ms exhausted before attempt could begin.`
|
|
31217
|
+
error: lastTransportError ? `${lastTransportError}; request budget of ${requestBudgetMs}ms exhausted before next attempt could begin.` : `Request budget of ${requestBudgetMs}ms exhausted before attempt could begin.`
|
|
31193
31218
|
};
|
|
31194
31219
|
}
|
|
31195
31220
|
const attemptTimeoutMs = Math.min(REQUEST_TIMEOUT_MS, remaining);
|
|
31196
|
-
|
|
31221
|
+
let attemptRes;
|
|
31222
|
+
try {
|
|
31223
|
+
attemptRes = await executeFetch(method, url2, headers, fetchBody, attemptTimeoutMs);
|
|
31224
|
+
} catch (err) {
|
|
31225
|
+
const desc = describeTransportError(err, method, attemptTimeoutMs);
|
|
31226
|
+
lastTransportError = desc;
|
|
31227
|
+
if (!isRetryable || attempt === MAX_429_RETRIES) {
|
|
31228
|
+
return { ok: false, status: 0, error: desc };
|
|
31229
|
+
}
|
|
31230
|
+
const delay2 = compute429DelayMs(null, attempt);
|
|
31231
|
+
const elapsed3 = Date.now() - startedAt;
|
|
31232
|
+
if (requestBudgetMs - elapsed3 - delay2 <= 0) {
|
|
31233
|
+
return { ok: false, status: 0, error: `${desc}; request budget exhausted before retry.` };
|
|
31234
|
+
}
|
|
31235
|
+
debugLog(
|
|
31236
|
+
` -> transport error (attempt ${attempt + 1}/${MAX_429_RETRIES + 1}): ${desc}, retrying in ${delay2}ms`
|
|
31237
|
+
);
|
|
31238
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
31239
|
+
continue;
|
|
31240
|
+
}
|
|
31241
|
+
res = attemptRes;
|
|
31197
31242
|
if (res.status !== 429 || attempt === MAX_429_RETRIES || !isRetryable) break;
|
|
31198
31243
|
const delay = compute429DelayMs(res.headers.get("retry-after"), attempt);
|
|
31199
31244
|
const elapsed2 = Date.now() - startedAt;
|
|
@@ -31210,24 +31255,33 @@ async function apiRequest(method, path, body, options) {
|
|
|
31210
31255
|
const etag = response.headers.get("etag") || void 0;
|
|
31211
31256
|
const elapsed = Date.now() - startedAt;
|
|
31212
31257
|
debugLog(` <- ${response.status} (${elapsed}ms)`);
|
|
31213
|
-
|
|
31214
|
-
|
|
31258
|
+
try {
|
|
31259
|
+
if (options?.acceptRaw) {
|
|
31260
|
+
const rawBody = await response.text();
|
|
31261
|
+
if (!response.ok) {
|
|
31262
|
+
const error51 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, rawBody) : extractErrorMessage(rawBody);
|
|
31263
|
+
return { ok: false, status: response.status, error: error51, rawBody, etag };
|
|
31264
|
+
}
|
|
31265
|
+
return { ok: true, status: response.status, rawBody, etag };
|
|
31266
|
+
}
|
|
31215
31267
|
if (!response.ok) {
|
|
31216
|
-
const
|
|
31217
|
-
|
|
31268
|
+
const errorBody = await response.text();
|
|
31269
|
+
const error51 = response.status === 401 || response.status === 403 ? formatAuthError(response.status, errorBody) : extractErrorMessage(errorBody);
|
|
31270
|
+
return { ok: false, status: response.status, error: error51, etag };
|
|
31218
31271
|
}
|
|
31219
|
-
|
|
31220
|
-
|
|
31221
|
-
|
|
31222
|
-
const
|
|
31223
|
-
|
|
31224
|
-
|
|
31225
|
-
|
|
31226
|
-
|
|
31227
|
-
|
|
31272
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") {
|
|
31273
|
+
return { ok: true, status: response.status, etag };
|
|
31274
|
+
}
|
|
31275
|
+
const data = await response.json();
|
|
31276
|
+
return { ok: true, status: response.status, data, etag };
|
|
31277
|
+
} catch (err) {
|
|
31278
|
+
return {
|
|
31279
|
+
ok: false,
|
|
31280
|
+
status: response.status,
|
|
31281
|
+
error: `Failed to read response body from ${method} ${url2} (HTTP ${response.status}): ${err instanceof Error ? err.message : String(err)}`,
|
|
31282
|
+
etag
|
|
31283
|
+
};
|
|
31228
31284
|
}
|
|
31229
|
-
const data = await response.json();
|
|
31230
|
-
return { ok: true, status: response.status, data, etag };
|
|
31231
31285
|
});
|
|
31232
31286
|
}
|
|
31233
31287
|
async function apiGet(path, options) {
|
|
@@ -31335,25 +31389,25 @@ function parseReadonlyFlag(value) {
|
|
|
31335
31389
|
function filterTools(groups, options) {
|
|
31336
31390
|
const validNames = new Set(Object.keys(groups));
|
|
31337
31391
|
let profileGroups;
|
|
31338
|
-
let
|
|
31339
|
-
let
|
|
31392
|
+
let unknownProfile;
|
|
31393
|
+
let profileWouldFilter = false;
|
|
31340
31394
|
if (options.profile) {
|
|
31341
31395
|
const profileKey = options.profile.trim().toLowerCase();
|
|
31342
31396
|
if (Object.hasOwn(PROFILES, profileKey)) {
|
|
31343
31397
|
const preset = PROFILES[profileKey];
|
|
31344
|
-
|
|
31345
|
-
profileGroups =
|
|
31398
|
+
profileWouldFilter = preset.length > 0;
|
|
31399
|
+
profileGroups = profileWouldFilter ? [...preset] : void 0;
|
|
31346
31400
|
} else {
|
|
31347
|
-
|
|
31401
|
+
unknownProfile = profileKey;
|
|
31348
31402
|
}
|
|
31349
31403
|
}
|
|
31350
31404
|
const parsedTools = options.tools ? options.tools.split(",").map((s) => s.trim()).filter(Boolean) : null;
|
|
31351
|
-
const
|
|
31352
|
-
const explicitToolsAllUnknown =
|
|
31353
|
-
const effectiveExplicitTools = explicitToolsAllUnknown ? null :
|
|
31405
|
+
const explicitTools = parsedTools && parsedTools.length > 0 ? parsedTools : null;
|
|
31406
|
+
const explicitToolsAllUnknown = explicitTools?.every((g) => !validNames.has(g)) ?? false;
|
|
31407
|
+
const effectiveExplicitTools = explicitToolsAllUnknown ? null : explicitTools;
|
|
31354
31408
|
const effectiveGroups = effectiveExplicitTools ?? profileGroups ?? null;
|
|
31355
31409
|
const enabledGroups = effectiveGroups ? new Set(effectiveGroups) : null;
|
|
31356
|
-
const
|
|
31410
|
+
const unknownGroups = explicitTools ? explicitTools.filter((g) => !validNames.has(g)) : enabledGroups ? [...enabledGroups].filter((g) => !validNames.has(g)) : [];
|
|
31357
31411
|
const readonly2 = parseReadonlyFlag(options.readonly);
|
|
31358
31412
|
const out = [];
|
|
31359
31413
|
for (const [name, tools] of Object.entries(groups)) {
|
|
@@ -31363,11 +31417,11 @@ function filterTools(groups, options) {
|
|
|
31363
31417
|
out.push(t);
|
|
31364
31418
|
}
|
|
31365
31419
|
}
|
|
31366
|
-
const result = { tools: out, unknownGroups
|
|
31367
|
-
if (
|
|
31420
|
+
const result = { tools: out, unknownGroups };
|
|
31421
|
+
if (unknownProfile) result.unknownProfile = unknownProfile;
|
|
31368
31422
|
if (profileGroups && !effectiveExplicitTools) result.profileGroups = profileGroups;
|
|
31369
31423
|
if (effectiveExplicitTools) result.explicitTools = effectiveExplicitTools;
|
|
31370
|
-
if (
|
|
31424
|
+
if (profileWouldFilter) result.profileWouldFilter = true;
|
|
31371
31425
|
if (explicitToolsAllUnknown) result.toolsAllUnknown = true;
|
|
31372
31426
|
return result;
|
|
31373
31427
|
}
|
|
@@ -31581,8 +31635,11 @@ var aclTools = [
|
|
|
31581
31635
|
acceptRaw: true,
|
|
31582
31636
|
accept: "application/hujson"
|
|
31583
31637
|
});
|
|
31584
|
-
if (res.ok
|
|
31585
|
-
|
|
31638
|
+
if (res.ok) {
|
|
31639
|
+
const trimmed = res.rawBody?.trim();
|
|
31640
|
+
if (!trimmed || trimmed === "{}") {
|
|
31641
|
+
return { ...res, rawBody: "ACL policy is valid." };
|
|
31642
|
+
}
|
|
31586
31643
|
}
|
|
31587
31644
|
return res;
|
|
31588
31645
|
}
|
|
@@ -32532,7 +32589,7 @@ var keyTools = [
|
|
|
32532
32589
|
},
|
|
32533
32590
|
{
|
|
32534
32591
|
name: "tailscale_create_key",
|
|
32535
|
-
description: "Create a new key in your tailnet. Supports auth keys (for adding devices), OAuth clients (for programmatic API access), and federated identities (for OIDC-based CI/CD access). Returns the key value \u2014 save it immediately, as it cannot be retrieved again.\n\nExamples:\n- Auth key: {keyType:'auth', reusable:true, tags:['tag:ci']}\n- OAuth client: {keyType:'client', scopes:['devices:read','dns']}\n- Federated (GitHub Actions): {keyType:'federated', scopes:['devices:read'], issuer:'https://token.actions.githubusercontent.com', subject:'repo:my-org/my-repo:*'}",
|
|
32592
|
+
description: "Create a new key in your tailnet. Supports auth keys (for adding devices), OAuth clients (for programmatic API access), and federated identities (for OIDC-based CI/CD access). Returns the key value \u2014 save it immediately, as it cannot be retrieved again.\n\nSECURITY: the response body contains a long-lived credential verbatim. MCP clients commonly persist tool responses to logs and conversation transcripts; treat this response as sensitive (do not commit it, avoid re-sharing it in unrelated chat history).\n\nExamples:\n- Auth key: {keyType:'auth', reusable:true, tags:['tag:ci']}\n- OAuth client: {keyType:'client', scopes:['devices:read','dns']}\n- Federated (GitHub Actions): {keyType:'federated', scopes:['devices:read'], issuer:'https://token.actions.githubusercontent.com', subject:'repo:my-org/my-repo:*'}",
|
|
32536
32593
|
annotations: {
|
|
32537
32594
|
title: "Create key",
|
|
32538
32595
|
readOnlyHint: false,
|
|
@@ -32772,7 +32829,7 @@ var localCliTools = [
|
|
|
32772
32829
|
"Target hostname, IP, or MagicDNS name (e.g. 'my-laptop', '100.64.0.1', 'my-laptop.tail-scale.ts.net')"
|
|
32773
32830
|
),
|
|
32774
32831
|
count: external_exports.number().int().positive().max(20).optional().describe(
|
|
32775
|
-
"Number of ping attempts (default
|
|
32832
|
+
"Number of ping attempts (max 20). If omitted, no -c flag is passed and the tailscale CLI uses its built-in default (retries until a direct path is found, typically <10 attempts). Higher explicit counts give a more complete latency picture but block the tool call for longer."
|
|
32776
32833
|
)
|
|
32777
32834
|
}),
|
|
32778
32835
|
handler: async (input) => {
|
|
@@ -32882,7 +32939,9 @@ var logStreamingTools = [
|
|
|
32882
32939
|
logType: external_exports.enum(["configuration", "network"]).describe("The log type: 'configuration' for audit logs, 'network' for network flow logs"),
|
|
32883
32940
|
destinationType: external_exports.enum(["splunk", "elastic", "panther", "cribl", "datadog", "axiom", "s3"]).describe("The log streaming destination type"),
|
|
32884
32941
|
url: external_exports.string().optional().describe("Destination URL (required for non-s3 destinations)"),
|
|
32885
|
-
token: external_exports.string().optional().describe(
|
|
32942
|
+
token: external_exports.string().optional().describe(
|
|
32943
|
+
"Authentication token or API key for the destination. SENSITIVE: passed straight to Tailscale and not echoed back, but MCP clients may log the input value you supply."
|
|
32944
|
+
),
|
|
32886
32945
|
user: external_exports.string().optional().describe("Username for the destination (if required)"),
|
|
32887
32946
|
uploadPeriodMinutes: external_exports.number().int().positive().max(1440).optional().describe("Minutes to wait between uploads (1-1440). Optional."),
|
|
32888
32947
|
compressionFormat: external_exports.enum(["zstd", "gzip", "none"]).optional().describe("Compression algorithm for log uploads. Defaults to 'none'."),
|
|
@@ -32893,7 +32952,9 @@ var logStreamingTools = [
|
|
|
32893
32952
|
"(s3 only) Authentication mode. Required when destinationType is 's3'. Tailscale recommends 'rolearn'."
|
|
32894
32953
|
),
|
|
32895
32954
|
s3AccessKeyId: external_exports.string().optional().describe("(s3 only) AWS access key id. Required when s3AuthenticationType is 'accesskey'."),
|
|
32896
|
-
s3SecretAccessKey: external_exports.string().optional().describe(
|
|
32955
|
+
s3SecretAccessKey: external_exports.string().optional().describe(
|
|
32956
|
+
"(s3 only) AWS secret access key. Required when s3AuthenticationType is 'accesskey'. SENSITIVE: see the token field's note about MCP client logging."
|
|
32957
|
+
),
|
|
32897
32958
|
s3RoleArn: external_exports.string().optional().describe(
|
|
32898
32959
|
"(s3 only) IAM role ARN that Tailscale will assume. Required when s3AuthenticationType is 'rolearn'."
|
|
32899
32960
|
)
|
|
@@ -33067,7 +33128,9 @@ var postureTools = [
|
|
|
33067
33128
|
clientId: external_exports.string().optional().describe(
|
|
33068
33129
|
"Client ID for the provider (Intune: application UUID; Falcon/Jamf Pro: client id; Kandji/Kolide/Sentinel One: leave blank)"
|
|
33069
33130
|
),
|
|
33070
|
-
clientSecret: external_exports.string().describe(
|
|
33131
|
+
clientSecret: external_exports.string().describe(
|
|
33132
|
+
"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."
|
|
33133
|
+
),
|
|
33071
33134
|
tenantId: external_exports.string().optional().describe("Microsoft Intune directory (tenant) ID. Other providers leave blank."),
|
|
33072
33135
|
cloudId: external_exports.string().optional().describe(
|
|
33073
33136
|
"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."
|
|
@@ -33097,7 +33160,9 @@ var postureTools = [
|
|
|
33097
33160
|
inputSchema: external_exports.object({
|
|
33098
33161
|
integrationId: external_exports.string().describe("The posture integration ID to update"),
|
|
33099
33162
|
clientId: external_exports.string().optional().describe("Updated client ID for the provider"),
|
|
33100
|
-
clientSecret: external_exports.string().optional().describe(
|
|
33163
|
+
clientSecret: external_exports.string().optional().describe(
|
|
33164
|
+
"Updated client secret for the provider (omit to retain the existing secret). SENSITIVE: passed straight to Tailscale and not echoed back, but MCP clients may log the input value you supply."
|
|
33165
|
+
),
|
|
33101
33166
|
tenantId: external_exports.string().optional().describe("Updated tenant ID"),
|
|
33102
33167
|
cloudId: external_exports.string().optional().describe("Updated cloud identifier (e.g. 'us-1', 'global', or provider FQDN)")
|
|
33103
33168
|
}),
|
|
@@ -33622,7 +33687,7 @@ var webhookTools = [
|
|
|
33622
33687
|
},
|
|
33623
33688
|
{
|
|
33624
33689
|
name: "tailscale_create_webhook",
|
|
33625
|
-
description: "Create a new webhook.",
|
|
33690
|
+
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.",
|
|
33626
33691
|
annotations: {
|
|
33627
33692
|
title: "Create webhook",
|
|
33628
33693
|
readOnlyHint: false,
|
|
@@ -33685,7 +33750,7 @@ var webhookTools = [
|
|
|
33685
33750
|
},
|
|
33686
33751
|
{
|
|
33687
33752
|
name: "tailscale_rotate_webhook_secret",
|
|
33688
|
-
description: "Rotate a webhook's secret. Returns the new secret \u2014 save it immediately, as it cannot be retrieved again. The old secret is immediately invalidated.",
|
|
33753
|
+
description: "Rotate a webhook's secret. Returns the new secret \u2014 save it immediately, as it cannot be retrieved again. The old secret is immediately invalidated.\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.",
|
|
33689
33754
|
annotations: {
|
|
33690
33755
|
title: "Rotate webhook secret",
|
|
33691
33756
|
readOnlyHint: false,
|
|
@@ -33722,20 +33787,21 @@ var webhookTools = [
|
|
|
33722
33787
|
];
|
|
33723
33788
|
|
|
33724
33789
|
// src/index.ts
|
|
33725
|
-
var version2 = true ? "0.13.
|
|
33790
|
+
var version2 = true ? "0.13.2" : resolveVersionFallback();
|
|
33726
33791
|
var subcommand = process.argv[2];
|
|
33792
|
+
var cliSubcommandHandled = false;
|
|
33727
33793
|
if (subcommand === "deploy-acl" || subcommand === "validate-acl") {
|
|
33794
|
+
cliSubcommandHandled = true;
|
|
33728
33795
|
const filePath = process.argv[3];
|
|
33729
33796
|
if (!filePath) {
|
|
33730
33797
|
console.error(`Usage: tailscale-mcp ${subcommand} <path-to-acl.json>`);
|
|
33731
33798
|
process.exit(1);
|
|
33732
33799
|
}
|
|
33733
33800
|
const run = subcommand === "deploy-acl" ? deployAcl : validateAcl;
|
|
33734
|
-
|
|
33801
|
+
run(filePath).then(() => process.exit(0)).catch((err) => {
|
|
33735
33802
|
console.error(`Fatal: ${err instanceof Error ? err.message : err}`);
|
|
33736
33803
|
process.exit(1);
|
|
33737
33804
|
});
|
|
33738
|
-
process.exit(0);
|
|
33739
33805
|
} else if (subcommand === "version" || subcommand === "--version") {
|
|
33740
33806
|
console.log(version2);
|
|
33741
33807
|
process.exit(0);
|
|
@@ -33744,104 +33810,110 @@ if (subcommand === "deploy-acl" || subcommand === "validate-acl") {
|
|
|
33744
33810
|
`@yawlabs/tailscale-mcp: unrecognized argument "${subcommand}" -- known subcommands: deploy-acl, validate-acl, version. Starting the MCP server.`
|
|
33745
33811
|
);
|
|
33746
33812
|
}
|
|
33747
|
-
|
|
33748
|
-
|
|
33749
|
-
|
|
33750
|
-
|
|
33751
|
-
|
|
33752
|
-
|
|
33753
|
-
|
|
33754
|
-
|
|
33755
|
-
|
|
33756
|
-
|
|
33757
|
-
|
|
33758
|
-
|
|
33759
|
-
|
|
33760
|
-
|
|
33761
|
-
|
|
33762
|
-
|
|
33763
|
-
|
|
33764
|
-
|
|
33765
|
-
|
|
33766
|
-
|
|
33767
|
-
|
|
33768
|
-
|
|
33769
|
-
|
|
33770
|
-
|
|
33771
|
-
|
|
33772
|
-
|
|
33773
|
-
|
|
33774
|
-
|
|
33775
|
-
|
|
33776
|
-
|
|
33777
|
-
|
|
33778
|
-
|
|
33779
|
-
|
|
33780
|
-
|
|
33781
|
-
|
|
33782
|
-
|
|
33813
|
+
if (!cliSubcommandHandled) {
|
|
33814
|
+
const toolGroups = {
|
|
33815
|
+
status: statusTools,
|
|
33816
|
+
devices: deviceTools,
|
|
33817
|
+
acl: aclTools,
|
|
33818
|
+
dns: dnsTools,
|
|
33819
|
+
keys: keyTools,
|
|
33820
|
+
users: userTools,
|
|
33821
|
+
tailnet: tailnetTools,
|
|
33822
|
+
webhooks: webhookTools,
|
|
33823
|
+
posture: postureTools,
|
|
33824
|
+
audit: auditTools,
|
|
33825
|
+
invites: inviteTools,
|
|
33826
|
+
services: serviceTools,
|
|
33827
|
+
"log-streaming": logStreamingTools
|
|
33828
|
+
};
|
|
33829
|
+
const localCliEnabled = isLocalCliEnabled(process.env);
|
|
33830
|
+
if (localCliEnabled) {
|
|
33831
|
+
toolGroups["local-cli"] = localCliTools;
|
|
33832
|
+
}
|
|
33833
|
+
const {
|
|
33834
|
+
tools: allTools,
|
|
33835
|
+
unknownGroups,
|
|
33836
|
+
unknownProfile,
|
|
33837
|
+
explicitTools,
|
|
33838
|
+
profileWouldFilter,
|
|
33839
|
+
toolsAllUnknown
|
|
33840
|
+
} = filterTools(toolGroups, {
|
|
33841
|
+
tools: process.env.TAILSCALE_TOOLS,
|
|
33842
|
+
readonly: process.env.TAILSCALE_READONLY,
|
|
33843
|
+
profile: process.env.TAILSCALE_PROFILE
|
|
33844
|
+
});
|
|
33845
|
+
if (unknownGroups.length > 0) {
|
|
33846
|
+
const validNames = Object.keys(toolGroups);
|
|
33847
|
+
const fallbackNote = toolsAllUnknown ? " Every requested group was unknown, so TAILSCALE_TOOLS was ignored and the default tool set was loaded instead." : "";
|
|
33848
|
+
console.error(
|
|
33849
|
+
`@yawlabs/tailscale-mcp: TAILSCALE_TOOLS includes unknown group(s): ${unknownGroups.join(", ")}. Valid groups: ${validNames.join(", ")}.${fallbackNote}`
|
|
33850
|
+
);
|
|
33851
|
+
}
|
|
33852
|
+
if (unknownProfile) {
|
|
33853
|
+
console.error(
|
|
33854
|
+
`@yawlabs/tailscale-mcp: TAILSCALE_PROFILE="${unknownProfile}" is not a known profile. Valid profiles: minimal, core, full. Falling back to no profile filter.`
|
|
33855
|
+
);
|
|
33856
|
+
}
|
|
33857
|
+
const server = new McpServer({
|
|
33858
|
+
name: "@yawlabs/tailscale-mcp",
|
|
33859
|
+
version: version2
|
|
33860
|
+
});
|
|
33861
|
+
for (const tool of allTools) {
|
|
33862
|
+
server.tool(tool.name, tool.description, tool.inputSchema.shape, tool.annotations, wrapToolHandler(tool));
|
|
33863
|
+
}
|
|
33864
|
+
server.resource(
|
|
33865
|
+
"tailnet-status",
|
|
33866
|
+
"tailscale://tailnet/status",
|
|
33867
|
+
{ description: "Current tailnet status including device count and settings", mimeType: "application/json" },
|
|
33868
|
+
tailnetStatusResource
|
|
33783
33869
|
);
|
|
33784
|
-
|
|
33785
|
-
|
|
33786
|
-
|
|
33787
|
-
|
|
33870
|
+
server.resource(
|
|
33871
|
+
"tailnet-devices",
|
|
33872
|
+
"tailscale://tailnet/devices",
|
|
33873
|
+
{ description: "List of all devices in the tailnet with their status", mimeType: "application/json" },
|
|
33874
|
+
tailnetDevicesResource
|
|
33788
33875
|
);
|
|
33789
|
-
|
|
33790
|
-
|
|
33791
|
-
|
|
33792
|
-
|
|
33793
|
-
|
|
33794
|
-
|
|
33795
|
-
server.
|
|
33796
|
-
|
|
33797
|
-
|
|
33798
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
33804
|
-
|
|
33805
|
-
|
|
33806
|
-
|
|
33807
|
-
|
|
33808
|
-
);
|
|
33809
|
-
|
|
33810
|
-
|
|
33811
|
-
|
|
33812
|
-
|
|
33813
|
-
|
|
33814
|
-
|
|
33815
|
-
|
|
33816
|
-
|
|
33817
|
-
|
|
33818
|
-
|
|
33819
|
-
description: "DNS configuration including nameservers, search paths, split DNS, and MagicDNS status",
|
|
33820
|
-
mimeType: "application/json"
|
|
33821
|
-
},
|
|
33822
|
-
tailnetDnsResource
|
|
33823
|
-
);
|
|
33824
|
-
var transport = new StdioServerTransport();
|
|
33825
|
-
await server.connect(transport);
|
|
33826
|
-
var readonlyMode = parseReadonlyFlag(process.env.TAILSCALE_READONLY);
|
|
33827
|
-
var filterSuffix = formatBannerFilterSuffix({
|
|
33828
|
-
unknownProfile,
|
|
33829
|
-
explicitTools,
|
|
33830
|
-
profileWouldFilter,
|
|
33831
|
-
profileEnv: process.env.TAILSCALE_PROFILE,
|
|
33832
|
-
readonlyMode,
|
|
33833
|
-
localCliEnabled
|
|
33834
|
-
});
|
|
33835
|
-
console.error(
|
|
33836
|
-
`@yawlabs/tailscale-mcp v${version2} ready (${allTools.length} tools${filterSuffix ? `, ${filterSuffix}` : ""})`
|
|
33837
|
-
);
|
|
33838
|
-
var hasCreds = !!process.env.TAILSCALE_API_KEY || !!process.env.TAILSCALE_OAUTH_CLIENT_ID && !!process.env.TAILSCALE_OAUTH_CLIENT_SECRET;
|
|
33839
|
-
if (!filterSuffix && hasCreds) {
|
|
33840
|
-
const profileCount = (groups) => groups.reduce((n, g) => n + (toolGroups[g]?.length ?? 0), 0);
|
|
33841
|
-
const coreCount = profileCount(PROFILES.core);
|
|
33842
|
-
const minimalCount = profileCount(PROFILES.minimal);
|
|
33876
|
+
server.resource(
|
|
33877
|
+
"tailnet-acl",
|
|
33878
|
+
"tailscale://tailnet/acl",
|
|
33879
|
+
{ description: "Current ACL policy (HuJSON with comments preserved)", mimeType: "application/hujson" },
|
|
33880
|
+
tailnetAclResource
|
|
33881
|
+
);
|
|
33882
|
+
server.resource(
|
|
33883
|
+
"tailnet-dns",
|
|
33884
|
+
"tailscale://tailnet/dns",
|
|
33885
|
+
{
|
|
33886
|
+
description: "DNS configuration including nameservers, search paths, split DNS, and MagicDNS status",
|
|
33887
|
+
mimeType: "application/json"
|
|
33888
|
+
},
|
|
33889
|
+
tailnetDnsResource
|
|
33890
|
+
);
|
|
33891
|
+
const transport = new StdioServerTransport();
|
|
33892
|
+
server.connect(transport).catch((err) => {
|
|
33893
|
+
process.stderr.write(`tailscale-mcp: ${err instanceof Error ? err.message : String(err)}
|
|
33894
|
+
`);
|
|
33895
|
+
process.exit(1);
|
|
33896
|
+
});
|
|
33897
|
+
const readonlyMode = parseReadonlyFlag(process.env.TAILSCALE_READONLY);
|
|
33898
|
+
const filterSuffix = formatBannerFilterSuffix({
|
|
33899
|
+
unknownProfile,
|
|
33900
|
+
explicitTools,
|
|
33901
|
+
profileWouldFilter,
|
|
33902
|
+
profileEnv: process.env.TAILSCALE_PROFILE,
|
|
33903
|
+
readonlyMode,
|
|
33904
|
+
localCliEnabled
|
|
33905
|
+
});
|
|
33843
33906
|
console.error(
|
|
33844
|
-
`@yawlabs/tailscale-mcp
|
|
33907
|
+
`@yawlabs/tailscale-mcp v${version2} ready (${allTools.length} tools${filterSuffix ? `, ${filterSuffix}` : ""})`
|
|
33845
33908
|
);
|
|
33909
|
+
const hasCreds = !!process.env.TAILSCALE_API_KEY || !!process.env.TAILSCALE_OAUTH_CLIENT_ID && !!process.env.TAILSCALE_OAUTH_CLIENT_SECRET;
|
|
33910
|
+
if (!filterSuffix && hasCreds) {
|
|
33911
|
+
const profileCount = (groups) => groups.reduce((n, g) => n + (toolGroups[g]?.length ?? 0), 0);
|
|
33912
|
+
const coreCount = profileCount(PROFILES.core);
|
|
33913
|
+
const minimalCount = profileCount(PROFILES.minimal);
|
|
33914
|
+
console.error(
|
|
33915
|
+
`@yawlabs/tailscale-mcp: tip \u2014 set TAILSCALE_PROFILE=core (${coreCount} tools) or =minimal (${minimalCount}) to load a smaller tool surface. See README.`
|
|
33916
|
+
);
|
|
33917
|
+
}
|
|
33846
33918
|
}
|
|
33847
33919
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/tailscale-mcp",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"mcpName": "io.github.YawLabs/tailscale-mcp",
|
|
5
5
|
"description": "Tailscale MCP server for managing your tailnet from AI assistants",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"lint:fix": "biome check --write src/",
|
|
39
39
|
"prepublishOnly": "npm run clean && npm run build"
|
|
40
40
|
},
|
|
41
|
-
"dependencies": {},
|
|
42
41
|
"overrides": {
|
|
43
42
|
"hono": "^4.12.21",
|
|
44
43
|
"@hono/node-server": "^1.19.13",
|
|
@@ -50,7 +49,8 @@
|
|
|
50
49
|
"@biomejs/biome": "^2.4.12",
|
|
51
50
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
52
51
|
"@types/node": "^25.6.0",
|
|
53
|
-
"esbuild": "^0.28.
|
|
52
|
+
"esbuild": "^0.28.1",
|
|
53
|
+
"postject": "^1.0.0-alpha.6",
|
|
54
54
|
"typescript": "^6.0.3",
|
|
55
55
|
"zod": "^4.3.6"
|
|
56
56
|
},
|