feat(tailscale): add Tailscale integration with 20 API operations (#3868)

* feat(tailscale): add Tailscale integration with 20 API operations

* fix(tailscale): fix transformResponse signatures and block output types

* fix(tailscale): safe response.json() pattern, trim apiKey, guard expirySeconds
This commit is contained in:
Waleed
2026-03-31 16:26:17 -07:00
committed by GitHub
parent 7898e5d75f
commit d99dd86bf2
32 changed files with 3018 additions and 0 deletions
+39
View File
@@ -683,6 +683,45 @@ export function SerperIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='none' {...props}>
<path
fill='currentColor'
opacity={0.2}
d='M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8'
/>
<path
fill='currentColor'
d='M65.6 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9S1.8 219 1.8 254.2s28.6 63.9 63.8 63.9'
/>
<path
fill='currentColor'
opacity={0.2}
d='M65.6 512c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512'
/>
<path
fill='currentColor'
d='M257.2 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9'
/>
<path
fill='currentColor'
opacity={0.2}
d='M257.2 127.7c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.3 0 63.9-28.6 63.9-63.9S481.6 0 446.4 0c-35.3 0-63.9 28.6-63.9 63.9s28.6 63.8 63.9 63.8'
/>
<path
fill='currentColor'
d='M446.4 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9'
/>
<path
fill='currentColor'
opacity={0.2}
d='M446.4 512c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9'
/>
</svg>
)
}
export function TavilyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox='0 0 600 600' xmlns='http://www.w3.org/2000/svg' {...props}>
+2
View File
@@ -156,6 +156,7 @@ import {
StagehandIcon,
StripeIcon,
SupabaseIcon,
TailscaleIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
@@ -335,6 +336,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
stripe: StripeIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
tailscale: TailscaleIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract_v2: TextractIcon,
@@ -153,6 +153,7 @@
"stt",
"supabase",
"table",
"tailscale",
"tavily",
"telegram",
"textract",
@@ -0,0 +1,490 @@
---
title: Tailscale
description: Manage devices and network settings in your Tailscale tailnet
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="tailscale"
color="#2E2D2D"
/>
## Overview
[Tailscale](https://tailscale.com) is a zero-config mesh VPN built on WireGuard that makes it easy to connect devices, services, and users across any network. The Tailscale block lets you automate network management tasks like device provisioning, access control, route management, and DNS configuration directly from your Sim workflows.
## Authentication
The Tailscale block uses API key authentication. To get an API key:
1. Go to the [Tailscale admin console](https://login.tailscale.com/admin/settings/keys)
2. Navigate to **Settings > Keys**
3. Click **Generate API key**
4. Set an expiry (1-90 days) and copy the key (starts with `tskey-api-`)
You must have an **Owner**, **Admin**, **IT admin**, or **Network admin** role to generate API keys.
## Tailnet Identifier
Every operation requires a **tailnet** parameter. This is typically your organization's domain name (e.g., `example.com`). You can also use `"-"` to refer to your default tailnet.
## Common Use Cases
- **Device inventory**: List and monitor all devices connected to your network
- **Automated provisioning**: Create and manage auth keys to pre-authorize new devices
- **Access control**: Authorize or deauthorize devices, manage device tags for ACL policies
- **Route management**: View and enable subnet routes for devices acting as subnet routers
- **DNS management**: Configure nameservers, MagicDNS, and search paths
- **Key lifecycle**: Create, list, inspect, and revoke auth keys
- **User auditing**: List all users in the tailnet and their roles
- **Policy review**: Retrieve the current ACL policy for inspection or backup
## Tools
### `tailscale_list_devices`
List all devices in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `devices` | array | List of devices in the tailnet |
| ↳ `id` | string | Device ID |
| ↳ `name` | string | Device name |
| ↳ `hostname` | string | Device hostname |
| ↳ `user` | string | Associated user |
| ↳ `os` | string | Operating system |
| ↳ `clientVersion` | string | Tailscale client version |
| ↳ `addresses` | array | Tailscale IP addresses |
| ↳ `tags` | array | Device tags |
| ↳ `authorized` | boolean | Whether the device is authorized |
| ↳ `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
| ↳ `lastSeen` | string | Last seen timestamp |
| ↳ `created` | string | Creation timestamp |
| `count` | number | Total number of devices |
### `tailscale_get_device`
Get details of a specific device by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Device ID |
| `name` | string | Device name |
| `hostname` | string | Device hostname |
| `user` | string | Associated user |
| `os` | string | Operating system |
| `clientVersion` | string | Tailscale client version |
| `addresses` | array | Tailscale IP addresses |
| `tags` | array | Device tags |
| `authorized` | boolean | Whether the device is authorized |
| `blocksIncomingConnections` | boolean | Whether the device blocks incoming connections |
| `lastSeen` | string | Last seen timestamp |
| `created` | string | Creation timestamp |
| `enabledRoutes` | array | Approved subnet routes |
| `advertisedRoutes` | array | Requested subnet routes |
| `isExternal` | boolean | Whether the device is external |
| `updateAvailable` | boolean | Whether an update is available |
| `machineKey` | string | Machine key |
| `nodeKey` | string | Node key |
### `tailscale_delete_device`
Remove a device from the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the device was successfully deleted |
| `deviceId` | string | ID of the deleted device |
### `tailscale_authorize_device`
Authorize or deauthorize a device on the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID to authorize |
| `authorized` | boolean | Yes | Whether to authorize \(true\) or deauthorize \(false\) the device |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation succeeded |
| `deviceId` | string | Device ID |
| `authorized` | boolean | Authorization status after the operation |
### `tailscale_set_device_tags`
Set tags on a device in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `tags` | string | Yes | Comma-separated list of tags \(e.g., "tag:server,tag:production"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the tags were successfully set |
| `deviceId` | string | Device ID |
| `tags` | array | Tags set on the device |
### `tailscale_get_device_routes`
Get the subnet routes for a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `advertisedRoutes` | array | Subnet routes the device is advertising |
| `enabledRoutes` | array | Subnet routes that are approved/enabled |
### `tailscale_set_device_routes`
Set the enabled subnet routes for a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `routes` | string | Yes | Comma-separated list of subnet routes to enable \(e.g., "10.0.0.0/24,192.168.1.0/24"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `advertisedRoutes` | array | Subnet routes the device is advertising |
| `enabledRoutes` | array | Subnet routes that are now enabled |
### `tailscale_update_device_key`
Enable or disable key expiry on a device
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `deviceId` | string | Yes | Device ID |
| `keyExpiryDisabled` | boolean | Yes | Whether to disable key expiry \(true\) or enable it \(false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the operation succeeded |
| `deviceId` | string | Device ID |
| `keyExpiryDisabled` | boolean | Whether key expiry is now disabled |
### `tailscale_list_dns_nameservers`
Get the DNS nameservers configured for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `dns` | array | List of DNS nameserver addresses |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_set_dns_nameservers`
Set the DNS nameservers for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `dns` | string | Yes | Comma-separated list of DNS nameserver IP addresses \(e.g., "8.8.8.8,8.8.4.4"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `dns` | array | Updated list of DNS nameserver addresses |
### `tailscale_get_dns_preferences`
Get the DNS preferences for the tailnet including MagicDNS status
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `magicDNS` | boolean | Whether MagicDNS is enabled |
### `tailscale_set_dns_preferences`
Set DNS preferences for the tailnet (enable/disable MagicDNS)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `magicDNS` | boolean | Yes | Whether to enable \(true\) or disable \(false\) MagicDNS |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `magicDNS` | boolean | Updated MagicDNS status |
### `tailscale_get_dns_searchpaths`
Get the DNS search paths configured for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchPaths` | array | List of DNS search path domains |
### `tailscale_set_dns_searchpaths`
Set the DNS search paths for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `searchPaths` | string | Yes | Comma-separated list of DNS search path domains \(e.g., "corp.example.com,internal.example.com"\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `searchPaths` | array | Updated list of DNS search path domains |
### `tailscale_list_users`
List all users in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `users` | array | List of users in the tailnet |
| ↳ `id` | string | User ID |
| ↳ `displayName` | string | Display name |
| ↳ `loginName` | string | Login name / email |
| ↳ `profilePicURL` | string | Profile picture URL |
| ↳ `role` | string | User role \(owner, admin, member, etc.\) |
| ↳ `status` | string | User status \(active, suspended, etc.\) |
| ↳ `type` | string | User type \(member, shared, tagged\) |
| ↳ `created` | string | Creation timestamp |
| ↳ `lastSeen` | string | Last seen timestamp |
| ↳ `deviceCount` | number | Number of devices owned by user |
| `count` | number | Total number of users |
### `tailscale_create_auth_key`
Create a new auth key for the tailnet to pre-authorize devices
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `reusable` | boolean | No | Whether the key can be used more than once |
| `ephemeral` | boolean | No | Whether devices authenticated with this key are ephemeral |
| `preauthorized` | boolean | No | Whether devices are pre-authorized \(skip manual approval\) |
| `tags` | string | Yes | Comma-separated list of tags for devices using this key \(e.g., "tag:server,tag:prod"\) |
| `description` | string | No | Description for the auth key |
| `expirySeconds` | number | No | Key expiry time in seconds \(default: 90 days\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Auth key ID |
| `key` | string | The auth key value \(only shown once at creation\) |
| `description` | string | Key description |
| `created` | string | Creation timestamp |
| `expires` | string | Expiration timestamp |
| `revoked` | string | Revocation timestamp \(empty if not revoked\) |
| `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices using this key |
### `tailscale_list_auth_keys`
List all auth keys in the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `keys` | array | List of auth keys |
| ↳ `id` | string | Auth key ID |
| ↳ `description` | string | Key description |
| ↳ `created` | string | Creation timestamp |
| ↳ `expires` | string | Expiration timestamp |
| ↳ `revoked` | string | Revocation timestamp |
| ↳ `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices |
| `count` | number | Total number of auth keys |
### `tailscale_get_auth_key`
Get details of a specific auth key
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `keyId` | string | Yes | Auth key ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | string | Auth key ID |
| `description` | string | Key description |
| `created` | string | Creation timestamp |
| `expires` | string | Expiration timestamp |
| `revoked` | string | Revocation timestamp |
| `capabilities` | object | Key capabilities |
| ↳ `reusable` | boolean | Whether the key is reusable |
| ↳ `ephemeral` | boolean | Whether devices are ephemeral |
| ↳ `preauthorized` | boolean | Whether devices are pre-authorized |
| ↳ `tags` | array | Tags applied to devices using this key |
### `tailscale_delete_auth_key`
Revoke and delete an auth key
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
| `keyId` | string | Yes | Auth key ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `success` | boolean | Whether the auth key was successfully deleted |
| `keyId` | string | ID of the deleted auth key |
### `tailscale_get_acl`
Get the current ACL policy for the tailnet
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Tailscale API key |
| `tailnet` | string | Yes | Tailnet name \(e.g., example.com\) or "-" for default |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `acl` | string | ACL policy as JSON string |
| `etag` | string | ETag for the current ACL version \(use with If-Match header for updates\) |
@@ -156,6 +156,7 @@ import {
StagehandIcon,
StripeIcon,
SupabaseIcon,
TailscaleIcon,
TavilyIcon,
TelegramIcon,
TextractIcon,
@@ -335,6 +336,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
stripe: StripeIcon,
stt_v2: STTIcon,
supabase: SupabaseIcon,
tailscale: TailscaleIcon,
tavily: TavilyIcon,
telegram: TelegramIcon,
textract_v2: TextractIcon,
@@ -10521,6 +10521,105 @@
"integrationType": "databases",
"tags": ["cloud", "data-warehouse", "vector-search"]
},
{
"type": "tailscale",
"slug": "tailscale",
"name": "Tailscale",
"description": "Manage devices and network settings in your Tailscale tailnet",
"longDescription": "Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.",
"bgColor": "#2E2D2D",
"iconName": "TailscaleIcon",
"docsUrl": "https://docs.sim.ai/tools/tailscale",
"operations": [
{
"name": "List Devices",
"description": "List all devices in the tailnet"
},
{
"name": "Get Device",
"description": "Get details of a specific device by ID"
},
{
"name": "Delete Device",
"description": "Remove a device from the tailnet"
},
{
"name": "Authorize Device",
"description": "Authorize or deauthorize a device on the tailnet"
},
{
"name": "Set Device Tags",
"description": "Set tags on a device in the tailnet"
},
{
"name": "Get Device Routes",
"description": "Get the subnet routes for a device"
},
{
"name": "Set Device Routes",
"description": "Set the enabled subnet routes for a device"
},
{
"name": "Update Device Key",
"description": "Enable or disable key expiry on a device"
},
{
"name": "List DNS Nameservers",
"description": "Get the DNS nameservers configured for the tailnet"
},
{
"name": "Set DNS Nameservers",
"description": "Set the DNS nameservers for the tailnet"
},
{
"name": "Get DNS Preferences",
"description": "Get the DNS preferences for the tailnet including MagicDNS status"
},
{
"name": "Set DNS Preferences",
"description": "Set DNS preferences for the tailnet (enable/disable MagicDNS)"
},
{
"name": "Get DNS Search Paths",
"description": "Get the DNS search paths configured for the tailnet"
},
{
"name": "Set DNS Search Paths",
"description": "Set the DNS search paths for the tailnet"
},
{
"name": "List Users",
"description": "List all users in the tailnet"
},
{
"name": "Create Auth Key",
"description": "Create a new auth key for the tailnet to pre-authorize devices"
},
{
"name": "List Auth Keys",
"description": "List all auth keys in the tailnet"
},
{
"name": "Get Auth Key",
"description": "Get details of a specific auth key"
},
{
"name": "Delete Auth Key",
"description": "Revoke and delete an auth key"
},
{
"name": "Get ACL",
"description": "Get the current ACL policy for the tailnet"
}
],
"operationCount": 20,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
"category": "tools",
"integrationType": "security",
"tags": ["monitoring"]
},
{
"type": "tavily",
"slug": "tavily",
+341
View File
@@ -0,0 +1,341 @@
import { TailscaleIcon } from '@/components/icons'
import type { BlockConfig } from '@/blocks/types'
import { AuthMode, IntegrationType } from '@/blocks/types'
export const TailscaleBlock: BlockConfig = {
type: 'tailscale',
name: 'Tailscale',
description: 'Manage devices and network settings in your Tailscale tailnet',
longDescription:
'Interact with the Tailscale API to manage devices, DNS, ACLs, auth keys, users, and routes across your tailnet.',
docsLink: 'https://docs.sim.ai/tools/tailscale',
category: 'tools',
integrationType: IntegrationType.Security,
tags: ['monitoring'],
bgColor: '#2E2D2D',
icon: TailscaleIcon,
authMode: AuthMode.ApiKey,
subBlocks: [
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
{ label: 'List Devices', id: 'list_devices' },
{ label: 'Get Device', id: 'get_device' },
{ label: 'Delete Device', id: 'delete_device' },
{ label: 'Authorize Device', id: 'authorize_device' },
{ label: 'Set Device Tags', id: 'set_device_tags' },
{ label: 'Get Device Routes', id: 'get_device_routes' },
{ label: 'Set Device Routes', id: 'set_device_routes' },
{ label: 'Update Device Key', id: 'update_device_key' },
{ label: 'List DNS Nameservers', id: 'list_dns_nameservers' },
{ label: 'Set DNS Nameservers', id: 'set_dns_nameservers' },
{ label: 'Get DNS Preferences', id: 'get_dns_preferences' },
{ label: 'Set DNS Preferences', id: 'set_dns_preferences' },
{ label: 'Get DNS Search Paths', id: 'get_dns_searchpaths' },
{ label: 'Set DNS Search Paths', id: 'set_dns_searchpaths' },
{ label: 'List Users', id: 'list_users' },
{ label: 'Create Auth Key', id: 'create_auth_key' },
{ label: 'List Auth Keys', id: 'list_auth_keys' },
{ label: 'Get Auth Key', id: 'get_auth_key' },
{ label: 'Delete Auth Key', id: 'delete_auth_key' },
{ label: 'Get ACL', id: 'get_acl' },
],
value: () => 'list_devices',
},
{
id: 'apiKey',
title: 'API Key',
type: 'short-input',
password: true,
placeholder: 'tskey-api-...',
required: true,
},
{
id: 'tailnet',
title: 'Tailnet',
type: 'short-input',
placeholder: 'example.com or "-" for default',
required: true,
},
{
id: 'deviceId',
title: 'Device ID',
type: 'short-input',
placeholder: 'Enter device ID',
condition: {
field: 'operation',
value: [
'get_device',
'delete_device',
'authorize_device',
'set_device_tags',
'get_device_routes',
'set_device_routes',
'update_device_key',
],
},
required: {
field: 'operation',
value: [
'get_device',
'delete_device',
'authorize_device',
'set_device_tags',
'get_device_routes',
'set_device_routes',
'update_device_key',
],
},
},
{
id: 'authorized',
title: 'Authorized',
type: 'dropdown',
options: [
{ label: 'Authorize', id: 'true' },
{ label: 'Deauthorize', id: 'false' },
],
value: () => 'true',
condition: { field: 'operation', value: 'authorize_device' },
},
{
id: 'keyExpiryDisabled',
title: 'Key Expiry Disabled',
type: 'dropdown',
options: [
{ label: 'Disable Expiry', id: 'true' },
{ label: 'Enable Expiry', id: 'false' },
],
value: () => 'true',
condition: { field: 'operation', value: 'update_device_key' },
},
{
id: 'tags',
title: 'Tags',
type: 'short-input',
placeholder: 'tag:server,tag:production',
condition: { field: 'operation', value: ['set_device_tags', 'create_auth_key'] },
required: { field: 'operation', value: 'set_device_tags' },
wandConfig: {
enabled: true,
prompt:
'Generate a comma-separated list of Tailscale ACL tags. Each tag must start with "tag:" (e.g., tag:server,tag:production). Return ONLY the comma-separated tags - no explanations, no extra text.',
},
},
{
id: 'routes',
title: 'Routes',
type: 'short-input',
placeholder: '10.0.0.0/24,192.168.1.0/24',
condition: { field: 'operation', value: 'set_device_routes' },
required: { field: 'operation', value: 'set_device_routes' },
wandConfig: {
enabled: true,
prompt:
'Generate a comma-separated list of subnet routes in CIDR notation (e.g., 10.0.0.0/24,192.168.1.0/24). Return ONLY the comma-separated routes - no explanations, no extra text.',
},
},
{
id: 'dnsServers',
title: 'DNS Nameservers',
type: 'short-input',
placeholder: '8.8.8.8,8.8.4.4',
condition: { field: 'operation', value: 'set_dns_nameservers' },
required: { field: 'operation', value: 'set_dns_nameservers' },
},
{
id: 'magicDNS',
title: 'MagicDNS',
type: 'dropdown',
options: [
{ label: 'Enable', id: 'true' },
{ label: 'Disable', id: 'false' },
],
value: () => 'true',
condition: { field: 'operation', value: 'set_dns_preferences' },
},
{
id: 'searchPaths',
title: 'Search Paths',
type: 'short-input',
placeholder: 'corp.example.com,internal.example.com',
condition: { field: 'operation', value: 'set_dns_searchpaths' },
required: { field: 'operation', value: 'set_dns_searchpaths' },
},
{
id: 'keyId',
title: 'Auth Key ID',
type: 'short-input',
placeholder: 'Enter auth key ID',
condition: { field: 'operation', value: ['get_auth_key', 'delete_auth_key'] },
required: { field: 'operation', value: ['get_auth_key', 'delete_auth_key'] },
},
{
id: 'reusable',
title: 'Reusable',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'create_auth_key' },
mode: 'advanced',
},
{
id: 'ephemeral',
title: 'Ephemeral',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
condition: { field: 'operation', value: 'create_auth_key' },
mode: 'advanced',
},
{
id: 'preauthorized',
title: 'Preauthorized',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'true',
condition: { field: 'operation', value: 'create_auth_key' },
mode: 'advanced',
},
{
id: 'authKeyDescription',
title: 'Description',
type: 'short-input',
placeholder: 'Auth key description',
condition: { field: 'operation', value: 'create_auth_key' },
mode: 'advanced',
},
{
id: 'expirySeconds',
title: 'Expiry (seconds)',
type: 'short-input',
placeholder: '7776000 (90 days)',
condition: { field: 'operation', value: 'create_auth_key' },
mode: 'advanced',
},
],
tools: {
access: [
'tailscale_list_devices',
'tailscale_get_device',
'tailscale_delete_device',
'tailscale_authorize_device',
'tailscale_set_device_tags',
'tailscale_get_device_routes',
'tailscale_set_device_routes',
'tailscale_update_device_key',
'tailscale_list_dns_nameservers',
'tailscale_set_dns_nameservers',
'tailscale_get_dns_preferences',
'tailscale_set_dns_preferences',
'tailscale_get_dns_searchpaths',
'tailscale_set_dns_searchpaths',
'tailscale_list_users',
'tailscale_create_auth_key',
'tailscale_list_auth_keys',
'tailscale_get_auth_key',
'tailscale_delete_auth_key',
'tailscale_get_acl',
],
config: {
tool: (params) => `tailscale_${params.operation}`,
params: (params) => {
const mapped: Record<string, unknown> = {
apiKey: params.apiKey,
tailnet: params.tailnet,
}
if (params.deviceId) mapped.deviceId = params.deviceId
if (params.keyId) mapped.keyId = params.keyId
if (params.tags) mapped.tags = params.tags
if (params.routes) mapped.routes = params.routes
if (params.dnsServers) mapped.dns = params.dnsServers
if (params.searchPaths) mapped.searchPaths = params.searchPaths
if (params.authorized !== undefined) mapped.authorized = params.authorized === 'true'
if (params.keyExpiryDisabled !== undefined)
mapped.keyExpiryDisabled = params.keyExpiryDisabled === 'true'
if (params.magicDNS !== undefined) mapped.magicDNS = params.magicDNS === 'true'
if (params.authKeyDescription) mapped.description = params.authKeyDescription
if (params.reusable !== undefined) mapped.reusable = params.reusable === 'true'
if (params.ephemeral !== undefined) mapped.ephemeral = params.ephemeral === 'true'
if (params.preauthorized !== undefined)
mapped.preauthorized = params.preauthorized === 'true'
if (params.expirySeconds) mapped.expirySeconds = Number(params.expirySeconds)
return mapped
},
},
},
inputs: {
apiKey: { type: 'string', description: 'Tailscale API key' },
tailnet: { type: 'string', description: 'Tailnet name' },
deviceId: { type: 'string', description: 'Device ID' },
keyId: { type: 'string', description: 'Auth key ID' },
authorized: { type: 'string', description: 'Authorization status' },
keyExpiryDisabled: { type: 'string', description: 'Whether to disable key expiry' },
tags: { type: 'string', description: 'Comma-separated tags' },
routes: { type: 'string', description: 'Comma-separated subnet routes' },
dnsServers: { type: 'string', description: 'Comma-separated DNS nameserver IPs' },
magicDNS: { type: 'string', description: 'Enable or disable MagicDNS' },
searchPaths: { type: 'string', description: 'Comma-separated DNS search path domains' },
reusable: { type: 'string', description: 'Whether the auth key is reusable' },
ephemeral: { type: 'string', description: 'Whether devices are ephemeral' },
preauthorized: { type: 'string', description: 'Whether devices are pre-authorized' },
authKeyDescription: { type: 'string', description: 'Auth key description' },
expirySeconds: { type: 'string', description: 'Auth key expiry in seconds' },
},
outputs: {
devices: { type: 'json', description: 'List of devices in the tailnet' },
count: { type: 'number', description: 'Total count of items returned' },
id: { type: 'string', description: 'Device or auth key ID' },
name: { type: 'string', description: 'Device name' },
hostname: { type: 'string', description: 'Device hostname' },
user: { type: 'string', description: 'Associated user' },
os: { type: 'string', description: 'Operating system' },
clientVersion: { type: 'string', description: 'Tailscale client version' },
addresses: { type: 'json', description: 'Tailscale IP addresses' },
tags: { type: 'json', description: 'Device or auth key tags' },
authorized: { type: 'boolean', description: 'Whether the device is authorized' },
blocksIncomingConnections: {
type: 'boolean',
description: 'Whether the device blocks incoming connections',
},
lastSeen: { type: 'string', description: 'Last seen timestamp' },
created: { type: 'string', description: 'Creation timestamp' },
enabledRoutes: { type: 'json', description: 'Enabled subnet routes' },
advertisedRoutes: { type: 'json', description: 'Advertised subnet routes' },
isExternal: { type: 'boolean', description: 'Whether the device is external' },
updateAvailable: { type: 'boolean', description: 'Whether an update is available' },
machineKey: { type: 'string', description: 'Machine key' },
nodeKey: { type: 'string', description: 'Node key' },
success: { type: 'boolean', description: 'Whether the operation succeeded' },
deviceId: { type: 'string', description: 'Device ID' },
keyExpiryDisabled: { type: 'boolean', description: 'Whether key expiry is disabled' },
dns: { type: 'json', description: 'DNS nameserver addresses' },
magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' },
searchPaths: { type: 'json', description: 'DNS search paths' },
users: { type: 'json', description: 'List of users in the tailnet' },
keys: { type: 'json', description: 'List of auth keys' },
key: { type: 'string', description: 'Auth key value (only at creation)' },
keyId: { type: 'string', description: 'Auth key ID' },
description: { type: 'string', description: 'Auth key description' },
expires: { type: 'string', description: 'Expiration timestamp' },
revoked: { type: 'string', description: 'Revocation timestamp' },
capabilities: { type: 'json', description: 'Auth key capabilities' },
acl: { type: 'string', description: 'ACL policy as JSON string' },
etag: { type: 'string', description: 'ACL ETag for conditional updates' },
},
}
+2
View File
@@ -176,6 +176,7 @@ import { StripeBlock } from '@/blocks/blocks/stripe'
import { SttBlock, SttV2Block } from '@/blocks/blocks/stt'
import { SupabaseBlock } from '@/blocks/blocks/supabase'
import { TableBlock } from '@/blocks/blocks/table'
import { TailscaleBlock } from '@/blocks/blocks/tailscale'
import { TavilyBlock } from '@/blocks/blocks/tavily'
import { TelegramBlock } from '@/blocks/blocks/telegram'
import { TextractBlock, TextractV2Block } from '@/blocks/blocks/textract'
@@ -405,6 +406,7 @@ export const registry: Record<string, BlockConfig> = {
stt_v2: SttV2Block,
supabase: SupabaseBlock,
table: TableBlock,
tailscale: TailscaleBlock,
tavily: TavilyBlock,
telegram: TelegramBlock,
textract: TextractBlock,
+39
View File
@@ -683,6 +683,45 @@ export function SerperIcon(props: SVGProps<SVGSVGElement>) {
)
}
export function TailscaleIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='none' {...props}>
<path
fill='currentColor'
opacity={0.2}
d='M65.6 127.7c35.3 0 63.9-28.6 63.9-63.9S100.9 0 65.6 0 1.8 28.6 1.8 63.9s28.6 63.8 63.8 63.8'
/>
<path
fill='currentColor'
d='M65.6 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9S1.8 219 1.8 254.2s28.6 63.9 63.8 63.9'
/>
<path
fill='currentColor'
opacity={0.2}
d='M65.6 512c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.8 28.7-63.8 63.9S30.4 512 65.6 512'
/>
<path
fill='currentColor'
d='M257.2 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9m0 193.9c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9'
/>
<path
fill='currentColor'
opacity={0.2}
d='M257.2 127.7c35.3 0 63.9-28.6 63.9-63.9S292.5 0 257.2 0s-63.9 28.6-63.9 63.9 28.6 63.8 63.9 63.8m189.2 0c35.3 0 63.9-28.6 63.9-63.9S481.6 0 446.4 0c-35.3 0-63.9 28.6-63.9 63.9s28.6 63.8 63.9 63.8'
/>
<path
fill='currentColor'
d='M446.4 318.1c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9'
/>
<path
fill='currentColor'
opacity={0.2}
d='M446.4 512c35.3 0 63.9-28.6 63.9-63.9s-28.6-63.9-63.9-63.9-63.9 28.6-63.9 63.9 28.6 63.9 63.9 63.9'
/>
</svg>
)
}
export function TavilyIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg viewBox='0 0 600 600' xmlns='http://www.w3.org/2000/svg' {...props}>
+42
View File
@@ -2268,6 +2268,28 @@ import {
tableUpdateRowTool,
tableUpsertRowTool,
} from '@/tools/table'
import {
tailscaleAuthorizeDeviceTool,
tailscaleCreateAuthKeyTool,
tailscaleDeleteAuthKeyTool,
tailscaleDeleteDeviceTool,
tailscaleGetAclTool,
tailscaleGetAuthKeyTool,
tailscaleGetDeviceRoutesTool,
tailscaleGetDeviceTool,
tailscaleGetDnsPreferencesTool,
tailscaleGetDnsSearchpathsTool,
tailscaleListAuthKeysTool,
tailscaleListDevicesTool,
tailscaleListDnsNameserversTool,
tailscaleListUsersTool,
tailscaleSetDeviceRoutesTool,
tailscaleSetDeviceTagsTool,
tailscaleSetDnsNameserversTool,
tailscaleSetDnsPreferencesTool,
tailscaleSetDnsSearchpathsTool,
tailscaleUpdateDeviceKeyTool,
} from '@/tools/tailscale'
import { tavilyCrawlTool, tavilyExtractTool, tavilyMapTool, tavilySearchTool } from '@/tools/tavily'
import {
telegramDeleteMessageTool,
@@ -2971,6 +2993,26 @@ export const tools: Record<string, ToolConfig> = {
supabase_storage_delete_bucket: supabaseStorageDeleteBucketTool,
supabase_storage_get_public_url: supabaseStorageGetPublicUrlTool,
supabase_storage_create_signed_url: supabaseStorageCreateSignedUrlTool,
tailscale_list_devices: tailscaleListDevicesTool,
tailscale_get_device: tailscaleGetDeviceTool,
tailscale_delete_device: tailscaleDeleteDeviceTool,
tailscale_authorize_device: tailscaleAuthorizeDeviceTool,
tailscale_set_device_tags: tailscaleSetDeviceTagsTool,
tailscale_get_device_routes: tailscaleGetDeviceRoutesTool,
tailscale_set_device_routes: tailscaleSetDeviceRoutesTool,
tailscale_update_device_key: tailscaleUpdateDeviceKeyTool,
tailscale_list_dns_nameservers: tailscaleListDnsNameserversTool,
tailscale_set_dns_nameservers: tailscaleSetDnsNameserversTool,
tailscale_get_dns_preferences: tailscaleGetDnsPreferencesTool,
tailscale_set_dns_preferences: tailscaleSetDnsPreferencesTool,
tailscale_get_dns_searchpaths: tailscaleGetDnsSearchpathsTool,
tailscale_set_dns_searchpaths: tailscaleSetDnsSearchpathsTool,
tailscale_list_users: tailscaleListUsersTool,
tailscale_create_auth_key: tailscaleCreateAuthKeyTool,
tailscale_list_auth_keys: tailscaleListAuthKeysTool,
tailscale_get_auth_key: tailscaleGetAuthKeyTool,
tailscale_delete_auth_key: tailscaleDeleteAuthKeyTool,
tailscale_get_acl: tailscaleGetAclTool,
calendly_get_current_user: calendlyGetCurrentUserTool,
calendly_list_event_types: calendlyListEventTypesTool,
calendly_get_event_type: calendlyGetEventTypeTool,
@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleAuthorizeDeviceParams, TailscaleAuthorizeDeviceResponse } from './types'
export const tailscaleAuthorizeDeviceTool: ToolConfig<
TailscaleAuthorizeDeviceParams,
TailscaleAuthorizeDeviceResponse
> = {
id: 'tailscale_authorize_device',
name: 'Tailscale Authorize Device',
description: 'Authorize or deauthorize a device on the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
deviceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID to authorize',
},
authorized: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'Whether to authorize (true) or deauthorize (false) the device',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/authorized`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
authorized: params.authorized,
}),
},
transformResponse: async (response: Response, params?: TailscaleAuthorizeDeviceParams) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { success: false, deviceId: '', authorized: false },
error: (data as Record<string, string>).message ?? 'Failed to authorize device',
}
}
return {
success: true,
output: {
success: true,
deviceId: params?.deviceId ?? '',
authorized: params?.authorized ?? true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the operation succeeded' },
deviceId: { type: 'string', description: 'Device ID' },
authorized: { type: 'boolean', description: 'Authorization status after the operation' },
},
}
+172
View File
@@ -0,0 +1,172 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleCreateAuthKeyParams, TailscaleCreateAuthKeyResponse } from './types'
export const tailscaleCreateAuthKeyTool: ToolConfig<
TailscaleCreateAuthKeyParams,
TailscaleCreateAuthKeyResponse
> = {
id: 'tailscale_create_auth_key',
name: 'Tailscale Create Auth Key',
description: 'Create a new auth key for the tailnet to pre-authorize devices',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
reusable: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether the key can be used more than once',
default: false,
},
ephemeral: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether devices authenticated with this key are ephemeral',
default: false,
},
preauthorized: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Whether devices are pre-authorized (skip manual approval)',
default: true,
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Comma-separated list of tags for devices using this key (e.g., "tag:server,tag:prod")',
},
description: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Description for the auth key',
},
expirySeconds: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Key expiry time in seconds (default: 90 days)',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => {
const tags = params.tags
? params.tags
.split(',')
.map((t) => t.trim())
.filter(Boolean)
: []
const createCaps: Record<string, unknown> = {
reusable: params.reusable ?? false,
ephemeral: params.ephemeral ?? false,
preauthorized: params.preauthorized ?? true,
}
if (tags.length > 0) {
createCaps.tags = tags
}
const body: Record<string, unknown> = {
capabilities: {
devices: {
create: createCaps,
},
},
}
if (params.description) body.description = params.description
if (params.expirySeconds !== undefined && params.expirySeconds !== null)
body.expirySeconds = params.expirySeconds
return body
},
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: {
id: '',
key: '',
description: '',
created: '',
expires: '',
revoked: '',
capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] },
},
error: (data as Record<string, string>).message ?? 'Failed to create auth key',
}
}
const data = await response.json()
const deviceCaps = data.capabilities?.devices?.create ?? {}
return {
success: true,
output: {
id: data.id ?? null,
key: data.key ?? null,
description: data.description ?? null,
created: data.created ?? null,
expires: data.expires ?? null,
revoked: data.revoked ?? null,
capabilities: {
reusable: deviceCaps.reusable ?? false,
ephemeral: deviceCaps.ephemeral ?? false,
preauthorized: deviceCaps.preauthorized ?? false,
tags: deviceCaps.tags ?? [],
},
},
}
},
outputs: {
id: { type: 'string', description: 'Auth key ID' },
key: { type: 'string', description: 'The auth key value (only shown once at creation)' },
description: { type: 'string', description: 'Key description', optional: true },
created: { type: 'string', description: 'Creation timestamp' },
expires: { type: 'string', description: 'Expiration timestamp' },
revoked: {
type: 'string',
description: 'Revocation timestamp (empty if not revoked)',
optional: true,
},
capabilities: {
type: 'object',
description: 'Key capabilities',
properties: {
reusable: { type: 'boolean', description: 'Whether the key is reusable' },
ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' },
preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' },
tags: { type: 'array', description: 'Tags applied to devices using this key' },
},
},
},
}
@@ -0,0 +1,78 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface TailscaleDeleteAuthKeyParams {
apiKey: string
tailnet: string
keyId: string
}
interface TailscaleDeleteAuthKeyResponse extends ToolResponse {
output: {
success: boolean
keyId: string
}
}
export const tailscaleDeleteAuthKeyTool: ToolConfig<
TailscaleDeleteAuthKeyParams,
TailscaleDeleteAuthKeyResponse
> = {
id: 'tailscale_delete_auth_key',
name: 'Tailscale Delete Auth Key',
description: 'Revoke and delete an auth key',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
keyId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Auth key ID to delete',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response: Response, params?: TailscaleDeleteAuthKeyParams) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { success: false, keyId: '' },
error: (data as Record<string, string>).message ?? 'Failed to delete auth key',
}
}
return {
success: true,
output: {
success: true,
keyId: params?.keyId ?? '',
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the auth key was successfully deleted' },
keyId: { type: 'string', description: 'ID of the deleted auth key' },
},
}
+66
View File
@@ -0,0 +1,66 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleDeleteDeviceResponse, TailscaleDeviceParams } from './types'
export const tailscaleDeleteDeviceTool: ToolConfig<
TailscaleDeviceParams,
TailscaleDeleteDeviceResponse
> = {
id: 'tailscale_delete_device',
name: 'Tailscale Delete Device',
description: 'Remove a device from the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
deviceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID to delete',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}`,
method: 'DELETE',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response: Response, params?: TailscaleDeviceParams) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { success: false, deviceId: '' },
error: (data as Record<string, string>).message ?? 'Failed to delete device',
}
}
return {
success: true,
output: {
success: true,
deviceId: params?.deviceId ?? '',
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the device was successfully deleted' },
deviceId: { type: 'string', description: 'ID of the deleted device' },
},
}
+72
View File
@@ -0,0 +1,72 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { TailscaleBaseParams } from './types'
interface TailscaleGetAclResponse extends ToolResponse {
output: {
acl: string
etag: string
}
}
export const tailscaleGetAclTool: ToolConfig<TailscaleBaseParams, TailscaleGetAclResponse> = {
id: 'tailscale_get_acl',
name: 'Tailscale Get ACL',
description: 'Get the current ACL policy for the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/acl`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
Accept: 'application/json',
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { acl: '', etag: '' },
error: (data as Record<string, string>).message ?? 'Failed to get ACL',
}
}
const etag = response.headers.get('ETag') ?? ''
const data = await response.json()
return {
success: true,
output: {
acl: JSON.stringify(data, null, 2),
etag,
},
}
},
outputs: {
acl: { type: 'string', description: 'ACL policy as JSON string' },
etag: {
type: 'string',
description: 'ETag for the current ACL version (use with If-Match header for updates)',
optional: true,
},
},
}
+119
View File
@@ -0,0 +1,119 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface TailscaleGetAuthKeyParams {
apiKey: string
tailnet: string
keyId: string
}
interface TailscaleGetAuthKeyResponse extends ToolResponse {
output: {
id: string
description: string
created: string
expires: string
revoked: string
capabilities: {
reusable: boolean
ephemeral: boolean
preauthorized: boolean
tags: string[]
}
}
}
export const tailscaleGetAuthKeyTool: ToolConfig<
TailscaleGetAuthKeyParams,
TailscaleGetAuthKeyResponse
> = {
id: 'tailscale_get_auth_key',
name: 'Tailscale Get Auth Key',
description: 'Get details of a specific auth key',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
keyId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Auth key ID',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys/${encodeURIComponent(params.keyId.trim())}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: {
id: '',
description: '',
created: '',
expires: '',
revoked: '',
capabilities: { reusable: false, ephemeral: false, preauthorized: false, tags: [] },
},
error: (data as Record<string, string>).message ?? 'Failed to get auth key',
}
}
const data = await response.json()
const deviceCaps = data.capabilities?.devices?.create ?? {}
return {
success: true,
output: {
id: data.id ?? null,
description: data.description ?? null,
created: data.created ?? null,
expires: data.expires ?? null,
revoked: data.revoked ?? null,
capabilities: {
reusable: deviceCaps.reusable ?? false,
ephemeral: deviceCaps.ephemeral ?? false,
preauthorized: deviceCaps.preauthorized ?? false,
tags: deviceCaps.tags ?? [],
},
},
}
},
outputs: {
id: { type: 'string', description: 'Auth key ID' },
description: { type: 'string', description: 'Key description', optional: true },
created: { type: 'string', description: 'Creation timestamp' },
expires: { type: 'string', description: 'Expiration timestamp' },
revoked: { type: 'string', description: 'Revocation timestamp', optional: true },
capabilities: {
type: 'object',
description: 'Key capabilities',
properties: {
reusable: { type: 'boolean', description: 'Whether the key is reusable' },
ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' },
preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' },
tags: { type: 'array', description: 'Tags applied to devices using this key' },
},
},
},
}
+121
View File
@@ -0,0 +1,121 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleDeviceParams, TailscaleGetDeviceResponse } from './types'
export const tailscaleGetDeviceTool: ToolConfig<TailscaleDeviceParams, TailscaleGetDeviceResponse> =
{
id: 'tailscale_get_device',
name: 'Tailscale Get Device',
description: 'Get details of a specific device by ID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
deviceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: {
id: '',
name: '',
hostname: '',
user: '',
os: '',
clientVersion: '',
addresses: [],
tags: [],
authorized: false,
blocksIncomingConnections: false,
lastSeen: '',
created: '',
isExternal: false,
updateAvailable: false,
machineKey: '',
nodeKey: '',
},
error: (data as Record<string, string>).message ?? 'Failed to get device',
}
}
const data = await response.json()
return {
success: true,
output: {
id: data.id ?? null,
name: data.name ?? null,
hostname: data.hostname ?? null,
user: data.user ?? null,
os: data.os ?? null,
clientVersion: data.clientVersion ?? null,
addresses: data.addresses ?? [],
tags: data.tags ?? [],
authorized: data.authorized ?? false,
blocksIncomingConnections: data.blocksIncomingConnections ?? false,
lastSeen: data.lastSeen ?? null,
created: data.created ?? null,
isExternal: data.isExternal ?? false,
updateAvailable: data.updateAvailable ?? false,
machineKey: data.machineKey ?? null,
nodeKey: data.nodeKey ?? null,
},
}
},
outputs: {
id: { type: 'string', description: 'Device ID' },
name: { type: 'string', description: 'Device name' },
hostname: { type: 'string', description: 'Device hostname' },
user: { type: 'string', description: 'Associated user' },
os: { type: 'string', description: 'Operating system' },
clientVersion: { type: 'string', description: 'Tailscale client version' },
addresses: { type: 'array', description: 'Tailscale IP addresses' },
tags: { type: 'array', description: 'Device tags' },
authorized: { type: 'boolean', description: 'Whether the device is authorized' },
blocksIncomingConnections: {
type: 'boolean',
description: 'Whether the device blocks incoming connections',
},
lastSeen: { type: 'string', description: 'Last seen timestamp' },
created: { type: 'string', description: 'Creation timestamp' },
isExternal: {
type: 'boolean',
description: 'Whether the device is external',
optional: true,
},
updateAvailable: {
type: 'boolean',
description: 'Whether an update is available',
optional: true,
},
machineKey: { type: 'string', description: 'Machine key', optional: true },
nodeKey: { type: 'string', description: 'Node key', optional: true },
},
}
@@ -0,0 +1,67 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleDeviceParams, TailscaleGetDeviceRoutesResponse } from './types'
export const tailscaleGetDeviceRoutesTool: ToolConfig<
TailscaleDeviceParams,
TailscaleGetDeviceRoutesResponse
> = {
id: 'tailscale_get_device_routes',
name: 'Tailscale Get Device Routes',
description: 'Get the subnet routes for a device',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
deviceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { advertisedRoutes: [], enabledRoutes: [] },
error: (data as Record<string, string>).message ?? 'Failed to get device routes',
}
}
const data = await response.json()
return {
success: true,
output: {
advertisedRoutes: data.advertisedRoutes ?? [],
enabledRoutes: data.enabledRoutes ?? [],
},
}
},
outputs: {
advertisedRoutes: { type: 'array', description: 'Subnet routes the device is advertising' },
enabledRoutes: { type: 'array', description: 'Subnet routes that are approved/enabled' },
},
}
@@ -0,0 +1,65 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { TailscaleBaseParams } from './types'
interface TailscaleGetDnsPreferencesResponse extends ToolResponse {
output: {
magicDNS: boolean
}
}
export const tailscaleGetDnsPreferencesTool: ToolConfig<
TailscaleBaseParams,
TailscaleGetDnsPreferencesResponse
> = {
id: 'tailscale_get_dns_preferences',
name: 'Tailscale Get DNS Preferences',
description: 'Get the DNS preferences for the tailnet including MagicDNS status',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { magicDNS: false },
error: (data as Record<string, string>).message ?? 'Failed to get DNS preferences',
}
}
const data = await response.json()
return {
success: true,
output: {
magicDNS: data.magicDNS ?? false,
},
}
},
outputs: {
magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' },
},
}
@@ -0,0 +1,65 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { TailscaleBaseParams } from './types'
interface TailscaleGetDnsSearchpathsResponse extends ToolResponse {
output: {
searchPaths: string[]
}
}
export const tailscaleGetDnsSearchpathsTool: ToolConfig<
TailscaleBaseParams,
TailscaleGetDnsSearchpathsResponse
> = {
id: 'tailscale_get_dns_searchpaths',
name: 'Tailscale Get DNS Search Paths',
description: 'Get the DNS search paths configured for the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { searchPaths: [] },
error: (data as Record<string, string>).message ?? 'Failed to get DNS search paths',
}
}
const data = await response.json()
return {
success: true,
output: {
searchPaths: data.searchPaths ?? [],
},
}
},
outputs: {
searchPaths: { type: 'array', description: 'List of DNS search path domains' },
},
}
+21
View File
@@ -0,0 +1,21 @@
export { tailscaleAuthorizeDeviceTool } from './authorize_device'
export { tailscaleCreateAuthKeyTool } from './create_auth_key'
export { tailscaleDeleteAuthKeyTool } from './delete_auth_key'
export { tailscaleDeleteDeviceTool } from './delete_device'
export { tailscaleGetAclTool } from './get_acl'
export { tailscaleGetAuthKeyTool } from './get_auth_key'
export { tailscaleGetDeviceTool } from './get_device'
export { tailscaleGetDeviceRoutesTool } from './get_device_routes'
export { tailscaleGetDnsPreferencesTool } from './get_dns_preferences'
export { tailscaleGetDnsSearchpathsTool } from './get_dns_searchpaths'
export { tailscaleListAuthKeysTool } from './list_auth_keys'
export { tailscaleListDevicesTool } from './list_devices'
export { tailscaleListDnsNameserversTool } from './list_dns_nameservers'
export { tailscaleListUsersTool } from './list_users'
export { tailscaleSetDeviceRoutesTool } from './set_device_routes'
export { tailscaleSetDeviceTagsTool } from './set_device_tags'
export { tailscaleSetDnsNameserversTool } from './set_dns_nameservers'
export { tailscaleSetDnsPreferencesTool } from './set_dns_preferences'
export { tailscaleSetDnsSearchpathsTool } from './set_dns_searchpaths'
export * from './types'
export { tailscaleUpdateDeviceKeyTool } from './update_device_key'
+126
View File
@@ -0,0 +1,126 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { TailscaleBaseParams } from './types'
interface TailscaleAuthKeyOutput {
id: string
description: string
created: string
expires: string
revoked: string
capabilities: {
reusable: boolean
ephemeral: boolean
preauthorized: boolean
tags: string[]
}
}
interface TailscaleListAuthKeysResponse extends ToolResponse {
output: {
keys: TailscaleAuthKeyOutput[]
count: number
}
}
export const tailscaleListAuthKeysTool: ToolConfig<
TailscaleBaseParams,
TailscaleListAuthKeysResponse
> = {
id: 'tailscale_list_auth_keys',
name: 'Tailscale List Auth Keys',
description: 'List all auth keys in the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/keys`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { keys: [], count: 0 },
error: (data as Record<string, string>).message ?? 'Failed to list auth keys',
}
}
const data = await response.json()
const keys = (data.keys ?? []).map((key: Record<string, unknown>) => {
const caps = (key.capabilities as Record<string, unknown>)?.devices as Record<string, unknown>
const create = caps?.create as Record<string, unknown>
return {
id: (key.id as string) ?? null,
description: (key.description as string) ?? null,
created: (key.created as string) ?? null,
expires: (key.expires as string) ?? null,
revoked: (key.revoked as string) ?? null,
capabilities: {
reusable: (create?.reusable as boolean) ?? false,
ephemeral: (create?.ephemeral as boolean) ?? false,
preauthorized: (create?.preauthorized as boolean) ?? false,
tags: (create?.tags as string[]) ?? [],
},
}
})
return {
success: true,
output: {
keys,
count: keys.length,
},
}
},
outputs: {
keys: {
type: 'array',
description: 'List of auth keys',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Auth key ID' },
description: { type: 'string', description: 'Key description' },
created: { type: 'string', description: 'Creation timestamp' },
expires: { type: 'string', description: 'Expiration timestamp' },
revoked: { type: 'string', description: 'Revocation timestamp' },
capabilities: {
type: 'object',
description: 'Key capabilities',
properties: {
reusable: { type: 'boolean', description: 'Whether the key is reusable' },
ephemeral: { type: 'boolean', description: 'Whether devices are ephemeral' },
preauthorized: { type: 'boolean', description: 'Whether devices are pre-authorized' },
tags: { type: 'array', description: 'Tags applied to devices' },
},
},
},
},
},
count: {
type: 'number',
description: 'Total number of auth keys',
},
},
}
+102
View File
@@ -0,0 +1,102 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleBaseParams, TailscaleListDevicesResponse } from './types'
export const tailscaleListDevicesTool: ToolConfig<
TailscaleBaseParams,
TailscaleListDevicesResponse
> = {
id: 'tailscale_list_devices',
name: 'Tailscale List Devices',
description: 'List all devices in the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/devices`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { devices: [], count: 0 },
error: (data as Record<string, string>).message ?? 'Failed to list devices',
}
}
const data = await response.json()
const devices = (data.devices ?? []).map((device: Record<string, unknown>) => ({
id: (device.id as string) ?? null,
name: (device.name as string) ?? null,
hostname: (device.hostname as string) ?? null,
user: (device.user as string) ?? null,
os: (device.os as string) ?? null,
clientVersion: (device.clientVersion as string) ?? null,
addresses: (device.addresses as string[]) ?? [],
tags: (device.tags as string[]) ?? [],
authorized: (device.authorized as boolean) ?? false,
blocksIncomingConnections: (device.blocksIncomingConnections as boolean) ?? false,
lastSeen: (device.lastSeen as string) ?? null,
created: (device.created as string) ?? null,
}))
return {
success: true,
output: {
devices,
count: devices.length,
},
}
},
outputs: {
devices: {
type: 'array',
description: 'List of devices in the tailnet',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Device ID' },
name: { type: 'string', description: 'Device name' },
hostname: { type: 'string', description: 'Device hostname' },
user: { type: 'string', description: 'Associated user' },
os: { type: 'string', description: 'Operating system' },
clientVersion: { type: 'string', description: 'Tailscale client version' },
addresses: { type: 'array', description: 'Tailscale IP addresses' },
tags: { type: 'array', description: 'Device tags' },
authorized: { type: 'boolean', description: 'Whether the device is authorized' },
blocksIncomingConnections: {
type: 'boolean',
description: 'Whether the device blocks incoming connections',
},
lastSeen: { type: 'string', description: 'Last seen timestamp' },
created: { type: 'string', description: 'Creation timestamp' },
},
},
},
count: {
type: 'number',
description: 'Total number of devices',
},
},
}
@@ -0,0 +1,61 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleBaseParams, TailscaleListDnsNameserversResponse } from './types'
export const tailscaleListDnsNameserversTool: ToolConfig<
TailscaleBaseParams,
TailscaleListDnsNameserversResponse
> = {
id: 'tailscale_list_dns_nameservers',
name: 'Tailscale List DNS Nameservers',
description: 'Get the DNS nameservers configured for the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { dns: [], magicDNS: false },
error: (data as Record<string, string>).message ?? 'Failed to list DNS nameservers',
}
}
const data = await response.json()
return {
success: true,
output: {
dns: data.dns ?? [],
magicDNS: data.magicDNS ?? false,
},
}
},
outputs: {
dns: { type: 'array', description: 'List of DNS nameserver addresses' },
magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' },
},
}
+96
View File
@@ -0,0 +1,96 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleBaseParams, TailscaleListUsersResponse } from './types'
export const tailscaleListUsersTool: ToolConfig<TailscaleBaseParams, TailscaleListUsersResponse> = {
id: 'tailscale_list_users',
name: 'Tailscale List Users',
description: 'List all users in the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/users`,
method: 'GET',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { users: [], count: 0 },
error: (data as Record<string, string>).message ?? 'Failed to list users',
}
}
const data = await response.json()
const users = (data.users ?? []).map((user: Record<string, unknown>) => ({
id: (user.id as string) ?? null,
displayName: (user.displayName as string) ?? null,
loginName: (user.loginName as string) ?? null,
profilePicURL: (user.profilePicURL as string) ?? null,
role: (user.role as string) ?? null,
status: (user.status as string) ?? null,
type: (user.type as string) ?? null,
created: (user.created as string) ?? null,
lastSeen: (user.lastSeen as string) ?? null,
deviceCount: (user.deviceCount as number) ?? 0,
}))
return {
success: true,
output: {
users,
count: users.length,
},
}
},
outputs: {
users: {
type: 'array',
description: 'List of users in the tailnet',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
displayName: { type: 'string', description: 'Display name' },
loginName: { type: 'string', description: 'Login name / email' },
profilePicURL: { type: 'string', description: 'Profile picture URL', optional: true },
role: { type: 'string', description: 'User role (owner, admin, member, etc.)' },
status: { type: 'string', description: 'User status (active, suspended, etc.)' },
type: { type: 'string', description: 'User type (member, shared, tagged)' },
created: { type: 'string', description: 'Creation timestamp' },
lastSeen: { type: 'string', description: 'Last seen timestamp', optional: true },
deviceCount: {
type: 'number',
description: 'Number of devices owned by user',
optional: true,
},
},
},
},
count: {
type: 'number',
description: 'Total number of users',
},
},
}
@@ -0,0 +1,81 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleSetDeviceRoutesParams, TailscaleSetDeviceRoutesResponse } from './types'
export const tailscaleSetDeviceRoutesTool: ToolConfig<
TailscaleSetDeviceRoutesParams,
TailscaleSetDeviceRoutesResponse
> = {
id: 'tailscale_set_device_routes',
name: 'Tailscale Set Device Routes',
description: 'Set the enabled subnet routes for a device',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
deviceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID',
},
routes: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated list of subnet routes to enable (e.g., "10.0.0.0/24,192.168.1.0/24")',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/routes`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
routes: params.routes
.split(',')
.map((r) => r.trim())
.filter(Boolean),
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { advertisedRoutes: [], enabledRoutes: [] },
error: (data as Record<string, string>).message ?? 'Failed to set device routes',
}
}
const data = await response.json()
return {
success: true,
output: {
advertisedRoutes: data.advertisedRoutes ?? [],
enabledRoutes: data.enabledRoutes ?? [],
},
}
},
outputs: {
advertisedRoutes: { type: 'array', description: 'Subnet routes the device is advertising' },
enabledRoutes: { type: 'array', description: 'Subnet routes that are now enabled' },
},
}
@@ -0,0 +1,88 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleSetDeviceTagsParams, TailscaleSetDeviceTagsResponse } from './types'
export const tailscaleSetDeviceTagsTool: ToolConfig<
TailscaleSetDeviceTagsParams,
TailscaleSetDeviceTagsResponse
> = {
id: 'tailscale_set_device_tags',
name: 'Tailscale Set Device Tags',
description: 'Set tags on a device in the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
deviceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID',
},
tags: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of tags (e.g., "tag:server,tag:production")',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/tags`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
tags: params.tags
.split(',')
.map((t) => t.trim())
.filter(Boolean),
}),
},
transformResponse: async (response: Response, params?: TailscaleSetDeviceTagsParams) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { success: false, deviceId: '', tags: [] },
error: (data as Record<string, string>).message ?? 'Failed to set device tags',
}
}
const tags = params?.tags
? params.tags
.split(',')
.map((t) => t.trim())
.filter(Boolean)
: []
return {
success: true,
output: {
success: true,
deviceId: params?.deviceId ?? '',
tags,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the tags were successfully set' },
deviceId: { type: 'string', description: 'Device ID' },
tags: { type: 'array', description: 'Tags set on the device' },
},
}
@@ -0,0 +1,86 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface TailscaleSetDnsNameserversParams {
apiKey: string
tailnet: string
dns: string
}
interface TailscaleSetDnsNameserversResponse extends ToolResponse {
output: {
dns: string[]
magicDNS: boolean
}
}
export const tailscaleSetDnsNameserversTool: ToolConfig<
TailscaleSetDnsNameserversParams,
TailscaleSetDnsNameserversResponse
> = {
id: 'tailscale_set_dns_nameservers',
name: 'Tailscale Set DNS Nameservers',
description: 'Set the DNS nameservers for the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
dns: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Comma-separated list of DNS nameserver IP addresses (e.g., "8.8.8.8,8.8.4.4")',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/nameservers`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
dns: params.dns
.split(',')
.map((s) => s.trim())
.filter(Boolean),
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { dns: [], magicDNS: false },
error: (data as Record<string, string>).message ?? 'Failed to set DNS nameservers',
}
}
const data = await response.json()
return {
success: true,
output: {
dns: data.dns ?? [],
magicDNS: data.magicDNS ?? false,
},
}
},
outputs: {
dns: { type: 'array', description: 'Updated list of DNS nameserver addresses' },
magicDNS: { type: 'boolean', description: 'Whether MagicDNS is enabled' },
},
}
@@ -0,0 +1,80 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface TailscaleSetDnsPreferencesParams {
apiKey: string
tailnet: string
magicDNS: boolean
}
interface TailscaleSetDnsPreferencesResponse extends ToolResponse {
output: {
magicDNS: boolean
}
}
export const tailscaleSetDnsPreferencesTool: ToolConfig<
TailscaleSetDnsPreferencesParams,
TailscaleSetDnsPreferencesResponse
> = {
id: 'tailscale_set_dns_preferences',
name: 'Tailscale Set DNS Preferences',
description: 'Set DNS preferences for the tailnet (enable/disable MagicDNS)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
magicDNS: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'Whether to enable (true) or disable (false) MagicDNS',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/preferences`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
magicDNS: params.magicDNS,
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { magicDNS: false },
error: (data as Record<string, string>).message ?? 'Failed to set DNS preferences',
}
}
const data = await response.json()
return {
success: true,
output: {
magicDNS: data.magicDNS ?? false,
},
}
},
outputs: {
magicDNS: { type: 'boolean', description: 'Updated MagicDNS status' },
},
}
@@ -0,0 +1,84 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface TailscaleSetDnsSearchpathsParams {
apiKey: string
tailnet: string
searchPaths: string
}
interface TailscaleSetDnsSearchpathsResponse extends ToolResponse {
output: {
searchPaths: string[]
}
}
export const tailscaleSetDnsSearchpathsTool: ToolConfig<
TailscaleSetDnsSearchpathsParams,
TailscaleSetDnsSearchpathsResponse
> = {
id: 'tailscale_set_dns_searchpaths',
name: 'Tailscale Set DNS Search Paths',
description: 'Set the DNS search paths for the tailnet',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
searchPaths: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'Comma-separated list of DNS search path domains (e.g., "corp.example.com,internal.example.com")',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/dns/searchpaths`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
searchPaths: params.searchPaths
.split(',')
.map((s) => s.trim())
.filter(Boolean),
}),
},
transformResponse: async (response) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { searchPaths: [] },
error: (data as Record<string, string>).message ?? 'Failed to set DNS search paths',
}
}
const data = await response.json()
return {
success: true,
output: {
searchPaths: data.searchPaths ?? [],
},
}
},
outputs: {
searchPaths: { type: 'array', description: 'Updated list of DNS search path domains' },
},
}
+155
View File
@@ -0,0 +1,155 @@
import type { ToolResponse } from '@/tools/types'
export interface TailscaleBaseParams {
apiKey: string
tailnet: string
}
export interface TailscaleDeviceParams extends TailscaleBaseParams {
deviceId: string
}
export interface TailscaleSetDeviceTagsParams extends TailscaleDeviceParams {
tags: string
}
export interface TailscaleAuthorizeDeviceParams extends TailscaleDeviceParams {
authorized: boolean
}
export interface TailscaleSetDeviceRoutesParams extends TailscaleDeviceParams {
routes: string
}
export interface TailscaleCreateAuthKeyParams extends TailscaleBaseParams {
reusable: boolean
ephemeral: boolean
preauthorized: boolean
tags?: string
description?: string
expirySeconds?: number
}
export interface TailscaleDeviceOutput {
id: string
name: string
hostname: string
user: string
os: string
clientVersion: string
addresses: string[]
tags: string[]
authorized: boolean
blocksIncomingConnections: boolean
lastSeen: string
created: string
}
export interface TailscaleUserOutput {
id: string
displayName: string
loginName: string
profilePicURL: string
role: string
status: string
type: string
created: string
lastSeen: string
deviceCount: number
}
export interface TailscaleListDevicesResponse extends ToolResponse {
output: {
devices: TailscaleDeviceOutput[]
count: number
}
}
export interface TailscaleGetDeviceResponse extends ToolResponse {
output: TailscaleDeviceOutput & {
isExternal: boolean
updateAvailable: boolean
machineKey: string
nodeKey: string
}
}
export interface TailscaleUpdateDeviceKeyParams extends TailscaleDeviceParams {
keyExpiryDisabled: boolean
}
export interface TailscaleUpdateDeviceKeyResponse extends ToolResponse {
output: {
success: boolean
deviceId: string
keyExpiryDisabled: boolean
}
}
export interface TailscaleDeleteDeviceResponse extends ToolResponse {
output: {
success: boolean
deviceId: string
}
}
export interface TailscaleAuthorizeDeviceResponse extends ToolResponse {
output: {
success: boolean
deviceId: string
authorized: boolean
}
}
export interface TailscaleSetDeviceTagsResponse extends ToolResponse {
output: {
success: boolean
deviceId: string
tags: string[]
}
}
export interface TailscaleGetDeviceRoutesResponse extends ToolResponse {
output: {
advertisedRoutes: string[]
enabledRoutes: string[]
}
}
export interface TailscaleSetDeviceRoutesResponse extends ToolResponse {
output: {
advertisedRoutes: string[]
enabledRoutes: string[]
}
}
export interface TailscaleListDnsNameserversResponse extends ToolResponse {
output: {
dns: string[]
magicDNS: boolean
}
}
export interface TailscaleListUsersResponse extends ToolResponse {
output: {
users: TailscaleUserOutput[]
count: number
}
}
export interface TailscaleCreateAuthKeyResponse extends ToolResponse {
output: {
id: string
key: string
description: string
created: string
expires: string
revoked: string
capabilities: {
reusable: boolean
ephemeral: boolean
preauthorized: boolean
tags: string[]
}
}
}
@@ -0,0 +1,78 @@
import type { ToolConfig } from '@/tools/types'
import type { TailscaleUpdateDeviceKeyParams, TailscaleUpdateDeviceKeyResponse } from './types'
export const tailscaleUpdateDeviceKeyTool: ToolConfig<
TailscaleUpdateDeviceKeyParams,
TailscaleUpdateDeviceKeyResponse
> = {
id: 'tailscale_update_device_key',
name: 'Tailscale Update Device Key',
description: 'Enable or disable key expiry on a device',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Tailscale API key',
},
tailnet: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Tailnet name (e.g., example.com) or "-" for default',
},
deviceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Device ID',
},
keyExpiryDisabled: {
type: 'boolean',
required: true,
visibility: 'user-or-llm',
description: 'Whether to disable key expiry (true) or enable it (false)',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/key`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
}),
body: (params) => ({
keyExpiryDisabled: params.keyExpiryDisabled,
}),
},
transformResponse: async (response: Response, params?: TailscaleUpdateDeviceKeyParams) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { success: false, deviceId: '', keyExpiryDisabled: false },
error: (data as Record<string, string>).message ?? 'Failed to update device key',
}
}
return {
success: true,
output: {
success: true,
deviceId: params?.deviceId ?? '',
keyExpiryDisabled: params?.keyExpiryDisabled ?? true,
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the operation succeeded' },
deviceId: { type: 'string', description: 'Device ID' },
keyExpiryDisabled: { type: 'boolean', description: 'Whether key expiry is now disabled' },
},
}