@yawlabs/caddy-mcp 2.1.0 → 2.2.0
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/caddy-mcp.mjs +97 -3
- package/package.json +1 -1
package/bin/caddy-mcp.mjs
CHANGED
|
@@ -28,18 +28,46 @@
|
|
|
28
28
|
* For an MCP host config, point straight at oam and skip this file:
|
|
29
29
|
* { "command": "oam", "args": ["run", "<abs>/dist/index.js"] }
|
|
30
30
|
*
|
|
31
|
+
* THE `--permission` SANDBOX (oam 0.9.0+, opt-in)
|
|
32
|
+
* `CADDY_MCP_SANDBOX=1` runs the server under oam's permission model.
|
|
33
|
+
*
|
|
34
|
+
* The admin API endpoint is DERIVED from CADDY_ADMIN_URL (default
|
|
35
|
+
* http://127.0.0.1:2019), host and port both pinned -- grants are prefix-matched,
|
|
36
|
+
* so a bare host would also admit every other port on it. Filesystem AND
|
|
37
|
+
* child-process both stay denied: this server drives Caddy entirely over its
|
|
38
|
+
* admin HTTP API and never shells out to the `caddy` binary (the only
|
|
39
|
+
* execFileSync calls in the repo are in src/tests/).
|
|
40
|
+
*
|
|
41
|
+
* Opt-in, not default: a denied environment variable is ABSENT from process.env
|
|
42
|
+
* rather than throwing, so an under-granted CADDY_API_TOKEN reads as
|
|
43
|
+
* "unauthenticated". The env list is derived from the shipped bundle.
|
|
44
|
+
*
|
|
45
|
+
* MINIMUM OAM VERSION
|
|
46
|
+
* 0.9.0. Below it `child_process.execFile` ran its arguments through a SHELL,
|
|
47
|
+
* `exec` accepted `timeout` and ignored it, `spawnSync` truncated at
|
|
48
|
+
* `maxBuffer` while reporting success, and `stdio: 'inherit'`/`'ignore'` both
|
|
49
|
+
* behaved as `'pipe'`. This server spawns nothing, so the floor is
|
|
50
|
+
* enforced for consistency across @yawlabs/*-mcp rather than because this
|
|
51
|
+
* launcher was exposed.
|
|
52
|
+
* An older oam is not an error: the launcher falls back to Node and says so on
|
|
53
|
+
* stderr. Pinning the floor here is what makes that fallback automatic.
|
|
54
|
+
*
|
|
31
55
|
* SELECTION
|
|
32
56
|
* CADDY_MCP_RUNTIME=oam require oam; fail loudly if it is missing
|
|
33
57
|
* CADDY_MCP_RUNTIME=node never use oam
|
|
34
58
|
* CADDY_MCP_RUNTIME=auto prefer oam, silently fall back (default)
|
|
59
|
+
* CADDY_MCP_SANDBOX=1 run oam under --permission (oam 0.9.0+)
|
|
35
60
|
* OAM_BIN=/path/to/oam explicit binary, checked before any discovery
|
|
36
61
|
*/
|
|
37
62
|
|
|
38
|
-
import { spawn } from "node:child_process";
|
|
63
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
39
64
|
import { existsSync } from "node:fs";
|
|
40
65
|
import { constants, homedir } from "node:os";
|
|
41
66
|
import { delimiter, join } from "node:path";
|
|
42
|
-
import { fileURLToPath } from "node:url";
|
|
67
|
+
import { fileURLToPath } from "node:url";
|
|
68
|
+
|
|
69
|
+
/** Oldest oam whose `child_process` matches Node. See MINIMUM OAM VERSION above. */
|
|
70
|
+
const OAM_MIN = [0, 9, 0];
|
|
43
71
|
|
|
44
72
|
// Two forms, deliberately. `import()` on Windows REJECTS a bare `C:\...` path
|
|
45
73
|
// with ERR_UNSUPPORTED_ESM_URL_SCHEME (it reads `c:` as a protocol), so the
|
|
@@ -89,6 +117,72 @@ function findOam() {
|
|
|
89
117
|
return null;
|
|
90
118
|
}
|
|
91
119
|
|
|
120
|
+
/**
|
|
121
|
+
* `oam --version` -> [major, minor, patch], or null when it cannot be read.
|
|
122
|
+
* A pre-release suffix (0.9.0-rc.1) truncates to its base version.
|
|
123
|
+
*/
|
|
124
|
+
function oamVersion(cmd) {
|
|
125
|
+
try {
|
|
126
|
+
const out = execFileSync(cmd, ["--version"], {
|
|
127
|
+
encoding: "utf-8",
|
|
128
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
129
|
+
});
|
|
130
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(out);
|
|
131
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
132
|
+
} catch {
|
|
133
|
+
// Not executable, wrong arch, or deleted since the stat. Caller degrades.
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** True when `v` is at least `min`, comparing major/minor/patch in order. */
|
|
139
|
+
function atLeast(v, min) {
|
|
140
|
+
if (!v) return false;
|
|
141
|
+
for (let i = 0; i < min.length; i++) {
|
|
142
|
+
if (v[i] > min[i]) return true;
|
|
143
|
+
if (v[i] < min[i]) return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The `--permission` grant list, or [] when the sandbox is not requested.
|
|
150
|
+
*
|
|
151
|
+
* These are oam's PROCESS-level flags: they belong before the `run` subcommand,
|
|
152
|
+
* not after it. `oam run --permission file.js` is rejected outright, which is a
|
|
153
|
+
* good failure but only because it is loud -- ordering here is load-bearing.
|
|
154
|
+
*
|
|
155
|
+
* Net grants prefix-match `host` for fetch and `host:port` for sockets.
|
|
156
|
+
* A denied environment variable is ABSENT from process.env rather than throwing,
|
|
157
|
+
* so the env list below is derived from what the bundle actually reads; trimming
|
|
158
|
+
* it produces silent misbehaviour, not a clear denial.
|
|
159
|
+
*/
|
|
160
|
+
function sandboxFlags() {
|
|
161
|
+
if (process.env.CADDY_MCP_SANDBOX !== "1") return [];
|
|
162
|
+
|
|
163
|
+
// Derived, not hardcoded: the only endpoint this server may reach is the one
|
|
164
|
+
// it was configured to reach. Grants are prefix-matched against "host:port"
|
|
165
|
+
// for sockets, so host alone would also admit any other port on that host --
|
|
166
|
+
// pin both. A DSN we cannot parse falls back to a bare grant rather than a
|
|
167
|
+
// broken one, because a wrong narrow grant fails at connect time.
|
|
168
|
+
const dsn = process.env.CADDY_ADMIN_URL ?? "http://127.0.0.1:2019";
|
|
169
|
+
let netFlag = "--allow-net";
|
|
170
|
+
if (dsn) {
|
|
171
|
+
try {
|
|
172
|
+
const u = new URL(dsn);
|
|
173
|
+
if (u.hostname) netFlag = `--allow-net=${u.hostname}:${u.port || 2019}`;
|
|
174
|
+
} catch {
|
|
175
|
+
// Unparseable CADDY_ADMIN_URL: leave the grant open. The server will fail on
|
|
176
|
+
// its own connection error, which names the real problem.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const env = ["CADDY_ADMIN_URL","CADDY_API_TOKEN","CADDY_LOAD_TIMEOUT","CADDY_MAX_RETRIES","CADDY_TIMEOUT"];
|
|
181
|
+
|
|
182
|
+
const flags = ["--permission", netFlag, `--allow-env=${env.join(",")}`];
|
|
183
|
+
return flags;
|
|
184
|
+
}
|
|
185
|
+
|
|
92
186
|
/** Run the server in THIS process. The zero-overhead fallback. */
|
|
93
187
|
async function runInProcess() {
|
|
94
188
|
// A server may gate its bootstrap on being the process ENTRY POINT --
|
|
@@ -129,7 +223,7 @@ if (mode === "node") {
|
|
|
129
223
|
} else {
|
|
130
224
|
// `--` separates oam's own flags from the script's argv, so `caddy-mcp
|
|
131
225
|
// --version` and any host-supplied flags survive the hop unchanged.
|
|
132
|
-
const child = spawn(oam, ["run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
|
|
226
|
+
const child = spawn(oam, [...sandboxFlags(), "run", SERVER_ENTRY, "--", ...process.argv.slice(2)], {
|
|
133
227
|
// inherit keeps the SAME fds, so MCP's newline-delimited JSON framing on
|
|
134
228
|
// stdin/stdout is untouched and the host's stdin-close still reaches the
|
|
135
229
|
// server's shutdown path.
|