@yawlabs/tailscale-mcp 0.5.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/index.js +211 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/YawLabs/tailscale-mcp/stargazers)
|
|
6
6
|
[](https://github.com/YawLabs/tailscale-mcp/actions/workflows/ci.yml) [](https://github.com/YawLabs/tailscale-mcp/actions/workflows/release.yml)
|
|
7
7
|
|
|
8
|
-
**Manage your Tailscale tailnet from Claude Code, Cursor, and any MCP client.**
|
|
8
|
+
**Manage your Tailscale tailnet from Claude Code, Cursor, and any MCP client.** 99 tools + 4 resources. One env var. Works on first try.
|
|
9
9
|
|
|
10
10
|
Built and maintained by [YawLabs](https://yaw.sh).
|
|
11
11
|
|
|
@@ -96,7 +96,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
|
|
|
96
96
|
| ACL Policy | `tailscale://tailnet/acl` | Full ACL policy (HuJSON preserved) |
|
|
97
97
|
| DNS Config | `tailscale://tailnet/dns` | Nameservers, search paths, split DNS, MagicDNS |
|
|
98
98
|
|
|
99
|
-
## Tools (
|
|
99
|
+
## Tools (99)
|
|
100
100
|
|
|
101
101
|
<details>
|
|
102
102
|
<summary><strong>Status</strong> (1 tool)</summary>
|
|
@@ -297,7 +297,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
|
|
|
297
297
|
</details>
|
|
298
298
|
|
|
299
299
|
<details>
|
|
300
|
-
<summary><strong>Device Invites</strong> (
|
|
300
|
+
<summary><strong>Device Invites</strong> (6 tools)</summary>
|
|
301
301
|
|
|
302
302
|
| Tool | Description |
|
|
303
303
|
|------|-------------|
|
|
@@ -305,6 +305,7 @@ MCP Resources expose read-only data that clients can browse without tool calls.
|
|
|
305
305
|
| `tailscale_create_device_invite` | Create a device invite |
|
|
306
306
|
| `tailscale_get_device_invite` | Get a device invite |
|
|
307
307
|
| `tailscale_delete_device_invite` | Delete a device invite |
|
|
308
|
+
| `tailscale_accept_device_invite` | Accept a device share invitation |
|
|
308
309
|
| `tailscale_resend_device_invite` | Resend a device invite email |
|
|
309
310
|
|
|
310
311
|
</details>
|
package/dist/index.js
CHANGED
|
@@ -21079,6 +21079,9 @@ function getTailnet() {
|
|
|
21079
21079
|
function encPath(segment) {
|
|
21080
21080
|
return encodeURIComponent(segment);
|
|
21081
21081
|
}
|
|
21082
|
+
function sanitizeDescription(value) {
|
|
21083
|
+
return value.replace(/[/_]/g, "-").replace(/[^a-zA-Z0-9 -]/g, "").replace(/ {2,}/g, " ").trim().slice(0, 50);
|
|
21084
|
+
}
|
|
21082
21085
|
async function apiRequest(method, path, body, options) {
|
|
21083
21086
|
const auth = await getAuthHeader();
|
|
21084
21087
|
const headers = {
|
|
@@ -21255,7 +21258,7 @@ Pass this ETag to tailscale_update_acl when updating the policy.`
|
|
|
21255
21258
|
acceptRaw: true,
|
|
21256
21259
|
accept: "application/hujson"
|
|
21257
21260
|
});
|
|
21258
|
-
if (res.ok && !res.rawBody) {
|
|
21261
|
+
if (res.ok && (!res.rawBody || !res.rawBody.trim())) {
|
|
21259
21262
|
return { ...res, rawBody: "ACL policy is valid." };
|
|
21260
21263
|
}
|
|
21261
21264
|
return res;
|
|
@@ -21290,8 +21293,7 @@ Pass this ETag to tailscale_update_acl when updating the policy.`
|
|
|
21290
21293
|
|
|
21291
21294
|
// src/tools/audit.ts
|
|
21292
21295
|
function assertRFC3339(value, label) {
|
|
21293
|
-
|
|
21294
|
-
if (Number.isNaN(d.getTime())) {
|
|
21296
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
21295
21297
|
throw new Error(`${label} must be a valid RFC3339 date-time (e.g. '2026-04-01T00:00:00Z'), got: '${value}'`);
|
|
21296
21298
|
}
|
|
21297
21299
|
}
|
|
@@ -21357,11 +21359,21 @@ var deviceTools = [
|
|
|
21357
21359
|
inputSchema: external_exports.object({
|
|
21358
21360
|
fields: external_exports.string().optional().describe(
|
|
21359
21361
|
"Comma-separated list of fields to include. Omit for all fields. Valid fields: addresses, advertisedRoutes, authorized, blocksIncomingConnections, clientConnectivity, clientVersion, connectedToControl, created, distro, enabledRoutes, expires, hostname, id, isExternal, keyExpiryDisabled, lastSeen, machineKey, name, nodeId, nodeKey, os, sshEnabled, tags, tailnetLockError, tailnetLockKey, updateAvailable, user. Use 'all' for every field."
|
|
21362
|
+
),
|
|
21363
|
+
filters: external_exports.record(external_exports.string(), external_exports.string()).optional().describe(
|
|
21364
|
+
"Server-side filters as key-value pairs. Filter by any top-level device property (e.g. { isEphemeral: 'true', os: 'linux', tags: 'tag:prod' }). Multiple filters are ANDed together."
|
|
21360
21365
|
)
|
|
21361
21366
|
}),
|
|
21362
21367
|
handler: async (input) => {
|
|
21363
|
-
const params =
|
|
21364
|
-
|
|
21368
|
+
const params = new URLSearchParams();
|
|
21369
|
+
if (input.fields) params.set("fields", input.fields);
|
|
21370
|
+
if (input.filters) {
|
|
21371
|
+
for (const [key, value] of Object.entries(input.filters)) {
|
|
21372
|
+
params.set(key, value);
|
|
21373
|
+
}
|
|
21374
|
+
}
|
|
21375
|
+
const qs = params.toString();
|
|
21376
|
+
return apiGet(`/tailnet/${getTailnet()}/devices${qs ? `?${qs}` : ""}`);
|
|
21365
21377
|
}
|
|
21366
21378
|
},
|
|
21367
21379
|
{
|
|
@@ -21375,7 +21387,7 @@ var deviceTools = [
|
|
|
21375
21387
|
openWorldHint: true
|
|
21376
21388
|
},
|
|
21377
21389
|
inputSchema: external_exports.object({
|
|
21378
|
-
deviceId: external_exports.string().describe("The device ID (numeric or
|
|
21390
|
+
deviceId: external_exports.string().describe("The device ID (numeric id or nodeId, NOT the nodeKey)")
|
|
21379
21391
|
}),
|
|
21380
21392
|
handler: async (input) => {
|
|
21381
21393
|
return apiGet(`/device/${encPath(input.deviceId)}`);
|
|
@@ -21563,6 +21575,9 @@ var deviceTools = [
|
|
|
21563
21575
|
attributeKey: external_exports.string().describe("The attribute key to delete (e.g. 'custom:lastAuditDate')")
|
|
21564
21576
|
}),
|
|
21565
21577
|
handler: async (input) => {
|
|
21578
|
+
if (!input.attributeKey.startsWith("custom:")) {
|
|
21579
|
+
throw new Error(`attributeKey must start with 'custom:' prefix, got: '${input.attributeKey}'`);
|
|
21580
|
+
}
|
|
21566
21581
|
return apiDelete(`/device/${encPath(input.deviceId)}/attributes/${encPath(input.attributeKey)}`);
|
|
21567
21582
|
}
|
|
21568
21583
|
},
|
|
@@ -21642,6 +21657,17 @@ var deviceTools = [
|
|
|
21642
21657
|
)
|
|
21643
21658
|
}),
|
|
21644
21659
|
handler: async (input) => {
|
|
21660
|
+
const invalidKeys = [];
|
|
21661
|
+
for (const attrs of Object.values(input.attributes)) {
|
|
21662
|
+
for (const key of Object.keys(attrs)) {
|
|
21663
|
+
if (!key.startsWith("custom:")) invalidKeys.push(key);
|
|
21664
|
+
}
|
|
21665
|
+
}
|
|
21666
|
+
if (invalidKeys.length > 0) {
|
|
21667
|
+
throw new Error(
|
|
21668
|
+
`All attribute keys must start with 'custom:' prefix. Invalid keys: ${[...new Set(invalidKeys)].join(", ")}`
|
|
21669
|
+
);
|
|
21670
|
+
}
|
|
21645
21671
|
return apiPatch(`/tailnet/${getTailnet()}/device-attributes`, input.attributes);
|
|
21646
21672
|
}
|
|
21647
21673
|
}
|
|
@@ -21865,7 +21891,7 @@ var inviteTools = [
|
|
|
21865
21891
|
},
|
|
21866
21892
|
{
|
|
21867
21893
|
name: "tailscale_create_device_invite",
|
|
21868
|
-
description: "Create a
|
|
21894
|
+
description: "Create a device share invitation that allows an external user to access a specific device in your tailnet.",
|
|
21869
21895
|
annotations: {
|
|
21870
21896
|
title: "Create device invite",
|
|
21871
21897
|
readOnlyHint: false,
|
|
@@ -21921,6 +21947,23 @@ var inviteTools = [
|
|
|
21921
21947
|
return apiDelete(`/device-invites/${encPath(input.inviteId)}`);
|
|
21922
21948
|
}
|
|
21923
21949
|
},
|
|
21950
|
+
{
|
|
21951
|
+
name: "tailscale_accept_device_invite",
|
|
21952
|
+
description: "Accept a device share invitation using the invite URL or code.",
|
|
21953
|
+
annotations: {
|
|
21954
|
+
title: "Accept device invite",
|
|
21955
|
+
readOnlyHint: false,
|
|
21956
|
+
destructiveHint: false,
|
|
21957
|
+
idempotentHint: true,
|
|
21958
|
+
openWorldHint: true
|
|
21959
|
+
},
|
|
21960
|
+
inputSchema: external_exports.object({
|
|
21961
|
+
invite: external_exports.string().describe("The device invite URL or invite code")
|
|
21962
|
+
}),
|
|
21963
|
+
handler: async (input) => {
|
|
21964
|
+
return apiPost("/device-invites/-/accept", { invite: input.invite });
|
|
21965
|
+
}
|
|
21966
|
+
},
|
|
21924
21967
|
// --- User Invites ---
|
|
21925
21968
|
{
|
|
21926
21969
|
name: "tailscale_list_user_invites",
|
|
@@ -22032,17 +22075,20 @@ var inviteTools = [
|
|
|
22032
22075
|
var keyTools = [
|
|
22033
22076
|
{
|
|
22034
22077
|
name: "tailscale_list_keys",
|
|
22035
|
-
description: "List
|
|
22078
|
+
description: "List keys in your tailnet. By default lists auth keys only. Set 'all' to true to include OAuth clients and federated identities.",
|
|
22036
22079
|
annotations: {
|
|
22037
|
-
title: "List
|
|
22080
|
+
title: "List keys",
|
|
22038
22081
|
readOnlyHint: true,
|
|
22039
22082
|
destructiveHint: false,
|
|
22040
22083
|
idempotentHint: true,
|
|
22041
22084
|
openWorldHint: true
|
|
22042
22085
|
},
|
|
22043
|
-
inputSchema: external_exports.object({
|
|
22044
|
-
|
|
22045
|
-
|
|
22086
|
+
inputSchema: external_exports.object({
|
|
22087
|
+
all: external_exports.boolean().optional().describe("When true, returns all key types (auth keys, OAuth clients, federated identities). Default: false")
|
|
22088
|
+
}),
|
|
22089
|
+
handler: async (input) => {
|
|
22090
|
+
const qs = input.all ? "?all=true" : "";
|
|
22091
|
+
return apiGet(`/tailnet/${getTailnet()}/keys${qs}`);
|
|
22046
22092
|
}
|
|
22047
22093
|
},
|
|
22048
22094
|
{
|
|
@@ -22064,21 +22110,35 @@ var keyTools = [
|
|
|
22064
22110
|
},
|
|
22065
22111
|
{
|
|
22066
22112
|
name: "tailscale_create_key",
|
|
22067
|
-
description: "Create a new auth
|
|
22113
|
+
description: "Create a new key in your tailnet. Supports auth keys (for adding devices), OAuth clients (for programmatic API access), and federated identities (for OIDC-based CI/CD access). Returns the key value \u2014 save it immediately, as it cannot be retrieved again.",
|
|
22068
22114
|
annotations: {
|
|
22069
|
-
title: "Create
|
|
22115
|
+
title: "Create key",
|
|
22070
22116
|
readOnlyHint: false,
|
|
22071
22117
|
destructiveHint: false,
|
|
22072
22118
|
idempotentHint: false,
|
|
22073
22119
|
openWorldHint: true
|
|
22074
22120
|
},
|
|
22075
22121
|
inputSchema: external_exports.object({
|
|
22076
|
-
|
|
22077
|
-
|
|
22078
|
-
|
|
22079
|
-
|
|
22080
|
-
|
|
22081
|
-
|
|
22122
|
+
keyType: external_exports.enum(["auth", "client", "federated"]).optional().describe(
|
|
22123
|
+
"Key type: 'auth' (default) for device auth keys, 'client' for OAuth clients, 'federated' for OIDC federation"
|
|
22124
|
+
),
|
|
22125
|
+
description: external_exports.string().optional().describe("Description for this key (max 50 chars, alphanumeric/hyphens/spaces)"),
|
|
22126
|
+
// Auth key fields
|
|
22127
|
+
reusable: external_exports.boolean().optional().describe("(auth only) Whether the key can be used more than once (default: false)"),
|
|
22128
|
+
ephemeral: external_exports.boolean().optional().describe("(auth only) Whether devices using this key are ephemeral (default: false)"),
|
|
22129
|
+
preauthorized: external_exports.boolean().optional().describe("(auth only) Whether devices are pre-authorized (default: false)"),
|
|
22130
|
+
expirySeconds: external_exports.number().optional().describe("(auth only) Key expiry in seconds (default: 90 days)"),
|
|
22131
|
+
// Shared fields
|
|
22132
|
+
tags: external_exports.array(external_exports.string()).optional().describe(
|
|
22133
|
+
"ACL tags (must start with 'tag:'). Required for client/federated if scopes include 'devices:core' or 'auth_keys'"
|
|
22134
|
+
),
|
|
22135
|
+
// Client + Federated fields
|
|
22136
|
+
scopes: external_exports.array(external_exports.string()).optional().describe("(client/federated) OAuth scopes to grant (e.g. ['devices:read', 'dns', 'acl'])"),
|
|
22137
|
+
// Federated-only fields
|
|
22138
|
+
issuer: external_exports.string().optional().describe("(federated only) OIDC issuer URL (e.g. 'https://token.actions.githubusercontent.com')"),
|
|
22139
|
+
subject: external_exports.string().optional().describe("(federated only) Expected subject claim, supports * wildcards"),
|
|
22140
|
+
audience: external_exports.string().optional().describe("(federated only) Expected audience claim"),
|
|
22141
|
+
customClaimRules: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("(federated only) Custom claim mapping rules")
|
|
22082
22142
|
}),
|
|
22083
22143
|
handler: async (input) => {
|
|
22084
22144
|
if (input.tags && input.tags.length > 0) {
|
|
@@ -22087,8 +22147,19 @@ var keyTools = [
|
|
|
22087
22147
|
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
22088
22148
|
}
|
|
22089
22149
|
}
|
|
22090
|
-
const
|
|
22091
|
-
|
|
22150
|
+
const keyType = input.keyType ?? "auth";
|
|
22151
|
+
if (keyType !== "auth") {
|
|
22152
|
+
const authOnlyFields = ["reusable", "ephemeral", "preauthorized", "expirySeconds"];
|
|
22153
|
+
const wrongFields = authOnlyFields.filter((f) => input[f] !== void 0);
|
|
22154
|
+
if (wrongFields.length > 0) {
|
|
22155
|
+
throw new Error(`${wrongFields.join(", ")} can only be used with keyType 'auth', not '${keyType}'`);
|
|
22156
|
+
}
|
|
22157
|
+
}
|
|
22158
|
+
const body = {};
|
|
22159
|
+
if (keyType !== "auth") body.keyType = keyType;
|
|
22160
|
+
if (input.description !== void 0) body.description = sanitizeDescription(input.description);
|
|
22161
|
+
if (keyType === "auth") {
|
|
22162
|
+
body.capabilities = {
|
|
22092
22163
|
devices: {
|
|
22093
22164
|
create: {
|
|
22094
22165
|
reusable: input.reusable ?? false,
|
|
@@ -22097,10 +22168,23 @@ var keyTools = [
|
|
|
22097
22168
|
tags: input.tags ?? []
|
|
22098
22169
|
}
|
|
22099
22170
|
}
|
|
22171
|
+
};
|
|
22172
|
+
if (input.expirySeconds !== void 0) body.expirySeconds = input.expirySeconds;
|
|
22173
|
+
} else {
|
|
22174
|
+
if (!input.scopes || input.scopes.length === 0) {
|
|
22175
|
+
throw new Error(`scopes are required for keyType '${keyType}'`);
|
|
22100
22176
|
}
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22177
|
+
body.scopes = input.scopes;
|
|
22178
|
+
if (input.tags) body.tags = input.tags;
|
|
22179
|
+
if (keyType === "federated") {
|
|
22180
|
+
if (!input.issuer) throw new Error("issuer is required for federated keys");
|
|
22181
|
+
if (!input.subject) throw new Error("subject is required for federated keys");
|
|
22182
|
+
body.issuer = input.issuer;
|
|
22183
|
+
body.subject = input.subject;
|
|
22184
|
+
if (input.audience !== void 0) body.audience = input.audience;
|
|
22185
|
+
if (input.customClaimRules !== void 0) body.customClaimRules = input.customClaimRules;
|
|
22186
|
+
}
|
|
22187
|
+
}
|
|
22104
22188
|
return apiPost(`/tailnet/${getTailnet()}/keys`, body);
|
|
22105
22189
|
}
|
|
22106
22190
|
},
|
|
@@ -22123,21 +22207,42 @@ var keyTools = [
|
|
|
22123
22207
|
},
|
|
22124
22208
|
{
|
|
22125
22209
|
name: "tailscale_update_key",
|
|
22126
|
-
description: "Update an existing
|
|
22210
|
+
description: "Update an existing key's description or configuration. For OAuth clients and federated identities, scopes, tags, and identity fields can also be updated. Auth keys only support description updates.",
|
|
22127
22211
|
annotations: {
|
|
22128
|
-
title: "Update
|
|
22212
|
+
title: "Update key",
|
|
22129
22213
|
readOnlyHint: false,
|
|
22130
22214
|
destructiveHint: false,
|
|
22131
22215
|
idempotentHint: true,
|
|
22132
22216
|
openWorldHint: true
|
|
22133
22217
|
},
|
|
22134
22218
|
inputSchema: external_exports.object({
|
|
22135
|
-
keyId: external_exports.string().describe("The
|
|
22136
|
-
description: external_exports.string().optional().describe("Updated description
|
|
22219
|
+
keyId: external_exports.string().describe("The key ID to update"),
|
|
22220
|
+
description: external_exports.string().optional().describe("Updated description (max 50 chars, alphanumeric/hyphens/spaces)"),
|
|
22221
|
+
scopes: external_exports.array(external_exports.string()).optional().describe("(client/federated) Updated OAuth scopes"),
|
|
22222
|
+
tags: external_exports.array(external_exports.string()).optional().describe("Updated ACL tags (must start with 'tag:')"),
|
|
22223
|
+
issuer: external_exports.string().optional().describe("(federated only) Updated OIDC issuer URL"),
|
|
22224
|
+
subject: external_exports.string().optional().describe("(federated only) Updated subject claim pattern"),
|
|
22225
|
+
audience: external_exports.string().optional().describe("(federated only) Updated audience claim"),
|
|
22226
|
+
customClaimRules: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("(federated only) Updated custom claim rules")
|
|
22137
22227
|
}),
|
|
22138
22228
|
handler: async (input) => {
|
|
22229
|
+
if (input.tags && input.tags.length > 0) {
|
|
22230
|
+
const invalid = input.tags.filter((t) => !t.startsWith("tag:"));
|
|
22231
|
+
if (invalid.length > 0) {
|
|
22232
|
+
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
22233
|
+
}
|
|
22234
|
+
}
|
|
22139
22235
|
const body = {};
|
|
22140
|
-
if (input.description !== void 0) body.description = input.description;
|
|
22236
|
+
if (input.description !== void 0) body.description = sanitizeDescription(input.description);
|
|
22237
|
+
if (input.scopes !== void 0) body.scopes = input.scopes;
|
|
22238
|
+
if (input.tags !== void 0) body.tags = input.tags;
|
|
22239
|
+
if (input.issuer !== void 0) body.issuer = input.issuer;
|
|
22240
|
+
if (input.subject !== void 0) body.subject = input.subject;
|
|
22241
|
+
if (input.audience !== void 0) body.audience = input.audience;
|
|
22242
|
+
if (input.customClaimRules !== void 0) body.customClaimRules = input.customClaimRules;
|
|
22243
|
+
if (Object.keys(body).length === 0) {
|
|
22244
|
+
throw new Error("No fields to update. Provide at least one field (description, scopes, tags, etc.).");
|
|
22245
|
+
}
|
|
22141
22246
|
return apiPut(`/tailnet/${getTailnet()}/keys/${encPath(input.keyId)}`, body);
|
|
22142
22247
|
}
|
|
22143
22248
|
}
|
|
@@ -22200,9 +22305,7 @@ var logStreamingTools = [
|
|
|
22200
22305
|
},
|
|
22201
22306
|
inputSchema: external_exports.object({
|
|
22202
22307
|
logType: external_exports.enum(["configuration", "network"]).describe("The log type: 'configuration' for audit logs, 'network' for network flow logs"),
|
|
22203
|
-
destinationType: external_exports.
|
|
22204
|
-
"The destination type (e.g. 'axiom', 'datadog', 'splunk', 'elasticsearch', 'panther', 's3', 'gcs', 'cribl')"
|
|
22205
|
-
),
|
|
22308
|
+
destinationType: external_exports.enum(["splunk", "elastic", "panther", "cribl", "datadog", "axiom", "s3"]).describe("The log streaming destination type"),
|
|
22206
22309
|
url: external_exports.string().optional().describe("Destination URL (required for most destination types)"),
|
|
22207
22310
|
token: external_exports.string().optional().describe("Authentication token or API key for the destination"),
|
|
22208
22311
|
user: external_exports.string().optional().describe("Username for the destination (if required)")
|
|
@@ -22352,12 +22455,12 @@ var oauthClientTools = [
|
|
|
22352
22455
|
openWorldHint: true
|
|
22353
22456
|
},
|
|
22354
22457
|
inputSchema: external_exports.object({
|
|
22355
|
-
name: external_exports.string().describe("A human-readable name for this OAuth client"),
|
|
22458
|
+
name: external_exports.string().describe("A human-readable name for this OAuth client (max 50 chars, alphanumeric/hyphens/spaces)"),
|
|
22356
22459
|
scopes: external_exports.array(external_exports.string()).describe(
|
|
22357
22460
|
"OAuth scopes to grant (e.g. ['devices:read', 'dns', 'acl']). See Tailscale docs for available scopes."
|
|
22358
22461
|
),
|
|
22359
22462
|
tags: external_exports.array(external_exports.string()).optional().describe("ACL tags to assign to the OAuth client"),
|
|
22360
|
-
description: external_exports.string().optional().describe("Description for this OAuth client")
|
|
22463
|
+
description: external_exports.string().optional().describe("Description for this OAuth client (max 50 chars, alphanumeric/hyphens/spaces)")
|
|
22361
22464
|
}),
|
|
22362
22465
|
handler: async (input) => {
|
|
22363
22466
|
if (input.tags && input.tags.length > 0) {
|
|
@@ -22366,7 +22469,10 @@ var oauthClientTools = [
|
|
|
22366
22469
|
throw new Error(`All tags must start with 'tag:' prefix. Invalid tags: ${invalid.join(", ")}`);
|
|
22367
22470
|
}
|
|
22368
22471
|
}
|
|
22369
|
-
|
|
22472
|
+
const body = { ...input };
|
|
22473
|
+
body.name = sanitizeDescription(input.name);
|
|
22474
|
+
if (input.description !== void 0) body.description = sanitizeDescription(input.description);
|
|
22475
|
+
return apiPost(`/tailnet/${getTailnet()}/oauth-clients`, body);
|
|
22370
22476
|
}
|
|
22371
22477
|
},
|
|
22372
22478
|
{
|
|
@@ -22391,6 +22497,12 @@ var oauthClientTools = [
|
|
|
22391
22497
|
for (const [key, value] of Object.entries(body)) {
|
|
22392
22498
|
if (value !== void 0) cleanBody[key] = value;
|
|
22393
22499
|
}
|
|
22500
|
+
if (cleanBody.name !== void 0) cleanBody.name = sanitizeDescription(cleanBody.name);
|
|
22501
|
+
if (cleanBody.description !== void 0)
|
|
22502
|
+
cleanBody.description = sanitizeDescription(cleanBody.description);
|
|
22503
|
+
if (Object.keys(cleanBody).length === 0) {
|
|
22504
|
+
throw new Error("No fields to update. Provide at least one of: name, scopes, description.");
|
|
22505
|
+
}
|
|
22394
22506
|
return apiPatch(`/tailnet/${getTailnet()}/oauth-clients/${encPath(clientId)}`, cleanBody);
|
|
22395
22507
|
}
|
|
22396
22508
|
},
|
|
@@ -22465,7 +22577,14 @@ var postureTools = [
|
|
|
22465
22577
|
cloudEnvironment: external_exports.string().optional().describe("Cloud environment (e.g. 'us-1', 'eu-1')")
|
|
22466
22578
|
}),
|
|
22467
22579
|
handler: async (input) => {
|
|
22468
|
-
|
|
22580
|
+
const body = {
|
|
22581
|
+
provider: input.provider,
|
|
22582
|
+
clientId: input.clientId,
|
|
22583
|
+
clientSecret: input.clientSecret
|
|
22584
|
+
};
|
|
22585
|
+
if (input.tenantId !== void 0) body.tenantId = input.tenantId;
|
|
22586
|
+
if (input.cloudEnvironment !== void 0) body.cloudEnvironment = input.cloudEnvironment;
|
|
22587
|
+
return apiPost(`/tailnet/${getTailnet()}/posture/integrations`, body);
|
|
22469
22588
|
}
|
|
22470
22589
|
},
|
|
22471
22590
|
{
|
|
@@ -22491,6 +22610,11 @@ var postureTools = [
|
|
|
22491
22610
|
for (const [key, value] of Object.entries(body)) {
|
|
22492
22611
|
if (value !== void 0) cleanBody[key] = value;
|
|
22493
22612
|
}
|
|
22613
|
+
if (Object.keys(cleanBody).length === 0) {
|
|
22614
|
+
throw new Error(
|
|
22615
|
+
"No fields to update. Provide at least one of: clientId, clientSecret, tenantId, cloudEnvironment."
|
|
22616
|
+
);
|
|
22617
|
+
}
|
|
22494
22618
|
return apiPatch(`/tailnet/${getTailnet()}/posture/integrations/${encPath(integrationId)}`, cleanBody);
|
|
22495
22619
|
}
|
|
22496
22620
|
},
|
|
@@ -22580,6 +22704,9 @@ var serviceTools = [
|
|
|
22580
22704
|
for (const [key, value] of Object.entries(body)) {
|
|
22581
22705
|
if (value !== void 0) cleanBody[key] = value;
|
|
22582
22706
|
}
|
|
22707
|
+
if (Object.keys(cleanBody).length === 0) {
|
|
22708
|
+
throw new Error("No fields to update. Provide at least one of: ports, tags, autoApproveHosts.");
|
|
22709
|
+
}
|
|
22583
22710
|
return apiPut(`/tailnet/${getTailnet()}/services/${encPath(serviceName)}`, cleanBody);
|
|
22584
22711
|
}
|
|
22585
22712
|
},
|
|
@@ -22743,6 +22870,9 @@ var tailnetTools = [
|
|
|
22743
22870
|
for (const [key, value] of Object.entries(input)) {
|
|
22744
22871
|
if (value !== void 0) body[key] = value;
|
|
22745
22872
|
}
|
|
22873
|
+
if (Object.keys(body).length === 0) {
|
|
22874
|
+
throw new Error("No fields to update. Provide at least one setting to change.");
|
|
22875
|
+
}
|
|
22746
22876
|
return apiPatch(`/tailnet/${getTailnet()}/settings`, body);
|
|
22747
22877
|
}
|
|
22748
22878
|
},
|
|
@@ -22820,9 +22950,16 @@ var userTools = [
|
|
|
22820
22950
|
idempotentHint: true,
|
|
22821
22951
|
openWorldHint: true
|
|
22822
22952
|
},
|
|
22823
|
-
inputSchema: external_exports.object({
|
|
22824
|
-
|
|
22825
|
-
|
|
22953
|
+
inputSchema: external_exports.object({
|
|
22954
|
+
type: external_exports.enum(["member", "shared", "all"]).optional().describe("Filter by user type: 'member' (direct members), 'shared' (shared-in users), or 'all' (default)"),
|
|
22955
|
+
role: external_exports.enum(["owner", "admin", "it-admin", "network-admin", "billing-admin", "auditor", "member"]).optional().describe("Filter by user role")
|
|
22956
|
+
}),
|
|
22957
|
+
handler: async (input) => {
|
|
22958
|
+
const params = new URLSearchParams();
|
|
22959
|
+
if (input.type) params.set("type", input.type);
|
|
22960
|
+
if (input.role) params.set("role", input.role);
|
|
22961
|
+
const qs = params.toString();
|
|
22962
|
+
return apiGet(`/tailnet/${getTailnet()}/users${qs ? `?${qs}` : ""}`);
|
|
22826
22963
|
}
|
|
22827
22964
|
},
|
|
22828
22965
|
{
|
|
@@ -22931,6 +23068,26 @@ var userTools = [
|
|
|
22931
23068
|
];
|
|
22932
23069
|
|
|
22933
23070
|
// src/tools/webhooks.ts
|
|
23071
|
+
var webhookEventTypes = [
|
|
23072
|
+
"nodeCreated",
|
|
23073
|
+
"nodeNeedsApproval",
|
|
23074
|
+
"nodeApproved",
|
|
23075
|
+
"nodeKeyExpiringInOneDay",
|
|
23076
|
+
"nodeKeyExpired",
|
|
23077
|
+
"nodeDeleted",
|
|
23078
|
+
"nodeSigned",
|
|
23079
|
+
"nodeNeedsSignature",
|
|
23080
|
+
"policyUpdate",
|
|
23081
|
+
"userCreated",
|
|
23082
|
+
"userNeedsApproval",
|
|
23083
|
+
"userSuspended",
|
|
23084
|
+
"userRestored",
|
|
23085
|
+
"userDeleted",
|
|
23086
|
+
"userApproved",
|
|
23087
|
+
"userRoleUpdated",
|
|
23088
|
+
"subnetIPForwardingNotEnabled",
|
|
23089
|
+
"exitNodeIPForwardingNotEnabled"
|
|
23090
|
+
];
|
|
22934
23091
|
var webhookTools = [
|
|
22935
23092
|
{
|
|
22936
23093
|
name: "tailscale_list_webhooks",
|
|
@@ -22976,9 +23133,7 @@ var webhookTools = [
|
|
|
22976
23133
|
},
|
|
22977
23134
|
inputSchema: external_exports.object({
|
|
22978
23135
|
endpointUrl: external_exports.string().describe("The URL to send webhook events to"),
|
|
22979
|
-
subscriptions: external_exports.array(external_exports.
|
|
22980
|
-
"Event types to subscribe to (e.g. ['nodeCreated', 'nodeDeleted', 'nodeApproved', 'policyUpdate', 'userCreated', 'userDeleted'])"
|
|
22981
|
-
)
|
|
23136
|
+
subscriptions: external_exports.array(external_exports.enum(webhookEventTypes)).describe("Event types to subscribe to")
|
|
22982
23137
|
}),
|
|
22983
23138
|
handler: async (input) => {
|
|
22984
23139
|
return apiPost(`/tailnet/${getTailnet()}/webhooks`, {
|
|
@@ -23000,14 +23155,15 @@ var webhookTools = [
|
|
|
23000
23155
|
inputSchema: external_exports.object({
|
|
23001
23156
|
webhookId: external_exports.string().describe("The webhook ID to update"),
|
|
23002
23157
|
endpointUrl: external_exports.string().optional().describe("New URL to send webhook events to"),
|
|
23003
|
-
subscriptions: external_exports.array(external_exports.
|
|
23004
|
-
"Updated list of event types to subscribe to (e.g. ['nodeCreated', 'nodeDeleted', 'nodeApproved', 'policyUpdate', 'userCreated', 'userDeleted'])"
|
|
23005
|
-
)
|
|
23158
|
+
subscriptions: external_exports.array(external_exports.enum(webhookEventTypes)).optional().describe("Updated list of event types to subscribe to")
|
|
23006
23159
|
}),
|
|
23007
23160
|
handler: async (input) => {
|
|
23008
23161
|
const body = {};
|
|
23009
23162
|
if (input.endpointUrl !== void 0) body.endpointUrl = input.endpointUrl;
|
|
23010
23163
|
if (input.subscriptions !== void 0) body.subscriptions = input.subscriptions;
|
|
23164
|
+
if (Object.keys(body).length === 0) {
|
|
23165
|
+
throw new Error("No fields to update. Provide at least one of: endpointUrl, subscriptions.");
|
|
23166
|
+
}
|
|
23011
23167
|
return apiPatch(`/webhooks/${encPath(input.webhookId)}`, body);
|
|
23012
23168
|
}
|
|
23013
23169
|
},
|
|
@@ -23109,13 +23265,15 @@ var workloadIdentityTools = [
|
|
|
23109
23265
|
openWorldHint: true
|
|
23110
23266
|
},
|
|
23111
23267
|
inputSchema: external_exports.object({
|
|
23112
|
-
name: external_exports.string().describe("A human-readable name for this provider"),
|
|
23268
|
+
name: external_exports.string().describe("A human-readable name for this provider (max 50 chars, alphanumeric/hyphens/spaces)"),
|
|
23113
23269
|
issuerUrl: external_exports.string().describe("The OIDC issuer URL (e.g. 'https://token.actions.githubusercontent.com' for GitHub Actions)"),
|
|
23114
23270
|
audience: external_exports.string().optional().describe("Expected audience claim in the OIDC token"),
|
|
23115
23271
|
claimMappings: external_exports.record(external_exports.string(), external_exports.string()).optional().describe("Map of Tailscale attributes to OIDC token claims (e.g. { 'tag': 'repository' })")
|
|
23116
23272
|
}),
|
|
23117
23273
|
handler: async (input) => {
|
|
23118
|
-
|
|
23274
|
+
const body = { ...input };
|
|
23275
|
+
body.name = sanitizeDescription(input.name);
|
|
23276
|
+
return apiPost(`/tailnet/${getTailnet()}/workload-identity/providers`, body);
|
|
23119
23277
|
}
|
|
23120
23278
|
},
|
|
23121
23279
|
{
|
|
@@ -23140,6 +23298,10 @@ var workloadIdentityTools = [
|
|
|
23140
23298
|
for (const [key, value] of Object.entries(body)) {
|
|
23141
23299
|
if (value !== void 0) cleanBody[key] = value;
|
|
23142
23300
|
}
|
|
23301
|
+
if (cleanBody.name !== void 0) cleanBody.name = sanitizeDescription(cleanBody.name);
|
|
23302
|
+
if (Object.keys(cleanBody).length === 0) {
|
|
23303
|
+
throw new Error("No fields to update. Provide at least one of: name, audience, claimMappings.");
|
|
23304
|
+
}
|
|
23143
23305
|
return apiPatch(`/tailnet/${getTailnet()}/workload-identity/providers/${encPath(providerId)}`, cleanBody);
|
|
23144
23306
|
}
|
|
23145
23307
|
},
|
|
@@ -23163,7 +23325,7 @@ var workloadIdentityTools = [
|
|
|
23163
23325
|
];
|
|
23164
23326
|
|
|
23165
23327
|
// src/index.ts
|
|
23166
|
-
var version2 = true ? "0.
|
|
23328
|
+
var version2 = true ? "0.6.3" : (await null).createRequire(import.meta.url)("../package.json").version;
|
|
23167
23329
|
var subcommand = process.argv[2];
|
|
23168
23330
|
if (subcommand === "deploy-acl") {
|
|
23169
23331
|
const filePath = process.argv[3];
|