@yawlabs/tailscale-mcp 0.1.1

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +218 -0
  3. package/dist/api.d.ts +30 -0
  4. package/dist/api.js +108 -0
  5. package/dist/api.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +70 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/tools/acl.d.ts +58 -0
  10. package/dist/tools/acl.js +72 -0
  11. package/dist/tools/acl.js.map +1 -0
  12. package/dist/tools/audit.d.ts +19 -0
  13. package/dist/tools/audit.js +24 -0
  14. package/dist/tools/audit.js.map +1 -0
  15. package/dist/tools/devices.d.ts +144 -0
  16. package/dist/tools/devices.js +112 -0
  17. package/dist/tools/devices.js.map +1 -0
  18. package/dist/tools/dns.d.ts +74 -0
  19. package/dist/tools/dns.js +83 -0
  20. package/dist/tools/dns.js.map +1 -0
  21. package/dist/tools/keys.d.ts +66 -0
  22. package/dist/tools/keys.js +64 -0
  23. package/dist/tools/keys.js.map +1 -0
  24. package/dist/tools/network-lock.d.ts +7 -0
  25. package/dist/tools/network-lock.js +13 -0
  26. package/dist/tools/network-lock.js.map +1 -0
  27. package/dist/tools/posture.d.ts +62 -0
  28. package/dist/tools/posture.js +47 -0
  29. package/dist/tools/posture.js.map +1 -0
  30. package/dist/tools/status.d.ts +18 -0
  31. package/dist/tools/status.js +28 -0
  32. package/dist/tools/status.js.map +1 -0
  33. package/dist/tools/tailnet.d.ts +85 -0
  34. package/dist/tools/tailnet.js +48 -0
  35. package/dist/tools/tailnet.js.map +1 -0
  36. package/dist/tools/users.d.ts +20 -0
  37. package/dist/tools/users.js +23 -0
  38. package/dist/tools/users.js.map +1 -0
  39. package/dist/tools/webhooks.d.ts +50 -0
  40. package/dist/tools/webhooks.js +49 -0
  41. package/dist/tools/webhooks.js.map +1 -0
  42. package/package.json +45 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YawLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # @yawlabs/tailscale-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@yawlabs/tailscale-mcp)](https://www.npmjs.com/package/@yawlabs/tailscale-mcp)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+ [![GitHub stars](https://img.shields.io/github/stars/YawLabs/tailscale-mcp)](https://github.com/YawLabs/tailscale-mcp/stargazers)
