@yawlabs/tailscale-mcp 0.6.5 → 0.7.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 +4 -0
- package/dist/index.js +30 -43
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
Built and maintained by [YawLabs](https://yaw.sh).
|
|
11
11
|
|
|
12
|
+
[](https://mcp.hosting/install?name=Tailscale&command=npx&args=-y%2C%40yawlabs%2Ftailscale-mcp&env=TAILSCALE_API_KEY&description=Manage%20your%20Tailscale%20tailnet%20-%20devices%2C%20ACLs%2C%20DNS%2C%20keys&source=https%3A%2F%2Fgithub.com%2FYawLabs%2Ftailscale-mcp)
|
|
13
|
+
|
|
14
|
+
One click adds this to your [mcp.hosting](https://mcp.hosting) account so it syncs to every MCP client you use. Or install manually below.
|
|
15
|
+
|
|
12
16
|
## Why this one?
|
|
13
17
|
|
|
14
18
|
Other Tailscale MCP servers were vibe-coded in a weekend and abandoned. This one was built for production use and tested against the real Tailscale API.
|
package/dist/index.js
CHANGED
|
@@ -21015,21 +21015,23 @@ var BASE_URL = "https://api.tailscale.com/api/v2";
|
|
|
21015
21015
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
21016
21016
|
var oauthToken = null;
|
|
21017
21017
|
var oauthRefreshPromise = null;
|
|
21018
|
-
function
|
|
21018
|
+
function getAuthConfig() {
|
|
21019
21019
|
const apiKey = process.env.TAILSCALE_API_KEY;
|
|
21020
21020
|
const oauthClientId = process.env.TAILSCALE_OAUTH_CLIENT_ID;
|
|
21021
21021
|
const oauthClientSecret = process.env.TAILSCALE_OAUTH_CLIENT_SECRET;
|
|
21022
|
-
|
|
21023
|
-
|
|
21024
|
-
|
|
21025
|
-
|
|
21026
|
-
|
|
21027
|
-
);
|
|
21022
|
+
if (apiKey) {
|
|
21023
|
+
if (apiKey.trim() === "") {
|
|
21024
|
+
throw new Error("TAILSCALE_API_KEY is set but empty. Provide a valid API key.");
|
|
21025
|
+
}
|
|
21026
|
+
return { kind: "apiKey", apiKey };
|
|
21028
21027
|
}
|
|
21029
|
-
if (
|
|
21030
|
-
|
|
21028
|
+
if (oauthClientId && oauthClientSecret) {
|
|
21029
|
+
return { kind: "oauth", clientId: oauthClientId, clientSecret: oauthClientSecret };
|
|
21031
21030
|
}
|
|
21032
|
-
|
|
21031
|
+
const hint = process.platform === "win32" ? ' On Windows, env vars set in bash/WSL profiles are not visible to MCP servers launched via cmd. Either add "env": {"TAILSCALE_API_KEY": "tskey-api-..."} to your .mcp.json, or set it as a Windows user environment variable.' : "";
|
|
21032
|
+
throw new Error(
|
|
21033
|
+
`No Tailscale credentials configured. Set TAILSCALE_API_KEY, or set both TAILSCALE_OAUTH_CLIENT_ID and TAILSCALE_OAUTH_CLIENT_SECRET.${hint}`
|
|
21034
|
+
);
|
|
21033
21035
|
}
|
|
21034
21036
|
async function getOAuthAccessToken(clientId, clientSecret) {
|
|
21035
21037
|
if (oauthToken && Date.now() < oauthToken.expires_at - 6e4) {
|
|
@@ -21067,11 +21069,11 @@ async function getOAuthAccessToken(clientId, clientSecret) {
|
|
|
21067
21069
|
return oauthRefreshPromise;
|
|
21068
21070
|
}
|
|
21069
21071
|
async function getAuthHeader() {
|
|
21070
|
-
const config2 =
|
|
21071
|
-
if (config2.apiKey) {
|
|
21072
|
+
const config2 = getAuthConfig();
|
|
21073
|
+
if (config2.kind === "apiKey") {
|
|
21072
21074
|
return `Basic ${Buffer.from(`${config2.apiKey}:`).toString("base64")}`;
|
|
21073
21075
|
}
|
|
21074
|
-
const token = await getOAuthAccessToken(config2.
|
|
21076
|
+
const token = await getOAuthAccessToken(config2.clientId, config2.clientSecret);
|
|
21075
21077
|
return `Bearer ${token}`;
|
|
21076
21078
|
}
|
|
21077
21079
|
function getTailnet() {
|
|
@@ -21080,6 +21082,13 @@ function getTailnet() {
|
|
|
21080
21082
|
function encPath(segment) {
|
|
21081
21083
|
return encodeURIComponent(segment);
|
|
21082
21084
|
}
|
|
21085
|
+
function validateTags(tags) {
|
|
21086
|
+
if (!tags || tags.length === 0) return;
|
|
21087
|
+
const invalid = tags.filter((t) => !t.startsWith("tag:"));
|
|
21088
|
+
if (invalid.length > 0) {
|
|
21089
|
+
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
21090
|
+
}
|
|
21091
|
+
}
|
|
21083
21092
|
function sanitizeDescription(value) {
|
|
21084
21093
|
return value.replace(/[/_]/g, "-").replace(/[^a-zA-Z0-9 -]/g, "").replace(/ {2,}/g, " ").trim().slice(0, 50);
|
|
21085
21094
|
}
|
|
@@ -21622,10 +21631,7 @@ var deviceTools = [
|
|
|
21622
21631
|
tags: external_exports.array(external_exports.string()).describe("Full list of ACL tags (e.g. ['tag:server', 'tag:production']). Replaces all existing tags.")
|
|
21623
21632
|
}),
|
|
21624
21633
|
handler: async (input) => {
|
|
21625
|
-
|
|
21626
|
-
if (invalid.length > 0) {
|
|
21627
|
-
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
21628
|
-
}
|
|
21634
|
+
validateTags(input.tags);
|
|
21629
21635
|
return apiPost(`/device/${encPath(input.deviceId)}/tags`, { tags: input.tags });
|
|
21630
21636
|
}
|
|
21631
21637
|
},
|
|
@@ -22136,7 +22142,7 @@ var keyTools = [
|
|
|
22136
22142
|
},
|
|
22137
22143
|
{
|
|
22138
22144
|
name: "tailscale_create_key",
|
|
22139
|
-
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.",
|
|
22145
|
+
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:*'}",
|
|
22140
22146
|
annotations: {
|
|
22141
22147
|
title: "Create key",
|
|
22142
22148
|
readOnlyHint: false,
|
|
@@ -22167,12 +22173,7 @@ var keyTools = [
|
|
|
22167
22173
|
customClaimRules: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("(federated only) Custom claim mapping rules")
|
|
22168
22174
|
}),
|
|
22169
22175
|
handler: async (input) => {
|
|
22170
|
-
|
|
22171
|
-
const invalid = input.tags.filter((t) => !t.startsWith("tag:"));
|
|
22172
|
-
if (invalid.length > 0) {
|
|
22173
|
-
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
22174
|
-
}
|
|
22175
|
-
}
|
|
22176
|
+
validateTags(input.tags);
|
|
22176
22177
|
const keyType = input.keyType ?? "auth";
|
|
22177
22178
|
if (keyType !== "auth") {
|
|
22178
22179
|
const authOnlyFields = ["reusable", "ephemeral", "preauthorized", "expirySeconds"];
|
|
@@ -22252,12 +22253,7 @@ var keyTools = [
|
|
|
22252
22253
|
customClaimRules: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("(federated only) Updated custom claim rules")
|
|
22253
22254
|
}),
|
|
22254
22255
|
handler: async (input) => {
|
|
22255
|
-
|
|
22256
|
-
const invalid = input.tags.filter((t) => !t.startsWith("tag:"));
|
|
22257
|
-
if (invalid.length > 0) {
|
|
22258
|
-
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
22259
|
-
}
|
|
22260
|
-
}
|
|
22256
|
+
validateTags(input.tags);
|
|
22261
22257
|
const body = {};
|
|
22262
22258
|
if (input.description !== void 0) body.description = sanitizeDescription(input.description);
|
|
22263
22259
|
if (input.scopes !== void 0) body.scopes = input.scopes;
|
|
@@ -22489,12 +22485,7 @@ var oauthClientTools = [
|
|
|
22489
22485
|
description: external_exports.string().optional().describe("Description for this OAuth client (max 50 chars, alphanumeric/hyphens/spaces)")
|
|
22490
22486
|
}),
|
|
22491
22487
|
handler: async (input) => {
|
|
22492
|
-
|
|
22493
|
-
const invalid = input.tags.filter((t) => !t.startsWith("tag:"));
|
|
22494
|
-
if (invalid.length > 0) {
|
|
22495
|
-
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
22496
|
-
}
|
|
22497
|
-
}
|
|
22488
|
+
validateTags(input.tags);
|
|
22498
22489
|
const body = { ...input };
|
|
22499
22490
|
body.name = sanitizeDescription(input.name);
|
|
22500
22491
|
if (input.description !== void 0) body.description = sanitizeDescription(input.description);
|
|
@@ -22719,12 +22710,7 @@ var serviceTools = [
|
|
|
22719
22710
|
autoApproveHosts: external_exports.boolean().optional().describe("Whether to auto-approve devices that want to host this service")
|
|
22720
22711
|
}),
|
|
22721
22712
|
handler: async (input) => {
|
|
22722
|
-
|
|
22723
|
-
const invalid = input.tags.filter((t) => !t.startsWith("tag:"));
|
|
22724
|
-
if (invalid.length > 0) {
|
|
22725
|
-
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
22726
|
-
}
|
|
22727
|
-
}
|
|
22713
|
+
validateTags(input.tags);
|
|
22728
22714
|
const { serviceName, ...body } = input;
|
|
22729
22715
|
const cleanBody = {};
|
|
22730
22716
|
for (const [key, value] of Object.entries(body)) {
|
|
@@ -23351,7 +23337,7 @@ var workloadIdentityTools = [
|
|
|
23351
23337
|
];
|
|
23352
23338
|
|
|
23353
23339
|
// src/index.ts
|
|
23354
|
-
var version2 = true ? "0.
|
|
23340
|
+
var version2 = true ? "0.7.0" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
23355
23341
|
var subcommand = process.argv[2];
|
|
23356
23342
|
if (subcommand === "deploy-acl") {
|
|
23357
23343
|
const filePath = process.argv[3];
|
|
@@ -23487,4 +23473,5 @@ server.resource(
|
|
|
23487
23473
|
);
|
|
23488
23474
|
var transport = new StdioServerTransport();
|
|
23489
23475
|
await server.connect(transport);
|
|
23476
|
+
console.error(`@yawlabs/tailscale-mcp v${version2} ready (${allTools.length} tools)`);
|
|
23490
23477
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/tailscale-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Tailscale MCP server for managing your tailnet from AI assistants",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "YawLabs <contact@yaw.sh>",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"tailscale-mcp": "dist/index.js"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"dist/index.js"
|
|
25
|
+
"dist/index.js",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md"
|
|
26
28
|
],
|
|
27
29
|
"scripts": {
|
|
28
30
|
"build": "tsc && node build.mjs",
|
|
@@ -35,6 +37,10 @@
|
|
|
35
37
|
"prepublishOnly": "npm run build"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {},
|
|
40
|
+
"overrides": {
|
|
41
|
+
"hono": "^4.12.14",
|
|
42
|
+
"@hono/node-server": "^1.19.13"
|
|
43
|
+
},
|
|
38
44
|
"devDependencies": {
|
|
39
45
|
"@biomejs/biome": "^1.9.4",
|
|
40
46
|
"@modelcontextprotocol/sdk": "^1.29.0",
|