@yawlabs/tailscale-mcp 0.4.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +33 -16
  2. package/dist/index.js +360 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![GitHub stars](https://img.shields.io/github/stars/YawLabs/tailscale-mcp)](https://github.com/YawLabs/tailscale-mcp/stargazers)
6
6
  [![CI](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml) [![Release](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml/badge.svg)](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml)
7
7
 
8
- **Manage your Tailscale tailnet from Claude Code, Cursor, and any MCP client.** 81 tools + 4 resources. One env var. Works on first try.
8
+ **Manage your Tailscale tailnet from Claude Code, Cursor, and any MCP client.** 98 tools + 4 resources. One env var. Works on first try.
9
9
 
10
10
  Built and maintained by [YawLabs](https://yaw.sh).
11
11
 
@@ -96,7 +96,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
96
96
  | ACL Policy | `tailscale://tailnet/acl` | Full ACL policy (HuJSON preserved) |
97
97
  | DNS Config | `tailscale://tailnet/dns` | Nameservers, search paths, split DNS, MagicDNS |
98
98
 
99
- ## Tools (81)
99
+ ## Tools (98)
100
100
 
101
101
  <details>
102
102
  <summary><strong>Status</strong> (1 tool)</summary>
@@ -108,7 +108,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
108
108
  </details>
109
109
 
110
110
  <details>
111
- <summary><strong>Devices</strong> (13 tools)</summary>
111
+ <summary><strong>Devices</strong> (16 tools)</summary>
112
112
 
113
113
  | Tool | Description |
114
114
  |------|-------------|
@@ -125,6 +125,9 @@ MCP Resources expose read-only data that clients can browse without tool calls.
125
125
  | `tailscale_set_device_posture_attribute` | Set a custom posture attribute (with optional expiry) |
126
126
  | `tailscale_delete_device_posture_attribute` | Delete a custom posture attribute |
127
127
  | `tailscale_set_device_tags` | Set ACL tags on a device |
128
+ | `tailscale_set_device_ip` | Set a device's Tailscale IPv4 address |
129
+ | `tailscale_update_device_key` | Update device key settings (e.g. disable key expiry) |
130
+ | `tailscale_batch_update_posture_attributes` | Batch update custom posture attributes across devices |
128
131
 
129
132
  </details>
130
133
 
@@ -141,7 +144,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
141
144
  </details>
142
145
 
143
146
  <details>
144
- <summary><strong>DNS</strong> (8 tools)</summary>
147
+ <summary><strong>DNS</strong> (11 tools)</summary>
145
148
 
146
149
  | Tool | Description |
147
150
  |------|-------------|
@@ -150,14 +153,17 @@ MCP Resources expose read-only data that clients can browse without tool calls.
150
153
  | `tailscale_get_search_paths` | Get DNS search paths |
151
154
  | `tailscale_set_search_paths` | Set DNS search paths |
152
155
  | `tailscale_get_split_dns` | Get split DNS configuration |
153
- | `tailscale_set_split_dns` | Set split DNS configuration |
156
+ | `tailscale_set_split_dns` | Set split DNS configuration (full replace) |
157
+ | `tailscale_update_split_dns` | Update split DNS configuration (partial merge) |
154
158
  | `tailscale_get_dns_preferences` | Get DNS preferences (MagicDNS) |
155
159
  | `tailscale_set_dns_preferences` | Set DNS preferences (MagicDNS) |
160
+ | `tailscale_get_dns_configuration` | Get unified DNS configuration (all settings in one call) |
161
+ | `tailscale_set_dns_configuration` | Set unified DNS configuration (all settings in one call) |
156
162
 
157
163
  </details>
158
164
 
159
165
  <details>
160
- <summary><strong>Auth Keys</strong> (4 tools)</summary>
166
+ <summary><strong>Auth Keys</strong> (5 tools)</summary>
161
167
 
162
168
  | Tool | Description |
163
169
  |------|-------------|
@@ -165,11 +171,12 @@ MCP Resources expose read-only data that clients can browse without tool calls.
165
171
  | `tailscale_get_key` | Get details for an auth key |
166
172
  | `tailscale_create_key` | Create a new auth key |
167
173
  | `tailscale_delete_key` | Delete an auth key |
174
+ | `tailscale_update_key` | Update an existing auth key |
168
175
 
169
176
  </details>
170
177
 
171
178
  <details>
172
- <summary><strong>Users</strong> (6 tools)</summary>
179
+ <summary><strong>Users</strong> (7 tools)</summary>
173
180
 
174
181
  | Tool | Description |
175
182
  |------|-------------|
@@ -179,18 +186,20 @@ MCP Resources expose read-only data that clients can browse without tool calls.
179
186
  | `tailscale_suspend_user` | Suspend a user, revoking access |
180
187
  | `tailscale_restore_user` | Restore a suspended user |
181
188
  | `tailscale_update_user_role` | Update a user's role (owner, admin, member, etc.) |
189
+ | `tailscale_delete_user` | Delete a user and all their devices |
182
190
 
183
191
  </details>
184
192
 
185
193
  <details>
186
- <summary><strong>Tailnet Settings</strong> (4 tools)</summary>
194
+ <summary><strong>Tailnet Settings</strong> (5 tools)</summary>
187
195
 
188
196
  | Tool | Description |
189
197
  |------|-------------|
190
198
  | `tailscale_get_tailnet_settings` | Get tailnet settings (HTTPS, device approval, key expiry, etc.) |
191
- | `tailscale_update_tailnet_settings` | Update tailnet settings (HTTPS certificates, approval, auto-updates, key expiry, posture, regional routing, network flow logging) |
199
+ | `tailscale_update_tailnet_settings` | Update tailnet settings (HTTPS certificates, approval, auto-updates, key expiry, posture, regional routing, network flow logging, external ACL management) |
192
200
  | `tailscale_get_contacts` | Get tailnet contacts |
193
201
  | `tailscale_set_contacts` | Set tailnet contacts |
202
+ | `tailscale_resend_contact_verification` | Resend verification email for a contact |
194
203
 
195
204
  </details>
196
205
 
@@ -204,7 +213,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
204
213
  </details>
205
214
 
206
215
  <details>
207
- <summary><strong>Webhooks</strong> (6 tools)</summary>
216
+ <summary><strong>Webhooks</strong> (7 tools)</summary>
208
217
 
209
218
  | Tool | Description |
210
219
  |------|-------------|
@@ -214,6 +223,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
214
223
  | `tailscale_update_webhook` | Update a webhook's endpoint URL and/or subscriptions |
215
224
  | `tailscale_delete_webhook` | Delete a webhook |
216
225
  | `tailscale_rotate_webhook_secret` | Rotate a webhook's secret |
226
+ | `tailscale_test_webhook` | Send a test event to verify webhook delivery |
217
227
 
218
228
  </details>
219
229
 
@@ -231,7 +241,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
231
241
  </details>
232
242
 
233
243
  <details>
234
- <summary><strong>Tailscale Services</strong> (5 tools)</summary>
244
+ <summary><strong>Tailscale Services</strong> (7 tools)</summary>
235
245
 
236
246
  | Tool | Description |
237
247
  |------|-------------|
@@ -240,18 +250,23 @@ MCP Resources expose read-only data that clients can browse without tool calls.
240
250
  | `tailscale_update_service` | Update a service's configuration |
241
251
  | `tailscale_delete_service` | Delete a service |
242
252
  | `tailscale_list_service_hosts` | List devices hosting a service |
253
+ | `tailscale_get_service_device_approval` | Get approval status of a device for a service |
254
+ | `tailscale_set_service_device_approval` | Approve or reject a device to host a service |
243
255
 
244
256
  </details>
245
257
 
246
258
  <details>
247
- <summary><strong>Log Streaming</strong> (4 tools)</summary>
259
+ <summary><strong>Log Streaming</strong> (7 tools)</summary>
248
260
 
249
261
  | Tool | Description |
250
262
  |------|-------------|
251
- | `tailscale_list_log_stream_configs` | List log streaming configurations |
263
+ | `tailscale_list_log_stream_configs` | List log streaming configurations (both audit and network) |
252
264
  | `tailscale_get_log_stream_config` | Get log streaming config for a log type |
253
265
  | `tailscale_set_log_stream_config` | Set where logs are sent (Axiom, Datadog, Splunk, etc.) |
254
266
  | `tailscale_delete_log_stream_config` | Delete a log streaming configuration |
267
+ | `tailscale_get_log_stream_status` | Check if log streaming is delivering successfully |
268
+ | `tailscale_create_aws_external_id` | Create/get AWS external ID for S3 log streaming |
269
+ | `tailscale_validate_aws_trust_policy` | Validate AWS IAM role trust policy for S3 log streaming |
255
270
 
256
271
  </details>
257
272
 
@@ -282,19 +297,20 @@ MCP Resources expose read-only data that clients can browse without tool calls.
282
297
  </details>
283
298
 
284
299
  <details>
285
- <summary><strong>Device Invites</strong> (4 tools)</summary>
300
+ <summary><strong>Device Invites</strong> (5 tools)</summary>
286
301
 
287
302
  | Tool | Description |
288
303
  |------|-------------|
289
- | `tailscale_list_device_invites` | List device invites |
304
+ | `tailscale_list_device_invites` | List device invites for a specific device |
290
305
  | `tailscale_create_device_invite` | Create a device invite |
291
306
  | `tailscale_get_device_invite` | Get a device invite |
292
307
  | `tailscale_delete_device_invite` | Delete a device invite |
308
+ | `tailscale_resend_device_invite` | Resend a device invite email |
293
309
 
294
310
  </details>
295
311
 
296
312
  <details>
297
- <summary><strong>User Invites</strong> (4 tools)</summary>
313
+ <summary><strong>User Invites</strong> (5 tools)</summary>
298
314
 
299
315
  | Tool | Description |
300
316
  |------|-------------|
@@ -302,6 +318,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
302
318
  | `tailscale_create_user_invite` | Create a user invite |
303
319
  | `tailscale_get_user_invite` | Get a user invite |
304
320
  | `tailscale_delete_user_invite` | Delete a user invite |
321
+ | `tailscale_resend_user_invite` | Resend a user invite email |
305
322
 
306
323
  </details>
307
324
 
package/dist/index.js CHANGED
@@ -21577,6 +21577,63 @@ var deviceTools = [
21577
21577
  }
21578
21578
  return apiPost(`/device/${encPath(input.deviceId)}/tags`, { tags: input.tags });
21579
21579
  }
21580
+ },
21581
+ {
21582
+ name: "tailscale_set_device_ip",
21583
+ description: "Set the Tailscale IPv4 address for a device.",
21584
+ annotations: {
21585
+ title: "Set device IP",
21586
+ readOnlyHint: false,
21587
+ destructiveHint: false,
21588
+ idempotentHint: true,
21589
+ openWorldHint: true
21590
+ },
21591
+ inputSchema: external_exports.object({
21592
+ deviceId: external_exports.string().describe("The device ID"),
21593
+ ipv4: external_exports.string().describe("The new Tailscale IPv4 address for the device (e.g. '100.64.0.1')")
21594
+ }),
21595
+ handler: async (input) => {
21596
+ return apiPost(`/device/${encPath(input.deviceId)}/ip`, { ipv4: input.ipv4 });
21597
+ }
21598
+ },
21599
+ {
21600
+ name: "tailscale_update_device_key",
21601
+ description: "Update a device's key settings, such as disabling key expiry. Useful for servers that should never need to re-authenticate.",
21602
+ annotations: {
21603
+ title: "Update device key",
21604
+ readOnlyHint: false,
21605
+ destructiveHint: false,
21606
+ idempotentHint: true,
21607
+ openWorldHint: true
21608
+ },
21609
+ inputSchema: external_exports.object({
21610
+ deviceId: external_exports.string().describe("The device ID"),
21611
+ keyExpiryDisabled: external_exports.boolean().describe("Whether to disable key expiry for this device")
21612
+ }),
21613
+ handler: async (input) => {
21614
+ return apiPost(`/device/${encPath(input.deviceId)}/key`, {
21615
+ keyExpiryDisabled: input.keyExpiryDisabled
21616
+ });
21617
+ }
21618
+ },
21619
+ {
21620
+ name: "tailscale_batch_update_posture_attributes",
21621
+ description: "Batch update custom posture attributes across multiple devices. Each attribute key must start with 'custom:'.",
21622
+ annotations: {
21623
+ title: "Batch update posture attributes",
21624
+ readOnlyHint: false,
21625
+ destructiveHint: false,
21626
+ idempotentHint: true,
21627
+ openWorldHint: true
21628
+ },
21629
+ inputSchema: external_exports.object({
21630
+ attributes: external_exports.record(external_exports.string(), external_exports.record(external_exports.string(), external_exports.unknown())).describe(
21631
+ 'Map of device ID to attribute map (e.g. { "12345": { "custom:compliant": "true" }, "67890": { "custom:compliant": "false" } })'
21632
+ )
21633
+ }),
21634
+ handler: async (input) => {
21635
+ return apiPatch(`/tailnet/${getTailnet()}/device-attributes`, input.attributes);
21636
+ }
21580
21637
  }
21581
21638
  ];
