@yawlabs/caddy-mcp 2.4.1 → 2.4.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/caddy-mcp.mjs CHANGED
@@ -28,8 +28,37 @@
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
+ * ALREADY RUNNING ON OAM
32
+ * A host can resolve this package's `bin` and launch `oam run <this file>`
33
+ * instead of `node <this file>` -- Yaw MCP does, and so does oam's sidecar
34
+ * regression matrix. This launcher used to discover oam and spawn it anyway,
35
+ * so one server cost two runtime boots: measured on Windows, oam.exe with a
36
+ * NESTED oam.exe + conhost.exe underneath it. Now, when `process.versions.oam`
37
+ * clears the same MINIMUM OAM VERSION a discovered binary has to, the server is
38
+ * imported into THIS process exactly as the Node fallback is -- no discovery,
39
+ * no `oam --version` probe, no second oam. OAM_BIN is a discovery input, so it
40
+ * is not consulted on that path: the host has already chosen which oam runs.
41
+ *
42
+ * Two cases still take the discovery path, deliberately. CADDY_MCP_SANDBOX=1,
43
+ * because `--permission` is a process-level flag that only a FRESH oam can
44
+ * apply -- serving in-process on the host would drop the sandbox without a
45
+ * word, a security downgrade dressed up as an optimisation. And a host oam
46
+ * below the floor, which takes the discovery path exactly as it always did.
47
+ *
48
+ * The sandbox asks for a spawn; it does not guarantee one. Discovery can still
49
+ * come up empty -- no oam binary, one below the floor or unreadable, a spawn
50
+ * that fails -- and under CADDY_MCP_RUNTIME=auto that falls back exactly as it
51
+ * does on Node: the server runs in THIS process WITHOUT `--permission`. No oam
52
+ * (and no .cmd shim) and a failed spawn fall back silently; the too-old or
53
+ * unreadable binary and .cmd-shim cases print a note that does not mention the
54
+ * sandbox. Only CADDY_MCP_RUNTIME=oam turns that miss into a hard failure, so
55
+ * pair it with CADDY_MCP_SANDBOX=1 when the sandbox has to hold.
56
+ * CADDY_MCP_RUNTIME=node ignores the sandbox entirely.
57
+ *
31
58
  * THE `--permission` SANDBOX (oam 0.9.0+, opt-in)
32
- * `CADDY_MCP_SANDBOX=1` runs the server under oam's permission model.
59
+ * `CADDY_MCP_SANDBOX=1` runs the server under oam's permission model when an
60
+ * oam binary is found and launched -- see ALREADY RUNNING ON OAM for the
61
+ * fallback that runs it unsandboxed, and how to refuse that instead.
33
62
  *
