@yawlabs/tailscale-mcp 0.19.1 → 0.19.3

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 CHANGED
@@ -4,6 +4,7 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
  [![GitHub stars](https://img.shields.io/github/stars/YawLabs/tailscale-mcp)](https://github.com/YawLabs/tailscale-mcp/stargazers)
6
6
  [![Release](https://img.shields.io/badge/release-local-blue)](./release.sh)
7
+ [![Follow @TokenLimitNews on X](https://img.shields.io/badge/follow-%40TokenLimitNews-000000?logo=x&logoColor=white)](https://x.com/TokenLimitNews)
7
8
 
8
9
  **Ask your agent questions about your tailnet and have it act on the answers.** 97 admin-API tools + 6 optional local-CLI diagnostics + 1 always-on catalog tool + 4 resources spanning the [Tailscale v2 API](https://tailscale.com/api) — devices, ACLs, DNS, keys and trust credentials, users, invites, webhooks, log streaming, posture, services, and organization tailnets. Backed by 1100+ unit tests and an opt-in live-tailnet integration suite.
9
10
 
@@ -22,6 +22,27 @@
22
22
  * For an MCP host config, point straight at oam and skip this file:
23
23
  * { "command": "oam", "args": ["run", "<abs>/dist/index.js"] }
24
24
  *
25
+ * ALREADY RUNNING ON OAM
26
+ * A host can resolve this package's `bin` and launch `oam run <this file>`
27
+ * instead of `node <this file>` -- Yaw MCP does, and so does oam's sidecar
28
+ * regression matrix. This launcher used to discover oam and spawn it anyway,
29
+ * so one server cost two runtime boots: measured on Windows, oam.exe with a
30
+ * NESTED oam.exe + conhost.exe underneath it. Now, when `process.versions.oam`
31
+ * clears the same MINIMUM OAM VERSION a discovered binary has to, the server is
32
+ * imported into THIS process exactly as the Node fallback is -- no discovery,
33
+ * no `oam --version` probe, no second oam. OAM_BIN is a discovery input, so it
34
+ * is not consulted on that path: the host has already chosen which oam runs.
35
+ *
36
+ * Two cases still go through discovery, deliberately. TAILSCALE_MCP_SANDBOX=1,
37
+ * because `--permission` is a process-level flag that only a FRESH oam can
38
+ * apply -- serving in-process there would drop the sandbox without a word, a
39
+ * security downgrade dressed up as an optimisation. And a host oam below the
40
+ * floor. Both then run exactly as they did before this shortcut existed,
41
+ * fallback included: discovery spawns a fresh oam only when it finds one at or
42
+ * above the floor that launches. Otherwise auto serves in-process as before --
43
+ * for a sandbox request that means WITHOUT --permission, so pair the sandbox
44
+ * with TAILSCALE_MCP_RUNTIME=oam to make that a hard failure instead.
45
+ *
25
46
  * THE `--permission` SANDBOX (oam 0.9.0+, opt-in)
26
47
  * `TAILSCALE_MCP_SANDBOX=1` runs the server under oam's permission model:
27
48
  * network limited to the one host the bundle actually calls
@@ -48,6 +69,7 @@
48
69
  *
49
70
  * SELECTION
50
71
  * TAILSCALE_MCP_RUNTIME=oam require oam; fail loudly if it is missing
72
+ * (already running on oam satisfies it)
51
73
  * TAILSCALE_MCP_RUNTIME=node never use oam
52
74
  * TAILSCALE_MCP_RUNTIME=auto prefer oam, silently fall back (default)
53
75
  * anything else warns on stderr, then behaves as auto
@@ -116,17 +138,27 @@ function findOam() {
116
138
  }
117
139
 
118
140
  /**
119
- * `oam --version` -> [major, minor, patch], or null when it cannot be read.
141
+ * Version text -> [major, minor, patch], or null when it holds no version.
120
142
  * A pre-release suffix (0.9.0-rc.1) truncates to its base version.
143
+ *
144
+ * Shared by the two places a version is read -- a discovered binary's
145
+ * `oam --version` output ("oam 0.15.1") and the host's own
146
+ * `process.versions.oam` ("0.15.1") -- so they cannot disagree about what a
147
+ * version string means, or which floor it has to clear.
121
148
  */
149
+ function parseVersion(text) {
150
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
151
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
152
+ }
153
+
154
+ /** `oam --version` -> [major, minor, patch], or null when it cannot be read. */
122
155
  function oamVersion(cmd) {
123
156
  try {
124
157
  const out = execFileSync(cmd, ["--version"], {
125
158
  encoding: "utf-8",
126
159
  stdio: ["ignore", "pipe", "ignore"],
127
160
  });
128
- const m = /(\d+)\.(\d+)\.(\d+)/.exec(out);
129
- return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
161
+ return parseVersion(out);
130
162
  } catch {
131
163
  // Not executable, wrong arch, or deleted since the stat. Caller degrades.
132
164
  return null;
@@ -143,6 +175,34 @@ function atLeast(v, min) {
143
175
  return true;
144
176
  }
145
177
 
178
+ /**
179
+ * Where the server runs, decided BEFORE any discovery:
180
+ * "in-process" import it into THIS process
181
+ * "discover" find an oam binary, gate its version, spawn it; when that
182
+ * fails, auto falls back in-process and `oam` exits 1
183
+ *
184
+ * `hostOam` is `process.versions.oam`: oam's own key, absent on Node, so on
185
+ * Node every mode but `node` is the discovery path it always was. Only `node`
186
+ * is distinguished here: `oam` is satisfied by a host that already is one, and
187
+ * an unrecognized value has been warned about and lands with auto, as it does
188
+ * in the discovery branch.
189
+ *
190
+ * `sandbox` is whether a spawn would carry flags only a fresh oam can apply;
191
+ * see ALREADY RUNNING ON OAM above for why that alone forces discovery. It does
192
+ * not guarantee a spawn: when discovery finds no launchable oam at the floor,
193
+ * auto still serves in-process, without --permission. The
194
+ * floor is OAM_MIN itself, not a parameter, so a host oam and a discovered one
195
+ * can never be held to different minimums.
196
+ *
197
+ * Pure on purpose: every input is passed in, so the whole decision is testable
198
+ * without booting a runtime.
199
+ */
200
+ function runtimePlan({ mode, hostOam, sandbox }) {
201
+ if (mode === "node") return "in-process";
202
+ if (sandbox) return "discover";
203
+ return atLeast(parseVersion(hostOam ?? ""), OAM_MIN) ? "in-process" : "discover";
204
+ }
205
+
146
206
  /**
147
207
  * The `--permission` grant list, or [] when the sandbox is not requested.
148
208
  *
@@ -280,7 +340,12 @@ if (requested && !RUNTIMES.includes(mode)) {
280
340
  );
281
341
  }
282
342
 
283
- if (mode === "node") {
343
+ // The sandbox is read off the grant list rather than TAILSCALE_MCP_SANDBOX, so
344
+ // "would the spawn carry --permission" cannot drift from what the spawn below
345
+ // actually passes.
346
+ const plan = runtimePlan({ mode, hostOam: process.versions.oam, sandbox: sandboxFlags().length > 0 });
347
+
348
+ if (plan === "in-process") {
284
349
  await runInProcess();
285
350
  } else {
286
351
  const oam = findOam();
package/dist/index.js CHANGED
@@ -34551,12 +34551,29 @@ function buildMetaTools(state) {
34551
34551
  }
34552
34552
 
34553
34553
  // src/index.ts
34554
- var version2 = true ? "0.19.1" : resolveVersionFallback();
34554
+ var version2 = true ? "0.19.3" : resolveVersionFallback();
34555
34555
  var subcommand = process.argv[2];
34556
+ var USAGE = `Usage: tailscale-mcp [command]
34557
+
34558
+ Commands:
34559
+ deploy-acl <path-to-acl.json> Deploy an ACL policy
34560
+ validate-acl <path-to-acl.json> Validate an ACL policy
34561
+ version Print the installed version
34562
+ help Print this message
34563
+
34564
+ Flags:
34565
+ --version, -V Print the installed version
34566
+ --help, -h Print this message
34567
+
34568
+ Run without a command to start the MCP server on stdio.`;
34556
34569
  var cliSubcommandHandled = false;
34557
34570
  if (subcommand === "deploy-acl" || subcommand === "validate-acl") {
34558
34571
  cliSubcommandHandled = true;
34559
34572
  const filePath = process.argv[3];
34573
+ if (filePath === "--help" || filePath === "-h") {
34574
+ console.log(`Usage: tailscale-mcp ${subcommand} <path-to-acl.json>`);
34575
+ process.exit(0);
34576
+ }
34560
34577
  if (!filePath) {
34561
34578
  console.error(`Usage: tailscale-mcp ${subcommand} <path-to-acl.json>`);
34562
34579
  process.exit(1);
@@ -34566,12 +34583,15 @@ if (subcommand === "deploy-acl" || subcommand === "validate-acl") {
34566
34583
  console.error(`Fatal: ${err instanceof Error ? err.message : err}`);
34567
34584
  process.exit(1);
34568
34585
  });
34569
- } else if (subcommand === "version" || subcommand === "--version") {
34586
+ } else if (subcommand === "version" || subcommand === "--version" || subcommand === "-V") {
34570
34587
  console.log(version2);
34571
34588
  process.exit(0);
34589
+ } else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
34590
+ console.log(USAGE);
34591
+ process.exit(0);
34572
34592
  } else if (subcommand !== void 0) {
34573
34593
  console.error(
34574
- `@yawlabs/tailscale-mcp: unrecognized argument "${subcommand}" -- known subcommands: deploy-acl, validate-acl, version. Starting the MCP server.`
34594
+ `@yawlabs/tailscale-mcp: unrecognized argument "${subcommand}" -- known subcommands: deploy-acl, validate-acl, version, help (or --help). Starting the MCP server.`
34575
34595
  );
34576
34596
  }
34577
34597
  if (!cliSubcommandHandled) {
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "mcpName": "io.github.YawLabs/tailscale-mcp",
5
- "description": "Tailscale MCP server for managing your tailnet from AI assistants",
5
+ "description": "Tailscale MCP server: admin-API tools for devices, ACLs, DNS, auth keys, users, and audit logs, plus a CLI to validate and deploy ACLs.",
6
6
  "license": "MIT",
7
7
  "author": "YawLabs <contact@yaw.sh>",
8
8
  "repository": {
@@ -11,11 +11,25 @@
11
11
  },
12
12
  "keywords": [
13
13
  "tailscale",
14
+ "tailnet",
15
+ "tailscale-api",
14
16
  "mcp",
15
17
  "model-context-protocol",
18
+ "mcp-server",
16
19
  "ai",
20
+ "vpn",
17
21
  "networking",
18
- "vpn"
22
+ "acl",
23
+ "dns",
24
+ "magicdns",
25
+ "auth-keys",
26
+ "oauth",
27
+ "webhooks",
28
+ "audit-log",
29
+ "gitops",
30
+ "claude-code",
31
+ "cursor",
32
+ "vscode"
19
33
  ],
20
34
  "type": "module",
21
35
  "main": "dist/index.js",
@@ -36,8 +50,8 @@
36
50
  "test": "npm run build && node --test-timeout=300000 --test \"dist/**/*.test.js\"",
37
51
  "test:ci": "npm run test",
38
52
  "test:coverage": "npm run build && node --test-timeout=300000 --experimental-test-coverage --test-coverage-exclude=\"**/*.test.js\" --test-coverage-exclude=\"**/dist/index.js\" --test \"dist/**/*.test.js\"",
39
- "lint": "biome check src/ bin/ scripts/ build.mjs",
40
- "lint:fix": "biome check --write src/ bin/ scripts/ build.mjs",
53
+ "lint": "node scripts/lint.mjs check src/ bin/ scripts/ build.mjs",
54
+ "lint:fix": "node scripts/lint.mjs check --write src/ bin/ scripts/ build.mjs",
41
55
  "check:oam": "oam check src/index.ts",
42
56
  "build:binary": "node scripts/build-binary.mjs",
43
57
  "build:binary:oam": "node scripts/build-binary-oam.mjs",
@@ -68,5 +82,6 @@
68
82
  "version": ">=22",
69
83
  "onFail": "error"
70
84
  }
71
- }
85
+ },
86
+ "homepage": "https://yaw.sh/mcp-servers/tailscale-mcp/"
72
87
  }