{
  "markdown": "# tailscale-mcp\n\nAn MCP server for [Tailscale](https://tailscale.com). The node it runs on is\ndriven through the `tailscale` command-line interface; the tailnet behind it is\ndriven through the control-plane REST API.\n\n[186 tools](docs/tools.md), one per verb, with real parameters and honest\nannotations. Tailscale's own JSON comes back unmodified, so anything you\nlearned from Tailscale's documentation still applies. When something fails you\nget a fixed error code and a hint, not a wall of standard error.\n\nBoth surfaces are optional and neither is fatal. No `tailscale` binary means the\ntools that drive this node are not offered; no control-plane credential means the\ntools that drive the tailnet are not. `tailscale-mcp diagnose` says which of the\ntwo this machine has.\n\nOn macOS that binary is the `tailscale` shim the Tailscale application installs\ninto `/usr/local/bin`. The executable inside the application bundle is not a\nsubstitute and is not accepted as one: run outside a login shell it starts the\nGUI rather than answering, so the local tools would be offered and then fail on\nevery call.\n\n## Install\n\n| Channel | How |\n|---|---|\n| npm | `npx -y @tailscale-mcp/tailscale-mcp` — downloads the release binary for your machine and refuses to run it unless the release's own `SHA256SUMS` vouches for it |\n| Container | `docker run -i --rm -e TAILSCALE_API_KEY ghcr.io/tailscale-mcp/tailscale-mcp` |\n| Homebrew | `brew trust tailscale-mcp/tap && brew install tailscale-mcp/tap/tailscale-mcp` — Homebrew 6 will not load a third-party tap until it is trusted, and reports that as an `Invalid formula` error once per platform it knows |\n| Bundle | Download the `.mcpb` for your platform from the [releases](https://github.com/tailscale-mcp/tailscale-mcp/releases) and open it — for MCP clients that install bundles, such as Claude Desktop |\n| From source | `cargo install tailscale-mcp` |\n\nRelease archives are on the\n[releases page](https://github.com/tailscale-mcp/tailscale-mcp/releases) with a\n`SHA256SUMS` beside them: macOS and Linux on `x86_64` and `arm64`, and Windows\non `x86_64`.\n\n## Point a client at it\n\n```sh\ntailscale-mcp setup claude-code\n```\n\nprints the snippet for your client — `claude-code`, `claude-desktop`, `vscode`,\n`cursor` or `zed` — and says where it goes. It writes nothing: the snippet is\nyours to paste, and it leaves the credential out, for the reason in the next\nsection.\n\nInstalled through npm there is nothing to install first, and the client can\ncarry the settings:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"tailscale\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@tailscale-mcp/tailscale-mcp\"],\n      \"env\": {\n        \"TAILSCALE_MCP_ALLOW_WRITE\": \"true\"\n      }\n    }\n  }\n}\n```\n\nThat much is enough to start it. Without a credential it offers the tools that\ndrive this node and hides the ones that act on the tailnet, and\n`tailscale-mcp diagnose` says which of the two you have.\n\n## Authenticate\n\n`setup` prints no credential of its own, because the file it prints into is one\npeople paste into issues and chats without rereading. Adding one is a separate,\ndeliberate step. Two shapes go in the `env` block above — an API access token:\n\n```jsonc\n\"env\": {\n  \"TAILSCALE_API_KEY\": \"tskey-api-…\",\n  \"TAILSCALE_MCP_ALLOW_WRITE\": \"true\"\n}\n```\n\nor an OAuth client:\n\n```jsonc\n\"env\": {\n  \"TAILSCALE_OAUTH_CLIENT_ID\": \"k123456CNTRL\",\n  \"TAILSCALE_OAUTH_CLIENT_SECRET\": \"tskey-client-…\",\n  \"TAILSCALE_MCP_ALLOW_WRITE\": \"true\"\n}\n```\n\nThe [credentials table](docs/configuration.md#credentials) has the other\nshapes, including the JWT file for federated identity. Every tool is offered\nwith either of these: the choice is not about what you can do with the server,\nwhich is why it is worth making on other grounds.\n\n**Prefer the OAuth client for anything you keep.** The reason is what a client\nconfiguration *is*: a file somebody writes once and then forgets, holding a\nsecret for as long as the tool is installed.\n\nAn API access token suits that badly on three counts. It belongs to a person\nand carries everything that person can do, so what leaks with the file is their\nwhole account rather than the tools you turned on. It expires — which in a file\nnobody has looked at since they wrote it does not present as an expired\ncredential, but as a server that has stopped working for no reason. And it is\nitself the bearer token, so the thing at rest is the thing that opens the door.\n\nAn OAuth client inverts each of those. It belongs to the tailnet, so it outlives\nwhoever set it up and is revoked without touching that person's access. Its\nscopes narrow it to what the toolsets you enabled actually call. It does not\nexpire on its own, and what sits in the file is not a key but the means of\nminting one — the token it hands this server lasts an hour, so a copy taken from\na backup or a screen share is worth very little by the time it is used.\n\nThe API access token is what the control plane offers you first, and is the\nquicker thing to try this out with. It is worth replacing once the file is one\nyou are going to keep.\n\n## Tiers and presets\n\nTwo independent dials decide what a session offers.\n\n**The tier** is how dangerous a tool is allowed to be. Read tools change nothing\nand are always offered. Write tools change configuration that can be changed\nback. Destructive tools remove something or expose something in a way that is\nnot simply undone — deleting a device, revoking a key, publishing a service to\nthe internet. Read is always on; write and destructive are off until they are\nturned on, and a tool above the permitted tier is *not listed*, not refused\nwhen called.\n\n**The preset** is how much surface area is offered at all. `minimal` is what an\nagent needs to answer questions and fix the common things; `core` adds the rest\nof everyday administration; `full` adds the tailnet-wide and irreversible\ncorners. Two toolsets are in no preset and must be asked for by name:\n`local-debug`, which is Tailscale's own diagnostic surface, and\n`local-passthrough`, which is one tool that runs an arbitrary `tailscale`\nsubcommand.\n\n| Preset | Read | With `--allow-write` | With `--allow-destructive` | Toolsets |\n|---|---|---|---|---|\n| `minimal` | 37 | 51 | 55 | 4 |\n| `core` (default) | 57 | 106 | 126 | 13 |\n| `full` | 68 | 126 | 155 | 18 |\n\nAdding `--toolsets +local-debug,+local-passthrough` to `full` reaches all 186.\n\nSome destructive tools ask for one more thing: a `confirm: true` argument. Those\nare the ones that affect the whole tailnet, or that can cut this server off from\nthe node or tailnet it is driving — logging the node out, deleting its own\ndevice, deploying a policy that locks the caller out. The server does not decide\nthey are wrong; it makes the caller say it meant them.\n\n- **[docs/tools.md](docs/tools.md)** — every tool, its tier and what it does.\n  Generated from the code.\n- **[docs/configuration.md](docs/configuration.md)** — every environment\n  variable and flag, with defaults.\n- **[docs/errors.md](docs/errors.md)** — every error code and what to do about\n  it.\n\n## Resources, prompts and completions\n\nNine resources — eight fixed and one template addressed by device identifier —\ngive a client the local node's status, preferences, netcheck report and tailnet\nlock state, and the tailnet's policy file, devices, DNS configuration and\nsettings. They are read-only, they appear only when their surface is on, and\nthere are no subscriptions.\n\nThree prompts steer a sequence of tool calls: `diagnose_connectivity`,\n`review_policy_change` and `audit_tailnet_access`. They follow the same rule as\nthe resources: each appears only when the surface it needs is on, and\n`diagnose_connectivity`, which reads from both, stops at the steps the session\ncan actually take.\n\nOf the four argument slots those two surfaces expose, three are completed, so\na client can offer the values rather than leave them to be guessed: the device\ntemplate's identifier, `diagnose_connectivity`'s peer, and\n`audit_tailnet_access`'s subject — which offers users, tags and devices, in\nthat order, so that a tailnet of thousands of devices still shows its handful\nof users within the hundred values the protocol sends. The fourth, `review_policy_change`'s goal, is\na sentence about intent, and completing it would be inventing one. Every value offered is one the server will\naccept back — a device is offered by its MagicDNS name, never by a hostname two\nmachines might share — and a source that cannot answer completes to nothing\nrather than to an error. The method is rate limited, as the specification asks,\nbecause each keystroke is a request.\n\nNote that the protocol completes a prompt argument and a resource template\nvariable, and nothing else: **tool arguments cannot be completed**, so this\nhelps the four slots above and none of the 186 tools. Client support is\nuneven — Claude Code completes resource templates, VS Code completes both, and\nseveral clients do neither — so this is an improvement where it is read and\ninert everywhere else.\n\n## Transports\n\nStdio by default. `--http` serves Streamable HTTP instead, on\n`127.0.0.1:8449`, behind a bearer token in `TAILSCALE_MCP_HTTP_TOKEN`, with\nhost and origin allow-lists, a body limit, a per-address rate limit and an open\nhealth endpoint. Binding anywhere but loopback needs either that token or\n`--http-no-auth` said out loud.\n\n## Security\n\n**What the tiers do.** A tool above the permitted tier is not in the tool list,\nso a caller cannot invoke it by guessing its name, and a model cannot be talked\ninto one that was never offered. The default is read-only. `confirm` on top of\nthe destructive tier is a second signal for the operations that are worth one.\nSecrets never reach an argument list, a log line or an error message; a minted\nkey or invite URL is returned once, verbatim, and nothing here keeps a copy.\nEvery tool result and every error goes through the same redaction on the way\nout.\n\n**What the tiers do not do.** They are not an authorization system, and they are\nnot a sandbox.\n\n- A tier is a property of the *server*, not of the caller. Everyone talking to\n  one server gets the same tools. If two callers should have different powers,\n  run two servers.\n- Permitting a tier permits every tool in it. There is no per-tool switch;\n  `--toolsets` is the coarser dial for narrowing what a session can reach.\n- The credential is the real boundary. A read-only OAuth client makes the\n  tailnet write tools fail at the control plane whatever this server offers,\n  and that is the boundary to lean on. Scope the credential, and use the tiers\n  to stop honest mistakes.\n- The local tools run as whoever runs this server. Tailscale's own operator\n  check applies — commands refused to a non-operator are refused here, with\n  `needs_operator` — but a server run as the operator can do what the operator\n  can do.\n- Read is not harmless. The read tier returns real network topology: node names,\n  addresses, users, policy. Anything that can read the tool output can read\n  that.\n\n**The server never escalates privileges.** It does not use `sudo`, does not ask\nfor elevation, and does not alter its own permissions or install anything. It\nruns the `tailscale` binary as the user it was started as and sends the\ncontrol-plane credential it was given. Where Tailscale refuses because of who is\nasking, that refusal is passed through rather than worked around. Four tools do\nwrite files: three of them — a certificate, a Taildrop delivery, a metrics file\n— to the path the call names, and `tailscale_configure_kubeconfig` to the\nkubeconfig the client itself would edit. All four write as the user running the\nserver, and so does `tailscale_run`, which is in no preset and, once asked for\nby name, can reach those same writes and anything else the `tailscale` binary\ndoes.\n\nOver HTTP the defaults are the cautious ones — loopback, a token required, no\nbrowser origin allowed — and each of those is widened by a flag, deliberately.\n\n## Compared with the other Tailscale MCP servers\n\nThis server is a strict superset of the three that came before it\n([rtailscale](https://github.com/dinglebear-ai/rtailscale),\n[HexSleeves/tailscale-mcp](https://github.com/HexSleeves/tailscale-mcp),\n[YawLabs/tailscale-mcp](https://github.com/YawLabs/tailscale-mcp)), with four\nexceptions listed below the table.\n\n| Capability | rtailscale | HexSleeves | YawLabs | this server |\n|---|---|---|---|---|\n| Devices: list, get | yes | list only | yes | yes, within `tailnet-devices` below |\n| Devices: authorize, delete, expire, rename, set IP, key expiry | authorize, delete | authorize, delete, expire | yes | yes, [15 tools](docs/tools.md#tailnet-devices) in all |\n| Device routes, tags | routes get | yes | yes | yes, within `tailnet-devices` below |\n| Posture attributes | — | — | yes | yes, within `tailnet-devices` below |\n| Policy file: get, set, preview, validate | get | get, set, validate | yes, with `If-Match` | yes, with the version identifier on every write |\n| DNS: nameservers, preferences, search paths, split DNS | get | yes | 11 tools | [11 tools](docs/tools.md#tailnet-dns) |\n| Auth keys | list | yes | yes | [5 tools](docs/tools.md#tailnet-keys) |\n| OAuth clients | — | — | 4 tools | [5 tools](docs/tools.md#tailnet-oauth-apps) |\n| Users | list | — | 7 tools | [7 tools](docs/tools.md#tailnet-users) |\n| Tailnet settings, contacts | — | partly | 5 tools | [5 tools](docs/tools.md#tailnet-settings) |\n| Webhooks | — | yes | 7 tools | [7 tools](docs/tools.md#tailnet-webhooks) |\n| Posture integrations | — | — | 5 tools | [5 tools](docs/tools.md#tailnet-posture) |\n| Audit and network flow logs, log streaming | — | — | 9 tools | [8 tools](docs/tools.md#tailnet-logging) |\n| Invites: device and user | — | — | 11 tools | [11 tools](docs/tools.md#tailnet-invites) |\n| Services | — | — | 7 tools | [7 tools](docs/tools.md#tailnet-services) |\n| Organization tailnets | — | — | 3 tools | [3 tools](docs/tools.md#tailnet-org) |\n| Local status, IP, whois, whoami, version | — | status, version | 4 tools | [25 tools](docs/tools.md#local-status) |\n| Ping, netcheck, routecheck, DNS query, exit nodes | — | ping, exit nodes | ping, netcheck | yes, within `local-status` above |\n| Preferences, up, down, login, logout, profiles | — | up, down | — | [8 tools](docs/tools.md#local-prefs) |\n| Serve and funnel | — | — | — | [10 tools](docs/tools.md#local-serve) |\n| Taildrive, file transfer, certificates, kubeconfig | — | — | — | [11 tools](docs/tools.md#local-files) |\n| Tailnet lock | — | stub | — | [8 tools](docs/tools.md#local-lock), plus status and log |\n| Tailscale's own debug surface | — | — | — | [30 tools](docs/tools.md#local-debug) |\n| Arbitrary `tailscale` subcommand | — | — | — | [1 tool](docs/tools.md#local-passthrough), off by default |\n| Resources | 1 | 4 | 4 | 9 |\n| Prompts | 1 | 2 | 0 | 3 |\n| Transports | stdio, HTTP | stdio, HTTP | stdio | stdio, HTTP |\n| Tool count | 1, with 10 actions | 19, one of them a stub | 102 | [186 tools](docs/tools.md) |\n\nOne row is lower than YawLabs' and no capability is missing behind it: two of\ntheir tools are aggregates over endpoints this server exposes one at a time —\none that reads both log types' stream configurations in a single call, and one\nthat authorizes a list of devices in a single call. Both are reachable here as\nrepeated calls to the per-item tool. They are counted in their column and not\nin this one, which is what makes the logging row read 9 against 8.\n\nFour things the others have and this does not, deliberately:\n\n1. **No configuration file.** rtailscale reads a `config.toml`. Everything here\n   is an environment variable or a flag, because that is what an MCP client\n   configuration can set, and a third source of truth is a third place for a\n   setting to hide.\n2. **No tool-schema resource.** rtailscale serves its own tool schema as a\n   resource. MCP already has `tools/list`; a second copy is one that can\n   disagree with the first.\n3. **No OAuth resource-server mode for browser clients**, in this release.\n   rtailscale can act as an OAuth resource server so a browser client can\n   authenticate to it. The HTTP transport here takes a bearer token. This is a\n   deferral, not a refusal.\n4. **No extra-enum environment knobs.** Several settings in the others accept\n   values outside the documented set, or are read from variables that are not\n   documented at all. Every setting here is in\n   [docs/configuration.md](docs/configuration.md), and a value outside the set\n   is a startup error naming the alternatives.\n\n## Development\n\n```sh\ncargo test --workspace --all-targets     # the whole suite, offline\ncargo clippy --workspace --all-targets -- -D warnings\ncargo fmt --all --check\n```\n\nThe suite needs no `tailscale` binary, no credential and no network: the\n`tailscale` binary is faked and the control plane is a fake HTTP server. The end-to-end\ntests that do want a real node and tailnet are gated on environment variables\nand report themselves skipped without them.\n\n`docs/tools.md` is generated. After adding or changing a tool:\n\n```sh\nUPDATE_DOCS=1 cargo test -p tailscale-mcp --test docs_are_current\n```\n\nArchitecture decisions are in [`docs/adr/`](docs/adr/), the vocabulary this\ncodebase holds itself to is in [`CONTEXT.md`](CONTEXT.md), every judgement call\nmade while building it is in [`DECISIONS.md`](DECISIONS.md), and how a release\nis made is in [`RELEASING.md`](RELEASING.md).\n\n## Licence\n\nApache-2.0.\n",
  "bytes": 17403,
  "sha": "b1ee50776d4c990bec8c7f4fb4ee90cd9788d9ca060c9b1052a13a472b9088c1",
  "repo_slug": "tailscale-mcp/tailscale-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_tailscale_mcp_tailscale_mcp_8bd0ee69/readme"
}