21582
21639
 
@@ -21715,6 +21772,64 @@ var dnsTools = [
21715
21772
  magicDNS: input.magicDNS
21716
21773
  });
21717
21774
  }
21775
+ },
21776
+ {
21777
+ name: "tailscale_update_split_dns",
21778
+ description: "Partially update split DNS configuration. Merges the provided domains with the existing config \u2014 only the specified domains are changed, others are untouched. Set a domain's nameservers to an empty array to remove it.",
21779
+ annotations: {
21780
+ title: "Update split DNS (partial)",
21781
+ readOnlyHint: false,
21782
+ destructiveHint: false,
21783
+ idempotentHint: true,
21784
+ openWorldHint: true
21785
+ },
21786
+ inputSchema: external_exports.object({
21787
+ splitDns: external_exports.record(external_exports.string(), external_exports.array(external_exports.string())).describe(
21788
+ 'Map of domain to nameserver list to merge (e.g. { "new.example.com": ["10.0.0.3"] }). Only specified domains are changed.'
21789
+ )
21790
+ }),
21791
+ handler: async (input) => {
21792
+ return apiPatch(`/tailnet/${getTailnet()}/dns/split-dns`, input.splitDns);
21793
+ }
21794
+ },
21795
+ {
21796
+ name: "tailscale_get_dns_configuration",
21797
+ description: "Get the unified DNS configuration for your tailnet, including nameservers, search paths, split DNS, and MagicDNS preference in a single call.",
21798
+ annotations: {
21799
+ title: "Get DNS configuration (unified)",
21800
+ readOnlyHint: true,
21801
+ destructiveHint: false,
21802
+ idempotentHint: true,
21803
+ openWorldHint: true
21804
+ },
21805
+ inputSchema: external_exports.object({}),
21806
+ handler: async () => {
21807
+ return apiGet(`/tailnet/${getTailnet()}/dns/configuration`);
21808
+ }
21809
+ },
21810
+ {
21811
+ name: "tailscale_set_dns_configuration",
21812
+ description: "Set the unified DNS configuration for your tailnet in a single call. Replaces all DNS settings (nameservers, search paths, split DNS, MagicDNS preference).",
21813
+ annotations: {
21814
+ title: "Set DNS configuration (unified)",
21815
+ readOnlyHint: false,
21816
+ destructiveHint: false,
21817
+ idempotentHint: true,
21818
+ openWorldHint: true
21819
+ },
21820
+ inputSchema: external_exports.object({
21821
+ dns: external_exports.array(external_exports.string()).optional().describe("List of DNS server IP addresses"),
21822
+ searchPaths: external_exports.array(external_exports.string()).optional().describe("List of DNS search domains"),
21823
+ splitDns: external_exports.record(external_exports.string(), external_exports.array(external_exports.string())).optional().describe("Map of domain to nameserver list for split DNS"),
21824
+ magicDNS: external_exports.boolean().optional().describe("Whether to enable MagicDNS")
21825
+ }),
21826
+ handler: async (input) => {
21827
+ const body = {};
21828
+ for (const [key, value] of Object.entries(input)) {
21829
+ if (value !== void 0) body[key] = value;
21830
+ }
21831
+ return apiPost(`/tailnet/${getTailnet()}/dns/configuration`, body);
21832
+ }
21718
21833
  }
