@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.
- package/README.md +88 -54
- package/dist/index.js +56 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,34 +3,61 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@yawlabs/tailscale-mcp)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
5
|
[](https://github.com/YawLabs/tailscale-mcp/stargazers)
|
|
6
|
-
[](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml) [](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml)
|
|
6
|
+
[](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml) [](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml) [](https://github.com/YawLabs/tailscale-mcp/actions/workflows/integration.yml)
|
|
7
7
|
|
|
8
|
-
**
|
|
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 [
|
|
10
|
+
Built and maintained by [Yaw Labs](https://yaw.sh).
|
|
11
11
|
|
|
12
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
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
|
-
##
|
|
16
|
+
## What's the point if the API already exists?
|
|
17
17
|
|
|
18
|
-
|
|
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
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
The server logs the active filter to stderr on startup so you can confirm what got loaded:
|
|
94
134
|
|
|
95
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
415
|
+
- Node.js 18+
|
|
416
|
+
- A Tailscale API key or OAuth client credentials
|
|
378
417
|
|
|
379
418
|
## Contributing
|
|
380
419
|
|
|
381
|
-
Contributions
|
|
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
|
|
390
|
-
npm run lint
|
|
391
|
-
npm
|
|
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
|
-
|
|
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.
|
|
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
|
|
23358
|
-
|
|
23359
|
-
|
|
23360
|
-
|
|
23361
|
-
|
|
23362
|
-
|
|
23363
|
-
|
|
23364
|
-
|
|
23365
|
-
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23370
|
-
|
|
23371
|
-
|
|
23372
|
-
|
|
23373
|
-
|
|
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
|
-
|
|
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
|