34
63
  * The admin API endpoint is DERIVED from CADDY_ADMIN_URL (default
35
64
  * http://localhost:2019 -- byte-identical to DEFAULT_URL in src/api.ts, see
@@ -68,9 +97,12 @@
68
97
  *
69
98
  * SELECTION
70
99
  * CADDY_MCP_RUNTIME=oam require oam; fail loudly if it is missing
100
+ * (already running on oam satisfies it, except
101
+ * under CADDY_MCP_SANDBOX=1)
71
102
  * CADDY_MCP_RUNTIME=node never use oam
72
103
  * CADDY_MCP_RUNTIME=auto prefer oam, silently fall back (default)
73
- * CADDY_MCP_SANDBOX=1 run oam under --permission (oam 0.9.0+)
104
+ * CADDY_MCP_SANDBOX=1 run oam under --permission (oam 0.9.0+); under
105
+ * auto a discovery miss still runs unsandboxed
74
106
  * OAM_BIN=/path/to/oam explicit binary, checked before any discovery
75
107
  */
76
108
 
@@ -143,17 +175,27 @@ function findOam() {
143
175
  }
144
176
 
145
177
  /**
146
- * `oam --version` -> [major, minor, patch], or null when it cannot be read.
178
+ * Version text -> [major, minor, patch], or null when it holds no version.
147
179
  * A pre-release suffix (0.9.0-rc.1) truncates to its base version.
180
+ *
181
+ * Shared by the two places a version is read -- a discovered binary's
182
+ * `oam --version` output ("oam 0.15.1") and the host's own
183
+ * `process.versions.oam` ("0.15.1") -- so they cannot disagree about what a
184
+ * version string means, or which floor it has to clear.
148
185
  */
186
+ function parseVersion(text) {
187
+ const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
188
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
189
+ }
190
+
191
+ /** `oam --version` -> [major, minor, patch], or null when it cannot be read. */
149
192
  function oamVersion(cmd) {
150
193
  try {
151
194
  const out = execFileSync(cmd, ["--version"], {
152
195
  encoding: "utf-8",
153
196
  stdio: ["ignore", "pipe", "ignore"],
154
197
  });
155
- const m = /(\d+)\.(\d+)\.(\d+)/.exec(out);
156
- return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
198
+ return parseVersion(out);
157
199
  } catch {
158
200
  // Not executable, wrong arch, or deleted since the stat. Caller degrades.
159
201
  return null;
@@ -170,6 +212,29 @@ function atLeast(v, min) {
170
212
  return true;
171
213
  }
172
214
 
215
+ /**
216
+ * Where the server runs, decided BEFORE any discovery:
217
+ * "in-process" import it into THIS process
218
+ * "discover" find an oam binary, gate its version, spawn it -- or fall
219
+ * back to Node in-process when that fails
220
+ *
221
+ * `hostOam` is `process.versions.oam`: oam's own key, absent on Node, so on
222
+ * Node every mode but `node` is the discovery path it always was. `sandbox`
223
+ * is whether a spawn would carry flags only a fresh oam can apply; see ALREADY
224
+ * RUNNING ON OAM above for why that alone forces discovery -- and why discovery
225
+ * is not a guaranteed spawn, since its miss still falls back in-process. The
226
+ * floor is OAM_MIN itself, not a parameter, so a host oam and a discovered one
227
+ * can never be held to different minimums.
228
+ *
229
+ * Pure on purpose: every input is passed in, so the whole decision is testable
230
+ * without booting a runtime.
231
+ */
232
+ function runtimePlan({ mode, hostOam, sandbox }) {
233
+ if (mode === "node") return "in-process";
234
+ if (sandbox) return "discover";
235
+ return atLeast(parseVersion(hostOam ?? ""), OAM_MIN) ? "in-process" : "discover";
236
+ }
237
+
173
238
  /**
174
239
  * The `--permission` grant list, or [] when the sandbox is not requested.
175
240
  *
@@ -361,7 +426,12 @@ async function runInProcess() {
361
426
 
362
427
  const mode = (process.env.CADDY_MCP_RUNTIME ?? "auto").toLowerCase();
363
428
 
364
- if (mode === "node") {
429
+ // The sandbox is read off the grant list rather than CADDY_MCP_SANDBOX, so
430
+ // "would the spawn carry --permission" cannot drift from what the spawn below
431
+ // actually passes.
432
+ const plan = runtimePlan({ mode, hostOam: process.versions.oam, sandbox: sandboxFlags().length > 0 });
433
+
434
+ if (plan === "in-process") {
365
435
  await runInProcess();
366
436
  } else {
367
437
  const oam = findOam();
@@ -10,10 +10,10 @@ export declare const METRICS_DEFAULT_MAX_LINES = 500;
10
10
  * metric name (blank lines, free-form `#` comments) are dropped when filtering.
11
11
  *
12
12
  * Truncation: if the resulting line count exceeds `maxLines`, output is cut at `maxLines` and a
13
- * trailing `# [truncated, N lines omitted -- use filter to narrow]` comment is appended. If the
14
- * input contained a `# EOF` end-of-file marker that would have been dropped by the cut, it is
15
- * re-appended after the truncation comment so strict downstream parsers still see a terminated
16
- * stream.
13
+ * trailing `# [truncated, N lines omitted; max_lines=M -- use filter or raise max_lines]` comment
14
+ * is appended, where N is the number of lines cut and M is `maxLines`. If the input contained a
15
+ * `# EOF` end-of-file marker that would have been dropped by the cut, it is re-appended after the
16
+ * truncation comment so strict downstream parsers still see a terminated stream.
17
17
  */
18
18
  export declare function applyMetricsControls(raw: string, filter: string | undefined, maxLines: number): string;
19
19
  export declare function registerOperationalTools(server: McpServer): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/caddy-mcp",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
4
4
  "mcpName": "io.github.YawLabs/caddy-mcp",
5
5
  "description": "Caddy MCP server for Claude Code, Cursor, and any MCP client: admin API, config, routes, reverse proxy, TLS, PKI, metrics",
6
6
  "license": "MIT",