toolfunnel 0.4.0 → 0.6.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 (78) hide show
  1. package/README.md +102 -213
  2. package/bin/toolfunnel.js +372 -147
  3. package/docs/KNOWN_BUGS.md +99 -225
  4. package/docs/MANUAL.pdf +0 -0
  5. package/docs/design.md +70 -0
  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 +31 -31
  10. package/mcp/expose.example.json +30 -30
  11. package/mcp/servers/mock-modern/server.js +147 -0
  12. package/mcp/servers/mock-upstream/server.js +192 -12
  13. package/package.json +1 -1
  14. package/src/auth/config.js +174 -174
  15. package/src/auth/install.js +181 -181
  16. package/src/auth/resource-server.js +344 -344
  17. package/src/core/config-home.js +134 -134
  18. package/src/core/events.js +105 -105
  19. package/src/core/hook-engine.js +275 -275
  20. package/src/core/hook-loader.js +791 -791
  21. package/src/core/hook-runner.js +346 -346
  22. package/src/core/logger.js +171 -171
  23. package/src/core/matcher.js +51 -51
  24. package/src/core/metrics.js +75 -75
  25. package/src/core/requires.js +138 -138
  26. package/src/core/server-config.js +98 -77
  27. package/src/core/tool-registry.js +180 -180
  28. package/src/extend/add-hook.md +152 -142
  29. package/src/extend/add-mcp.md +122 -121
  30. package/src/extend/configure.md +87 -0
  31. package/src/extend/create-tool.md +168 -152
  32. package/src/extend/howto.js +102 -98
  33. package/src/extend/package.md +187 -187
  34. package/src/extend/wrap.md +99 -0
  35. package/src/mcp/aggregator.js +1246 -807
  36. package/src/mcp/expose-store.js +460 -430
  37. package/src/mcp/gated-run.js +201 -194
  38. package/src/mcp/http-transport.js +1311 -981
  39. package/src/mcp/mcp-client.js +907 -530
  40. package/src/mcp/modern.js +372 -0
  41. package/src/mcp/protocol.js +468 -466
  42. package/src/mcp/server.js +2688 -1322
  43. package/src/packages/packager.js +715 -715
  44. package/src/tools/drift.js +138 -138
  45. package/src/tools/registry.js +513 -511
  46. package/src/tools/tool-state.js +170 -104
  47. package/src/ui/public/app.js +1174 -1055
  48. package/src/ui/public/index.html +286 -249
  49. package/src/ui/public/styles.css +535 -535
  50. package/src/ui/server.js +1348 -1190
  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 +112 -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 -102
  64. package/tools/scripts/tf-pack.js +237 -237
  65. package/tools/scripts/tf-tool-add.js +134 -129
  66. package/tools/scripts/tf-tool-set.js +142 -142
  67. package/tools/scripts/tf-wrap.js +103 -0
  68. package/tools/scripts/uuid.js +102 -102
  69. package/tools/tools.register.json +227 -216
  70. package/docs/admin-ui.md +0 -404
  71. package/docs/configuration.md +0 -402
  72. package/docs/hooks-and-gating.md +0 -428
  73. package/docs/logging.md +0 -223
  74. package/docs/management.md +0 -429
  75. package/docs/oauth.md +0 -103
  76. package/docs/releasing.md +0 -31
  77. package/docs/setup.md +0 -352
  78. package/docs/tools-and-upstreams.md +0 -469
@@ -1,104 +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
- * CommonJS, Node built-ins only. Reuses registry.js's atomicWriteJson (temp+rename).
28
- */
29
-
30
- const fs = require('node:fs');
31
- const { atomicWriteJson } = require('./registry');
32
-
33
- /** Load the overlay ({} if missing/malformed — never throws). */
34
- function loadToolState(statePath) {
35
- try {
36
- const d = JSON.parse(fs.readFileSync(statePath, 'utf8'));
37
- return (d && typeof d === 'object' && !Array.isArray(d)) ? d : {};
38
- } catch (_e) {
39
- return {};
40
- }
41
- }
42
-
43
- /** Default ON: enabled unless the overlay explicitly says enabled:false. */
44
- function isToolEnabled(state, id) {
45
- const e = state && state[id];
46
- return !(e && e.enabled === false);
47
- }
48
-
49
- /** Default NOT hidden: hidden only when the overlay explicitly says hidden:true.
50
- * `hidden` is a manager-list-only flag (declutter); it is INDEPENDENT of `enabled`
51
- * (MCP visibility). A tool can be active-but-hidden, or disabled-but-visible. */
52
- function isToolHidden(state, id) {
53
- const e = state && state[id];
54
- return !!(e && e.hidden === true);
55
- }
56
-
57
- /** Default OFF for local/upstream, ON for meta — the caller supplies the kind-appropriate
58
- * `defaultHot`. An explicit boolean in the overlay always wins; absence falls back to the
59
- * default. `hot` is the TOP-LEVEL (every-turn) promotion axis — INDEPENDENT of enabled/hidden,
60
- * though a disabled tool is filtered out of the top-level surface by the assembler, not here. */
61
- function isToolHot(state, id, defaultHot) {
62
- const e = state && state[id];
63
- if (e && typeof e.hot === 'boolean') return e.hot;
64
- return !!defaultHot;
65
- }
66
-
67
- /** Set a tool's enabled flag, MERGING (so `hidden`/`hot` are preserved). Persists atomically. */
68
- function setToolEnabled(statePath, id, enabled) {
69
- if (typeof id !== 'string' || !id) throw new Error('setToolEnabled: id required');
70
- const s = loadToolState(statePath);
71
- s[id] = { ...(s[id] || {}), enabled: !!enabled };
72
- atomicWriteJson(statePath, s);
73
- return s;
74
- }
75
-
76
- /** Set a tool's hot flag, MERGING (so `enabled`/`hidden` are preserved). Persists atomically. */
77
- function setToolHot(statePath, id, hot) {
78
- if (typeof id !== 'string' || !id) throw new Error('setToolHot: id required');
79
- const s = loadToolState(statePath);
80
- s[id] = { ...(s[id] || {}), hot: !!hot };
81
- atomicWriteJson(statePath, s);
82
- return s;
83
- }
84
-
85
- /** Set a tool's hidden flag, MERGING (so `enabled` is preserved). Persists atomically. */
86
- function setToolHidden(statePath, id, hidden) {
87
- if (typeof id !== 'string' || !id) throw new Error('setToolHidden: id required');
88
- const s = loadToolState(statePath);
89
- s[id] = { ...(s[id] || {}), hidden: !!hidden };
90
- atomicWriteJson(statePath, s);
91
- return s;
92
- }
93
-
94
- /** Drop a tool's overlay entry entirely (revert to defaults: active + not hidden).
95
- * No-op if the id was never toggled. Persists atomically. */
96
- function clearToolState(statePath, id) {
97
- if (typeof id !== 'string' || !id) throw new Error('clearToolState: id required');
98
- const s = loadToolState(statePath);
99
- delete s[id];
100
- atomicWriteJson(statePath, s);
101
- return s;
102
- }
103
-
104
- module.exports = { loadToolState, isToolEnabled, isToolHidden, isToolHot, setToolEnabled, setToolHidden, setToolHot, clearToolState };
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 };