toolfunnel 0.7.0 → 0.7.1

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 (67) hide show
  1. package/README.md +228 -235
  2. package/SECURITY.md +84 -84
  3. package/bin/toolfunnel.js +451 -451
  4. package/docs/KNOWN_BUGS.md +80 -80
  5. package/docs/design.md +84 -84
  6. package/docs/packaging.md +115 -115
  7. package/hooks/hooks.manifest.json +27 -27
  8. package/hooks/scripts/example-audit.js +21 -21
  9. package/hooks/scripts/example-deny-dangerous.js +39 -39
  10. package/mcp/expose.example.json +30 -30
  11. package/mcp/servers/mock-modern/server.js +147 -147
  12. package/mcp/servers/mock-upstream/server.js +380 -380
  13. package/package.json +67 -67
  14. package/src/auth/config.js +174 -174
  15. package/src/auth/install.js +181 -181
  16. package/src/auth/resource-server.js +353 -353
  17. package/src/core/child-reaper.js +61 -61
  18. package/src/core/config-home.js +134 -134
  19. package/src/core/events.js +105 -105
  20. package/src/core/hook-engine.js +329 -329
  21. package/src/core/hook-loader.js +824 -824
  22. package/src/core/hook-runner.js +566 -566
  23. package/src/core/logger.js +297 -297
  24. package/src/core/matcher.js +100 -100
  25. package/src/core/metrics.js +75 -75
  26. package/src/core/rename-migration.js +163 -163
  27. package/src/core/requires.js +138 -138
  28. package/src/core/server-config.js +98 -98
  29. package/src/extend/add-hook.md +152 -152
  30. package/src/extend/add-mcp.md +119 -119
  31. package/src/extend/configure.md +88 -88
  32. package/src/extend/create-tool.md +168 -168
  33. package/src/extend/howto.js +102 -102
  34. package/src/extend/package.md +190 -190
  35. package/src/extend/wrap.md +99 -99
  36. package/src/mcp/aggregator.js +1719 -1674
  37. package/src/mcp/expose-store.js +577 -577
  38. package/src/mcp/gated-run.js +210 -210
  39. package/src/mcp/http-transport.js +1563 -1563
  40. package/src/mcp/mcp-client.js +1070 -1070
  41. package/src/mcp/modern.js +406 -406
  42. package/src/mcp/protocol.js +468 -468
  43. package/src/mcp/server.js +3112 -3112
  44. package/src/tools/registry.js +661 -661
  45. package/src/tools/tool-state.js +170 -170
  46. package/src/ui/public/app.js +1187 -1187
  47. package/src/ui/public/index.html +286 -286
  48. package/src/ui/public/styles.css +535 -535
  49. package/src/ui/server.js +1616 -1616
  50. package/tools/scripts/base64.js +139 -139
  51. package/tools/scripts/danger.js +61 -61
  52. package/tools/scripts/echo.js +39 -39
  53. package/tools/scripts/hash.js +101 -101
  54. package/tools/scripts/json.js +109 -109
  55. package/tools/scripts/text-stats.js +131 -131
  56. package/tools/scripts/tf-env.js +43 -43
  57. package/tools/scripts/tf-hook-add.js +151 -151
  58. package/tools/scripts/tf-hook-set.js +120 -120
  59. package/tools/scripts/tf-list.js +192 -192
  60. package/tools/scripts/tf-log.js +86 -86
  61. package/tools/scripts/tf-mcp-add.js +117 -117
  62. package/tools/scripts/tf-mcp-set.js +121 -121
  63. package/tools/scripts/tf-pack.js +237 -237
  64. package/tools/scripts/tf-tool-add.js +134 -134
  65. package/tools/scripts/tf-tool-set.js +142 -142
  66. package/tools/scripts/tf-wrap.js +103 -103
  67. package/tools/scripts/uuid.js +102 -102