6
+
7
+ **Manage your Tailscale tailnet from Claude Code, Cursor, and any MCP client.** 43 tools. One env var. Works on first try.
8
+
9
+ Built and maintained by [YawLabs](https://yaw.sh).
10
+
11
+ ## Why this one?
12
+
13
+ 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 — every single endpoint.
14
+
15
+ - **Preserves ACL formatting** — reads and writes HuJSON (comments, trailing commas, indentation). Others compact your carefully formatted policy into a single line.
16
+ - **Safe ACL updates** — uses ETags to prevent overwriting concurrent changes. No silent data loss.
17
+ - **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.
18
+ - **One env var setup** — no config files, no setup wizards, no multi-step flows.
19
+ - **Every tool verified** — no placeholder endpoints that 404. If it's in the tool list, it works.
20
+
21
+ ## Quick start
22
+
23
+ Add to your MCP client config:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "tailscale": {
29
+ "command": "npx",
30
+ "args": ["-y", "@yawlabs/tailscale-mcp"],
31
+ "env": {
32
+ "TAILSCALE_API_KEY": "tskey-api-..."
33
+ }
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ Get your API key from [Tailscale Admin Console > Settings > Keys](https://login.tailscale.com/admin/settings/keys).
40
+
41
+ That's it. Now ask your AI assistant:
42
+
43
+ > "List my Tailscale devices"
44
+
45
+ ```
46
+ ┌────────────┬─────────┬────────────────┬──────────────────────┐
47
+ │ Hostname │ OS │ Tailscale IP │ Last Seen │
48
+ ├────────────┼─────────┼────────────────┼──────────────────────┤
49
+ │ web-prod │ Linux │ 100.x.x.1 │ 2026-04-03 21:09 UTC │
50
+ ├────────────┼─────────┼────────────────┼──────────────────────┤
51
+ │ db-staging │ Linux │ 100.x.x.2 │ 2026-04-03 21:09 UTC │
52
+ ├────────────┼─────────┼────────────────┼──────────────────────┤
53
+ │ dev-laptop │ macOS │ 100.x.x.3 │ 2026-04-03 21:09 UTC │
54
+ └────────────┴─────────┴────────────────┴──────────────────────┘
55
+ ```
56
+
57
+ > "Show me the ACL policy"
58
+
59
+ Returns your full policy with formatting, comments, and structure intact — plus an ETag for safe updates.
60
+
61
+ > "Who changed the DNS settings yesterday?"
62
+
63
+ Pulls the audit log so you can see exactly who did what and when.
64
+
65
+ ## Authentication
66
+
67
+ **API key (recommended):** Set `TAILSCALE_API_KEY`. Simplest option, works immediately.
68
+
69
+ **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).
70
+
71
+ 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.
72
+
73
+ **Tailnet:** Uses your default tailnet automatically. Set `TAILSCALE_TAILNET` to specify one explicitly.
74
+
75
+ ## Tools (43)
76
+
77
+ ### Status
78
+
79
+ | Tool | Description |
80
+ |------|-------------|
81
+ | `tailscale_status` | Verify API connection, see tailnet info and device count |
82
+
83
+ <details>
84
+ <summary><strong>Devices</strong> (10 tools)</summary>
85
+
86
+ | Tool | Description |
87
+ |------|-------------|
88
+ | `tailscale_list_devices` | List all devices with status, IPs, OS, and last seen |
89
+ | `tailscale_get_device` | Get detailed info for a specific device |
90
+ | `tailscale_authorize_device` | Authorize a pending device |
91
+ | `tailscale_deauthorize_device` | Deauthorize a device |
92
+ | `tailscale_delete_device` | Remove a device from the tailnet |
93
+ | `tailscale_rename_device` | Rename a device |
94
+ | `tailscale_expire_device` | Expire a device's key, forcing re-authentication |
95
+ | `tailscale_get_device_routes` | Get advertised and enabled subnet routes |
96
+ | `tailscale_set_device_routes` | Enable or disable subnet routes |
97
+ | `tailscale_set_device_tags` | Set ACL tags on a device |
98
+
99
+ </details>
100
+
101
+ <details>
102
+ <summary><strong>ACL / Policy</strong> (4 tools) — with HuJSON formatting preservation and ETag safety</summary>
103
+
104
+ | Tool | Description |
105
+ |------|-------------|
106
+ | `tailscale_get_acl` | Get ACL policy with formatting preserved (HuJSON) + ETag |
107
+ | `tailscale_update_acl` | Update ACL policy (requires ETag for safe concurrent edits) |
108
+ | `tailscale_validate_acl` | Validate a policy without applying it |
109
+ | `tailscale_preview_acl` | Preview rules that would apply to a user or IP |
110
+
111
+ </details>
112
+
113
+ <details>
114
+ <summary><strong>DNS</strong> (8 tools)</summary>
115
+
116
+ | Tool | Description |
117
+ |------|-------------|
118
+ | `tailscale_get_nameservers` | Get DNS nameservers |
119
+ | `tailscale_set_nameservers` | Set DNS nameservers |
120
+ | `tailscale_get_search_paths` | Get DNS search paths |
121
+ | `tailscale_set_search_paths` | Set DNS search paths |
122
+ | `tailscale_get_split_dns` | Get split DNS configuration |
123
+ | `tailscale_set_split_dns` | Set split DNS configuration |
124
+ | `tailscale_get_dns_preferences` | Get DNS preferences (MagicDNS) |
125
+ | `tailscale_set_dns_preferences` | Set DNS preferences (MagicDNS) |
126
+
127
+ </details>
128
+
129
+ <details>
130
+ <summary><strong>Auth Keys</strong> (4 tools)</summary>
131
+
132
+ | Tool | Description |
133
+ |------|-------------|
134
+ | `tailscale_list_keys` | List auth keys |
135
+ | `tailscale_get_key` | Get details for an auth key |
136
+ | `tailscale_create_key` | Create a new auth key |
137
+ | `tailscale_delete_key` | Delete an auth key |
138
+
139
+ </details>
140
+
141
+ <details>
142
+ <summary><strong>Users</strong> (2 tools)</summary>
143
+
144
+ | Tool | Description |
145
+ |------|-------------|
146
+ | `tailscale_list_users` | List all users in the tailnet |
147
+ | `tailscale_get_user` | Get details for a specific user |
148
+
149
+ </details>
150
+
151
+ <details>
152
+ <summary><strong>Tailnet Settings</strong> (5 tools)</summary>
153
+
154
+ | Tool | Description |
155
+ |------|-------------|
156
+ | `tailscale_get_tailnet_settings` | Get tailnet settings |
157
+ | `tailscale_update_tailnet_settings` | Update tailnet settings |
158
+ | `tailscale_get_contacts` | Get tailnet contacts |
159
+ | `tailscale_set_contacts` | Set tailnet contacts |
160
+ | `tailscale_get_tailnet_keys` | Get auth keys and tailnet lock signing key info |
161
+
162
+ </details>
163
+
164
+ <details>
165
+ <summary><strong>Webhooks</strong> (4 tools)</summary>
166
+
167
+ | Tool | Description |
168
+ |------|-------------|
169
+ | `tailscale_list_webhooks` | List webhooks |
170
+ | `tailscale_get_webhook` | Get a specific webhook |
171
+ | `tailscale_create_webhook` | Create a webhook |
172
+ | `tailscale_delete_webhook` | Delete a webhook |
173
+
174
+ </details>
175
+
176
+ <details>
177
+ <summary><strong>Posture Integrations</strong> (4 tools)</summary>
178
+
179
+ | Tool | Description |
180
+ |------|-------------|
181
+ | `tailscale_list_posture_integrations` | List posture integrations |
182
+ | `tailscale_get_posture_integration` | Get a posture integration |
183
+ | `tailscale_create_posture_integration` | Create a posture integration |
184
+ | `tailscale_delete_posture_integration` | Delete a posture integration |
185
+
186
+ </details>
187
+
188
+ <details>
189
+ <summary><strong>Audit Log</strong> (1 tool)</summary>
190
+
191
+ | Tool | Description |
192
+ |------|-------------|
193
+ | `tailscale_get_audit_log` | Get configuration audit log (who changed what, when) |
194
+
195
+ </details>
196
+
197
+ ## Contributing
198
+
199
+ 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.
200
+
201
+ To develop locally:
202
+
203
+ ```bash
204
+ git clone https://github.com/YawLabs/tailscale-mcp.git
205
+ cd tailscale-mcp
206
+ npm install
207
+ npm run build
208
+ ```
209
+
210
+ Test against your own tailnet by setting `TAILSCALE_API_KEY` and running:
211
+
212
+ ```bash
213
+ node dist/index.js
214
+ ```
215
+
216
+ ## License
217
+
218
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Tailscale API client with API key and OAuth authentication.
3
+ */
4
+ export declare function getTailnet(): string;
5
+ export interface ApiResponse<T = unknown> {
6
+ ok: boolean;
7
+ status: number;
8
+ data?: T;
9
+ error?: string;
10
+ rawBody?: string;
11
+ etag?: string;
12
+ }
13
+ export declare function apiRequest<T = unknown>(method: string, path: string, body?: unknown, options?: {
14
+ rawBody?: string;
15
+ acceptRaw?: boolean;
16
+ accept?: string;
17
+ contentType?: string;
18
+ ifMatch?: string;
19
+ }): Promise<ApiResponse<T>>;
20
+ export declare function apiGet<T = unknown>(path: string, options?: {
21
+ acceptRaw?: boolean;
22
+ accept?: string;
23
+ }): Promise<ApiResponse<T>>;
24
+ export declare function apiPost<T = unknown>(path: string, body?: unknown, options?: {
25
+ rawBody?: string;
26
+ contentType?: string;
27
+ ifMatch?: string;
28
+ }): Promise<ApiResponse<T>>;
29
+ export declare function apiPatch<T = unknown>(path: string, body?: unknown): Promise<ApiResponse<T>>;
30
+ export declare function apiDelete<T = unknown>(path: string): Promise<ApiResponse<T>>;
package/dist/api.js ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Tailscale API client with API key and OAuth authentication.
3
+ */
4
+ const BASE_URL = "https://api.tailscale.com/api/v2";
5
+ let oauthToken = null;
6
+ function getConfig() {
7
+ const apiKey = process.env.TAILSCALE_API_KEY;
8
+ const oauthClientId = process.env.TAILSCALE_OAUTH_CLIENT_ID;
9
+ const oauthClientSecret = process.env.TAILSCALE_OAUTH_CLIENT_SECRET;
10
+ const tailnet = process.env.TAILSCALE_TAILNET || "-";
11
+ if (!apiKey && !(oauthClientId && oauthClientSecret)) {
12
+ throw new Error("No Tailscale credentials configured. " +
13
+ "Set TAILSCALE_API_KEY, or set TAILSCALE_OAUTH_CLIENT_ID and TAILSCALE_OAUTH_CLIENT_SECRET.");
14
+ }
15
+ return { apiKey, oauthClientId, oauthClientSecret, tailnet };
16
+ }
17
+ async function getOAuthAccessToken(clientId, clientSecret) {
18
+ if (oauthToken && Date.now() < oauthToken.expires_at - 60_000) {
19
+ return oauthToken.access_token;
20
+ }
21
+ const res = await fetch("https://api.tailscale.com/api/v2/oauth/token", {
22
+ method: "POST",
23
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
24
+ body: new URLSearchParams({
25
+ client_id: clientId,
26
+ client_secret: clientSecret,
27
+ grant_type: "client_credentials",
28
+ }),
29
+ });
30
+ if (!res.ok) {
31
+ const body = await res.text();
32
+ throw new Error(`OAuth token exchange failed (${res.status}): ${body}`);
33
+ }
34
+ const data = (await res.json());
35
+ oauthToken = {
36
+ access_token: data.access_token,
37
+ expires_at: Date.now() + data.expires_in * 1000,
38
+ };
39
+ return oauthToken.access_token;
40
+ }
41
+ async function getAuthHeader() {
42
+ const config = getConfig();
43
+ if (config.apiKey) {
44
+ return `Basic ${Buffer.from(config.apiKey + ":").toString("base64")}`;
45
+ }
46
+ const token = await getOAuthAccessToken(config.oauthClientId, config.oauthClientSecret);
47
+ return `Bearer ${token}`;
48
+ }
49
+ export function getTailnet() {
50
+ return process.env.TAILSCALE_TAILNET || "-";
51
+ }
52
+ export async function apiRequest(method, path, body, options) {
53
+ const auth = await getAuthHeader();
54
+ const headers = {
55
+ Authorization: auth,
56
+ };
57
+ if (options?.accept) {
58
+ headers["Accept"] = options.accept;
59
+ }
60
+ if (options?.ifMatch) {
61
+ headers["If-Match"] = options.ifMatch;
62
+ }
63
+ let fetchBody;
64
+ if (options?.rawBody !== undefined) {
65
+ headers["Content-Type"] = options.contentType || "application/json";
66
+ fetchBody = options.rawBody;
67
+ }
68
+ else if (body !== undefined) {
69
+ headers["Content-Type"] = "application/json";
70
+ fetchBody = JSON.stringify(body);
71
+ }
72
+ const url = path.startsWith("http") ? path : `${BASE_URL}${path}`;
73
+ const res = await fetch(url, {
74
+ method,
75
+ headers,
76
+ body: fetchBody,
77
+ });
78
+ const etag = res.headers.get("etag") || undefined;
79
+ if (options?.acceptRaw) {
80
+ const rawBody = await res.text();
81
+ if (!res.ok) {
82
+ return { ok: false, status: res.status, error: rawBody, rawBody, etag };
83
+ }
84
+ return { ok: true, status: res.status, rawBody, etag };
85
+ }
86
+ if (!res.ok) {
87
+ const errorBody = await res.text();
88
+ return { ok: false, status: res.status, error: errorBody };
89
+ }
90
+ if (res.status === 204 || res.headers.get("content-length") === "0") {
91
+ return { ok: true, status: res.status };
92
+ }
93
+ const data = (await res.json());
94
+ return { ok: true, status: res.status, data };
95
+ }
96
+ export async function apiGet(path, options) {
97
+ return apiRequest("GET", path, undefined, options);
98
+ }
99
+ export async function apiPost(path, body, options) {
100
+ return apiRequest("POST", path, body, options);
101
+ }
102
+ export async function apiPatch(path, body) {
103
+ return apiRequest("PATCH", path, body);
104
+ }
105
+ export async function apiDelete(path) {
106
+ return apiRequest("DELETE", path);
107
+ }
108
+ //# sourceMappingURL=api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { deviceTools } from "./tools/devices.js";
5
+ import { aclTools } from "./tools/acl.js";
6
+ import { dnsTools } from "./tools/dns.js";
7
+ import { keyTools } from "./tools/keys.js";
8
+ import { userTools } from "./tools/users.js";
9
+ import { tailnetTools } from "./tools/tailnet.js";
10
+ import { webhookTools } from "./tools/webhooks.js";
11
+ import { networkLockTools } from "./tools/network-lock.js";
12
+ import { postureTools } from "./tools/posture.js";
13
+ import { auditTools } from "./tools/audit.js";
14
+ import { statusTools } from "./tools/status.js";
15
+ const allTools = [
16
+ ...statusTools,
17
+ ...deviceTools,
18
+ ...aclTools,
19
+ ...dnsTools,
20
+ ...keyTools,
21
+ ...userTools,
22
+ ...tailnetTools,
23
+ ...webhookTools,
24
+ ...networkLockTools,
25
+ ...postureTools,
26
+ ...auditTools,
27
+ ];
28
+ const server = new McpServer({
29
+ name: "@yawlabs/tailscale-mcp",
30
+ version: "0.1.0",
31
+ });
32
+ for (const tool of allTools) {
33
+ server.tool(tool.name, tool.description, tool.inputSchema.shape, async (input) => {
34
+ try {
35
+ const result = await tool.handler(input);
36
+ const response = result;
37
+ if (!response.ok) {
38
+ return {
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: `Error: ${response.error || "Unknown error"}`,
43
+ },
44
+ ],
45
+ isError: true,
46
+ };
47
+ }
48
+ const text = response.rawBody ?? JSON.stringify(response.data ?? { success: true }, null, 2);
49
+ return {
50
+ content: [{ type: "text", text }],
51
+ };
52
+ }
53
+ catch (err) {
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ return {
56
+ content: [{ type: "text", text: `Error: ${message}` }],
57
+ isError: true,
58
+ };
59
+ }
60
+ });
61
+ }
62
+ async function main() {
63
+ const transport = new StdioServerTransport();
64
+ await server.connect(transport);
65
+ }
66
+ main().catch((err) => {
67
+ console.error("Fatal:", err);
68
+ process.exit(1);
69
+ });
70
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ export declare const aclTools: readonly [{
3
+ readonly name: "tailscale_get_acl";
4
+ readonly description: "Get the current ACL policy for your tailnet. Returns the raw policy text with original formatting preserved, including comments and trailing commas (HuJSON). Also returns an ETag — you must pass it to tailscale_update_acl to safely update the policy.";
5
+ readonly inputSchema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
6
+ readonly handler: () => Promise<import("../api.js").ApiResponse<unknown>>;
7
+ }, {
8
+ readonly name: "tailscale_update_acl";
9
+ readonly description: "Update the ACL policy for your tailnet. Accepts the full policy as a string to preserve formatting, comments, and trailing commas (HuJSON). You MUST pass the ETag from tailscale_get_acl to prevent overwriting concurrent changes. Always get the current ACL first, make targeted edits to the text, and pass the full modified text back.";
10
+ readonly inputSchema: z.ZodObject<{
11
+ policy: z.ZodString;
12
+ etag: z.ZodString;
13
+ }, "strip", z.ZodTypeAny, {
14
+ etag: string;
15
+ policy: string;
16
+ }, {
17
+ etag: string;
18
+ policy: string;
19
+ }>;
20
+ readonly handler: (input: {
21
+ policy: string;
22
+ etag: string;
23
+ }) => Promise<import("../api.js").ApiResponse<unknown>>;
24
+ }, {
25
+ readonly name: "tailscale_validate_acl";
26
+ readonly description: "Validate an ACL policy without applying it. Returns any errors found.";
27
+ readonly inputSchema: z.ZodObject<{
28
+ policy: z.ZodString;
29
+ }, "strip", z.ZodTypeAny, {
30
+ policy: string;
31
+ }, {
32
+ policy: string;
33
+ }>;
34
+ readonly handler: (input: {
35
+ policy: string;
36
+ }) => Promise<import("../api.js").ApiResponse<unknown>>;
37
+ }, {
38
+ readonly name: "tailscale_preview_acl";
39
+ readonly description: "Preview the ACL rules that would apply to a specific user or IP address if a proposed policy were applied.";
40
+ readonly inputSchema: z.ZodObject<{
41
+ policy: z.ZodString;
42
+ type: z.ZodEnum<["user", "ipport"]>;
43
+ previewFor: z.ZodString;
44
+ }, "strip", z.ZodTypeAny, {
45
+ type: "user" | "ipport";
46
+ policy: string;
47
+ previewFor: string;
48
+ }, {
49
+ type: "user" | "ipport";
50
+ policy: string;
51
+ previewFor: string;
52
+ }>;
53
+ readonly handler: (input: {
54
+ policy: string;
55
+ type: string;
56
+ previewFor: string;
57
+ }) => Promise<import("../api.js").ApiResponse<unknown>>;
58
+ }];
@@ -0,0 +1,72 @@
1
+ import { z } from "zod";
2
+ import { apiGet, apiPost, getTailnet } from "../api.js";
3
+ export const aclTools = [
4
+ {
5
+ name: "tailscale_get_acl",
6
+ description: "Get the current ACL policy for your tailnet. Returns the raw policy text with original formatting preserved, including comments and trailing commas (HuJSON). Also returns an ETag — you must pass it to tailscale_update_acl to safely update the policy.",
7
+ inputSchema: z.object({}),
8
+ handler: async () => {
9
+ const res = await apiGet(`/tailnet/${getTailnet()}/acl`, {
10
+ acceptRaw: true,
11
+ accept: "application/hujson",
12
+ });
13
+ if (res.ok && res.etag) {
14
+ return {
15
+ ...res,
16
+ rawBody: `${res.rawBody}\n\n---\nETag: ${res.etag}\nPass this ETag to tailscale_update_acl when updating the policy.`,
17
+ };
18
+ }
19
+ return res;
20
+ },
21
+ },
22
+ {
23
+ name: "tailscale_update_acl",
24
+ description: "Update the ACL policy for your tailnet. Accepts the full policy as a string to preserve formatting, comments, and trailing commas (HuJSON). You MUST pass the ETag from tailscale_get_acl to prevent overwriting concurrent changes. Always get the current ACL first, make targeted edits to the text, and pass the full modified text back.",
25
+ inputSchema: z.object({
26
+ policy: z
27
+ .string()
28
+ .describe("The full ACL policy text. Preserve existing formatting, comments, and structure. Only modify the specific parts that need to change."),
29
+ etag: z
30
+ .string()
31
+ .describe("The ETag from tailscale_get_acl. Required to prevent concurrent edit conflicts."),
32
+ }),
33
+ handler: async (input) => {
34
+ return apiPost(`/tailnet/${getTailnet()}/acl`, undefined, {
35
+ rawBody: input.policy,
36
+ contentType: "application/hujson",
37
+ ifMatch: input.etag,
38
+ });
39
+ },
40
+ },
41
+ {
42
+ name: "tailscale_validate_acl",
43
+ description: "Validate an ACL policy without applying it. Returns any errors found.",
44
+ inputSchema: z.object({
45
+ policy: z.string().describe("The full ACL policy text to validate"),
46
+ }),
47
+ handler: async (input) => {
48
+ return apiPost(`/tailnet/${getTailnet()}/acl/validate`, undefined, {
49
+ rawBody: input.policy,
50
+ contentType: "application/hujson",
51
+ });
52
+ },
53
+ },
54
+ {
55
+ name: "tailscale_preview_acl",
56
+ description: "Preview the ACL rules that would apply to a specific user or IP address if a proposed policy were applied.",
57
+ inputSchema: z.object({
58
+ policy: z.string().describe("The proposed ACL policy text to preview"),
59
+ type: z
60
+ .enum(["user", "ipport"])
61
+ .describe("Preview type: 'user' to see rules for a user, 'ipport' to see rules for an IP"),
62
+ previewFor: z
63
+ .string()
64
+ .describe("The user email (for type 'user') or IP:port (for type 'ipport') to preview rules for"),
65
+ }),
66
+ handler: async (input) => {
67
+ const params = new URLSearchParams({ type: input.type, previewFor: input.previewFor });
68
+ return apiPost(`/tailnet/${getTailnet()}/acl/preview?${params}`, undefined, { rawBody: input.policy, contentType: "application/hujson" });
69
+ },
70
+ },
71
+ ];
72
+ //# sourceMappingURL=acl.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acl.js","sourceRoot":"","sources":["../../src/tools/acl.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ export declare const auditTools: readonly [{
3
+ readonly name: "tailscale_get_audit_log";
4
+ readonly description: "Get the tailnet audit/configuration log. Shows who changed what and when — useful for troubleshooting and compliance.";
5
+ readonly inputSchema: z.ZodObject<{
6
+ start: z.ZodString;
7
+ end: z.ZodOptional<z.ZodString>;
8
+ }, "strip", z.ZodTypeAny, {
9
+ start: string;
10
+ end?: string | undefined;
11
+ }, {
12
+ start: string;
13
+ end?: string | undefined;
14
+ }>;
15
+ readonly handler: (input: {
16
+ start: string;
17
+ end?: string;
18
+ }) => Promise<import("../api.js").ApiResponse<unknown>>;
19
+ }];
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { apiGet, getTailnet } from "../api.js";
3
+ export const auditTools = [
4
+ {
5
+ name: "tailscale_get_audit_log",
6
+ description: "Get the tailnet audit/configuration log. Shows who changed what and when — useful for troubleshooting and compliance.",
7
+ inputSchema: z.object({
8
+ start: z
9
+ .string()
10
+ .describe("Start time in RFC3339 format (e.g. '2026-04-01T00:00:00Z'). Required."),
11
+ end: z
12
+ .string()
13
+ .optional()
14
+ .describe("End time in RFC3339 format. Defaults to now."),
15
+ }),
16
+ handler: async (input) => {
17
+ const params = new URLSearchParams({ start: input.start });
18
+ if (input.end)
19
+ params.set("end", input.end);
20
+ return apiGet(`/tailnet/${getTailnet()}/logging/configuration?${params}`);
21
+ },
22
+ },
23
+ ];
24
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/tools/audit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB;QACE,IAAI,EAAE,yBAAyB;QAC/B,WAAW,EACT,uHAAuH;QACzH,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC;YACpB,KAAK,EAAE,CAAC;iBACL,MAAM,EAAE;iBACR,QAAQ,CAAC,uEAAuE,CAAC;YACpF,GAAG,EAAE,CAAC;iBACH,MAAM,EAAE;iBACR,QAAQ,EAAE;iBACV,QAAQ,CAAC,8CAA8C,CAAC;SAC5D,CAAC;QACF,OAAO,EAAE,KAAK,EAAE,KAAsC,EAAE,EAAE;YACxD,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;YAC3D,IAAI,KAAK,CAAC,GAAG;gBAAE,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;YAC5C,OAAO,MAAM,CACX,YAAY,UAAU,EAAE,0BAA0B,MAAM,EAAE,CAC3D,CAAC;QACJ,CAAC;KACF;CACO,CAAC"}