@yawlabs/tailscale-mcp 0.6.5 → 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 +92 -54
- package/dist/index.js +84 -61
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -3,30 +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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
16
|
+
## What's the point if the API already exists?
|
|
17
|
+
|
|
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
|
+
|
|
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).
|
|
24
55
|
|
|
25
56
|
## Quick start
|
|
26
57
|
|
|
27
58
|
**1. Set your API key**
|
|
28
59
|
|
|
29
|
-
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):
|
|
30
61
|
|
|
31
62
|
```bash
|
|
32
63
|
export TAILSCALE_API_KEY="tskey-api-..."
|
|
@@ -60,49 +91,64 @@ Windows:
|
|
|
60
91
|
}
|
|
61
92
|
```
|
|
62
93
|
|
|
63
|
-
> **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.
|
|
64
95
|
|
|
65
96
|
**3. Restart and approve**
|
|
66
97
|
|
|
67
98
|
Restart Claude Code (or your MCP client) and approve the Tailscale MCP server when prompted.
|
|
68
99
|
|
|
69
|
-
That's it. Now ask your
|
|
100
|
+
That's it. Now ask your agent:
|
|
70
101
|
|
|
71
|
-
> "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"
|
|
72
107
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
+
}
|
|
83
126
|
```
|
|
84
127
|
|
|
85
|
-
|
|
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.
|
|
86
130
|
|
|
87
|
-
|
|
131
|
+
Valid group names: `status`, `devices`, `acl`, `dns`, `keys`, `users`, `tailnet`, `webhooks`, `network-lock`, `posture`, `audit`, `invites`, `services`, `log-streaming`, `workload-identity`, `oauth-clients`.
|
|
88
132
|
|
|
89
|
-
|
|
133
|
+
The server logs the active filter to stderr on startup so you can confirm what got loaded:
|
|
90
134
|
|
|
91
|
-
|
|
135
|
+
```
|
|
136
|
+
@yawlabs/tailscale-mcp v0.8.0 ready (21 tools, groups=devices,acl,dns,audit, readonly)
|
|
137
|
+
```
|
|
92
138
|
|
|
93
139
|
## Authentication
|
|
94
140
|
|
|
95
|
-
**API key (
|
|
141
|
+
**API key (simplest):** Set `TAILSCALE_API_KEY` in your shell or MCP config.
|
|
96
142
|
|
|
97
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).
|
|
98
144
|
|
|
99
|
-
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.
|
|
100
146
|
|
|
101
147
|
**Tailnet:** Uses your default tailnet automatically. Set `TAILSCALE_TAILNET` to specify one explicitly.
|
|
102
148
|
|
|
103
149
|
## Resources (4)
|
|
104
150
|
|
|
105
|
-
MCP Resources expose read-only data
|
|
151
|
+
MCP Resources expose read-only data clients can browse without a tool call.
|
|
106
152
|
|
|
107
153
|
| Resource | URI | Description |
|
|
108
154
|
|----------|-----|-------------|
|
|
@@ -350,48 +396,40 @@ MCP Resources expose read-only data that clients can browse without tool calls.
|
|
|
350
396
|
|
|
351
397
|
## GitOps: deploy ACLs from CI
|
|
352
398
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
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:
|
|
356
400
|
|
|
357
401
|
```bash
|
|
358
402
|
npx @yawlabs/tailscale-mcp deploy-acl tailscale/acl.json
|
|
359
403
|
```
|
|
360
404
|
|
|
361
|
-
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.
|
|
362
406
|
|
|
363
|
-
**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:
|
|
364
408
|
|
|
365
|
-
```
|
|
366
409
|
> "Set aclsExternallyManagedOn to true and aclsExternalLink to our repo URL"
|
|
367
|
-
```
|
|
368
410
|
|
|
369
|
-
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.
|
|
370
412
|
|
|
371
413
|
## Requirements
|
|
372
414
|
|
|
373
|
-
- Node.js 18
|
|
415
|
+
- Node.js 18+
|
|
416
|
+
- A Tailscale API key or OAuth client credentials
|
|
374
417
|
|
|
375
418
|
## Contributing
|
|
376
419
|
|
|
377
|
-
Contributions
|
|
378
|
-
|
|
379
|
-
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.
|
|
380
421
|
|
|
381
422
|
```bash
|
|
382
423
|
git clone https://github.com/YawLabs/tailscale-mcp.git
|
|
383
424
|
cd tailscale-mcp
|
|
384
425
|
npm install
|
|
385
|
-
npm run
|
|
386
|
-
npm run lint
|
|
387
|
-
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)
|
|
388
430
|
```
|
|
389
431
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
```bash
|
|
393
|
-
node dist/index.js
|
|
394
|
-
```
|
|
432
|
+
For integration testing against your own tailnet: set `TAILSCALE_API_KEY` and run `node dist/index.js`.
|
|
395
433
|
|
|
396
434
|
## License
|
|
397
435
|
|
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
|
}
|
|
@@ -21207,6 +21216,25 @@ async function deployAcl(filePath) {
|
|
|
21207
21216
|
console.log("ACL deployed successfully");
|
|
21208
21217
|
}
|
|
21209
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
|
+
|
|
21210
21238
|
// src/tools/acl.ts
|
|
21211
21239
|
var aclTools = [
|
|
21212
21240
|
{
|
|
@@ -21622,10 +21650,7 @@ var deviceTools = [
|
|
|
21622
21650
|
tags: external_exports.array(external_exports.string()).describe("Full list of ACL tags (e.g. ['tag:server', 'tag:production']). Replaces all existing tags.")
|
|
21623
21651
|
}),
|
|
21624
21652
|
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
|
-
}
|
|
21653
|
+
validateTags(input.tags);
|
|
21629
21654
|
return apiPost(`/device/${encPath(input.deviceId)}/tags`, { tags: input.tags });
|
|
21630
21655
|
}
|
|
21631
21656
|
},
|
|
@@ -22136,7 +22161,7 @@ var keyTools = [
|
|
|
22136
22161
|
},
|
|
22137
22162
|
{
|
|
22138
22163
|
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.",
|
|
22164
|
+
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
22165
|
annotations: {
|
|
22141
22166
|
title: "Create key",
|
|
22142
22167
|
readOnlyHint: false,
|
|
@@ -22167,12 +22192,7 @@ var keyTools = [
|
|
|
22167
22192
|
customClaimRules: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("(federated only) Custom claim mapping rules")
|
|
22168
22193
|
}),
|
|
22169
22194
|
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
|
-
}
|
|
22195
|
+
validateTags(input.tags);
|
|
22176
22196
|
const keyType = input.keyType ?? "auth";
|
|
22177
22197
|
if (keyType !== "auth") {
|
|
22178
22198
|
const authOnlyFields = ["reusable", "ephemeral", "preauthorized", "expirySeconds"];
|
|
@@ -22252,12 +22272,7 @@ var keyTools = [
|
|
|
22252
22272
|
customClaimRules: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("(federated only) Updated custom claim rules")
|
|
22253
22273
|
}),
|
|
22254
22274
|
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
|
-
}
|
|
22275
|
+
validateTags(input.tags);
|
|
22261
22276
|
const body = {};
|
|
22262
22277
|
if (input.description !== void 0) body.description = sanitizeDescription(input.description);
|
|
22263
22278
|
if (input.scopes !== void 0) body.scopes = input.scopes;
|
|
@@ -22489,12 +22504,7 @@ var oauthClientTools = [
|
|
|
22489
22504
|
description: external_exports.string().optional().describe("Description for this OAuth client (max 50 chars, alphanumeric/hyphens/spaces)")
|
|
22490
22505
|
}),
|
|
22491
22506
|
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
|
-
}
|
|
22507
|
+
validateTags(input.tags);
|
|
22498
22508
|
const body = { ...input };
|
|
22499
22509
|
body.name = sanitizeDescription(input.name);
|
|
22500
22510
|
if (input.description !== void 0) body.description = sanitizeDescription(input.description);
|
|
@@ -22719,12 +22729,7 @@ var serviceTools = [
|
|
|
22719
22729
|
autoApproveHosts: external_exports.boolean().optional().describe("Whether to auto-approve devices that want to host this service")
|
|
22720
22730
|
}),
|
|
22721
22731
|
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
|
-
}
|
|
22732
|
+
validateTags(input.tags);
|
|
22728
22733
|
const { serviceName, ...body } = input;
|
|
22729
22734
|
const cleanBody = {};
|
|
22730
22735
|
for (const [key, value] of Object.entries(body)) {
|
|
@@ -23351,7 +23356,7 @@ var workloadIdentityTools = [
|
|
|
23351
23356
|
];
|
|
23352
23357
|
|
|
23353
23358
|
// src/index.ts
|
|
23354
|
-
var version2 = true ? "0.
|
|
23359
|
+
var version2 = true ? "0.8.0" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
23355
23360
|
var subcommand = process.argv[2];
|
|
23356
23361
|
if (subcommand === "deploy-acl") {
|
|
23357
23362
|
const filePath = process.argv[3];
|
|
@@ -23368,24 +23373,34 @@ if (subcommand === "deploy-acl") {
|
|
|
23368
23373
|
console.log(version2);
|
|
23369
23374
|
process.exit(0);
|
|
23370
23375
|
}
|
|
23371
|
-
var
|
|
23372
|
-
|
|
23373
|
-
|
|
23374
|
-
|
|
23375
|
-
|
|
23376
|
-
|
|
23377
|
-
|
|
23378
|
-
|
|
23379
|
-
|
|
23380
|
-
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
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
|
+
}
|
|
23389
23404
|
var server = new McpServer({
|
|
23390
23405
|
name: "@yawlabs/tailscale-mcp",
|
|
23391
23406
|
version: version2
|
|
@@ -23487,4 +23502,12 @@ server.resource(
|
|
|
23487
23502
|
);
|
|
23488
23503
|
var transport = new StdioServerTransport();
|
|
23489
23504
|
await server.connect(transport);
|
|
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
|
+
);
|
|
23490
23513
|
//# 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.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>",
|
|
@@ -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",
|