# Tailscale Mcp

> Tailscale MCP server for managing your tailnet from AI assistants

- **Type:** MCP server
- **Install:** `agentstack add mcp-yawlabs-tailscale-mcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [YawLabs](https://agentstack.voostack.com/s/yawlabs)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [YawLabs](https://github.com/YawLabs)
- **Source:** https://github.com/YawLabs/tailscale-mcp
- **Website:** https://yaw.sh/mcp-servers/tailscale-mcp/

## Install

```sh
agentstack add mcp-yawlabs-tailscale-mcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# @yawlabs/tailscale-mcp

[](https://www.npmjs.com/package/@yawlabs/tailscale-mcp)
[](https://opensource.org/licenses/MIT)
[](https://github.com/YawLabs/tailscale-mcp/stargazers)
[](./release.sh)
[](https://x.com/TokenLimitNews)

**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.

Built and maintained by [Yaw Labs](https://yaw.sh).

[](https://yaw.sh/mcp/install?name=Tailscale&command=npx&args=-y%2C%40yawlabs%2Ftailscale-mcp&description=Manage%20your%20Tailscale%20tailnet%20-%20devices%2C%20ACLs%2C%20DNS%2C%20keys&source=https%3A%2F%2Fgithub.com%2FYawLabs%2Ftailscale-mcp)

One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.

## What's the point if the API already exists?

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:

- **"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.
- **"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.
- **"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.
- **"Rotate every auth key older than 90 days and print the new ones"** — iterates, creates new keys with matching tags, revokes the old ones.
- **"Create an OAuth client for our CI pipeline scoped to `devices:read` and `dns`"** — creates a trust credential via `tailscale_create_key` with `keyType=client`, returns the credentials once (save them immediately).

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.

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.

## Why MCP vs. a skill or the `tailscale` CLI?

Reasonable question. Both have their place. Where this MCP is better:

- **Broad admin API coverage.** The `tailscale` CLI is scoped to the node it runs on. Admin concerns — ACLs, users, invites, webhooks, log streaming, posture integrations, auth keys, OAuth clients, and federated identities — live in the v2 HTTP API. You'd be shelling out to `curl` anyway.
- **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.
- **Cross-client, no user rewriting.** A Claude Code skill only loads in 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.
- **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.
- **Real tests.** 700+ unit tests covering every tool's input validation, API shape, and error handling. Plus an opt-in live-tailnet integration suite (`RUN_INTEGRATION_TESTS=1` + a tailnet API key) for shape-drift detection. Most skills are short markdown prompts without their own test layer — if the vendor changes output format, nothing catches it for you.

If you already have a skill that covers your 10% of Tailscale workflows, great — keep it. The MCP is for the other 90%.

## Trust signals

Fair critique from Reddit: a new repo claiming "actively maintained" with no visible tests is worth exactly zero trust. Here's what's actually verifiable:

- **700+ tests** (`node --test`) covering every tool's input validation, API shape, and error handling. Run `npm test` to see them pass locally.
- **Local release flow** via [`release.sh`](./release.sh): lint + test + bump + tag + push + npm publish + MCP Registry publish, all from the workstation. No CI workflow to babysit.
- **Dependabot alerts** surface on this repo and get fixed, not ignored.
- **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.

Issues and PRs are triaged. File one if something is off — [github.com/YawLabs/tailscale-mcp/issues](https://github.com/YawLabs/tailscale-mcp/issues).

## Quick start

**1. Set your API key**

Get an API key from [Tailscale Admin Console > Settings > Keys](https://console.tailscale.com/admin/settings/keys) and add it to your shell profile (`~/.bashrc`, `~/.zshrc`, or Windows system environment variables):

```bash
export TAILSCALE_API_KEY="tskey-api-..."
```

**2. Create `.mcp.json` in your project root**

macOS / Linux / WSL:

```json
{
  "mcpServers": {
    "tailscale": {
      "command": "npx",
      "args": ["-y", "@yawlabs/tailscale-mcp@latest"]
    }
  }
}
```

Windows:

```json
{
  "mcpServers": {
    "tailscale": {
      "command": "cmd",
      "args": ["/c", "npx", "-y", "@yawlabs/tailscale-mcp@latest"]
    }
  }
}
```

> **Why the extra step on Windows?** On Windows, `npx` is a `.cmd` file, and Node 20+ refuses to spawn `.cmd` files directly. Wrapping with `cmd /c` is the standard workaround.

**3. Restart and approve**

Restart Claude Code (or your MCP client) and approve the Tailscale MCP server when prompted.

That's it. Now ask your agent:

> "List my Tailscale devices that haven't been seen in the last 7 days"
>
> "Summarize every ACL change in the audit log from yesterday"
>
> "Draft an ACL rule that lets `tag:ci` reach `tag:registry` on port 5000 only"

## Too many tools? Subset them.

97 tools is a lot. If you've already got a dozen MCP servers and your client is feeling heavy, trim what this one exposes. Three knobs, combinable:

> The `env` blocks below show only the variable under discussion. Your credentials come from the environment, as set in [Quick start](#quick-start) — keep them in your shell profile rather than in the client's JSON config, which is world-readable on most systems and easy to commit by accident.

### Option 1: `TAILSCALE_PROFILE` (preset, easiest)

```json
{
  "env": {
    "TAILSCALE_PROFILE": "core"
  }
}
```

- **`minimal`** (20 tools) — `status`, `devices`, `audit`. Observe the tailnet, read the audit log.
- **`core`** (52 tools) — adds `acl`, `dns`, `keys`, `users`. The day-to-day admin surface.
- **`full`** (97 tools, default) — everything. Same as omitting the env var.

### Option 2: `TAILSCALE_TOOLS` (explicit group list)

```json
{
  "env": {
    "TAILSCALE_TOOLS": "devices,acl,dns,audit"
  }
}
```

Comma-separated group names. Overrides `TAILSCALE_PROFILE` when both are set — use this when the presets aren't quite right.

Valid group names: `status`, `devices`, `acl`, `dns`, `keys`, `users`, `tailnet`, `webhooks`, `posture`, `audit`, `invites`, `services`, `log-streaming`. The `local-cli` group is also available, but only when `TAILSCALE_LOCAL_CLI=1` is set — see [Local CLI integration](#local-cli-integration-opt-in).

### Option 3: `TAILSCALE_READONLY` (drop mutations)

```json
{
  "env": {
    "TAILSCALE_PROFILE": "core",
    "TAILSCALE_READONLY": "1"
  }
}
```

Set to `1` or `true` to drop every tool without `readOnlyHint: true`. Stacks with `TAILSCALE_PROFILE` or `TAILSCALE_TOOLS` as an intersection — combine for maximum minimalism.

### Confirming what loaded

The server logs the active filter to stderr on startup:

```
@yawlabs/tailscale-mcp v0.12.0 ready (20 tools, profile=minimal, readonly)
```

When both `TAILSCALE_PROFILE` and `TAILSCALE_TOOLS` are set, `TAILSCALE_TOOLS` wins. The banner marks the profile as overridden so the precedence is obvious at a glance — no need to guess which filter actually applied:

```
@yawlabs/tailscale-mcp v0.12.0 ready (22 tools, profile=core (overridden by TAILSCALE_TOOLS), groups=devices,acl)
```

The "(overridden)" marker only fires for substantive profiles (`minimal` / `core`); `profile=full` is a no-op preset, so it's shown without the marker when `TAILSCALE_TOOLS` is also set.

If you don't set any filter, startup prints a tip pointing you at the profiles.

### And how the *agent* knows

Everything above is stderr -- your MCP client's log. The model never sees it, so a
withheld tool and a tool that was never built look identical from the agent's side.
That is how an agent ends up working around a restriction instead of reporting it.

`tailscale_tool_groups` closes that gap. It is **always registered**, whatever the
filters say, and answers the question in-band:

```
> "Why can't you delete that device?"

  tailscale_tool_groups({ toolName: "tailscale_delete_device" })

  {
    "tool": "tailscale_delete_device",
    "available": false,
    "group": "devices",
    "kind": "write",
    "reason": "TAILSCALE_WRITE_GROUPS is set to \"dns\", which does not grant writes here",
    "toEnable": "add \"devices\" to TAILSCALE_WRITE_GROUPS (e.g. \"dns,devices\")"
  }
```

It separates the three cases an agent otherwise cannot tell apart:

| Case | What the agent should do |
|---|---|
| No such tool exists, under any configuration | Find another approach -- no setting will produce it |
| Exists, but its group is not loaded | Report `toEnable` to you; do not work around it |
| Exists and loaded, but writes are withheld there | Same -- the fix is yours, not a workaround |

Called with no arguments it lists every group with its availability and, for anything
withheld, the exact environment change that would restore it. It reads no network and
needs no credentials, so it works even when the server is misconfigured.

## Scoping writes to areas

`TAILSCALE_WRITE_GROUPS` names the areas an agent may **write** to. Everything else stays readable:

```json
{
  "env": {
    "TAILSCALE_WRITE_GROUPS": "devices,keys"
  }
}
```

That serves all 47 read tools plus the 18 writes in `devices` and `keys`, and withholds the other 38 writes — the ACL, DNS, users, webhooks, posture, services, invites, org-tailnets and log-streaming writes are simply not registered. Unset means no write gate, which is the shipped default.

> **Read this first: this filters the tool list, not your API token.** The server still holds one credential with full tailnet authority in every configuration. An agent that also has a shell can `curl api.tailscale.com` with that same token and do everything this knob withheld. Scope the Tailscale OAuth client itself to the areas you actually need — that bound survives outside this process; this one does not. `TAILSCALE_WRITE_GROUPS` is the low-friction complement to credential scoping, not a replacement for it.

### What a grant actually contains

Group names are the same ones `TAILSCALE_TOOLS` uses. Writes per group:

| Group | Writes | Group | Writes |
|---|---|---|---|
| `devices` | 13 | `webhooks` | 5 |
| `invites` | 7 | `posture` | 3 |
| `dns` | 6 | `services` | 3 |
| `keys` | 5 | `tailnet` | 3 |
| `users` | 5 | `log-streaming` | 3 |
| `org-tailnets` | 2 | `acl` | 1 |

`status`, `audit` and the opt-in `local-cli` group contain no writes at all, so granting them does nothing.

**`devices` is the widest grant, and the one most likely to be set.** In a compose file `write=devices` reads like "device admin", but it hands over `delete_device`, `set_devices_authorized`, `expire_device`, `deauthorize_device`, `set_device_routes`, `set_device_tags` and `update_device_key` alongside `rename_device`. There is no finer setting: an honest "safe subset" of `devices` is `rename_device` alone, and a knob whose useful value is one tool is not a knob.

### Three grants are tailnet-admin-equivalent

`keys`, `users` and `acl` are not blocked — CI key rotation legitimately needs `keys` — but grant them knowing:

- **`keys`** — `tailscale_create_key` mints an OAuth client with whatever scopes the caller asks for, including `acl`. That credential outlives the agent's session and is not subject to this or any other setting here.
- **`users`** — `tailscale_update_user_role` accepts `owner`.
- **`acl`** — `tailscale_update_acl` rewrites policy for every principal in the tailnet.

The server prints this on startup when your grant includes one of them.

### What it does and does not bound

It bounds **where** an agent may write. It does not bound **severity within** a granted area: inside a granted group, writes run unattended, including the grant-direction ones. Pair it with `TAILSCALE_REQUIRE_APPROVAL=1` when a human is at the keyboard — but understand that for an unattended agent that pairing contributes nothing, because a never-prompt client *denies* those calls rather than prompting.

### Precedence, and what happens when you get it wrong

| Situation | Result |
|---|---|
| Unset, empty, whitespace, or commas-only | No write gate. `-e VAR` with no value must not silently revoke every write. |
| `TAILSCALE_READONLY=1` also set | Readonly wins; banner says `readonly (TAILSCALE_WRITE_GROUPS ignored)`. |
| A name is misspelled (`devises`) | **Grants nothing** and names the typo. Unlike `TAILSCALE_TOOLS`, there is no fallback — a typo'd write grant that fell back would hand over all 56 writes at the moment you were restricting them. |
| Partly misspelled (`devices,dnss`) | Grants the valid half, warns about the rest. |
| A granted group is not loaded by `TAILSCALE_TOOLS` / `TAILSCALE_PROFILE` | The grant has no effect; a separate warning says so, since the name is not a typo. |
| `none`, `all`, `off`, `*` | Not reserved words — they are unknown group names, so they grant nothing. Use `TAILSCALE_READONLY=1` for no writes, or leave this unset for all writes. The server points you at the right spelling. |

Names are case-sensitive, matching `TAILSCALE_TOOLS`.

**The upgrade contract:** upgrading this package can never widen the set of *areas* an agent may write to. A new group is in nobody's grant until a human types its name. It can, however, add tools inside an area you already granted — a pinned test makes that a reviewed line in the diff rather than a silent change.

The startup banner shows what applied:

```
@yawlabs/tailscale-mcp v0.19.0 ready (59 tools, write=devices,keys)
```

## Requiring approval on irreversible tools

`readOnlyHint` / `destructiveHint` are *advisory* — the MCP spec says clients MUST treat annotations as untrusted, and most don't gate on them. `TAILSCALE_REQUIRE_APPROVAL=1` adds a stronger signal that supported clients enforce:

```json
{
  "env": {
    "TAILSCALE_REQUIRE_APPROVAL": "1"
  }
}
```

Nine tools are then advertised with `_meta["anthropic/requiresUserInteraction"]`, which forces a confirmation prompt even when an allow-rule would otherwise auto-approve the call:

| Tool | Why it's on the list |
|---|---|
| `tailscale_update_acl` | Can lock every device out of the tailnet; the previous HuJSON (comments included) is gone unless you captured it |
| `tailscale_delete_device` | The device must re-enroll |
| `tailscale_delete_user` | No undelete |
| `tailscale_dele

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [YawLabs](https://github.com/YawLabs)
- **Source:** [YawLabs/tailscale-mcp](https://github.com/YawLabs/tailscale-mcp)
- **License:** MIT
- **Homepage:** https://yaw.sh/mcp-servers/tailscale-mcp/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-yawlabs-tailscale-mcp
- Seller: https://agentstack.voostack.com/s/yawlabs
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
