toolfunnel 0.7.0 → 0.8.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.
Files changed (73) hide show
  1. package/README.md +238 -235
  2. package/SECURITY.md +84 -84
  3. package/bin/toolfunnel.js +530 -451
  4. package/docs/KNOWN_BUGS.md +100 -80
  5. package/docs/MANUAL.pdf +0 -0
  6. package/docs/design.md +84 -84
  7. package/docs/packaging.md +116 -115
  8. package/hooks/hooks.manifest.json +27 -27
  9. package/hooks/scripts/example-audit.js +21 -21
  10. package/hooks/scripts/example-deny-dangerous.js +39 -39
  11. package/mcp/expose.example.json +30 -30
  12. package/mcp/servers/mock-modern/server.js +147 -147
  13. package/mcp/servers/mock-upstream/server.js +379 -380
  14. package/package.json +67 -67
  15. package/src/auth/config.js +172 -174
  16. package/src/auth/install.js +181 -181
  17. package/src/auth/resource-server.js +358 -353
  18. package/src/core/atomic-write.js +97 -0
  19. package/src/core/child-reaper.js +61 -61
  20. package/src/core/config-home.js +226 -134
  21. package/src/core/events.js +105 -105
  22. package/src/core/hook-engine.js +304 -329
  23. package/src/core/hook-loader.js +766 -824
  24. package/src/core/hook-runner.js +615 -566
  25. package/src/core/logger.js +295 -297
  26. package/src/core/matcher.js +100 -100
  27. package/src/core/metrics.js +90 -75
  28. package/src/core/output-cap.js +31 -0
  29. package/src/core/rename-migration.js +178 -163
  30. package/src/core/requires.js +138 -138
  31. package/src/core/server-config.js +98 -98
  32. package/src/extend/add-hook.md +155 -152
  33. package/src/extend/add-mcp.md +119 -119
  34. package/src/extend/configure.md +88 -88
  35. package/src/extend/create-tool.md +168 -168
  36. package/src/extend/howto.js +102 -102
  37. package/src/extend/package.md +190 -190
  38. package/src/extend/wrap.md +99 -99
  39. package/src/mcp/aggregator.js +2248 -1674
  40. package/src/mcp/expose-store.js +755 -577
  41. package/src/mcp/gated-run.js +210 -210
  42. package/src/mcp/http-transport.js +1657 -1563
  43. package/src/mcp/mcp-client.js +1174 -1070
  44. package/src/mcp/modern.js +409 -406
  45. package/src/mcp/protocol.js +473 -468
  46. package/src/mcp/server.js +3317 -3112
  47. package/src/mcp/tool-approvals.js +396 -0
  48. package/src/tools/registry.js +607 -661
  49. package/src/tools/tool-state.js +170 -170
  50. package/src/ui/public/app.js +1246 -1187
  51. package/src/ui/public/index.html +286 -286
  52. package/src/ui/public/styles.css +535 -535
  53. package/src/ui/server.js +1742 -1616
  54. package/tools/scripts/base64.js +139 -139
  55. package/tools/scripts/danger.js +61 -61
  56. package/tools/scripts/echo.js +39 -39
  57. package/tools/scripts/hash.js +101 -101
  58. package/tools/scripts/json.js +109 -109
  59. package/tools/scripts/text-stats.js +131 -131
  60. package/tools/scripts/tf-accept-change.js +67 -0
  61. package/tools/scripts/tf-env.js +43 -43
  62. package/tools/scripts/tf-hook-add.js +151 -151
  63. package/tools/scripts/tf-hook-set.js +120 -120
  64. package/tools/scripts/tf-list.js +192 -192
  65. package/tools/scripts/tf-log.js +86 -86
  66. package/tools/scripts/tf-mcp-add.js +130 -117
  67. package/tools/scripts/tf-mcp-set.js +121 -121
  68. package/tools/scripts/tf-pack.js +237 -237
  69. package/tools/scripts/tf-tool-add.js +134 -134
  70. package/tools/scripts/tf-tool-set.js +142 -142
  71. package/tools/scripts/tf-wrap.js +103 -103
  72. package/tools/scripts/uuid.js +102 -102
  73. package/tools/tools.register.json +16 -5