21719
21834
  ];
21720
21835
 
@@ -21723,7 +21838,7 @@ var inviteTools = [
21723
21838
  // --- Device Invites ---
21724
21839
  {
21725
21840
  name: "tailscale_list_device_invites",
21726
- description: "List all device invites for your tailnet.",
21841
+ description: "List all device invites for a specific device.",
21727
21842
  annotations: {
21728
21843
  title: "List device invites",
21729
21844
  readOnlyHint: true,
@@ -21731,14 +21846,16 @@ var inviteTools = [
21731
21846
  idempotentHint: true,
21732
21847
  openWorldHint: true
21733
21848
  },
21734
- inputSchema: external_exports.object({}),
21735
- handler: async () => {
21736
- return apiGet(`/tailnet/${getTailnet()}/device-invites`);
21849
+ inputSchema: external_exports.object({
21850
+ deviceId: external_exports.string().describe("The device ID to list invites for")
21851
+ }),
21852
+ handler: async (input) => {
21853
+ return apiGet(`/device/${encPath(input.deviceId)}/device-invites`);
21737
21854
  }
21738
21855
  },
21739
21856
  {
21740
21857
  name: "tailscale_create_device_invite",
21741
- description: "Create a new device invite that allows someone to add a device to your tailnet.",
21858
+ description: "Create a new device invite that allows someone to add a specific device to your tailnet.",
21742
21859
  annotations: {
21743
21860
  title: "Create device invite",
21744
21861
  readOnlyHint: false,
@@ -21747,6 +21864,7 @@ var inviteTools = [
21747
21864
  openWorldHint: true
21748
21865
  },
21749
21866
  inputSchema: external_exports.object({
21867
+ deviceId: external_exports.string().describe("The device ID to create an invite for"),
21750
21868
  multiUse: external_exports.boolean().optional().describe("Whether the invite can be used more than once (default: false)"),
21751
21869
  allowExitNode: external_exports.boolean().optional().describe("Whether the invited device can be used as an exit node (default: false)"),
21752
21870
  email: external_exports.string().optional().describe("Email address to send the invite to")
@@ -21756,7 +21874,7 @@ var inviteTools = [
21756
21874
  if (input.multiUse !== void 0) body.multiUse = input.multiUse;
21757
21875
  if (input.allowExitNode !== void 0) body.allowExitNode = input.allowExitNode;
21758
21876
  if (input.email !== void 0) body.email = input.email;
21759
- return apiPost(`/tailnet/${getTailnet()}/device-invites`, body);
21877
+ return apiPost(`/device/${encPath(input.deviceId)}/device-invites`, body);
21760
21878
  }
21761
21879
  },
21762
21880
  {
@@ -21863,6 +21981,40 @@ var inviteTools = [
21863
21981
  handler: async (input) => {
21864
21982
  return apiDelete(`/user-invites/${encPath(input.inviteId)}`);
21865
21983
  }
21984
+ },
21985
+ {
21986
+ name: "tailscale_resend_device_invite",
21987
+ description: "Resend a device invite email.",
21988
+ annotations: {
21989
+ title: "Resend device invite",
21990
+ readOnlyHint: false,
21991
+ destructiveHint: false,
21992
+ idempotentHint: true,
21993
+ openWorldHint: true
21994
+ },
21995
+ inputSchema: external_exports.object({
21996
+ inviteId: external_exports.string().describe("The device invite ID to resend")
21997
+ }),
21998
+ handler: async (input) => {
21999
+ return apiPost(`/device-invites/${encPath(input.inviteId)}/resend`);
22000
+ }
22001
+ },
22002
+ {
22003
+ name: "tailscale_resend_user_invite",
22004
+ description: "Resend a user invite email.",
22005
+ annotations: {
22006
+ title: "Resend user invite",
22007
+ readOnlyHint: false,
22008
+ destructiveHint: false,
22009
+ idempotentHint: true,
22010
+ openWorldHint: true
22011
+ },
22012
+ inputSchema: external_exports.object({
22013
+ inviteId: external_exports.string().describe("The user invite ID to resend")
22014
+ }),
22015
+ handler: async (input) => {
22016
+ return apiPost(`/user-invites/${encPath(input.inviteId)}/resend`);
22017
+ }
21866
22018
  }
21867
22019
  ];
21868
22020
 
@@ -21952,6 +22104,26 @@ var keyTools = [
21952
22104
  handler: async (input) => {
21953
22105
  return apiDelete(`/tailnet/${getTailnet()}/keys/${encPath(input.keyId)}`);
21954
22106
  }
22107
+ },
22108
+ {
22109
+ name: "tailscale_update_key",
22110
+ description: "Update an existing auth key's description or capabilities.",
22111
+ annotations: {
22112
+ title: "Update auth key",
22113
+ readOnlyHint: false,
22114
+ destructiveHint: false,
22115
+ idempotentHint: true,
22116
+ openWorldHint: true
22117
+ },
22118
+ inputSchema: external_exports.object({
22119
+ keyId: external_exports.string().describe("The auth key ID to update"),
22120
+ description: external_exports.string().optional().describe("Updated description for the key")
22121
+ }),
22122
+ handler: async (input) => {
22123
+ const body = {};
22124
+ if (input.description !== void 0) body.description = input.description;
22125
+ return apiPut(`/tailnet/${getTailnet()}/keys/${encPath(input.keyId)}`, body);
22126
+ }
21955
22127
  }
21956
22128
  ];
21957
22129
 
@@ -21959,7 +22131,7 @@ var keyTools = [
21959
22131
  var logStreamingTools = [
21960
22132
  {
21961
22133
  name: "tailscale_list_log_stream_configs",
21962
- description: "List all log streaming configurations for your tailnet. Log streaming sends logs to external destinations like Axiom, Datadog, Splunk, Elasticsearch, S3, or GCS.",
22134
+ description: "List all log streaming configurations for your tailnet. Fetches both 'configuration' (audit) and 'network' (flow) log stream configs. Log streaming sends logs to external destinations like Axiom, Datadog, Splunk, Elasticsearch, S3, or GCS.",
21963
22135
  annotations: {
21964
22136
  title: "List log stream configs",
21965
22137
  readOnlyHint: true,
@@ -21969,7 +22141,18 @@ var logStreamingTools = [
21969
22141
  },
21970
22142
  inputSchema: external_exports.object({}),
21971
22143
  handler: async () => {
21972
- return apiGet(`/tailnet/${getTailnet()}/logging/stream`);
22144
+ const [configuration, network] = await Promise.all([
22145
+ apiGet(`/tailnet/${getTailnet()}/logging/configuration/stream`),
22146
+ apiGet(`/tailnet/${getTailnet()}/logging/network/stream`)
22147
+ ]);
22148
+ return {
22149
+ ok: true,
22150
+ status: 200,
22151
+ data: {
22152
+ configuration: configuration.ok ? configuration.data : { error: configuration.error },
22153
+ network: network.ok ? network.data : { error: network.error }
22154
+ }
22155
+ };
21973
22156
  }
21974
22157
  },
21975
22158
  {
@@ -21986,7 +22169,7 @@ var logStreamingTools = [
21986
22169
  logType: external_exports.enum(["configuration", "network"]).describe("The log type: 'configuration' for audit logs, 'network' for network flow logs")
21987
22170
  }),
21988
22171
  handler: async (input) => {
21989
- return apiGet(`/tailnet/${getTailnet()}/logging/stream/${encPath(input.logType)}`);
22172
+ return apiGet(`/tailnet/${getTailnet()}/logging/${encPath(input.logType)}/stream`);
21990
22173
  }
21991
22174
  },
21992
22175
  {
@@ -22014,7 +22197,7 @@ var logStreamingTools = [
22014
22197
  for (const [key, value] of Object.entries(body)) {
22015
22198
  if (value !== void 0) cleanBody[key] = value;
22016
22199
  }
22017
- return apiPut(`/tailnet/${getTailnet()}/logging/stream/${encPath(logType)}`, cleanBody);
22200
+ return apiPut(`/tailnet/${getTailnet()}/logging/${encPath(logType)}/stream`, cleanBody);
22018
22201
  }
22019
22202
  },
22020
22203
  {
@@ -22031,7 +22214,60 @@ var logStreamingTools = [
22031
22214
  logType: external_exports.enum(["configuration", "network"]).describe("The log type to stop streaming: 'configuration' or 'network'")
22032
22215
  }),
22033
22216
  handler: async (input) => {
22034
- return apiDelete(`/tailnet/${getTailnet()}/logging/stream/${encPath(input.logType)}`);
22217
+ return apiDelete(`/tailnet/${getTailnet()}/logging/${encPath(input.logType)}/stream`);
22218
+ }
22219
+ },
22220
+ {
22221
+ name: "tailscale_get_log_stream_status",
22222
+ description: "Get the status of log streaming for a specific log type. Shows whether logs are being delivered successfully.",
22223
+ annotations: {
22224
+ title: "Get log stream status",
22225
+ readOnlyHint: true,
22226
+ destructiveHint: false,
22227
+ idempotentHint: true,
22228
+ openWorldHint: true
22229
+ },
22230
+ inputSchema: external_exports.object({
22231
+ logType: external_exports.enum(["configuration", "network"]).describe("The log type: 'configuration' for audit logs, 'network' for network flow logs")
22232
+ }),
22233
+ handler: async (input) => {
22234
+ return apiGet(`/tailnet/${getTailnet()}/logging/${encPath(input.logType)}/stream/status`);
22235
+ }
22236
+ },
22237
+ {
22238
+ name: "tailscale_create_aws_external_id",
22239
+ description: "Create or get an AWS external ID for your tailnet. Used when configuring log streaming to S3 \u2014 the external ID is included in the IAM role trust policy.",
22240
+ annotations: {
22241
+ title: "Create AWS external ID",
22242
+ readOnlyHint: false,
22243
+ destructiveHint: false,
22244
+ idempotentHint: true,
22245
+ openWorldHint: true
22246
+ },
22247
+ inputSchema: external_exports.object({}),
22248
+ handler: async () => {
22249
+ return apiPost(`/tailnet/${getTailnet()}/aws-external-id`);
22250
+ }
22251
+ },
22252
+ {
22253
+ name: "tailscale_validate_aws_trust_policy",
22254
+ description: "Validate that an AWS IAM role trust policy is correctly configured with the Tailscale external ID. Use this after setting up the IAM role for S3 log streaming.",
22255
+ annotations: {
22256
+ title: "Validate AWS trust policy",
22257
+ readOnlyHint: true,
22258
+ destructiveHint: false,
22259
+ idempotentHint: true,
22260
+ openWorldHint: true
22261
+ },
22262
+ inputSchema: external_exports.object({
22263
+ externalId: external_exports.string().describe("The AWS external ID to validate"),
22264
+ roleArn: external_exports.string().describe("The AWS IAM role ARN to validate against")
22265
+ }),
22266
+ handler: async (input) => {
22267
+ return apiPost(
22268
+ `/tailnet/${getTailnet()}/aws-external-id/${encPath(input.externalId)}/validate-aws-trust-policy`,
22269
+ { roleArn: input.roleArn }
22270
+ );
22035
22271
  }
22036
22272
  }
22037
22273
  ];
@@ -22316,7 +22552,7 @@ var serviceTools = [
22316
22552
  for (const [key, value] of Object.entries(body)) {
22317
22553
  if (value !== void 0) cleanBody[key] = value;
22318
22554
  }
22319
- return apiPatch(`/tailnet/${getTailnet()}/services/${encPath(serviceName)}`, cleanBody);
22555
+ return apiPut(`/tailnet/${getTailnet()}/services/${encPath(serviceName)}`, cleanBody);
22320
22556
  }
22321
22557
  },
22322
22558
  {
@@ -22350,7 +22586,49 @@ var serviceTools = [
22350
22586
  serviceName: external_exports.string().describe("The service name")
22351
22587
  }),
22352
22588
  handler: async (input) => {
22353
- return apiGet(`/tailnet/${getTailnet()}/services/${encPath(input.serviceName)}/hosts`);
22589
+ return apiGet(`/tailnet/${getTailnet()}/services/${encPath(input.serviceName)}/devices`);
22590
+ }
22591
+ },
22592
+ {
22593
+ name: "tailscale_get_service_device_approval",
22594
+ description: "Get the approval status of a specific device for a Tailscale Service.",
22595
+ annotations: {
22596
+ title: "Get service device approval",
22597
+ readOnlyHint: true,
22598
+ destructiveHint: false,
22599
+ idempotentHint: true,
22600
+ openWorldHint: true
22601
+ },
22602
+ inputSchema: external_exports.object({
22603
+ serviceName: external_exports.string().describe("The service name"),
22604
+ deviceId: external_exports.string().describe("The device ID")
22605
+ }),
22606
+ handler: async (input) => {
22607
+ return apiGet(
22608
+ `/tailnet/${getTailnet()}/services/${encPath(input.serviceName)}/device/${encPath(input.deviceId)}/approved`
22609
+ );
22610
+ }
22611
+ },
22612
+ {
22613
+ name: "tailscale_set_service_device_approval",
22614
+ description: "Approve or reject a device to host a Tailscale Service.",
22615
+ annotations: {
22616
+ title: "Set service device approval",
22617
+ readOnlyHint: false,
22618
+ destructiveHint: false,
22619
+ idempotentHint: true,
22620
+ openWorldHint: true
22621
+ },
22622
+ inputSchema: external_exports.object({
22623
+ serviceName: external_exports.string().describe("The service name"),
22624
+ deviceId: external_exports.string().describe("The device ID"),
22625
+ approved: external_exports.boolean().describe("Whether to approve (true) or reject (false) the device")
22626
+ }),
22627
+ handler: async (input) => {
22628
+ return apiPost(
22629
+ `/tailnet/${getTailnet()}/services/${encPath(input.serviceName)}/device/${encPath(input.deviceId)}/approved`,
22630
+ { approved: input.approved }
22631
+ );
22354
22632
  }
22355
22633
  }
22356
22634
  ];
@@ -22428,21 +22706,15 @@ var tailnetTools = [
22428
22706
  networkFlowLoggingOn: external_exports.boolean().optional().describe("Whether network flow logging is enabled"),
22429
22707
  regionalRoutingOn: external_exports.boolean().optional().describe("Whether regional routing is enabled"),
22430
22708
  postureIdentityCollectionOn: external_exports.boolean().optional().describe("Whether posture identity collection is enabled"),
22431
- httpsEnabled: external_exports.boolean().optional().describe("Whether HTTPS certificates are enabled (for tailscale serve/funnel)")
22709
+ httpsEnabled: external_exports.boolean().optional().describe("Whether HTTPS certificates are enabled (for tailscale serve/funnel)"),
22710
+ aclsExternallyManagedOn: external_exports.boolean().optional().describe("Whether ACLs are externally managed (e.g. via GitOps)"),
22711
+ aclsExternalLink: external_exports.string().optional().describe("URL to the external ACL management system (shown in the admin console)")
22432
22712
  }),
22433
22713
  handler: async (input) => {
22434
22714
  const body = {};
22435
- if (input.devicesApprovalOn !== void 0) body.devicesApprovalOn = input.devicesApprovalOn;
22436
- if (input.devicesAutoUpdatesOn !== void 0) body.devicesAutoUpdatesOn = input.devicesAutoUpdatesOn;
22437
- if (input.devicesKeyDurationDays !== void 0) body.devicesKeyDurationDays = input.devicesKeyDurationDays;
22438
- if (input.usersApprovalOn !== void 0) body.usersApprovalOn = input.usersApprovalOn;
22439
- if (input.usersRoleAllowedToJoinExternalTailnets !== void 0)
22440
- body.usersRoleAllowedToJoinExternalTailnets = input.usersRoleAllowedToJoinExternalTailnets;
22441
- if (input.networkFlowLoggingOn !== void 0) body.networkFlowLoggingOn = input.networkFlowLoggingOn;
22442
- if (input.regionalRoutingOn !== void 0) body.regionalRoutingOn = input.regionalRoutingOn;
22443
- if (input.postureIdentityCollectionOn !== void 0)
22444
- body.postureIdentityCollectionOn = input.postureIdentityCollectionOn;
22445
- if (input.httpsEnabled !== void 0) body.httpsEnabled = input.httpsEnabled;
22715
+ for (const [key, value] of Object.entries(input)) {
22716
+ if (value !== void 0) body[key] = value;
22717
+ }
22446
22718
  return apiPatch(`/tailnet/${getTailnet()}/settings`, body);
22447
22719
  }
22448
22720
  },
