@yawlabs/tailscale-mcp 0.19.2 → 0.19.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/bin/tailscale-mcp.mjs +69 -4
- package/dist/index.js +23 -3
- package/package.json +1 -1
package/bin/tailscale-mcp.mjs
CHANGED
|
@@ -22,6 +22,27 @@
|
|
|
22
22
|
* For an MCP host config, point straight at oam and skip this file:
|
|
23
23
|
* { "command": "oam", "args": ["run", "<abs>/dist/index.js"] }
|
|
24
24
|
*
|
|
25
|
+
* ALREADY RUNNING ON OAM
|
|
26
|
+
* A host can resolve this package's `bin` and launch `oam run <this file>`
|
|
27
|
+
* instead of `node <this file>` -- Yaw MCP does, and so does oam's sidecar
|
|
28
|
+
* regression matrix. This launcher used to discover oam and spawn it anyway,
|
|
29
|
+
* so one server cost two runtime boots: measured on Windows, oam.exe with a
|
|
30
|
+
* NESTED oam.exe + conhost.exe underneath it. Now, when `process.versions.oam`
|
|
31
|
+
* clears the same MINIMUM OAM VERSION a discovered binary has to, the server is
|
|
32
|
+
* imported into THIS process exactly as the Node fallback is -- no discovery,
|
|
33
|
+
* no `oam --version` probe, no second oam. OAM_BIN is a discovery input, so it
|
|
34
|
+
* is not consulted on that path: the host has already chosen which oam runs.
|
|
35
|
+
*
|
|
36
|
+
* Two cases still go through discovery, deliberately. TAILSCALE_MCP_SANDBOX=1,
|
|
37
|
+
* because `--permission` is a process-level flag that only a FRESH oam can
|
|
38
|
+
* apply -- serving in-process there would drop the sandbox without a word, a
|
|
39
|
+
* security downgrade dressed up as an optimisation. And a host oam below the
|
|
40
|
+
* floor. Both then run exactly as they did before this shortcut existed,
|
|
41
|
+
* fallback included: discovery spawns a fresh oam only when it finds one at or
|
|
42
|
+
* above the floor that launches. Otherwise auto serves in-process as before --
|
|
43
|
+
* for a sandbox request that means WITHOUT --permission, so pair the sandbox
|
|
44
|
+
* with TAILSCALE_MCP_RUNTIME=oam to make that a hard failure instead.
|
|
45
|
+
*
|
|
25
46
|
* THE `--permission` SANDBOX (oam 0.9.0+, opt-in)
|
|
26
47
|
* `TAILSCALE_MCP_SANDBOX=1` runs the server under oam's permission model:
|
|
27
48
|
* network limited to the one host the bundle actually calls
|
|
@@ -48,6 +69,7 @@
|
|
|
48
69
|
*
|
|
49
70
|
* SELECTION
|
|
50
71
|
* TAILSCALE_MCP_RUNTIME=oam require oam; fail loudly if it is missing
|
|
72
|
+
* (already running on oam satisfies it)
|
|
51
73
|
* TAILSCALE_MCP_RUNTIME=node never use oam
|
|
52
74
|
* TAILSCALE_MCP_RUNTIME=auto prefer oam, silently fall back (default)
|
|
53
75
|
* anything else warns on stderr, then behaves as auto
|
|
@@ -116,17 +138,27 @@ function findOam() {
|
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
/**
|
|
119
|
-
*
|
|
141
|
+
* Version text -> [major, minor, patch], or null when it holds no version.
|
|
120
142
|
* A pre-release suffix (0.9.0-rc.1) truncates to its base version.
|
|
143
|
+
*
|
|
144
|
+
* Shared by the two places a version is read -- a discovered binary's
|
|
145
|
+
* `oam --version` output ("oam 0.15.1") and the host's own
|
|
146
|
+
* `process.versions.oam` ("0.15.1") -- so they cannot disagree about what a
|
|
147
|
+
* version string means, or which floor it has to clear.
|
|
121
148
|
*/
|
|
149
|
+
function parseVersion(text) {
|
|
150
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
|
|
151
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `oam --version` -> [major, minor, patch], or null when it cannot be read. */
|
|
122
155
|
function oamVersion(cmd) {
|
|
123
156
|
try {
|
|
124
157
|
const out = execFileSync(cmd, ["--version"], {
|
|
125
158
|
encoding: "utf-8",
|
|
126
159
|
stdio: ["ignore", "pipe", "ignore"],
|
|
127
160
|
});
|
|
128
|
-
|
|
129
|
-
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
161
|
+
return parseVersion(out);
|
|
130
162
|
} catch {
|
|
131
163
|
// Not executable, wrong arch, or deleted since the stat. Caller degrades.
|
|
132
164
|
return null;
|
|
@@ -143,6 +175,34 @@ function atLeast(v, min) {
|
|
|
143
175
|
return true;
|
|
144
176
|
}
|
|
145
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Where the server runs, decided BEFORE any discovery:
|
|
180
|
+
* "in-process" import it into THIS process
|
|
181
|
+
* "discover" find an oam binary, gate its version, spawn it; when that
|
|
182
|
+
* fails, auto falls back in-process and `oam` exits 1
|
|
183
|
+
*
|
|
184
|
+
* `hostOam` is `process.versions.oam`: oam's own key, absent on Node, so on
|
|
185
|
+
* Node every mode but `node` is the discovery path it always was. Only `node`
|
|
186
|
+
* is distinguished here: `oam` is satisfied by a host that already is one, and
|
|
187
|
+
* an unrecognized value has been warned about and lands with auto, as it does
|
|
188
|
+
* in the discovery branch.
|
|
189
|
+
*
|
|
190
|
+
* `sandbox` is whether a spawn would carry flags only a fresh oam can apply;
|
|
191
|
+
* see ALREADY RUNNING ON OAM above for why that alone forces discovery. It does
|
|
192
|
+
* not guarantee a spawn: when discovery finds no launchable oam at the floor,
|
|
193
|
+
* auto still serves in-process, without --permission. The
|
|
194
|
+
* floor is OAM_MIN itself, not a parameter, so a host oam and a discovered one
|
|
195
|
+
* can never be held to different minimums.
|
|
196
|
+
*
|
|
197
|
+
* Pure on purpose: every input is passed in, so the whole decision is testable
|
|
198
|
+
* without booting a runtime.
|
|
199
|
+
*/
|
|
200
|
+
function runtimePlan({ mode, hostOam, sandbox }) {
|
|
201
|
+
if (mode === "node") return "in-process";
|
|
202
|
+
if (sandbox) return "discover";
|
|
203
|
+
return atLeast(parseVersion(hostOam ?? ""), OAM_MIN) ? "in-process" : "discover";
|
|
204
|
+
}
|
|
205
|
+
|
|
146
206
|
/**
|
|
147
207
|
* The `--permission` grant list, or [] when the sandbox is not requested.
|
|
148
208
|
*
|
|
@@ -280,7 +340,12 @@ if (requested && !RUNTIMES.includes(mode)) {
|
|
|
280
340
|
);
|
|
281
341
|
}
|
|
282
342
|
|
|
283
|
-
|
|
343
|
+
// The sandbox is read off the grant list rather than TAILSCALE_MCP_SANDBOX, so
|
|
344
|
+
// "would the spawn carry --permission" cannot drift from what the spawn below
|
|
345
|
+
// actually passes.
|
|
346
|
+
const plan = runtimePlan({ mode, hostOam: process.versions.oam, sandbox: sandboxFlags().length > 0 });
|
|
347
|
+
|
|
348
|
+
if (plan === "in-process") {
|
|
284
349
|
await runInProcess();
|
|
285
350
|
} else {
|
|
286
351
|
const oam = findOam();
|
package/dist/index.js
CHANGED
|
@@ -34551,12 +34551,29 @@ function buildMetaTools(state) {
|
|
|
34551
34551
|
}
|
|
34552
34552
|
|
|
34553
34553
|
// src/index.ts
|
|
34554
|
-
var version2 = true ? "0.19.
|
|
34554
|
+
var version2 = true ? "0.19.3" : resolveVersionFallback();
|
|
34555
34555
|
var subcommand = process.argv[2];
|
|
34556
|
+
var USAGE = `Usage: tailscale-mcp [command]
|
|
34557
|
+
|
|
34558
|
+
Commands:
|
|
34559
|
+
deploy-acl <path-to-acl.json> Deploy an ACL policy
|
|
34560
|
+
validate-acl <path-to-acl.json> Validate an ACL policy
|
|
34561
|
+
version Print the installed version
|
|
34562
|
+
help Print this message
|
|
34563
|
+
|
|
34564
|
+
Flags:
|
|
34565
|
+
--version, -V Print the installed version
|
|
34566
|
+
--help, -h Print this message
|
|
34567
|
+
|
|
34568
|
+
Run without a command to start the MCP server on stdio.`;
|
|
34556
34569
|
var cliSubcommandHandled = false;
|
|
34557
34570
|
if (subcommand === "deploy-acl" || subcommand === "validate-acl") {
|
|
34558
34571
|
cliSubcommandHandled = true;
|
|
34559
34572
|
const filePath = process.argv[3];
|
|
34573
|
+
if (filePath === "--help" || filePath === "-h") {
|
|
34574
|
+
console.log(`Usage: tailscale-mcp ${subcommand} <path-to-acl.json>`);
|
|
34575
|
+
process.exit(0);
|
|
34576
|
+
}
|
|
34560
34577
|
if (!filePath) {
|
|
34561
34578
|
console.error(`Usage: tailscale-mcp ${subcommand} <path-to-acl.json>`);
|
|
34562
34579
|
process.exit(1);
|
|
@@ -34566,12 +34583,15 @@ if (subcommand === "deploy-acl" || subcommand === "validate-acl") {
|
|
|
34566
34583
|
console.error(`Fatal: ${err instanceof Error ? err.message : err}`);
|
|
34567
34584
|
process.exit(1);
|
|
34568
34585
|
});
|
|
34569
|
-
} else if (subcommand === "version" || subcommand === "--version") {
|
|
34586
|
+
} else if (subcommand === "version" || subcommand === "--version" || subcommand === "-V") {
|
|
34570
34587
|
console.log(version2);
|
|
34571
34588
|
process.exit(0);
|
|
34589
|
+
} else if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
|
|
34590
|
+
console.log(USAGE);
|
|
34591
|
+
process.exit(0);
|
|
34572
34592
|
} else if (subcommand !== void 0) {
|
|
34573
34593
|
console.error(
|
|
34574
|
-
`@yawlabs/tailscale-mcp: unrecognized argument "${subcommand}" -- known subcommands: deploy-acl, validate-acl, version. Starting the MCP server.`
|
|
34594
|
+
`@yawlabs/tailscale-mcp: unrecognized argument "${subcommand}" -- known subcommands: deploy-acl, validate-acl, version, help (or --help). Starting the MCP server.`
|
|
34575
34595
|
);
|
|
34576
34596
|
}
|
|
34577
34597
|
if (!cliSubcommandHandled) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yawlabs/tailscale-mcp",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.3",
|
|
4
4
|
"mcpName": "io.github.YawLabs/tailscale-mcp",
|
|
5
5
|
"description": "Tailscale MCP server: admin-API tools for devices, ACLs, DNS, auth keys, users, and audit logs, plus a CLI to validate and deploy ACLs.",
|
|
6
6
|
"license": "MIT",
|