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,170 +1,170 @@
1
- 'use strict';
2
-
3
- /**
4
- * tool-state.js - per-tool ACTIVE/DISABLED overlay for the register.
5
- *
6
- * Mirrors a sibling state overlay (see a config file): a small
7
- * overlay keyed by tool id, written atomically, loaded on startup -> survives
8
- * close/detach (persistent session-to-session). The register (tools.register.json)
9
- * holds ALL tools; this overlay decides which are surfaced to toolfunnel_list_tools.
10
- *
11
- * DEFAULT ON: a tool absent from the overlay is ACTIVE. Only an explicit
12
- * { enabled: false } disables it. So newly-added tools are active until you
13
- * untick them - and the file only ever lists the tools you've toggled.
14
- *
15
- * THREE independent axes (the visibility MATRIX), all keyed by the surfaced name
16
- * (a local tool id, OR an upstream's surfaced name `<upstream>_<tool>` / its `as`,
17
- * OR a meta-tool name like `toolfunnel_list_tools`):
18
- * - enabled : LEAN-VISIBLE - surfaced by toolfunnel_list_tools AND runnable. Default ON.
19
- * - hidden : manager-list declutter only (UI/tf_list). Default OFF. INDEPENDENT of enabled.
20
- * - hot : promoted to the TOP-LEVEL tools/list (injected EVERY turn). Default depends on
21
- * the tool KIND, supplied by the caller via isToolHot(state,id,defaultHot):
22
- * meta-tools default hot:true (the management surface), local/upstream default
23
- * hot:false (opt-in promotion). A disabled tool is never hot.
24
- *
25
- * Shape of tools.state.json: { "<id>": { "enabled": false, "hidden": true, "hot": true }, ... }
26
- *
27
- * PASSTHROUGH (0.6.0): one RESERVED top-level key, `"passthrough": "<upstreamId>"` (a STRING,
28
- * unambiguous against the per-tool object entries). When set, the gateway becomes a transparent
29
- * wrapper for that one upstream: its tools ARE the advertised surface (real schemas, implicitly
30
- * hot), the meta-tools and everything else are hidden and uncallable, and every call still fires
31
- * the PreToolUse gate. An explicit `enabled:false` on an upstream tool is still honoured (the
32
- * safety off-switch survives the wrap). Set/cleared by `toolfunnel wrap <id>` / `wrap --off`.
33
- *
34
- * CommonJS, Node built-ins only. Reuses registry.js's atomicWriteJson (temp+rename).
35
- */
36
-
37
- const fs = require('node:fs');
38
- const { atomicWriteJson } = require('./registry');
39
-
40
- /** Load the overlay ({} if missing/malformed - never throws). */
41
- function loadToolState(statePath) {
42
- try {
43
- const d = JSON.parse(fs.readFileSync(statePath, 'utf8'));
44
- return (d && typeof d === 'object' && !Array.isArray(d)) ? d : {};
45
- } catch (_e) {
46
- return {};
47
- }
48
- }
49
-
50
- /**
51
- * Like loadToolState, but distinguishes "file MISSING" (defaults are correct) from "file PRESENT
52
- * but unparseable / not an object" (a corrupt overlay - the caller may want to fail closed or warn
53
- * rather than silently serve defaults). Returns { state, parseError }. `parseError` is true ONLY
54
- * when the file exists but could not be read as a JSON object. NEVER throws.
55
- * @param {string} statePath
56
- * @returns {{ state: object, parseError: boolean }}
57
- */
58
- function loadToolStateResult(statePath) {
59
- let raw;
60
- try {
61
- raw = fs.readFileSync(statePath, 'utf8');
62
- } catch (_e) {
63
- return { state: {}, parseError: false }; // missing (or unreadable) -> defaults, not a corruption
64
- }
65
- try {
66
- const d = JSON.parse(raw);
67
- if (d && typeof d === 'object' && !Array.isArray(d)) return { state: d, parseError: false };
68
- return { state: {}, parseError: true }; // present but not a JSON object
69
- } catch (_e) {
70
- return { state: {}, parseError: true }; // present but not valid JSON
71
- }
72
- }
73
-
74
- /** Default ON: enabled unless the overlay explicitly says enabled:false. */
75
- function isToolEnabled(state, id) {
76
- const e = state && state[id];
77
- return !(e && e.enabled === false);
78
- }
79
-
80
- /** Default NOT hidden: hidden only when the overlay explicitly says hidden:true.
81
- * `hidden` is a manager-list-only flag (declutter); it is INDEPENDENT of `enabled`
82
- * (MCP visibility). A tool can be active-but-hidden, or disabled-but-visible. */
83
- function isToolHidden(state, id) {
84
- const e = state && state[id];
85
- return !!(e && e.hidden === true);
86
- }
87
-
88
- /** Default OFF for local/upstream, ON for meta - the caller supplies the kind-appropriate
89
- * `defaultHot`. An explicit boolean in the overlay always wins; absence falls back to the
90
- * default. `hot` is the TOP-LEVEL (every-turn) promotion axis - INDEPENDENT of enabled/hidden,
91
- * though a disabled tool is filtered out of the top-level surface by the assembler, not here. */
92
- function isToolHot(state, id, defaultHot) {
93
- const e = state && state[id];
94
- if (e && typeof e.hot === 'boolean') return e.hot;
95
- return !!defaultHot;
96
- }
97
-
98
- /** The one top-level key reserved for the passthrough wrap (a STRING value). A per-tool overlay
99
- * entry may NEVER use this key, else a per-tool toggle would clobber (or be clobbered by) the wrap
100
- * - they share the same flat object. The per-tool setters reject
101
- * it; a tool whose surfaced name is literally "passthrough" simply cannot carry an overlay entry
102
- * (it stays at defaults - enabled, not hot, not hidden), which is a fine, safe degradation. */
103
- const RESERVED_PASSTHROUGH_KEY = 'passthrough';
104
- function assertNotReserved(id, fn) {
105
- if (id === RESERVED_PASSTHROUGH_KEY) {
106
- throw new Error(`${fn}: "${RESERVED_PASSTHROUGH_KEY}" is a reserved state key (the passthrough wrap) and cannot be a per-tool id`);
107
- }
108
- }
109
-
110
- /** Set a tool's enabled flag, MERGING (so `hidden`/`hot` are preserved). Persists atomically. */
111
- function setToolEnabled(statePath, id, enabled) {
112
- if (typeof id !== 'string' || !id) throw new Error('setToolEnabled: id required');
113
- assertNotReserved(id, 'setToolEnabled');
114
- const s = loadToolState(statePath);
115
- s[id] = { ...(s[id] || {}), enabled: !!enabled };
116
- atomicWriteJson(statePath, s);
117
- return s;
118
- }
119
-
120
- /** Set a tool's hot flag, MERGING (so `enabled`/`hidden` are preserved). Persists atomically. */
121
- function setToolHot(statePath, id, hot) {
122
- if (typeof id !== 'string' || !id) throw new Error('setToolHot: id required');
123
- assertNotReserved(id, 'setToolHot');
124
- const s = loadToolState(statePath);
125
- s[id] = { ...(s[id] || {}), hot: !!hot };
126
- atomicWriteJson(statePath, s);
127
- return s;
128
- }
129
-
130
- /** Set a tool's hidden flag, MERGING (so `enabled` is preserved). Persists atomically. */
131
- function setToolHidden(statePath, id, hidden) {
132
- if (typeof id !== 'string' || !id) throw new Error('setToolHidden: id required');
133
- assertNotReserved(id, 'setToolHidden');
134
- const s = loadToolState(statePath);
135
- s[id] = { ...(s[id] || {}), hidden: !!hidden };
136
- atomicWriteJson(statePath, s);
137
- return s;
138
- }
139
-
140
- /** Drop a tool's overlay entry entirely (revert to defaults: active + not hidden).
141
- * No-op if the id was never toggled. Persists atomically. */
142
- function clearToolState(statePath, id) {
143
- if (typeof id !== 'string' || !id) throw new Error('clearToolState: id required');
144
- assertNotReserved(id, 'clearToolState'); // a register tool id'd "passthrough" must not clear a live wrap
145
- const s = loadToolState(statePath);
146
- delete s[id];
147
- atomicWriteJson(statePath, s);
148
- return s;
149
- }
150
-
151
- /** The active passthrough upstream id, or null. Only a non-empty STRING value counts - a
152
- * per-tool object entry that happens to be keyed "passthrough" is not a wrap. */
153
- function getPassthrough(state) {
154
- const v = state && state.passthrough;
155
- return typeof v === 'string' && v.length > 0 ? v : null;
156
- }
157
-
158
- /** Set (a non-empty string) or clear (null/'') the passthrough wrap. Persists atomically. */
159
- function setPassthrough(statePath, upstreamId) {
160
- const s = loadToolState(statePath);
161
- if (typeof upstreamId === 'string' && upstreamId.length > 0) {
162
- s.passthrough = upstreamId;
163
- } else {
164
- delete s.passthrough;
165
- }
166
- atomicWriteJson(statePath, s);
167
- return s;
168
- }
169
-
170
- module.exports = { loadToolState, loadToolStateResult, isToolEnabled, isToolHidden, isToolHot, setToolEnabled, setToolHidden, setToolHot, clearToolState, getPassthrough, setPassthrough };
1
+ 'use strict';
2
+
3
+ /**
4
+ * tool-state.js - per-tool ACTIVE/DISABLED overlay for the register.
5
+ *
6
+ * Mirrors a sibling state overlay (see a config file): a small
7
+ * overlay keyed by tool id, written atomically, loaded on startup -> survives
8
+ * close/detach (persistent session-to-session). The register (tools.register.json)
9
+ * holds ALL tools; this overlay decides which are surfaced to toolfunnel_list_tools.
10
+ *
11
+ * DEFAULT ON: a tool absent from the overlay is ACTIVE. Only an explicit
12
+ * { enabled: false } disables it. So newly-added tools are active until you
13
+ * untick them - and the file only ever lists the tools you've toggled.
14
+ *
15
+ * THREE independent axes (the visibility MATRIX), all keyed by the surfaced name
16
+ * (a local tool id, OR an upstream's surfaced name `<upstream>_<tool>` / its `as`,
17
+ * OR a meta-tool name like `toolfunnel_list_tools`):
18
+ * - enabled : LEAN-VISIBLE - surfaced by toolfunnel_list_tools AND runnable. Default ON.
19
+ * - hidden : manager-list declutter only (UI/tf_list). Default OFF. INDEPENDENT of enabled.
20
+ * - hot : promoted to the TOP-LEVEL tools/list (injected EVERY turn). Default depends on
21
+ * the tool KIND, supplied by the caller via isToolHot(state,id,defaultHot):
22
+ * meta-tools default hot:true (the management surface), local/upstream default
23
+ * hot:false (opt-in promotion). A disabled tool is never hot.
24
+ *
25
+ * Shape of tools.state.json: { "<id>": { "enabled": false, "hidden": true, "hot": true }, ... }
26
+ *
27
+ * PASSTHROUGH (0.6.0): one RESERVED top-level key, `"passthrough": "<upstreamId>"` (a STRING,
28
+ * unambiguous against the per-tool object entries). When set, the gateway becomes a transparent
29
+ * wrapper for that one upstream: its tools ARE the advertised surface (real schemas, implicitly
30
+ * hot), the meta-tools and everything else are hidden and uncallable, and every call still fires
31
+ * the PreToolUse gate. An explicit `enabled:false` on an upstream tool is still honoured (the
32
+ * safety off-switch survives the wrap). Set/cleared by `toolfunnel wrap <id>` / `wrap --off`.
33
+ *
34
+ * CommonJS, Node built-ins only. Reuses registry.js's atomicWriteJson (temp+rename).
35
+ */
36
+
37
+ const fs = require('node:fs');
38
+ const { atomicWriteJson } = require('./registry');
39
+
40
+ /** Load the overlay ({} if missing/malformed - never throws). */
41
+ function loadToolState(statePath) {
42
+ try {
43
+ const d = JSON.parse(fs.readFileSync(statePath, 'utf8'));
44
+ return (d && typeof d === 'object' && !Array.isArray(d)) ? d : {};
45
+ } catch (_e) {
46
+ return {};
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Like loadToolState, but distinguishes "file MISSING" (defaults are correct) from "file PRESENT
52
+ * but unparseable / not an object" (a corrupt overlay - the caller may want to fail closed or warn
53
+ * rather than silently serve defaults). Returns { state, parseError }. `parseError` is true ONLY
54
+ * when the file exists but could not be read as a JSON object. NEVER throws.
55
+ * @param {string} statePath
56
+ * @returns {{ state: object, parseError: boolean }}
57
+ */
58
+ function loadToolStateResult(statePath) {
59
+ let raw;
60
+ try {
61
+ raw = fs.readFileSync(statePath, 'utf8');
62
+ } catch (_e) {
63
+ return { state: {}, parseError: false }; // missing (or unreadable) -> defaults, not a corruption
64
+ }
65
+ try {
66
+ const d = JSON.parse(raw);
67
+ if (d && typeof d === 'object' && !Array.isArray(d)) return { state: d, parseError: false };
68
+ return { state: {}, parseError: true }; // present but not a JSON object
69
+ } catch (_e) {
70
+ return { state: {}, parseError: true }; // present but not valid JSON
71
+ }
72
+ }
73
+
74
+ /** Default ON: enabled unless the overlay explicitly says enabled:false. */
75
+ function isToolEnabled(state, id) {
76
+ const e = state && state[id];
77
+ return !(e && e.enabled === false);
78
+ }
79
+
80
+ /** Default NOT hidden: hidden only when the overlay explicitly says hidden:true.
81
+ * `hidden` is a manager-list-only flag (declutter); it is INDEPENDENT of `enabled`
82
+ * (MCP visibility). A tool can be active-but-hidden, or disabled-but-visible. */
83
+ function isToolHidden(state, id) {
84
+ const e = state && state[id];
85
+ return !!(e && e.hidden === true);
86
+ }
87
+
88
+ /** Default OFF for local/upstream, ON for meta - the caller supplies the kind-appropriate
89
+ * `defaultHot`. An explicit boolean in the overlay always wins; absence falls back to the
90
+ * default. `hot` is the TOP-LEVEL (every-turn) promotion axis - INDEPENDENT of enabled/hidden,
91
+ * though a disabled tool is filtered out of the top-level surface by the assembler, not here. */
92
+ function isToolHot(state, id, defaultHot) {
93
+ const e = state && state[id];
94
+ if (e && typeof e.hot === 'boolean') return e.hot;
95
+ return !!defaultHot;
96
+ }
97
+
98
+ /** The one top-level key reserved for the passthrough wrap (a STRING value). A per-tool overlay
99
+ * entry may NEVER use this key, else a per-tool toggle would clobber (or be clobbered by) the wrap
100
+ * - they share the same flat object. The per-tool setters reject
101
+ * it; a tool whose surfaced name is literally "passthrough" simply cannot carry an overlay entry
102
+ * (it stays at defaults - enabled, not hot, not hidden), which is a fine, safe degradation. */
103
+ const RESERVED_PASSTHROUGH_KEY = 'passthrough';
104
+ function assertNotReserved(id, fn) {
105
+ if (id === RESERVED_PASSTHROUGH_KEY) {
106
+ throw new Error(`${fn}: "${RESERVED_PASSTHROUGH_KEY}" is a reserved state key (the passthrough wrap) and cannot be a per-tool id`);
107
+ }
108
+ }
109
+
110
+ /** Set a tool's enabled flag, MERGING (so `hidden`/`hot` are preserved). Persists atomically. */
111
+ function setToolEnabled(statePath, id, enabled) {
112
+ if (typeof id !== 'string' || !id) throw new Error('setToolEnabled: id required');
113
+ assertNotReserved(id, 'setToolEnabled');
114
+ const s = loadToolState(statePath);
115
+ s[id] = { ...(s[id] || {}), enabled: !!enabled };
116
+ atomicWriteJson(statePath, s);
117
+ return s;
118
+ }
119
+
120
+ /** Set a tool's hot flag, MERGING (so `enabled`/`hidden` are preserved). Persists atomically. */
121
+ function setToolHot(statePath, id, hot) {
122
+ if (typeof id !== 'string' || !id) throw new Error('setToolHot: id required');
123
+ assertNotReserved(id, 'setToolHot');
124
+ const s = loadToolState(statePath);
125
+ s[id] = { ...(s[id] || {}), hot: !!hot };
126
+ atomicWriteJson(statePath, s);
127
+ return s;
128
+ }
129
+
130
+ /** Set a tool's hidden flag, MERGING (so `enabled` is preserved). Persists atomically. */
131
+ function setToolHidden(statePath, id, hidden) {
132
+ if (typeof id !== 'string' || !id) throw new Error('setToolHidden: id required');
133
+ assertNotReserved(id, 'setToolHidden');
134
+ const s = loadToolState(statePath);
135
+ s[id] = { ...(s[id] || {}), hidden: !!hidden };
136
+ atomicWriteJson(statePath, s);
137
+ return s;
138
+ }
139
+
140
+ /** Drop a tool's overlay entry entirely (revert to defaults: active + not hidden).
141
+ * No-op if the id was never toggled. Persists atomically. */
142
+ function clearToolState(statePath, id) {
143
+ if (typeof id !== 'string' || !id) throw new Error('clearToolState: id required');
144
+ assertNotReserved(id, 'clearToolState'); // a register tool id'd "passthrough" must not clear a live wrap
145
+ const s = loadToolState(statePath);
146
+ delete s[id];
147
+ atomicWriteJson(statePath, s);
148
+ return s;
149
+ }
150
+
151
+ /** The active passthrough upstream id, or null. Only a non-empty STRING value counts - a
152
+ * per-tool object entry that happens to be keyed "passthrough" is not a wrap. */
153
+ function getPassthrough(state) {
154
+ const v = state && state.passthrough;
155
+ return typeof v === 'string' && v.length > 0 ? v : null;
156
+ }
157
+
158
+ /** Set (a non-empty string) or clear (null/'') the passthrough wrap. Persists atomically. */
159
+ function setPassthrough(statePath, upstreamId) {
160
+ const s = loadToolState(statePath);
161
+ if (typeof upstreamId === 'string' && upstreamId.length > 0) {
162
+ s.passthrough = upstreamId;
163
+ } else {
164
+ delete s.passthrough;
165
+ }
166
+ atomicWriteJson(statePath, s);
167
+ return s;
168
+ }
169
+
170
+ module.exports = { loadToolState, loadToolStateResult, isToolEnabled, isToolHidden, isToolHot, setToolEnabled, setToolHidden, setToolHot, clearToolState, getPassthrough, setPassthrough };