chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -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' },
},
}
+77
View File
@@ -0,0 +1,77 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { TailscaleBaseParams } from './types'
interface TailscaleDeleteUserParams extends TailscaleBaseParams {
userId: string
}
interface TailscaleDeleteUserResponse extends ToolResponse {
output: {
success: boolean
userId: string
}
}
export const tailscaleDeleteUserTool: ToolConfig<
TailscaleDeleteUserParams,
TailscaleDeleteUserResponse
> = {
id: 'tailscale_delete_user',
name: 'Tailscale Delete User',
description: 'Delete a user 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',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID to delete',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/users/${encodeURIComponent(params.userId.trim())}/delete`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response: Response, params?: TailscaleDeleteUserParams) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { success: false, userId: '' },
error: (data as Record<string, string>).message ?? 'Failed to delete user',
}
}
return {
success: true,
output: {
success: true,
userId: params?.userId ?? '',
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the user was successfully deleted' },
userId: { type: 'string', description: 'ID of the deleted user' },
},
}
@@ -0,0 +1,74 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { TailscaleDeviceParams } from './types'
interface TailscaleExpireDeviceKeyResponse extends ToolResponse {
output: {
success: boolean
deviceId: string
}
}
export const tailscaleExpireDeviceKeyTool: ToolConfig<
TailscaleDeviceParams,
TailscaleExpireDeviceKeyResponse
> = {
id: 'tailscale_expire_device_key',
name: 'Tailscale Expire Device Key',
description:
"Immediately expire a device's node key, requiring it to re-authenticate before it can reconnect to 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 expire the key for',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/device/${encodeURIComponent(params.deviceId.trim())}/expire`,
method: 'POST',
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 expire device key',
}
}
return {
success: true,
output: {
success: true,
deviceId: params?.deviceId ?? '',
},
}
},
outputs: {
success: { type: 'boolean', description: "Whether the device's key was successfully expired" },
deviceId: { type: 'string', description: 'Device ID' },
},
}
+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' },
},
},
},
}
+138
View File
@@ -0,0 +1,138 @@
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: '',
nodeId: '',
name: '',
hostname: '',
user: '',
os: '',
clientVersion: '',
addresses: [],
tags: [],
authorized: false,
blocksIncomingConnections: false,
keyExpiryDisabled: false,
expires: '',
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,
nodeId: data.nodeId ?? 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,
keyExpiryDisabled: data.keyExpiryDisabled ?? false,
expires: data.expires ?? null,
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: 'Legacy device ID' },
nodeId: { type: 'string', description: 'Preferred device ID', optional: true },
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',
},
keyExpiryDisabled: {
type: 'boolean',
description: 'Whether the device key is exempt from expiring',
optional: true,
},
expires: {
type: 'string',
description: "The device's auth key expiration timestamp",
optional: true,
},
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' },
},
}
+25
View File
@@ -0,0 +1,25 @@
export { tailscaleAuthorizeDeviceTool } from './authorize_device'
export { tailscaleCreateAuthKeyTool } from './create_auth_key'
export { tailscaleDeleteAuthKeyTool } from './delete_auth_key'
export { tailscaleDeleteDeviceTool } from './delete_device'
export { tailscaleDeleteUserTool } from './delete_user'
export { tailscaleExpireDeviceKeyTool } from './expire_device_key'
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 { tailscaleSetAclTool } from './set_acl'
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 { tailscaleSuspendUserTool } from './suspend_user'
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?all=true`,
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',
},
},
}
+111
View File
@@ -0,0 +1,111 @@
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,
nodeId: (device.nodeId 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,
keyExpiryDisabled: (device.keyExpiryDisabled as boolean) ?? false,
expires: (device.expires as string) ?? null,
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: 'Legacy device ID' },
nodeId: { type: 'string', description: 'Preferred 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',
},
keyExpiryDisabled: {
type: 'boolean',
description: 'Whether the device key is exempt from expiring',
},
expires: { type: 'string', description: "The device's auth key expiration timestamp" },
lastSeen: { type: 'string', description: 'Last seen timestamp' },
created: { type: 'string', description: 'Creation timestamp' },
},
},
},
count: {
type: 'number',
description: 'Total number of devices',
},
},
}
@@ -0,0 +1,59 @@
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: [] },
error: (data as Record<string, string>).message ?? 'Failed to list DNS nameservers',
}
}
const data = await response.json()
return {
success: true,
output: {
dns: data.dns ?? [],
},
}
},
outputs: {
dns: { type: 'array', description: 'List of DNS nameserver addresses' },
},
}
+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',
},
},
}
+97
View File
@@ -0,0 +1,97 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
interface TailscaleSetAclParams {
apiKey: string
tailnet: string
acl: string
ifMatch?: string
}
interface TailscaleSetAclResponse extends ToolResponse {
output: {
acl: string
etag: string
}
}
export const tailscaleSetAclTool: ToolConfig<TailscaleSetAclParams, TailscaleSetAclResponse> = {
id: 'tailscale_set_acl',
name: 'Tailscale Set ACL',
description: 'Replace the ACL policy file 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',
},
acl: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The new ACL policy file, as a JSON string',
},
ifMatch: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'ETag from a prior Get ACL call to avoid overwriting concurrent updates. Use "ts-default" to only replace an untouched default policy file.',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/tailnet/${encodeURIComponent(params.tailnet.trim())}/acl`,
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${params.apiKey.trim()}`,
'Content-Type': 'application/json',
Accept: 'application/json',
}
if (params.ifMatch) headers['If-Match'] = `"${params.ifMatch.trim().replace(/^"|"$/g, '')}"`
return headers
},
body: (params) => params.acl.trim(),
},
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 set 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: 'Updated ACL policy as JSON string' },
etag: {
type: 'string',
description: 'ETag for the new ACL version (use with If-Match header for future updates)',
optional: true,
},
},
}
@@ -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' },
},
}
+77
View File
@@ -0,0 +1,77 @@
import type { ToolConfig, ToolResponse } from '@/tools/types'
import type { TailscaleBaseParams } from './types'
interface TailscaleSuspendUserParams extends TailscaleBaseParams {
userId: string
}
interface TailscaleSuspendUserResponse extends ToolResponse {
output: {
success: boolean
userId: string
}
}
export const tailscaleSuspendUserTool: ToolConfig<
TailscaleSuspendUserParams,
TailscaleSuspendUserResponse
> = {
id: 'tailscale_suspend_user',
name: 'Tailscale Suspend User',
description: "Suspend a user's access to 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',
},
userId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'User ID to suspend',
},
},
request: {
url: (params) =>
`https://api.tailscale.com/api/v2/users/${encodeURIComponent(params.userId.trim())}/suspend`,
method: 'POST',
headers: (params) => ({
Authorization: `Bearer ${params.apiKey.trim()}`,
}),
},
transformResponse: async (response: Response, params?: TailscaleSuspendUserParams) => {
if (!response.ok) {
const data = await response.json().catch(() => ({}))
return {
success: false,
output: { success: false, userId: '' },
error: (data as Record<string, string>).message ?? 'Failed to suspend user',
}
}
return {
success: true,
output: {
success: true,
userId: params?.userId ?? '',
},
}
},
outputs: {
success: { type: 'boolean', description: 'Whether the user was successfully suspended' },
userId: { type: 'string', description: 'ID of the suspended user' },
},
}
+157
View File
@@ -0,0 +1,157 @@
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
}
interface TailscaleDeviceOutput {
id: string
nodeId: string
name: string
hostname: string
user: string
os: string
clientVersion: string
addresses: string[]
tags: string[]
authorized: boolean
blocksIncomingConnections: boolean
keyExpiryDisabled: boolean
expires: string
lastSeen: string
created: string
}
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[]
}
}
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' },
},
}