@yawlabs/tailscale-mcp 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +51 -15
  2. package/dist/index.js +45 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -107,34 +107,70 @@ That's it. Now ask your agent:
107
107
 
108
108
  ## Too many tools? Subset them.
109
109
 
110
- 99 tools is a lot. If you've already got a dozen MCP servers and your client is feeling heavy, trim what this one exposes:
110
+ 99 tools is a lot. If you've already got a dozen MCP servers and your client is feeling heavy, trim what this one exposes. Three knobs, combinable:
111
+
112
+ ### Option 1: `TAILSCALE_PROFILE` (preset, easiest)
111
113
 
112
114
  ```json
113
115
  {
114
- "mcpServers": {
115
- "tailscale": {
116
- "command": "npx",
117
- "args": ["-y", "@yawlabs/tailscale-mcp"],
118
- "env": {
119
- "TAILSCALE_API_KEY": "tskey-api-...",
120
- "TAILSCALE_TOOLS": "devices,acl,dns,audit",
121
- "TAILSCALE_READONLY": "1"
122
- }
123
- }
116
+ "env": {
117
+ "TAILSCALE_API_KEY": "tskey-api-...",
118
+ "TAILSCALE_PROFILE": "core"
119
+ }
120
+ }
121
+ ```
122
+
123
+ - **`minimal`** (≈22 tools) — `status`, `devices`, `audit`. Observe the tailnet, read the audit log.
124
+ - **`core`** (≈49 tools) — adds `acl`, `dns`, `keys`, `users`. The day-to-day admin surface.
125
+ - **`full`** (99 tools, default) — everything. Same as omitting the env var.
126
+
127
+ ### Option 2: `TAILSCALE_TOOLS` (explicit group list)
128
+
129
+ ```json
130
+ {
131
+ "env": {
132
+ "TAILSCALE_API_KEY": "tskey-api-...",
133
+ "TAILSCALE_TOOLS": "devices,acl,dns,audit"
124
134
  }
125
135
  }
126
136
  ```
127
137
 
128
- - **`TAILSCALE_TOOLS`** comma-separated list of tool groups to expose. Omit for all groups.
129
- - **`TAILSCALE_READONLY`** — set to `1` or `true` to drop every mutating tool (only tools with `readOnlyHint: true` remain). Combine with `TAILSCALE_TOOLS` for maximum minimalism.
138
+ Comma-separated group names. Overrides `TAILSCALE_PROFILE` when both are set use this when the presets aren't quite right.
130
139
 
131
140
  Valid group names: `status`, `devices`, `acl`, `dns`, `keys`, `users`, `tailnet`, `webhooks`, `network-lock`, `posture`, `audit`, `invites`, `services`, `log-streaming`, `workload-identity`, `oauth-clients`.
132
141
 
133
- The server logs the active filter to stderr on startup so you can confirm what got loaded:
142
+ ### Option 3: `TAILSCALE_READONLY` (drop mutations)
134
143
 
144
+ ```json
145
+ {
146
+ "env": {
147
+ "TAILSCALE_API_KEY": "tskey-api-...",
148
+ "TAILSCALE_PROFILE": "core",
149
+ "TAILSCALE_READONLY": "1"
150
+ }
151
+ }
135
152
  ```
