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
@@ -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();
@@ -27,7 +27,7 @@
27
27
  {
28
28
  "id": "hash",
29
29
  "name": "Hash",
30
- "summary": "Compute a cryptographic digest (e.g. sha256) of input text.",
30
+ "summary": "Compute a sha256, sha1 or md5 digest of input text.",
31
31
  "category": "crypto",
32
32
  "instructions": "Args: { algo?: \"sha256\"|\"sha1\"|\"md5\" (default \"sha256\"), text: string }. Returns the lowercase hex digest of the UTF-8 text.",
33
33
  "invoke": {
@@ -82,7 +82,7 @@
82
82
  {
83
83
  "id": "tf_tool_add",
84
84
  "name": "tf_tool_add",
85
- "summary": "Register a new tool entry in the gateway tool register.",
85
+ "summary": "Register a new tool (a script or a shell command) in the gateway tool register.",
86
86
  "category": "management",
87
87
  "instructions": "Add a register entry. Args: { id, name, summary?, category?, instructions?, inputSchema?, invoke:{type:'script',path:'scripts/<file>'}|{type:'shell',command}, scriptText? }. inputSchema is a JSON Schema object for the tool's args - a HOT-promoted tool advertises it verbatim in the top-level tools/list, so author it if the tool may be promoted. If scriptText is given for a script invoke, the body is written to tools/scripts/<basename> first. Change is LIVE (no reconnect). Fails if id already exists.",
88
88
  "invoke": {
@@ -93,7 +93,7 @@
93
93
  {
94
94
  "id": "tf_tool_set",
95
95
  "name": "tf_tool_set",
96
- "summary": "Activate, deactivate, promote (hot), hide, or remove a register tool.",
96
+ "summary": "Enable, disable, promote (hot), hide, or remove a register tool.",
97
97
  "category": "management",
98
98
  "instructions": "Args: { id, action:'enable'|'disable'|'hot'|'unhot'|'hide'|'unhide'|'remove' }. enable/disable flips the active overlay (default ON) - disabled tools drop from toolfunnel_list_tools. hot/unhot promote/demote on the TOP-LEVEL every-turn surface (tools/list), making it directly callable (default OFF). hide/unhide declutter the MANAGER views only (tf_list + UI) and do NOT change what the AI sees. The id may be a local tool id, an upstream surfaced name, or a meta-tool name. remove deletes the register entry and purges its state key. All LIVE (no restart).",
99
99
  "invoke": {
@@ -123,10 +123,21 @@
123
123
  "path": "scripts/tf-mcp-set.js"
124
124
  }
125
125
  },
126
+ {
127
+ "id": "tf_accept_change",
128
+ "name": "tf_accept_change",
129
+ "summary": "Accept a tool description change ToolFunnel is holding back - ask the user first.",
130
+ "category": "management",
131
+ "instructions": "Args: { upstream, tool, hash }, all required - copy them from the refusal. When an upstream's tool changes its description or schema while the server version stays the same, ToolFunnel keeps serving the approved version and refuses calls to it, naming which fields changed; the user reviews the new text with `toolfunnel changes` or in the UI. ASK THE USER before running this. Only the version the user saw is accepted; to accept several changes at once the user runs `toolfunnel accept <upstream>` or uses the UI. Takes effect from the next call.",
132
+ "invoke": {
133
+ "type": "script",
134
+ "path": "scripts/tf-accept-change.js"
135
+ }
136
+ },
126
137
  {
127
138
  "id": "tf_wrap",
128
139
  "name": "tf_wrap",
129
- "summary": "Wrap ONE attached upstream MCP: its tools become the entire surface (transparent passthrough).",
140
+ "summary": "Wrap ONE attached upstream MCP: its tools become the entire surface (transparent passthrough). Also reports or clears the wrap.",
130
141
  "category": "management",
131
142
  "instructions": "Args: {} reports status ({ wrapping, upstreams }). { off:true } clears the wrap. { upstream:'<id>', confirm:true } sets it: that upstream's tools become the ENTIRE advertised surface under their ORIGINAL names, everything else (meta-tools INCLUDED) is hidden and uncallable, and every call still fires the PreToolUse gate. Because the gateway answers both MCP eras, this keeps a legacy-only MCP working with 2026-07-28-era clients. WARNING (why confirm is required): the wrap hides tf_wrap itself, so YOU cannot undo it in-band afterwards - recovery is the config web UI (--ui) or `toolfunnel wrap --off` on a terminal. A running host applies the change live.",
132
143
  "invoke": {
@@ -170,7 +181,7 @@
170
181
  {
171
182
  "id": "tf_log",
172
183
  "name": "tf_log",
173
- "summary": "Enable, disable, or check the gateway activity/audit log.",
184
+ "summary": "Enable, disable, or check the status of the gateway activity/audit log.",
174
185
  "category": "management",
175
186
  "instructions": "Args: { action:'enable'|'disable'|'status', path? }. Toggles ToolFunnel's JSONL audit log (tool runs + gate allow/deny decisions). Default OFF. 'status' returns {enabled,path} + recent count. Like any tool, tf_log can itself be disabled to revoke logging control.",
176
187
  "invoke": {