@@ -22477,11 +22749,33 @@ var tailnetTools = [
22477
22749
  security: external_exports.object({ email: external_exports.string() }).optional().describe("Security contact email")
22478
22750
  }),
22479
22751
  handler: async (input) => {
22480
- const body = {};
22481
- if (input.account !== void 0) body.account = input.account;
22482
- if (input.support !== void 0) body.support = input.support;
22483
- if (input.security !== void 0) body.security = input.security;
22484
- return apiPatch(`/tailnet/${getTailnet()}/contacts`, body);
22752
+ const results = {};
22753
+ for (const contactType of ["account", "support", "security"]) {
22754
+ const value = input[contactType];
22755
+ if (value !== void 0) {
22756
+ const res = await apiPatch(`/tailnet/${getTailnet()}/contacts/${encPath(contactType)}`, value);
22757
+ if (!res.ok) return res;
22758
+ results[contactType] = res.data;
22759
+ }
22760
+ }
22761
+ return { ok: true, status: 200, data: results };
22762
+ }
22763
+ },
22764
+ {
22765
+ name: "tailscale_resend_contact_verification",
22766
+ description: "Resend the verification email for a tailnet contact.",
22767
+ annotations: {
22768
+ title: "Resend contact verification",
22769
+ readOnlyHint: false,
22770
+ destructiveHint: false,
22771
+ idempotentHint: true,
22772
+ openWorldHint: true
22773
+ },
22774
+ inputSchema: external_exports.object({
22775
+ contactType: external_exports.enum(["account", "support", "security"]).describe("The contact type to resend verification for")
22776
+ }),
22777
+ handler: async (input) => {
22778
+ return apiPost(`/tailnet/${getTailnet()}/contacts/${encPath(input.contactType)}/resend-verification-email`);
22485
22779
  }
22486
22780
  }
