@yawlabs/tailscale-mcp 0.7.0 → 0.8.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 +88 -54
  2. package/dist/index.js +56 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -3,34 +3,61 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@yawlabs/tailscale-mcp)](https://www.npmjs.com/package/@yawlabs/tailscale-mcp)
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
- [![CI](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml) [![Release](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml/badge.svg)](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml)
6
+ [![CI](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml) [![Release](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml/badge.svg)](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml) [![Integration](https://github.com/YawLabs/tailscale-mcp/actions/workflows/integration.yml/badge.svg)](https://github.com/YawLabs/tailscale-mcp/actions/workflows/integration.yml)
7
7
 
8
- **Manage your Tailscale tailnet from Claude Code, Cursor, and any MCP client.** 99 tools + 4 resources. One env var. Works on first try.
8
+ **Ask your agent questions about your tailnet and have it act on the answers.** 99 tools + 4 resources covering the full [Tailscale v2 API](https://tailscale.com/api). Backed by 727 tests and a nightly integration run against a real tailnet.
9
9
 
10
- Built and maintained by [YawLabs](https://yaw.sh).
10
+ Built and maintained by [Yaw Labs](https://yaw.sh).
11
11
 
12
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
13
 
14
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
15
 
16
- ## Why this one?
16
+ ## What's the point if the API already exists?
17
17
 
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.
18
+ You could `curl` the Tailscale API. The point isn't replacing `curl` it's letting an agent compose multi-endpoint workflows in one turn without writing a script:
19
19
 
20
- - **Preserves ACL formatting** reads and writes HuJSON (comments, trailing commas, indentation). Others compact your carefully formatted policy into a single line.
21
- - **Safe ACL updates** — uses ETags to prevent overwriting concurrent changes. No silent data loss.
22
- - **Tool annotations** every tool declares `readOnlyHint`, `destructiveHint`, and `idempotentHint`, so MCP clients skip confirmation dialogs for safe operations.
23
- - **MCP Resources** — exposes tailnet status, device list, ACL policy, and DNS config as browsable resources.
24
- - **Instant startup** ships as a single self-contained bundle with zero runtime dependencies. `npx` downloads ~150 KB and starts immediately no 5-minute `node_modules` installs.
25
- - **Zero restarts** — the server always starts, even with missing credentials. Auth errors surface as clear tool-call errors, not silent crashes that force you to restart your AI assistant.
26
- - **One env var setup** no config files, no setup wizards, no multi-step flows.
27
- - **Every tool verified** — no placeholder endpoints that 404. If it's in the tool list, it works.
20
+ - **"Which devices haven't checked in for 30 days and have key expiry disabled?"** — lists devices, filters by `lastSeen`, filters by `keyExpiryDisabled`, returns a table. Three endpoints, one question.
21
+ - **"Someone broke DNS at 2am — who changed what in the last 24 hours?"** — pulls the audit log, filters by DNS-related actors and endpoints, reads each change's before/after, summarizes in English.
22
+ - **"Draft an ACL change that lets `tag:mobile` reach `tag:dashboard` but not `tag:db`, preserving my comments"** reads the current HuJSON, proposes a minimal diff, validates it against the API, returns the diff for you to apply.
23
+ - **"Show me the OIDC workload identity for our GitHub Actions and confirm its allowed subjects still match `repo:Acme/*`"** — fetches the workload identity, parses the JWT claim patterns, tells you whether the claim still matches your repo naming.
24
+ - **"Rotate every auth key older than 90 days and print the new ones"** iterates, creates new keys with matching tags, revokes the old ones.
25
+
26
+ A curl can do each step. The agent composes them. That's where the lift is, and that's what the tool surface is designed for — every read endpoint is first-class so the agent can synthesize, and every write endpoint is tagged `destructiveHint` or `idempotentHint` so your MCP client can gate mutations the way you configured it.
27
+
28
+ If all you need is one endpoint in a CI job, use `curl` — we even have a [CLI subcommand](#gitops-deploy-acls-from-ci) for the common ACL-from-git case. The MCP is for the interactive, exploratory, "I don't know what I need yet" work.
29
+
30
+ ## Why MCP vs. a skill or the `tailscale` CLI?
31
+
32
+ Reasonable question. Both have their place. Where this MCP is better:
33
+
34
+ - **Full admin API coverage.** The `tailscale` CLI is scoped to the node it runs on. Admin concerns — ACLs, users, invites, webhooks, log streaming, workload identity, OAuth clients, posture — live in the v2 HTTP API. You'd be shelling out to `curl` anyway.
35
+ - **Typed tool surface, not string parsing.** Every tool has a Zod-validated input schema and a structured response. No brittle `tailscale status --json | jq` pipelines that break when the schema evolves.
36
+ - **Cross-client, no user rewriting.** A Claude Code skill is tied to Claude Code. An MCP server works in Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, and anything else that speaks MCP. Version bumps ship through `npx` — users don't re-author their skill when Tailscale adds an endpoint.
37
+ - **Safe-by-default writes.** Every tool declares `readOnlyHint` / `destructiveHint` / `idempotentHint` so clients can skip confirmation on reads and require it on mutations. A skill that shells out to the CLI can't express that.
38
+ - **Real tests.** 727 unit tests + an integration suite hitting a live tailnet on every tag. A skill is usually 50 lines of README without tests, and if the vendor changes output format nothing catches it.
39
+
40
+ If you already have a skill that covers your 10% of Tailscale workflows, great — keep it. The MCP is for the other 90%.
41
+
42
+ ## Trust signals
43
+
44
+ Fair critique from Reddit: a week-old repo claiming "actively maintained" with no visible tests is worth exactly zero trust. Here's what's actually verifiable:
45
+
46
+ - **727 tests** (178 suites, `node --test`) covering every tool's input validation, API shape, and error handling. Run `npm test` to see them pass locally.
47
+ - **3 CI workflows** on GitHub Actions:
48
+ - [`ci.yml`](.github/workflows/ci.yml) — lint + typecheck + build + unit tests on every push and PR.
49
+ - [`integration.yml`](.github/workflows/integration.yml) — runs the full tool surface against a real tailnet.
50
+ - [`release.yml`](.github/workflows/release.yml) — publishes to npm from a signed tag.
51
+ - **Dependabot alerts** surface on this repo and get fixed, not ignored.
52
+ - **Every tool verified against the live API.** If it's in the tool list, it calls a real endpoint that exists in the current v2 API. No placeholder 404 tools.
53
+
54
+ Issues and PRs are triaged. File one if something is off — [github.com/YawLabs/tailscale-mcp/issues](https://github.com/YawLabs/tailscale-mcp/issues).
28
55
 
29
56
  ## Quick start
30
57
 
31
58
  **1. Set your API key**
32
59
 
33
- Get an API key from [Tailscale Admin Console > Settings > Keys](https://login.tailscale.com/admin/settings/keys) and add it to your shell profile (`~/.bashrc`, `~/.zshrc`, or system environment variables):
60
+ Get an API key from [Tailscale Admin Console > Settings > Keys](https://login.tailscale.com/admin/settings/keys) and add it to your shell profile (`~/.bashrc`, `~/.zshrc`, or Windows system environment variables):
34
61
 
35
62
  ```bash
36
63
  export TAILSCALE_API_KEY="tskey-api-..."
@@ -64,49 +91,64 @@ Windows:
64
91
  }
65
92
  ```
66
93
 
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.
94
+ > **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.
68
95
 
69
96
  **3. Restart and approve**
70
97
 
71
98
  Restart Claude Code (or your MCP client) and approve the Tailscale MCP server when prompted.
72
99
 
73
- That's it. Now ask your AI assistant:
100
+ That's it. Now ask your agent:
74
101
 
75
- > "List my Tailscale devices"
102
+ > "List my Tailscale devices that haven't been seen in the last 7 days"
103
+ >
104
+ > "Summarize every ACL change in the audit log from yesterday"
105
+ >
106
+ > "Draft an ACL rule that lets `tag:ci` reach `tag:registry` on port 5000 only"
76
107
 
77
- ```
78
- ┌────────────┬─────────┬────────────────┬──────────────────────┐
79
- │ Hostname │ OS │ Tailscale IP │ Last Seen │
80
- ���────────────┼─────────┼────────────────┼──────────────────────┤
81
- │ web-prod │ Linux │ 100.x.x.1 │ 2026-04-03 21:09 UTC │
82
- ├────────────┼─────────┼���───────────────┼──────────────────────┤
83
- │ db-staging │ Linux │ 100.x.x.2 │ 2026-04-03 21:09 UTC ��
84
- ���────────────┼──────���──┼────────────────┼──────────────────────┤
85
- │ dev-laptop │ macOS │ 100.x.x.3 │ 2026-04-03 21:09 UTC │
86
- └────────────┴─────────┴──���─────────────┴──────────────────────┘
108
+ ## Too many tools? Subset them.
109
+
110
+ 99 tools is a lot. If you've already got a dozen MCP servers and your client is feeling heavy, trim what this one exposes:
111
+
112
+ ```json
113
+ {
114
+ "mcpServers": {
115
+ "tailscale": {
116
+ "command": "npx",
117
+ "args": ["-y", "@yawlabs/tailscale-mcp"],
118
+ "env": {
119
+ "TAILSCALE_API_KEY": "tskey-api-...",
120
+ "TAILSCALE_TOOLS": "devices,acl,dns,audit",
121
+ "TAILSCALE_READONLY": "1"
122
+ }
123
+ }
124
+ }
125
+ }
87
126
  ```
88
127
 
89
- > "Show me the ACL policy"
128
+ - **`TAILSCALE_TOOLS`** comma-separated list of tool groups to expose. Omit for all groups.
129
+ - **`TAILSCALE_READONLY`** — set to `1` or `true` to drop every mutating tool (only tools with `readOnlyHint: true` remain). Combine with `TAILSCALE_TOOLS` for maximum minimalism.
90
130
 
91
- Returns your full policy with formatting, comments, and structure intact plus an ETag for safe updates.
131
+ Valid group names: `status`, `devices`, `acl`, `dns`, `keys`, `users`, `tailnet`, `webhooks`, `network-lock`, `posture`, `audit`, `invites`, `services`, `log-streaming`, `workload-identity`, `oauth-clients`.
92
132
 
93
- > "Who changed the DNS settings yesterday?"
133
+ The server logs the active filter to stderr on startup so you can confirm what got loaded:
94
134
 
95
- Pulls the audit log so you can see exactly who did what and when.
135
+ ```
136
+ @yawlabs/tailscale-mcp v0.8.0 ready (21 tools, groups=devices,acl,dns,audit, readonly)
137
+ ```
96
138
 
97
139
  ## Authentication
98
140
 
99
- **API key (recommended):** Set `TAILSCALE_API_KEY` in your shell profile. Simplest option, works immediately. You can also pass it inline via the `"env"` field in your MCP config if you prefer a self-contained setup.
141
+ **API key (simplest):** Set `TAILSCALE_API_KEY` in your shell or MCP config.
100
142
 
101
143
  **OAuth (scoped access):** For fine-grained permissions, set `TAILSCALE_OAUTH_CLIENT_ID` and `TAILSCALE_OAUTH_CLIENT_SECRET` instead. Create an OAuth client at [Tailscale Admin Console > Settings > OAuth](https://login.tailscale.com/admin/settings/oauth).
102
144
 
103
- The server checks for an API key first, then falls back to OAuth. If neither is set, tools return a clear error telling you what to configure.
145
+ The server checks for an API key first, then falls back to OAuth. If neither is set, tools return a clear error telling you what to configure — the server still starts, so your MCP client doesn't loop restarting.
104
146
 
105
147
  **Tailnet:** Uses your default tailnet automatically. Set `TAILSCALE_TAILNET` to specify one explicitly.
106
148
 
107
149
  ## Resources (4)
108
150
 
109
- MCP Resources expose read-only data that clients can browse without tool calls.
151
+ MCP Resources expose read-only data clients can browse without a tool call.
110
152
 
111
153
  | Resource | URI | Description |
112
154
  |----------|-----|-------------|
@@ -354,48 +396,40 @@ MCP Resources expose read-only data that clients can browse without tool calls.
354
396
 
355
397
  ## GitOps: deploy ACLs from CI
356
398
 
357
- The recommended workflow for ACL management is to keep your policy in git and deploy it automatically on merge. This gives you code review, history, and no accidental overwrites from stale browser tabs.
358
-
359
- The `deploy-acl` CLI subcommand handles everything — ETag fetching, validation, and deployment — in a single command:
399
+ For the simple "deploy ACL from git on merge" workflow, you don't need an MCP server or an agent use the built-in CLI:
360
400
 
361
401
  ```bash
362
402
  npx @yawlabs/tailscale-mcp deploy-acl tailscale/acl.json
363
403
  ```
364
404
 
365
- Works with any CI system just set `TAILSCALE_API_KEY` and `TAILSCALE_TAILNET` as env vars.
405
+ Handles ETag fetching, validation, and deployment in one command. Works in any CI system. Set `TAILSCALE_API_KEY` and `TAILSCALE_TAILNET` as env vars.
366
406
 
367
- **Optional:** Lock the Admin Console to prevent manual edits that drift from git:
407
+ **Optional:** Lock the Admin Console to prevent manual edits that drift from git. Ask your agent:
368
408
 
369
- ```
370
409
  > "Set aclsExternallyManagedOn to true and aclsExternalLink to our repo URL"
371
- ```
372
410
 
373
- This shows a read-only banner in the Tailscale Admin Console pointing to your repo. Use the MCP for reads and one-off operations (audit logs, device management, investigations), and let CI handle ACL deployments.
411
+ This shows a read-only banner in the Tailscale Admin Console pointing to your repo. Use the MCP for reads and investigations, and let CI handle the deploy.
374
412
 
375
413
  ## Requirements
376
414
 
377
- - Node.js 18 or higher
415
+ - Node.js 18+
416
+ - A Tailscale API key or OAuth client credentials
378
417
 
379
418
  ## Contributing
380
419
 
381
- Contributions are welcome. Please [open an issue](https://github.com/YawLabs/tailscale-mcp/issues) to discuss what you'd like to change before submitting a PR.
382
-
383
- To develop locally:
420
+ Contributions welcome. Please [open an issue](https://github.com/YawLabs/tailscale-mcp/issues) to discuss before a PR for anything beyond a typo fix.
384
421
 
385
422
  ```bash
386
423
  git clone https://github.com/YawLabs/tailscale-mcp.git
387
424
  cd tailscale-mcp
388
425
  npm install
389
- npm run build
390
- npm run lint
391
- npm test
426
+ npm run lint # Biome check
427
+ npm run lint:fix # Auto-fix
428
+ npm run build # tsc + esbuild bundle
429
+ npm test # node --test (727 tests)
392
430
  ```
393
431
 
394
- Test against your own tailnet by setting `TAILSCALE_API_KEY` and running:
395
-
396
- ```bash
397
- node dist/index.js
398
- ```
432
+ For integration testing against your own tailnet: set `TAILSCALE_API_KEY` and run `node dist/index.js`.
399
433
 
400
434
  ## License
401
435
 
package/dist/index.js CHANGED
@@ -21216,6 +21216,25 @@ async function deployAcl(filePath) {
21216
21216
  console.log("ACL deployed successfully");
21217
21217
  }
21218
21218
 
21219
+ // src/filter.ts
21220
+ function filterTools(groups, options) {
21221
+ const enabledGroups = options.tools ? new Set(
21222
+ options.tools.split(",").map((s) => s.trim()).filter(Boolean)
21223
+ ) : null;
21224
+ const readonly2 = options.readonly === "1" || options.readonly === "true";
21225
+ const validNames = new Set(Object.keys(groups));
21226
+ const unknownGroups2 = enabledGroups ? [...enabledGroups].filter((g) => !validNames.has(g)) : [];
21227
+ const out = [];
21228
+ for (const [name, tools] of Object.entries(groups)) {
21229
+ if (enabledGroups && !enabledGroups.has(name)) continue;
21230
+ for (const t of tools) {
21231
+ if (readonly2 && t.annotations.readOnlyHint !== true) continue;
21232
+ out.push(t);
21233
+ }
21234
+ }
21235
+ return { tools: out, unknownGroups: unknownGroups2 };
21236
+ }
21237
+
21219
21238
  // src/tools/acl.ts
21220
21239
  var aclTools = [
21221
21240
  {
@@ -23337,7 +23356,7 @@ var workloadIdentityTools = [
23337
23356
  ];
23338
23357
 
23339
23358
  // src/index.ts
23340
- var version2 = true ? "0.7.0" : (await null).createRequire(import.meta.url)("../package.json").version;
23359
+ var version2 = true ? "0.8.0" : (await null).createRequire(import.meta.url)("../package.json").version;
23341
23360
  var subcommand = process.argv[2];
23342
23361
  if (subcommand === "deploy-acl") {
23343
23362
  const filePath = process.argv[3];
@@ -23354,24 +23373,34 @@ if (subcommand === "deploy-acl") {
23354
23373
  console.log(version2);
23355
23374
  process.exit(0);
23356
23375
  }
23357
- var allTools = [
23358
- ...statusTools,
23359
- ...deviceTools,
23360
- ...aclTools,
23361
- ...dnsTools,
23362
- ...keyTools,
23363
- ...userTools,
23364
- ...tailnetTools,
23365
- ...webhookTools,
23366
- ...networkLockTools,
23367
- ...postureTools,
23368
- ...auditTools,
23369
- ...inviteTools,
23370
- ...serviceTools,
23371
- ...logStreamingTools,
23372
- ...workloadIdentityTools,
23373
- ...oauthClientTools
23374
- ];
23376
+ var toolGroups = {
23377
+ status: statusTools,
23378
+ devices: deviceTools,
23379
+ acl: aclTools,
23380
+ dns: dnsTools,
23381
+ keys: keyTools,
23382
+ users: userTools,
23383
+ tailnet: tailnetTools,
23384
+ webhooks: webhookTools,
23385
+ "network-lock": networkLockTools,
23386
+ posture: postureTools,
23387
+ audit: auditTools,
23388
+ invites: inviteTools,
23389
+ services: serviceTools,
23390
+ "log-streaming": logStreamingTools,
23391
+ "workload-identity": workloadIdentityTools,
23392
+ "oauth-clients": oauthClientTools
23393
+ };
23394
+ var { tools: allTools, unknownGroups } = filterTools(toolGroups, {
23395
+ tools: process.env.TAILSCALE_TOOLS,
23396
+ readonly: process.env.TAILSCALE_READONLY
23397
+ });
23398
+ if (unknownGroups.length > 0) {
23399
+ const validNames = Object.keys(toolGroups);
23400
+ console.error(
23401
+ `@yawlabs/tailscale-mcp: TAILSCALE_TOOLS includes unknown group(s): ${unknownGroups.join(", ")}. Valid groups: ${validNames.join(", ")}`
23402
+ );
23403
+ }
23375
23404
  var server = new McpServer({
23376
23405
  name: "@yawlabs/tailscale-mcp",
23377
23406
  version: version2
@@ -23473,5 +23502,12 @@ server.resource(
23473
23502
  );
23474
23503
  var transport = new StdioServerTransport();
23475
23504
  await server.connect(transport);
23476
- console.error(`@yawlabs/tailscale-mcp v${version2} ready (${allTools.length} tools)`);
23505
+ var readonlyMode = process.env.TAILSCALE_READONLY === "1" || process.env.TAILSCALE_READONLY === "true";
23506
+ var filterSuffix = [
23507
+ process.env.TAILSCALE_TOOLS ? `groups=${process.env.TAILSCALE_TOOLS}` : null,
23508
+ readonlyMode ? "readonly" : null
23509
+ ].filter(Boolean).join(", ");
23510
+ console.error(
23511
+ `@yawlabs/tailscale-mcp v${version2} ready (${allTools.length} tools${filterSuffix ? `, ${filterSuffix}` : ""})`
23512
+ );
23477
23513
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.8.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>",