136
- @yawlabs/tailscale-mcp v0.8.0 ready (21 tools, groups=devices,acl,dns,audit, readonly)
153
+
154
+ Set to `1` or `true` to drop every tool without `readOnlyHint: true`. Stacks with `TAILSCALE_PROFILE` or `TAILSCALE_TOOLS` as an intersection — combine for maximum minimalism.
155
+
156
+ ### Confirming what loaded
157
+
158
+ The server logs the active filter to stderr on startup:
159
+
137
160
  ```
161
+ @yawlabs/tailscale-mcp v0.8.1 ready (21 tools, profile=core, readonly)
162
+ ```
163
+
164
+ If you don't set any filter, startup prints a tip pointing you at the profiles.
165
+
166
+ ## Using with mcp.hosting / mcph
167
+
168
+ If you run this server through [mcp.hosting](https://mcp.hosting) (via the `@yawlabs/mcph` local agent), the two filtering layers compose cleanly:
169
+
170
+ 1. **Server-side** — `TAILSCALE_PROFILE` / `TAILSCALE_TOOLS` / `TAILSCALE_READONLY` reduce the tool surface *before* mcph sees it. The unloaded tools aren't registered at all.
171
+ 2. **Client-side** — mcph's `mcp_connect_activate({ tools: [...] })` filters further for what appears in `tools/list`. Tools not in that list stay reachable via `mcp_connect_dispatch`, so you don't lose capability.
172
+
173
+ Recommended pattern for mcph users: set `TAILSCALE_PROFILE=core` (or narrower) in your mcp.hosting server config, then let mcph handle per-conversation activation on top. The server stays lean by default, and `mcp_connect_dispatch` covers the long-tail tools for ad-hoc needs.
138
174
 
139
175
  ## Authentication
140
176
 
package/dist/index.js CHANGED
@@ -30320,13 +30320,30 @@ async function deployAcl(filePath) {
30320
30320
  }
30321
30321
 
30322
30322
  // src/filter.ts
30323
+ var PROFILES = {
30324
+ minimal: ["status", "devices", "audit"],
30325
+ core: ["status", "devices", "acl", "dns", "keys", "users", "audit"],
30326
+ full: []
30327
+ // empty = all groups
30328
+ };
30323
30329
  function filterTools(groups, options) {
30324
- const enabledGroups = options.tools ? new Set(
30325
- options.tools.split(",").map((s) => s.trim()).filter(Boolean)
30326
- ) : null;
30327
- const readonly2 = options.readonly === "1" || options.readonly === "true";
30328
30330
  const validNames = new Set(Object.keys(groups));
30331
+ let profileGroups;
30332
+ let unknownProfile2;
30333
+ if (options.profile) {
30334
+ const profileKey = options.profile.trim().toLowerCase();
30335
+ if (profileKey in PROFILES) {
30336
+ const preset = PROFILES[profileKey];
30337
+ profileGroups = preset.length > 0 ? [...preset] : void 0;
30338
+ } else {
30339
+ unknownProfile2 = profileKey;
30340
+ }
30341
+ }
30342
+ const explicitTools = options.tools ? options.tools.split(",").map((s) => s.trim()).filter(Boolean) : null;
30343
+ const effectiveGroups = explicitTools ?? profileGroups ?? null;
30344
+ const enabledGroups = effectiveGroups ? new Set(effectiveGroups) : null;
30329
30345
  const unknownGroups2 = enabledGroups ? [...enabledGroups].filter((g) => !validNames.has(g)) : [];
30346
+ const readonly2 = options.readonly === "1" || options.readonly === "true";
30330
30347
  const out = [];
30331
30348
  for (const [name, tools] of Object.entries(groups)) {
30332
30349
  if (enabledGroups && !enabledGroups.has(name)) continue;
@@ -30335,7 +30352,10 @@ function filterTools(groups, options) {
30335
30352
  out.push(t);
30336
30353
  }
30337
30354
  }
30338
- return { tools: out, unknownGroups: unknownGroups2 };
30355
+ const result = { tools: out, unknownGroups: unknownGroups2 };
30356
+ if (unknownProfile2) result.unknownProfile = unknownProfile2;
30357
+ if (profileGroups && !explicitTools) result.profileGroups = profileGroups;
30358
+ return result;
30339
30359
  }
30340
30360
 
30341
30361
  // src/tools/acl.ts
@@ -32459,7 +32479,7 @@ var workloadIdentityTools = [
32459
32479
  ];
32460
32480
 
32461
32481
  // src/index.ts
32462
- var version2 = true ? "0.8.1" : (await null).createRequire(import.meta.url)("../package.json").version;
32482
+ var version2 = true ? "0.8.2" : (await null).createRequire(import.meta.url)("../package.json").version;
32463
32483
  var subcommand = process.argv[2];
32464
32484
  if (subcommand === "deploy-acl") {
32465
32485
  const filePath = process.argv[3];
@@ -32494,9 +32514,14 @@ var toolGroups = {
32494
32514
  "workload-identity": workloadIdentityTools,
32495
32515
  "oauth-clients": oauthClientTools
32496
32516
  };
32497
- var { tools: allTools, unknownGroups } = filterTools(toolGroups, {
32517
+ var {
32518
+ tools: allTools,
32519
+ unknownGroups,
32520
+ unknownProfile
32521
+ } = filterTools(toolGroups, {
32498
32522
  tools: process.env.TAILSCALE_TOOLS,
32499
- readonly: process.env.TAILSCALE_READONLY
32523
+ readonly: process.env.TAILSCALE_READONLY,
32524
+ profile: process.env.TAILSCALE_PROFILE
32500
32525
  });
32501
32526
  if (unknownGroups.length > 0) {
32502
32527
  const validNames = Object.keys(toolGroups);
@@ -32504,6 +32529,11 @@ if (unknownGroups.length > 0) {
32504
32529
  `@yawlabs/tailscale-mcp: TAILSCALE_TOOLS includes unknown group(s): ${unknownGroups.join(", ")}. Valid groups: ${validNames.join(", ")}`
32505
32530
  );
32506
32531
  }
32532
+ if (unknownProfile) {
32533
+ console.error(
32534
+ `@yawlabs/tailscale-mcp: TAILSCALE_PROFILE="${unknownProfile}" is not a known profile. Valid profiles: minimal, core, full. Falling back to no profile filter.`
32535
+ );
32536
+ }
32507
32537
  var server = new McpServer({
32508
32538
  name: "@yawlabs/tailscale-mcp",
32509
32539
  version: version2
@@ -32606,11 +32636,18 @@ server.resource(
32606
32636
  var transport = new StdioServerTransport();
32607
32637
  await server.connect(transport);
32608
32638
  var readonlyMode = process.env.TAILSCALE_READONLY === "1" || process.env.TAILSCALE_READONLY === "true";
32639
+ var profileApplied = process.env.TAILSCALE_PROFILE && !unknownProfile ? process.env.TAILSCALE_PROFILE : null;
32609
32640
  var filterSuffix = [
32641
+ profileApplied ? `profile=${profileApplied}` : null,
32610
32642
  process.env.TAILSCALE_TOOLS ? `groups=${process.env.TAILSCALE_TOOLS}` : null,
32611
32643
  readonlyMode ? "readonly" : null
32612
32644
  ].filter(Boolean).join(", ");
32613
32645
  console.error(
32614
32646
  `@yawlabs/tailscale-mcp v${version2} ready (${allTools.length} tools${filterSuffix ? `, ${filterSuffix}` : ""})`
32615
32647
  );
32648
+ if (!filterSuffix) {
32649
+ console.error(
32650
+ "@yawlabs/tailscale-mcp: tip \u2014 set TAILSCALE_PROFILE=core (\u224849 tools) or =minimal (\u224822) to load a smaller tool surface. See README."
32651
+ );
32652
+ }
32616
32653
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/tailscale-mcp",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Tailscale MCP server for managing your tailnet from AI assistants",
5
5
  "license": "MIT",
6
6
  "author": "YawLabs <contact@yaw.sh>",