toolfunnel 0.6.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 (72) hide show
  1. package/README.md +228 -231
  2. package/SECURITY.md +84 -0
  3. package/bin/toolfunnel.js +451 -372
  4. package/docs/KNOWN_BUGS.md +80 -99
  5. package/docs/MANUAL.pdf +0 -0
  6. package/docs/design.md +84 -70
  7. package/docs/packaging.md +115 -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 -31
  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 +380 -350
  14. package/package.json +3 -1
  15. package/src/auth/config.js +174 -174
  16. package/src/auth/install.js +181 -181
  17. package/src/auth/resource-server.js +353 -344
  18. package/src/core/child-reaper.js +61 -0
  19. package/src/core/config-home.js +134 -134
  20. package/src/core/events.js +105 -105
  21. package/src/core/hook-engine.js +329 -275
  22. package/src/core/hook-loader.js +824 -791
  23. package/src/core/hook-runner.js +566 -346
  24. package/src/core/logger.js +297 -171
  25. package/src/core/matcher.js +100 -51
  26. package/src/core/metrics.js +75 -75
  27. package/src/core/rename-migration.js +163 -0
  28. package/src/core/requires.js +138 -138
  29. package/src/core/server-config.js +98 -98
  30. package/src/extend/add-hook.md +152 -152
  31. package/src/extend/add-mcp.md +119 -122
  32. package/src/extend/configure.md +88 -87
  33. package/src/extend/create-tool.md +168 -168
  34. package/src/extend/howto.js +102 -102
  35. package/src/extend/package.md +190 -187
  36. package/src/extend/wrap.md +99 -99
  37. package/src/mcp/aggregator.js +1719 -1246
  38. package/src/mcp/expose-store.js +577 -460
  39. package/src/mcp/gated-run.js +210 -201
  40. package/src/mcp/http-transport.js +1563 -1311
  41. package/src/mcp/mcp-client.js +1070 -907
  42. package/src/mcp/modern.js +406 -372
  43. package/src/mcp/protocol.js +468 -468
  44. package/src/mcp/server.js +3112 -2688
  45. package/src/tools/registry.js +661 -513
  46. package/src/tools/tool-state.js +170 -170
  47. package/src/ui/public/app.js +1187 -1174
  48. package/src/ui/public/index.html +286 -286
  49. package/src/ui/public/styles.css +535 -535
  50. package/src/ui/server.js +1616 -1348
  51. package/tools/scripts/base64.js +139 -139
  52. package/tools/scripts/danger.js +61 -61
  53. package/tools/scripts/echo.js +39 -39
  54. package/tools/scripts/hash.js +101 -101
  55. package/tools/scripts/json.js +109 -109
  56. package/tools/scripts/text-stats.js +131 -131
  57. package/tools/scripts/tf-env.js +43 -43
  58. package/tools/scripts/tf-hook-add.js +151 -151
  59. package/tools/scripts/tf-hook-set.js +120 -112
  60. package/tools/scripts/tf-list.js +192 -192
  61. package/tools/scripts/tf-log.js +86 -86
  62. package/tools/scripts/tf-mcp-add.js +117 -117
  63. package/tools/scripts/tf-mcp-set.js +121 -121
  64. package/tools/scripts/tf-pack.js +237 -237
  65. package/tools/scripts/tf-tool-add.js +134 -134
  66. package/tools/scripts/tf-tool-set.js +142 -142
  67. package/tools/scripts/tf-wrap.js +103 -103
  68. package/tools/scripts/uuid.js +102 -102
  69. package/tools/tools.register.json +227 -227
  70. package/src/core/tool-registry.js +0 -180
  71. package/src/packages/packager.js +0 -715
  72. package/src/tools/drift.js +0 -138
@@ -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();