@@ -1,103 +1,103 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * tf-wrap.js - gateway-run MANAGEMENT tool: set, clear, or report the PASSTHROUGH wrap.
6
- *
7
- * Wrapping turns the gateway into a transparent wrapper for ONE attached upstream MCP: its
8
- * tools become the ENTIRE advertised surface (under their ORIGINAL names), the meta-tools and
9
- * every other tool are hidden and uncallable, and every call still fires the PreToolUse gate.
10
- * Because the gateway answers both MCP eras, this is how a legacy-only (or unmaintained) MCP
11
- * keeps working with 2026-07-28-era clients.
12
- *
13
- * ⚠ SELF-LOCKOUT: the wrap hides ALL ToolFunnel tools - INCLUDING tf_wrap itself. The AI that
14
- * sets a wrap loses in-band access to undo it. That is why `confirm: true` is REQUIRED to set
15
- * one. Undo paths that survive the wrap: the config web UI (--ui), or `toolfunnel wrap --off`
16
- * on a terminal. A running host picks either up live.
17
- *
18
- * Contract with the register (`defaultRunScript`)
19
- * ----------------------------------------------------------
20
- * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if absent).
21
- * Shape: {} (status) | { off:true } (clear) | { upstream:'<id>', confirm:true } (set).
22
- * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical failure is
23
- * reported as { ok:false, error }.
24
- *
25
- * Output (stdout), exactly one JSON object:
26
- * status: { ok:true, wrapping:(id|null), upstreams:[id...] }
27
- * clear: { ok:true, cleared:true, was:(id|null) }
28
- * set: { ok:true, wrapping:id, warning:'...' }
29
- * failure:{ ok:false, error }
30
- */
31
-
32
- const path = require('node:path');
33
- const { HOME, srcRequire } = require('./tf-env');
34
- const { loadExposeStore } = srcRequire('mcp/expose-store');
35
- const { loadToolState, getPassthrough, setPassthrough } = srcRequire('tools/tool-state');
36
-
37
- const STATE_PATH = path.join(HOME, 'tools', 'tools.state.json');
38
- const EXPOSE_PATH = path.join(HOME, 'mcp', 'expose.json');
39
-
40
- const LOCKOUT_WARNING =
41
- 'The wrap hides ALL ToolFunnel tools - including tf_wrap itself. In-band undo is now ' +
42
- 'impossible for you: recover via the config web UI (--ui) or `toolfunnel wrap --off` on a terminal.';
43
-
44
- function parseArgs() {
45
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
46
- if (raw === undefined || raw === null || raw === '') return {};
47
- const parsed = JSON.parse(raw);
48
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
49
- throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
50
- }
51
- return parsed;
52
- }
53
-
54
- function run() {
55
- const args = parseArgs();
56
-
57
- if (args.off === true) {
58
- const was = getPassthrough(loadToolState(STATE_PATH));
59
- setPassthrough(STATE_PATH, null);
60
- return { ok: true, cleared: true, was };
61
- }
62
-
63
- const store = loadExposeStore(EXPOSE_PATH);
64
- const ids = store.listUpstreams().map((u) => u.id);
65
-
66
- if (args.upstream === undefined) {
67
- return { ok: true, wrapping: getPassthrough(loadToolState(STATE_PATH)), upstreams: ids };
68
- }
69
-
70
- const id = args.upstream;
71
- if (typeof id !== 'string' || id.length === 0) {
72
- throw new Error('args.upstream must be a non-empty string (an attached upstream id)');
73
- }
74
- const upstream = store.getUpstream(id);
75
- if (!upstream) {
76
- throw new Error(`no upstream with id "${id}" - attached: ${ids.join(', ') || '(none)'}`);
77
- }
78
- if (upstream.enabled === false) {
79
- throw new Error(`upstream "${id}" is disabled in mcp/expose.json - enable it first (tf_mcp_set)`);
80
- }
81
- if (args.confirm !== true) {
82
- throw new Error(
83
- 'confirm:true is required. ' + LOCKOUT_WARNING +
84
- ' Call again with { "upstream": "' + id + '", "confirm": true } to proceed.'
85
- );
86
- }
87
-
88
- setPassthrough(STATE_PATH, id);
89
- return { ok: true, wrapping: id, warning: LOCKOUT_WARNING };
90
- }
91
-
92
- function main() {
93
- let payload;
94
- try {
95
- payload = run();
96
- } catch (err) {
97
- payload = { ok: false, error: (err && err.message) || String(err) };
98
- }
99
- process.stdout.write(JSON.stringify(payload) + '\n');
100
- process.exit(0);
101
- }
102
-
103
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * tf-wrap.js - gateway-run MANAGEMENT tool: set, clear, or report the PASSTHROUGH wrap.
6
+ *
7
+ * Wrapping turns the gateway into a transparent wrapper for ONE attached upstream MCP: its
8
+ * tools become the ENTIRE advertised surface (under their ORIGINAL names), the meta-tools and
9
+ * every other tool are hidden and uncallable, and every call still fires the PreToolUse gate.
10
+ * Because the gateway answers both MCP eras, this is how a legacy-only (or unmaintained) MCP
11
+ * keeps working with 2026-07-28-era clients.
12
+ *
13
+ * ⚠ SELF-LOCKOUT: the wrap hides ALL ToolFunnel tools - INCLUDING tf_wrap itself. The AI that
14
+ * sets a wrap loses in-band access to undo it. That is why `confirm: true` is REQUIRED to set
15
+ * one. Undo paths that survive the wrap: the config web UI (--ui), or `toolfunnel wrap --off`
16
+ * on a terminal. A running host picks either up live.
17
+ *
18
+ * Contract with the register (`defaultRunScript`)
19
+ * ----------------------------------------------------------
20
+ * - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if absent).
21
+ * Shape: {} (status) | { off:true } (clear) | { upstream:'<id>', confirm:true } (set).
22
+ * - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical failure is
23
+ * reported as { ok:false, error }.
24
+ *
25
+ * Output (stdout), exactly one JSON object:
26
+ * status: { ok:true, wrapping:(id|null), upstreams:[id...] }
27
+ * clear: { ok:true, cleared:true, was:(id|null) }
28
+ * set: { ok:true, wrapping:id, warning:'...' }
29
+ * failure:{ ok:false, error }
30
+ */
31
+
32
+ const path = require('node:path');
33
+ const { HOME, srcRequire } = require('./tf-env');
34
+ const { loadExposeStore } = srcRequire('mcp/expose-store');
35
+ const { loadToolState, getPassthrough, setPassthrough } = srcRequire('tools/tool-state');
36
+
37
+ const STATE_PATH = path.join(HOME, 'tools', 'tools.state.json');
38
+ const EXPOSE_PATH = path.join(HOME, 'mcp', 'expose.json');
39
+
40
+ const LOCKOUT_WARNING =
41
+ 'The wrap hides ALL ToolFunnel tools - including tf_wrap itself. In-band undo is now ' +
42
+ 'impossible for you: recover via the config web UI (--ui) or `toolfunnel wrap --off` on a terminal.';
43
+
44
+ function parseArgs() {
45
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
46
+ if (raw === undefined || raw === null || raw === '') return {};
47
+ const parsed = JSON.parse(raw);
48
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
49
+ throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
50
+ }
51
+ return parsed;
52
+ }
53
+
54
+ function run() {
55
+ const args = parseArgs();
56
+
57
+ if (args.off === true) {
58
+ const was = getPassthrough(loadToolState(STATE_PATH));
59
+ setPassthrough(STATE_PATH, null);
60
+ return { ok: true, cleared: true, was };
61
+ }
62
+
63
+ const store = loadExposeStore(EXPOSE_PATH);
64
+ const ids = store.listUpstreams().map((u) => u.id);
65
+
66
+ if (args.upstream === undefined) {
67
+ return { ok: true, wrapping: getPassthrough(loadToolState(STATE_PATH)), upstreams: ids };
68
+ }
69
+
70
+ const id = args.upstream;
71
+ if (typeof id !== 'string' || id.length === 0) {
72
+ throw new Error('args.upstream must be a non-empty string (an attached upstream id)');
73
+ }
74
+ const upstream = store.getUpstream(id);
75
+ if (!upstream) {
76
+ throw new Error(`no upstream with id "${id}" - attached: ${ids.join(', ') || '(none)'}`);
77
+ }
78
+ if (upstream.enabled === false) {
79
+ throw new Error(`upstream "${id}" is disabled in mcp/expose.json - enable it first (tf_mcp_set)`);
80
+ }
81
+ if (args.confirm !== true) {
82
+ throw new Error(
83
+ 'confirm:true is required. ' + LOCKOUT_WARNING +
84
+ ' Call again with { "upstream": "' + id + '", "confirm": true } to proceed.'
85
+ );
86
+ }
87
+
88
+ setPassthrough(STATE_PATH, id);
89
+ return { ok: true, wrapping: id, warning: LOCKOUT_WARNING };
90
+ }
91
+
92
+ function main() {
93
+ let payload;
94
+ try {
95
+ payload = run();
96
+ } catch (err) {
97
+ payload = { ok: false, error: (err && err.message) || String(err) };
98
+ }
99
+ process.stdout.write(JSON.stringify(payload) + '\n');
100
+ process.exit(0);
101
+ }
102
+
103
+ main();
@@ -1,102 +1,102 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * uuid-tool.js - a safe, dependency-free local UUID generator.
6
- *
7
- * Purpose
8
- * -------
9
- * Generate one or more RFC 4122 v4 UUIDs via the Node built-in
10
- * `node:crypto.randomUUID()`. Useful from the Tool Manager whenever a fresh
11
- * identifier is needed. No network, no filesystem, no new deps.
12
- *
13
- * Contract with the register (the register's `defaultRunScript`)
14
- * ----------------------------------------------------------
15
- * - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
16
- * - Args: { count?: <integer> } - how many UUIDs to mint. Defaults to 1.
17
- * count is clamped to the range [1, 100] so a runaway request cannot ask
18
- * for an unbounded number.
19
- * - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
20
- * on bad input (reported as { ok:false, error }), never a thrown exception.
21
- *
22
- * Output (stdout), exactly one JSON object:
23
- * success: { ok:true, uuids: [ "...", ... ] }
24
- * failure: { ok:false, error }
25
- *
26
- * Safety invariants (mirror the isolation rule):
27
- * - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
28
- * - count is hard-capped at 100; a non-integer / out-of-range count is an
29
- * error rather than a silent surprise.
30
- * - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
31
- */
32
-
33
- const crypto = require('node:crypto');
34
-
35
- const MAX_COUNT = 100;
36
-
37
- /**
38
- * Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
39
- * @returns {{ value: any } | { parseError: string }}
40
- */
41
- function parseStructuredArgs() {
42
- const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
43
- if (raw === undefined || raw === null || raw === '') {
44
- return { value: null };
45
- }
46
- try {
47
- return { value: JSON.parse(raw) };
48
- } catch (_err) {
49
- return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
50
- }
51
- }
52
-
53
- /**
54
- * Pure core: given the parsed args, produce the result object. Never throws.
55
- *
56
- * @param {any} args may be null (treated as { count: 1 })
57
- * @returns {object} the JSON-serialisable result
58
- */
59
- function run(args) {
60
- // No args (or null) is valid - default to a single UUID.
61
- let count = 1;
62
- if (args !== null && args !== undefined) {
63
- if (typeof args !== 'object' || Array.isArray(args)) {
64
- return { ok: false, error: 'args must be an object { count? }' };
65
- }
66
- if (args.count !== undefined && args.count !== null) {
67
- if (typeof args.count !== 'number' || !Number.isInteger(args.count)) {
68
- return { ok: false, error: 'count must be an integer' };
69
- }
70
- if (args.count < 1) {
71
- return { ok: false, error: 'count must be >= 1' };
72
- }
73
- if (args.count > MAX_COUNT) {
74
- return { ok: false, error: `count must be <= ${MAX_COUNT}` };
75
- }
76
- count = args.count;
77
- }
78
- }
79
- const uuids = [];
80
- for (let i = 0; i < count; i += 1) {
81
- uuids.push(crypto.randomUUID());
82
- }
83
- return { ok: true, uuids };
84
- }
85
-
86
- function main() {
87
- const parsed = parseStructuredArgs();
88
- let payload;
89
- if (parsed.parseError) {
90
- payload = { ok: false, error: parsed.parseError };
91
- } else {
92
- try {
93
- payload = run(parsed.value);
94
- } catch (err) {
95
- payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
96
- }
97
- }
98
- process.stdout.write(JSON.stringify(payload) + '\n');
99
- process.exit(0);
100
- }
101
-
102
- main();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * uuid-tool.js - a safe, dependency-free local UUID generator.
6
+ *
7
+ * Purpose
8
+ * -------
9
+ * Generate one or more RFC 4122 v4 UUIDs via the Node built-in
10
+ * `node:crypto.randomUUID()`. Useful from the Tool Manager whenever a fresh
11
+ * identifier is needed. No network, no filesystem, no new deps.
12
+ *
13
+ * Contract with the register (the register's `defaultRunScript`)
14
+ * ----------------------------------------------------------
15
+ * - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS.
16
+ * - Args: { count?: <integer> } - how many UUIDs to mint. Defaults to 1.
17
+ * count is clamped to the range [1, 100] so a runaway request cannot ask
18
+ * for an unbounded number.
19
+ * - Prints a SINGLE JSON object to stdout and exits 0 - ALWAYS exit 0, even
20
+ * on bad input (reported as { ok:false, error }), never a thrown exception.
21
+ *
22
+ * Output (stdout), exactly one JSON object:
23
+ * success: { ok:true, uuids: [ "...", ... ] }
24
+ * failure: { ok:false, error }
25
+ *
26
+ * Safety invariants (mirror the isolation rule):
27
+ * - NO filesystem, NO network, NO process mutation. Pure CPU on the args.
28
+ * - count is hard-capped at 100; a non-integer / out-of-range count is an
29
+ * error rather than a silent surprise.
30
+ * - NEVER throws for ordinary bad input - only well-shaped JSON on stdout.
31
+ */
32
+
33
+ const crypto = require('node:crypto');
34
+
35
+ const MAX_COUNT = 100;
36
+
37
+ /**
38
+ * Parse the structured args handed in via env TOOLFUNNEL_TOOL_ARGS.
39
+ * @returns {{ value: any } | { parseError: string }}
40
+ */
41
+ function parseStructuredArgs() {
42
+ const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
43
+ if (raw === undefined || raw === null || raw === '') {
44
+ return { value: null };
45
+ }
46
+ try {
47
+ return { value: JSON.parse(raw) };
48
+ } catch (_err) {
49
+ return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Pure core: given the parsed args, produce the result object. Never throws.
55
+ *
56
+ * @param {any} args may be null (treated as { count: 1 })
57
+ * @returns {object} the JSON-serialisable result
58
+ */
59
+ function run(args) {
60
+ // No args (or null) is valid - default to a single UUID.
61
+ let count = 1;
62
+ if (args !== null && args !== undefined) {
63
+ if (typeof args !== 'object' || Array.isArray(args)) {
64
+ return { ok: false, error: 'args must be an object { count? }' };
65
+ }
66
+ if (args.count !== undefined && args.count !== null) {
67
+ if (typeof args.count !== 'number' || !Number.isInteger(args.count)) {
68
+ return { ok: false, error: 'count must be an integer' };
69
+ }
70
+ if (args.count < 1) {
71
+ return { ok: false, error: 'count must be >= 1' };
72
+ }
73
+ if (args.count > MAX_COUNT) {
74
+ return { ok: false, error: `count must be <= ${MAX_COUNT}` };
75
+ }
76
+ count = args.count;
77
+ }
78
+ }
79
+ const uuids = [];
80
+ for (let i = 0; i < count; i += 1) {
81
+ uuids.push(crypto.randomUUID());
82
+ }
83
+ return { ok: true, uuids };
84
+ }
85
+
86
+ function main() {
87
+ const parsed = parseStructuredArgs();
88
+ let payload;
89
+ if (parsed.parseError) {
90
+ payload = { ok: false, error: parsed.parseError };
91
+ } else {
92
+ try {
93
+ payload = run(parsed.value);
94
+ } catch (err) {
95
+ payload = { ok: false, error: `unexpected error: ${(err && err.message) || String(err)}` };
96
+ }
97
+ }
98
+ process.stdout.write(JSON.stringify(payload) + '\n');
99
+ process.exit(0);
100
+ }
101
+
102
+ main();