22487
22781
  ];
@@ -22586,7 +22880,24 @@ var userTools = [
22586
22880
  role: external_exports.enum(["owner", "admin", "it-admin", "network-admin", "billing-admin", "auditor", "member"]).describe("The new role to assign")
22587
22881
  }),
22588
22882
  handler: async (input) => {
22589
- return apiPatch(`/users/${encPath(input.userId)}/role`, { role: input.role });
22883
+ return apiPost(`/users/${encPath(input.userId)}/role`, { role: input.role });
22884
+ }
22885
+ },
22886
+ {
22887
+ name: "tailscale_delete_user",
22888
+ description: "Delete a user from the tailnet. This is irreversible \u2014 the user and all their devices will be removed.",
22889
+ annotations: {
22890
+ title: "Delete user",
22891
+ readOnlyHint: false,
22892
+ destructiveHint: true,
22893
+ idempotentHint: false,
22894
+ openWorldHint: true
22895
+ },
22896
+ inputSchema: external_exports.object({
22897
+ userId: external_exports.string().describe("The user ID to delete")
22898
+ }),
22899
+ handler: async (input) => {
22900
+ return apiPost(`/users/${encPath(input.userId)}/delete`);
22590
22901
  }
22591
22902
  }
22592
22903
  ];
@@ -22705,6 +23016,23 @@ var webhookTools = [
22705
23016
  handler: async (input) => {
22706
23017
  return apiPost(`/webhooks/${encPath(input.webhookId)}/rotate`);
22707
23018
  }
23019
+ },
23020
+ {
23021
+ name: "tailscale_test_webhook",
23022
+ description: "Send a test event to a webhook endpoint to verify it is configured correctly and receiving events.",
23023
+ annotations: {
23024
+ title: "Test webhook",
23025
+ readOnlyHint: false,
23026
+ destructiveHint: false,
23027
+ idempotentHint: true,
23028
+ openWorldHint: true
23029
+ },
23030
+ inputSchema: external_exports.object({
23031
+ webhookId: external_exports.string().describe("The webhook ID to test")
23032
+ }),
23033
+ handler: async (input) => {
23034
+ return apiPost(`/webhooks/${encPath(input.webhookId)}/test`);
23035
+ }
22708
23036
  }
22709
23037
  ];
22710
23038
 
@@ -22807,7 +23135,7 @@ var workloadIdentityTools = [
22807
23135
  ];
22808
23136
 
22809
23137
  // src/index.ts
22810
- var version2 = true ? "0.4.0" : (await null).createRequire(import.meta.url)("../package.json").version;
23138
+ var version2 = true ? "0.5.0" : (await null).createRequire(import.meta.url)("../package.json").version;
22811
23139
  var subcommand = process.argv[2];
22812
23140
  if (subcommand === "deploy-acl") {
22813
23141
  const filePath = process.argv[3];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.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>",