@yawlabs/tailscale-mcp 0.6.4 → 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.
Files changed (3) hide show
  1. package/README.md +20 -1
  2. package/dist/index.js +30 -43
  3. 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
+ [![Add to mcp.hosting](https://mcp.hosting/install-button.svg)](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.
@@ -34,6 +38,8 @@ export TAILSCALE_API_KEY="tskey-api-..."
34
38
 
35
39
  **2. Create `.mcp.json` in your project root**
36
40
 
41
+ macOS / Linux / WSL:
42
+
37
43
  ```json
38
44
  {
39
45
  "mcpServers": {
@@ -45,7 +51,20 @@ export TAILSCALE_API_KEY="tskey-api-..."
45
51
  }
46
52
  ```
47
53
 
48
- > **Tip:** This file is safe to commit — it contains no secrets. Teammates who set their own `TAILSCALE_API_KEY` will get the MCP server automatically. Works on macOS, Linux, and Windows — no platform-specific config needed.
54
+ Windows:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "tailscale": {
60
+ "command": "cmd",
61
+ "args": ["/c", "npx", "-y", "@yawlabs/tailscale-mcp"]
62
+ }
63
+ }
64
+ }
65
+ ```
66
+
67
+ > **Why the extra step on Windows?** Since Node 20, `child_process.spawn` cannot directly execute `.cmd` files (that's what `npx` is on Windows). Wrapping with `cmd /c` is the standard workaround and is what MCP clients expect. This file is safe to commit — it contains no secrets.
49
68
 
50
69
  **3. Restart and approve**
51
70
 
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 getConfig() {
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
- const tailnet = process.env.TAILSCALE_TAILNET || "-";
21023
- if (!apiKey && !(oauthClientId && oauthClientSecret)) {
21024
- 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.' : "";
21025
- throw new Error(
21026
- `No Tailscale credentials configured. Set TAILSCALE_API_KEY, or set both TAILSCALE_OAUTH_CLIENT_ID and TAILSCALE_OAUTH_CLIENT_SECRET.${hint}`
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 (apiKey && apiKey.trim() === "") {
21030
- throw new Error("TAILSCALE_API_KEY is set but empty. Provide a valid API key.");
21028
+ if (oauthClientId && oauthClientSecret) {
21029
+ return { kind: "oauth", clientId: oauthClientId, clientSecret: oauthClientSecret };
21031
21030
  }
21032
- return { apiKey, oauthClientId, oauthClientSecret, tailnet };
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 = getConfig();
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.oauthClientId, config2.oauthClientSecret);
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
- const invalid = input.tags.filter((t) => !t.startsWith("tag:"));
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
- if (input.tags && input.tags.length > 0) {
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
- if (input.tags && input.tags.length > 0) {
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
- if (input.tags && input.tags.length > 0) {
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
- if (input.tags && input.tags.length > 0) {
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.6.4" : (await null).createRequire(import.meta.url)("../package.json").version;
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.6.4",
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",