package/docs/packaging.md CHANGED
@@ -1,115 +1,116 @@
1
- # Packaging: ship what you built
2
-
3
- ToolFunnel's config is a folder. That single fact is the whole packaging story: everything you
4
- build in a gateway - your tools and their scripts, which of them are promoted hot or hidden, the
5
- upstream MCPs you attached and the tools you chose to expose from them, the policy hooks that gate
6
- every call, and the identity the server introduces itself with - lives in one directory tree, the
7
- **config home**. Ship the tree and you have shipped the product.
8
-
9
- ## The config home
10
-
11
- By default the home is the package/clone root - a git clone works exactly as it always did. Point
12
- the gateway elsewhere and the config lives there instead:
13
-
14
- ```
15
- toolfunnel --config-dir D:\my-setup # flag (wins)
16
- TOOLFUNNEL_HOME=D:\my-setup toolfunnel # env var
17
- ```
18
-
19
- An empty home is **seeded** on first use from the shipped defaults (the register + management
20
- scripts, an empty `mcp/expose.json`, the default hooks manifest) and is **never overwritten**
21
- afterwards - which is the point: with the home outside the package, `npm update toolfunnel`
22
- can never eat your tools.
23
-
24
- ```
25
- <home>/
26
- toolfunnel.json # identity (serverName/serverVersion), port defaults, `requires`
27
- tools/
28
- tools.register.json # your tool entries (incl. their inputSchema)
29
- tools.state.json # the hot/hidden/enabled curation - part of the product
30
- scripts/ # the tool scripts
31
- mcp/expose.json # upstream references + curated exposure
32
- hooks/ # the policy gate: manifest + hook scripts
33
- ```
34
-
35
- ## `tf_pack` - one call to a deployment artifact
36
-
37
- The `tf_pack` management tool snapshots the LIVE setup into `<home>/dist/<name>/` - always a
38
- separate location, never the live tree. `auth/` (environment-specific) and `logs/` never ship.
39
-
40
- **`{ format: "home" }`** → a portable config home. Zip it, commit it to a repo, or run it as-is:
41
-
42
- ```
43
- toolfunnel --config-dir path/to/the-pack
44
- ```
45
-
46
- **`{ format: "npm", name: "my-mcp", version: "1.0.0" }`** → a publishable npm package:
47
-
48
- ```
49
- dist/my-mcp/
50
- package.json # name, bin, files, dependencies: { "toolfunnel": "^<current version>" }
51
- bin/my-mcp.js # 2 lines: pin --config-dir at ../home, delegate to toolfunnel
52
- home/ # the bundled config home (identity rewritten to my-mcp@1.0.0)
53
- README.md # a stub to make your own
54
- ```
55
-
56
- Review it, then `cd dist/my-mcp && npm publish`. Your users run `npx my-mcp` and get **your**
57
- server: your name in the MCP `initialize` handshake, your tools with your schemas, your curation,
58
- and - the part nobody else can say - **your gate**. The hooks travel inside the pack and enforce
59
- your policy on the recipient's machine regardless of which MCP client calls it.
60
-
61
- ### Depend, never copy
62
-
63
- The generated package **depends on** `toolfunnel` (caret range) rather than vendoring it. Your own
64
- runtime dependencies go in **your** `package.json` exactly as with any npm package - npm installs
65
- the whole tree on `npx my-mcp`; toolfunnel itself adds zero. Why depending matters:
66
-
67
- - security and bug fixes reach every wrapped MCP through a normal `npm update` (the pack pins the caret range of the version that built it, so patch releases flow automatically) - no stale forks
68
- carrying old bugs;
69
- - your package stays a few KB of config instead of a copied engine;
70
- - (and honestly) every install of your MCP counts as a toolfunnel download, which keeps the thing
71
- you depend on alive.
72
-
73
- Your users can move the bundled home out of `node_modules` (so *their* edits survive *your*
74
- updates) via the `<NAME>_HOME` env var the generated launcher supports.
75
-
76
- ## Declaring runtimes: `requires`
77
-
78
- Your tools may need runtimes the gateway doesn't (python, git, ffmpeg...). Declare them in the
79
- home's `toolfunnel.json` and they travel with every pack:
80
-
81
- ```json
82
- {
83
- "serverName": "my-mcp",
84
- "requires": [
85
- { "command": "python", "min": "3.10", "why": "the pdf-extract tools" },
86
- { "command": "git" }
87
- ]
88
- }
89
- ```
90
-
91
- The gateway probes them once at startup and prints one clear stderr line per problem - found
92
- version vs needed, plus your `why`. Advisory by design: a missing runtime breaks only the tools
93
- that need it, so the server still starts and everything else keeps working.
94
-
95
- ## Composite packs
96
-
97
- A pack is not just your own scripts. Because `mcp/expose.json` lives in the home, a pack carries
98
- **upstream references too**: "attach `@example/weather-mcp` (pin the version) and expose exactly
99
- these four tools under these names." The upstream itself travels as a *reference* - `npx` fetches
100
- it on the recipient's machine - while your **curation** and your **gate** travel as config. One
101
- pack = your tools + other people's servers, curated, renamed, and policed, as a single artifact.
102
-
103
- ## Audit honesty
104
-
105
- Packs spawn commands - that is what they are for. Before you run anyone's pack (including one of
106
- ours), read **three files** - every command it can run is declared in plain JSON:
107
-
108
- - `tools/tools.register.json` - the tool invokes (scripts and shell commands);
109
- - `mcp/expose.json` - the upstream server commands it will spawn;
110
- - `toolfunnel.json` - the `requires` probes. Note these run **at gateway startup** (each declared
111
- program is executed once with its version flag), which is why a `requires` command must be a
112
- bare program name - the gateway rejects paths, spaces, and shell characters outright.
113
-
114
- Ship your own packs expecting the same scrutiny, and say so in your README (the `tf_pack` stub
115
- already does).
1
+ # Packaging: ship what you built
2
+
3
+ ToolFunnel's config is a folder. That single fact is the whole packaging story: everything you
4
+ build in a gateway - your tools and their scripts, which of them are promoted hot or hidden, the
5
+ upstream MCPs you attached and the tools you chose to expose from them, the policy hooks that gate
6
+ every call, and the identity the server introduces itself with - lives in one directory tree, the
7
+ **config home**. Ship the tree and you have shipped the product.
8
+
9
+ ## The config home
10
+
11
+ By default the home is the package/clone root - a git clone works exactly as it always did. Point
12
+ the gateway elsewhere and the config lives there instead:
13
+
14
+ ```
15
+ toolfunnel --config-dir D:\my-setup # flag (wins)
16
+ TOOLFUNNEL_HOME=D:\my-setup toolfunnel # env var
17
+ ```
18
+
19
+ An empty home is **seeded** on first use from the shipped defaults (the register + management
20
+ scripts, an empty `mcp/expose.json`, the default hooks manifest) and is **never overwritten**
21
+ afterwards - which is the point: with the home outside the package, `npm update toolfunnel`
22
+ can never eat your tools. The one thing an update adds is a tool new in that release, appended
23
+ to your register once; a shipped tool you removed is never put back.
24
+
25
+ ```
26
+ <home>/
27
+ toolfunnel.json # identity (serverName/serverVersion), port defaults, `requires`
28
+ tools/
29
+ tools.register.json # your tool entries (incl. their inputSchema)
30
+ tools.state.json # the hot/hidden/enabled curation - part of the product
31
+ scripts/ # the tool scripts
32
+ mcp/expose.json # upstream references + curated exposure
33
+ hooks/ # the policy gate: manifest + hook scripts
34
+ ```
35
+
36
+ ## `tf_pack` - one call to a deployment artifact
37
+
38
+ The `tf_pack` management tool snapshots the LIVE setup into `<home>/dist/<name>/` - always a
39
+ separate location, never the live tree. `auth/` (environment-specific) and `logs/` never ship.
40
+
41
+ **`{ format: "home" }`** → a portable config home. Zip it, commit it to a repo, or run it as-is:
42
+
43
+ ```
44
+ toolfunnel --config-dir path/to/the-pack
45
+ ```
46
+
47
+ **`{ format: "npm", name: "my-mcp", version: "1.0.0" }`** → a publishable npm package:
48
+
49
+ ```
50
+ dist/my-mcp/
51
+ package.json # name, bin, files, dependencies: { "toolfunnel": "^<current version>" }
52
+ bin/my-mcp.js # 2 lines: pin --config-dir at ../home, delegate to toolfunnel
53
+ home/ # the bundled config home (identity rewritten to my-mcp@1.0.0)
54
+ README.md # a stub to make your own
55
+ ```
56
+
57
+ Review it, then `cd dist/my-mcp && npm publish`. Your users run `npx my-mcp` and get **your**
58
+ server: your name in the MCP `initialize` handshake, your tools with your schemas, your curation,
59
+ and - the part nobody else can say - **your gate**. The hooks travel inside the pack and enforce
60
+ your policy on the recipient's machine regardless of which MCP client calls it.
61
+
62
+ ### Depend, never copy
63
+
64
+ The generated package **depends on** `toolfunnel` (caret range) rather than vendoring it. Your own
65
+ runtime dependencies go in **your** `package.json` exactly as with any npm package - npm installs
66
+ the whole tree on `npx my-mcp`; toolfunnel itself adds zero. Why depending matters:
67
+
68
+ - security and bug fixes reach every wrapped MCP through a normal `npm update` (the pack pins the caret range of the version that built it, so patch releases flow automatically) - no stale forks
69
+ carrying old bugs;
70
+ - your package stays a few KB of config instead of a copied engine;
71
+ - (and honestly) every install of your MCP counts as a toolfunnel download, which keeps the thing
72
+ you depend on alive.
73
+
74
+ Your users can move the bundled home out of `node_modules` (so *their* edits survive *your*
75
+ updates) via the `<NAME>_HOME` env var the generated launcher supports.
76
+
77
+ ## Declaring runtimes: `requires`
78
+
79
+ Your tools may need runtimes the gateway doesn't (python, git, ffmpeg...). Declare them in the
80
+ home's `toolfunnel.json` and they travel with every pack:
81
+
82
+ ```json
83
+ {
84
+ "serverName": "my-mcp",
85
+ "requires": [
86
+ { "command": "python", "min": "3.10", "why": "the pdf-extract tools" },
87
+ { "command": "git" }
88
+ ]
89
+ }
90
+ ```
91
+
92
+ The gateway probes them once at startup and prints one clear stderr line per problem - found
93
+ version vs needed, plus your `why`. Advisory by design: a missing runtime breaks only the tools
94
+ that need it, so the server still starts and everything else keeps working.
95
+
96
+ ## Composite packs
97
+
98
+ A pack is not just your own scripts. Because `mcp/expose.json` lives in the home, a pack carries
99
+ **upstream references too**: "attach `@example/weather-mcp` (pin the version) and expose exactly
100
+ these four tools under these names." The upstream itself travels as a *reference* - `npx` fetches
101
+ it on the recipient's machine - while your **curation** and your **gate** travel as config. One
102
+ pack = your tools + other people's servers, curated, renamed, and policed, as a single artifact.
103
+
104
+ ## Audit honesty
105
+
106
+ Packs spawn commands - that is what they are for. Before you run anyone's pack (including one of
107
+ ours), read **three files** - every command it can run is declared in plain JSON:
108
+
109
+ - `tools/tools.register.json` - the tool invokes (scripts and shell commands);
110
+ - `mcp/expose.json` - the upstream server commands it will spawn;
111
+ - `toolfunnel.json` - the `requires` probes. Note these run **at gateway startup** (each declared
112
+ program is executed once with its version flag), which is why a `requires` command must be a
113
+ bare program name - the gateway rejects paths, spaces, and shell characters outright.
114
+
115
+ Ship your own packs expecting the same scrutiny, and say so in your README (the `tf_pack` stub
116
+ already does).
@@ -1,27 +1,27 @@
1
- {
2
- "version": 1,
3
- "hooks": [
4
- {
5
- "id": "pre-tool-use/example-deny-dangerous",
6
- "event": "PreToolUse",
7
- "matcher": "*",
8
- "type": "command",
9
- "command": "node \"${HOOKS_DIR}/scripts/example-deny-dangerous.js\"",
10
- "script": "scripts/example-deny-dangerous.js",
11
- "timeout": 10,
12
- "enabled": false,
13
- "description": "EXAMPLE (disabled by default). A PreToolUse policy gate: denies any tool call whose arguments contain an obviously-destructive shell pattern (rm -rf, mkfs, drive format, fork bomb). Exit 2 denies and blocks the call before it runs; exit 0 allows. Any OTHER exit (a crash, a missing script, a timeout) is a non-blocking error and the call PROCEEDS - write denies as exit 2, never as a bare non-zero. Enable it in the UI Hooks tab (or set enabled:true here) to see the gate bite; edit scripts/example-deny-dangerous.js to encode your own policy."
14
- },
15
- {
16
- "id": "post-tool-use/example-audit",
17
- "event": "PostToolUse",
18
- "matcher": "*",
19
- "type": "command",
20
- "command": "node \"${HOOKS_DIR}/scripts/example-audit.js\"",
21
- "script": "scripts/example-audit.js",
22
- "timeout": 10,
23
- "enabled": false,
24
- "description": "EXAMPLE (disabled by default). A PostToolUse observer: runs AFTER a tool executes (advisory - it cannot un-run the call) and receives the tool result on stdin. The stub exits 0 (clean pass); replace its body to forward results to your own audit sink, metrics, or SIEM. Enable it in the UI Hooks tab (or set enabled:true here)."
25
- }
26
- ]
27
- }
1
+ {
2
+ "version": 1,
3
+ "hooks": [
4
+ {
5
+ "id": "pre-tool-use/example-deny-dangerous",
6
+ "event": "PreToolUse",
7
+ "matcher": "*",
8
+ "type": "command",
9
+ "command": "node \"${HOOKS_DIR}/scripts/example-deny-dangerous.js\"",
10
+ "script": "scripts/example-deny-dangerous.js",
11
+ "timeout": 10,
12
+ "enabled": false,
13
+ "description": "EXAMPLE (disabled by default). A PreToolUse policy gate: denies any tool call whose arguments contain an obviously-destructive shell pattern (rm -rf, mkfs, drive format, fork bomb). Exit 2 denies and blocks the call before it runs; exit 0 allows. Any OTHER exit (a crash, a missing script, a timeout) is a non-blocking error and the call PROCEEDS - write denies as exit 2, never as a bare non-zero. Enable it in the UI Hooks tab (or set enabled:true here) to see the gate bite; edit scripts/example-deny-dangerous.js to encode your own policy."
14
+ },
15
+ {
16
+ "id": "post-tool-use/example-audit",
17
+ "event": "PostToolUse",
18
+ "matcher": "*",
19
+ "type": "command",
20
+ "command": "node \"${HOOKS_DIR}/scripts/example-audit.js\"",
21
+ "script": "scripts/example-audit.js",
22
+ "timeout": 10,
23
+ "enabled": false,
24
+ "description": "EXAMPLE (disabled by default). A PostToolUse observer: runs AFTER a tool executes (advisory - it cannot un-run the call) and receives the tool result on stdin. The stub exits 0 (clean pass); replace its body to forward results to your own audit sink, metrics, or SIEM. Enable it in the UI Hooks tab (or set enabled:true here)."
25
+ }
26
+ ]
27
+ }
@@ -1,21 +1,21 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * EXAMPLE PostToolUse observer - DISABLED by default.
6
- *
7
- * PostToolUse is ADVISORY: it runs AFTER a tool has executed and CANNOT un-run it - it observes the
8
- * outcome. The lifecycle event (including the tool result) arrives as JSON on stdin. This stub just
9
- * exits 0 (a clean pass). Replace the body to forward results to your own audit sink, metrics, an
10
- * external SIEM, etc. Exit 0 = observed; a non-zero exit on PostToolUse is a non-blocking error.
11
- *
12
- * Zero dependencies - Node built-ins only.
13
- */
14
-
15
- let raw = '';
16
- process.stdin.on('data', (c) => { raw += c; });
17
- process.stdin.on('end', () => {
18
- // let ev = JSON.parse(raw || '{}'); // { tool_name, tool_input, tool_response, ... }
19
- // ... forward ev to your audit sink here ...
20
- process.exit(0);
21
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * EXAMPLE PostToolUse observer - DISABLED by default.
6
+ *
7
+ * PostToolUse is ADVISORY: it runs AFTER a tool has executed and CANNOT un-run it - it observes the
8
+ * outcome. The lifecycle event (including the tool result) arrives as JSON on stdin. This stub just
9
+ * exits 0 (a clean pass). Replace the body to forward results to your own audit sink, metrics, an
10
+ * external SIEM, etc. Exit 0 = observed; a non-zero exit on PostToolUse is a non-blocking error.
11
+ *
12
+ * Zero dependencies - Node built-ins only.
13
+ */
14
+
15
+ let raw = '';
16
+ process.stdin.on('data', (c) => { raw += c; });
17
+ process.stdin.on('end', () => {
18
+ // let ev = JSON.parse(raw || '{}'); // { tool_name, tool_input, tool_response, ... }
19
+ // ... forward ev to your audit sink here ...
20
+ process.exit(0);
21
+ });
@@ -1,39 +1,39 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * EXAMPLE PreToolUse gate - DISABLED by default (enable it in hooks/hooks.manifest.json, or via the
6
- * UI Hooks tab / the per-tool "Pre" toggle on the Tools tab).
7
- *
8
- * This is the shape of a real policy gate: it runs INSIDE the gateway, BEFORE a tool executes, and
9
- * `process.exit(2)` DENIES the call. The lifecycle event arrives as JSON on stdin; here we block any
10
- * call whose arguments contain an obviously-destructive shell pattern.
11
- *
12
- * Block protocol (the simple one): write the reason to stderr and `process.exit(2)`. (A richer JSON
13
- * protocol is also supported - see docs/MANUAL.pdf section 10.) Exit 0 = allow.
14
- *
15
- * EXIT 2, NOT "non-zero" - the difference matters when you write your own. Every OTHER exit (a
16
- * crash, a syntax error, a missing file, a timeout) is a NON-BLOCKING ERROR and the call proceeds,
17
- * so `exit(1)` on an error path allows the very call you meant to stop. If your policy cannot
18
- * reach a decision, exit 2 deliberately. (The gateway does refuse a decision it cannot READ -
19
- * output past the cap, unparseable JSON - because an unreadable answer is not permission. It
20
- * cannot do the same for a script that never answered: one broken hook file would take every tool
21
- * offline.) test/gate-semantics.test.js pins all of this.
22
- *
23
- * Zero dependencies - Node built-ins only.
24
- */
25
-
26
- let raw = '';
27
- process.stdin.on('data', (c) => { raw += c; });
28
- process.stdin.on('end', () => {
29
- let ev = {};
30
- try { ev = JSON.parse(raw || '{}'); } catch (_e) { /* malformed payload -> allow (don't block on a parse error) */ }
31
- const args = JSON.stringify((ev && ev.tool_input) || {});
32
- // A small illustrative denylist: recursive-force delete, mkfs, drive format, fork bomb.
33
- const DANGER = /\brm\s+-[a-z]*r[a-z]*f|\bmkfs\b|\bformat\s+[a-z]:|:\(\)\s*\{\s*:/i;
34
- if (DANGER.test(args)) {
35
- process.stderr.write('example-deny-dangerous: blocked - the arguments contain a destructive pattern.\n');
36
- process.exit(2); // DENY - the gateway refuses to run the tool
37
- }
38
- process.exit(0); // allow
39
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * EXAMPLE PreToolUse gate - DISABLED by default (enable it in hooks/hooks.manifest.json, or via the
6
+ * UI Hooks tab / the per-tool "Pre" toggle on the Tools tab).
7
+ *
8
+ * This is the shape of a real policy gate: it runs INSIDE the gateway, BEFORE a tool executes, and
9
+ * `process.exit(2)` DENIES the call. The lifecycle event arrives as JSON on stdin; here we block any
10
+ * call whose arguments contain an obviously-destructive shell pattern.
11
+ *
12
+ * Block protocol (the simple one): write the reason to stderr and `process.exit(2)`. (A richer JSON
13
+ * protocol is also supported - see docs/MANUAL.pdf section 10.) Exit 0 = allow.
14
+ *
15
+ * EXIT 2, NOT "non-zero" - the difference matters when you write your own. Every OTHER exit (a
16
+ * crash, a syntax error, a missing file, a timeout) is a NON-BLOCKING ERROR and the call proceeds,
17
+ * so `exit(1)` on an error path allows the very call you meant to stop. If your policy cannot
18
+ * reach a decision, exit 2 deliberately. (The gateway does refuse a decision it cannot READ -
19
+ * output past the cap, unparseable JSON - because an unreadable answer is not permission. It
20
+ * cannot do the same for a script that never answered: one broken hook file would take every tool
21
+ * offline.) test/gate-semantics.test.js pins all of this.
22
+ *
23
+ * Zero dependencies - Node built-ins only.
24
+ */
25
+
26
+ let raw = '';
27
+ process.stdin.on('data', (c) => { raw += c; });
28
+ process.stdin.on('end', () => {
29
+ let ev = {};
30
+ try { ev = JSON.parse(raw || '{}'); } catch (_e) { /* malformed payload -> allow (don't block on a parse error) */ }
31
+ const args = JSON.stringify((ev && ev.tool_input) || {});
32
+ // A small illustrative denylist: recursive-force delete, mkfs, drive format, fork bomb.
33
+ const DANGER = /\brm\s+-[a-z]*r[a-z]*f|\bmkfs\b|\bformat\s+[a-z]:|:\(\)\s*\{\s*:/i;
34
+ if (DANGER.test(args)) {
35
+ process.stderr.write('example-deny-dangerous: blocked - the arguments contain a destructive pattern.\n');
36
+ process.exit(2); // DENY - the gateway refuses to run the tool
37
+ }
38
+ process.exit(0); // allow
39
+ });
@@ -1,30 +1,30 @@
1
- {
2
- "_README": "SAMPLE config - NOT the live expose.json. This wires the bundled demo upstream at mcp/servers/mock-upstream/server.js so the MCP Manager has a real target. TO USE: either (A) copy this file over mcp/expose.json to pre-seed the host (drop the _README key - the loader ignores it, but copy cleanly), OR (B) leave expose.json empty and add the same upstream + expose entries through the MCP Manager UI (Add upstream -> Discover -> tick to expose). The upstream offers ping, add, echo; this sample exposes only TWO (ping, add) to demonstrate curation. IMPORTANT: the args path MUST resolve INSIDE the gateway root - the aggregator's isolation guard rejects any arg path that escapes the sandbox. A relative path like 'mcp/servers/mock-upstream/server.js' is resolved against the gateway root and stays inside it; 'node' in the command slot is the interpreter (not guarded). Do NOT edit the live mcp/expose.json to point outside the gateway root.",
3
- "version": 1,
4
- "upstreams": [
5
- {
6
- "id": "mock",
7
- "transport": "stdio",
8
- "command": "node",
9
- "args": ["mcp/servers/mock-upstream/server.js"],
10
- "enabled": true,
11
- "description": "Bundled demo upstream MCP (zero-dependency, stdio): ping, add, echo. No network, no fs writes, no process mutation."
12
- }
13
- ],
14
- "expose": [
15
- {
16
- "upstream": "mock",
17
- "tool": "ping",
18
- "as": "mock_ping",
19
- "category": "demo",
20
- "enabled": true
21
- },
22
- {
23
- "upstream": "mock",
24
- "tool": "add",
25
- "as": "mock_add",
26
- "category": "demo",
27
- "enabled": true
28
- }
29
- ]
30
- }
1
+ {
2
+ "_README": "SAMPLE config - NOT the live expose.json. This wires the bundled demo upstream at mcp/servers/mock-upstream/server.js so the MCP Manager has a real target. TO USE: either (A) copy this file over mcp/expose.json to pre-seed the host (drop the _README key - the loader ignores it, but copy cleanly), OR (B) leave expose.json empty and add the same upstream + expose entries through the MCP Manager UI (Add upstream -> Discover -> tick to expose). The upstream offers ping, add, echo; this sample exposes only TWO (ping, add) to demonstrate curation. IMPORTANT: the args path MUST resolve INSIDE the gateway root - the aggregator's isolation guard rejects any arg path that escapes the sandbox. A relative path like 'mcp/servers/mock-upstream/server.js' is resolved against the gateway root and stays inside it; 'node' in the command slot is the interpreter (not guarded). Do NOT edit the live mcp/expose.json to point outside the gateway root.",
3
+ "version": 1,
4
+ "upstreams": [
5
+ {
6
+ "id": "mock",
7
+ "transport": "stdio",
8
+ "command": "node",
9
+ "args": ["mcp/servers/mock-upstream/server.js"],
10
+ "enabled": true,
11
+ "description": "Bundled demo upstream MCP (zero-dependency, stdio): ping, add, echo. No network, no fs writes, no process mutation."
12
+ }
13
+ ],
14
+ "expose": [
15
+ {
16
+ "upstream": "mock",
17
+ "tool": "ping",
18
+ "as": "mock_ping",
19
+ "category": "demo",
20
+ "enabled": true
21
+ },
22
+ {
23
+ "upstream": "mock",
24
+ "tool": "add",
25
+ "as": "mock_add",
26
+ "category": "demo",
27
+ "enabled": true
28
+ }
29
+ ]
30
+ }