toolfunnel 0.5.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 (69) hide show
  1. package/README.md +231 -167
  2. package/bin/toolfunnel.js +372 -147
  3. package/docs/KNOWN_BUGS.md +99 -33
  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 -1012
  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 -1405
  43. package/src/packages/packager.js +715 -715
  44. package/src/tools/drift.js +138 -138
  45. package/src/tools/registry.js +513 -513
  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
@@ -1,138 +1,138 @@
1
- 'use strict';
2
-
3
- /**
4
- * drift.js — tool register drift detector.
5
- *
6
- * The register can go stale when entries are not maintained alongside the files
7
- * they describe (a config file left unmaintained over time, tools missing). A
8
- * behavioural "update it immediately" rule is fragile. This is the STRUCTURAL
9
- * guard (fix-the-tool, not the discipline): scan the real scripts directory
10
- * against the register and surface drift so it can never silently rot again.
11
- *
12
- * Mirrors the host's auto-detect (the folder is the source of truth for what
13
- * EXISTS; the register carries config). Read-only — it never edits anything,
14
- * inside or outside the gateway (honours the isolation rule: MAY READ the live
15
- * scripts directory, MUST NOT write it).
16
- *
17
- * detectDrift({ registerPath, toolsDir }) -> {
18
- * ok, registerPath, toolsDir,
19
- * registeredCount, // distinct .js basenames referenced by the register
20
- * onDiskCount, // tool files found under toolsDir
21
- * missingFromRegister: [str], // on disk under toolsDir but NOT referenced by any entry
22
- * deadEntries: [{id, path}], // entries whose referenced script no longer exists
23
- * note,
24
- * }
25
- *
26
- * Matching is by FILENAME (basename), case-insensitive: an entry "covers" a file
27
- * if the file's basename appears anywhere in the entry's `path` or invoke.command
28
- * (so grouped family entries that list their variants in `path` cover them).
29
- *
30
- * CommonJS, Node built-ins only.
31
- */
32
-
33
- const fs = require('node:fs');
34
- const path = require('node:path');
35
-
36
- // dirs/files that are not tools — never count as drift
37
- const SKIP_DIRS = new Set(['.archive', 'node_modules', 'scripts', 'docs', 'setup']);
38
- const TOOL_EXTS = new Set(['.js']);
39
- // obvious non-tool scripts (probes/tests) — surfaced separately, not as "missing"
40
- const NONTOOL_RE = /^(test_|_)/i;
41
-
42
- /** Every .js basename (lowercased) referenced by the register's path + invoke fields. */
43
- function registeredBasenames(data) {
44
- const set = new Set();
45
- const tools = Array.isArray(data && data.tools) ? data.tools : [];
46
- const grab = (s) => {
47
- if (typeof s !== 'string') return;
48
- // filename run ending in .js — NO spaces in the class (so "wraps helper.js"
49
- // yields "helper.js", not the whole phrase). path/dir prefixes are excluded too.
50
- const m = s.match(/[A-Za-z0-9_.\-]+\.js/g);
51
- if (m) for (const f of m) set.add(path.basename(f).toLowerCase());
52
- };
53
- for (const t of tools) {
54
- grab(t.path);
55
- if (t.invoke && typeof t.invoke.command === 'string') grab(t.invoke.command);
56
- // a family entry may explicitly name the member files it covers
57
- if (Array.isArray(t.covers)) for (const f of t.covers) {
58
- if (typeof f === 'string') set.add(path.basename(f).toLowerCase());
59
- }
60
- }
61
- return set;
62
- }
63
-
64
- /** Recursively collect .js files under dir, skipping SKIP_DIRS and dotdirs. */
65
- function scanTools(dir) {
66
- const out = [];
67
- let entries;
68
- try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return out; }
69
- for (const e of entries) {
70
- if (e.isDirectory()) {
71
- if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) continue;
72
- out.push(...scanTools(path.join(dir, e.name)));
73
- } else if (e.isFile() && TOOL_EXTS.has(path.extname(e.name).toLowerCase())) {
74
- out.push(path.join(dir, e.name));
75
- }
76
- }
77
- return out;
78
- }
79
-
80
- function detectDrift({ registerPath, toolsDir } = {}) {
81
- if (!registerPath || !toolsDir) {
82
- return { ok: false, error: 'detectDrift: registerPath and toolsDir are required' };
83
- }
84
- let data;
85
- try {
86
- data = JSON.parse(fs.readFileSync(registerPath, 'utf8'));
87
- } catch (err) {
88
- return { ok: false, error: `cannot read/parse register: ${err.message}` };
89
- }
90
-
91
- const registered = registeredBasenames(data);
92
- const onDiskFiles = scanTools(toolsDir);
93
- const onDiskBasenames = new Set(onDiskFiles.map((f) => path.basename(f).toLowerCase()));
94
-
95
- // on disk but not referenced by any register entry (skip obvious probes/tests)
96
- const missingFromRegister = [];
97
- const nonToolScripts = [];
98
- for (const f of onDiskFiles) {
99
- const base = path.basename(f);
100
- const lower = base.toLowerCase();
101
- if (registered.has(lower)) continue;
102
- if (NONTOOL_RE.test(base)) { nonToolScripts.push(base); continue; }
103
- missingFromRegister.push(base);
104
- }
105
-
106
- // entries whose script-invoke path references a file no longer present under scriptsRoot
107
- const deadEntries = [];
108
- const tools = Array.isArray(data.tools) ? data.tools : [];
109
- for (const t of tools) {
110
- const p = typeof t.path === 'string' ? t.path : '';
111
- const cmd = t.invoke && typeof t.invoke.command === 'string' ? t.invoke.command : '';
112
- const m = p.match(/[A-Za-z0-9_\-]+\.js/) || cmd.match(/[A-Za-z0-9_\-]+\.js/);
113
- if (m && !onDiskBasenames.has(m[0].toLowerCase())) {
114
- deadEntries.push({ id: t.id, path: p });
115
- }
116
- }
117
-
118
- missingFromRegister.sort();
119
- nonToolScripts.sort();
120
-
121
- return {
122
- ok: true,
123
- registerPath,
124
- toolsDir,
125
- registeredCount: registered.size,
126
- onDiskCount: onDiskFiles.length,
127
- missingFromRegister,
128
- deadEntries,
129
- nonToolScripts,
130
- note:
131
- 'Scope: scans toolsDir (the scripts root) only. Scripts that live outside it ' +
132
- 'may be referenced by the register but are not drift-checked here. Matching is ' +
133
- 'by filename. nonToolScripts (test_*/_*) are listed separately, not counted as ' +
134
- 'missing.',
135
- };
136
- }
137
-
138
- module.exports = { detectDrift, registeredBasenames, scanTools };
1
+ 'use strict';
2
+
3
+ /**
4
+ * drift.js - tool register drift detector.
5
+ *
6
+ * The register can go stale when entries are not maintained alongside the files
7
+ * they describe (a config file left unmaintained over time, tools missing). A
8
+ * behavioural "update it immediately" rule is fragile. This is the STRUCTURAL
9
+ * guard (fix-the-tool, not the discipline): scan the real scripts directory
10
+ * against the register and surface drift so it can never silently rot again.
11
+ *
12
+ * Mirrors the host's auto-detect (the folder is the source of truth for what
13
+ * EXISTS; the register carries config). Read-only - it never edits anything,
14
+ * inside or outside the gateway (honours the isolation rule: MAY READ the live
15
+ * scripts directory, MUST NOT write it).
16
+ *
17
+ * detectDrift({ registerPath, toolsDir }) -> {
18
+ * ok, registerPath, toolsDir,
19
+ * registeredCount, // distinct .js basenames referenced by the register
20
+ * onDiskCount, // tool files found under toolsDir
21
+ * missingFromRegister: [str], // on disk under toolsDir but NOT referenced by any entry
22
+ * deadEntries: [{id, path}], // entries whose referenced script no longer exists
23
+ * note,
24
+ * }
25
+ *
26
+ * Matching is by FILENAME (basename), case-insensitive: an entry "covers" a file
27
+ * if the file's basename appears anywhere in the entry's `path` or invoke.command
28
+ * (so grouped family entries that list their variants in `path` cover them).
29
+ *
30
+ * CommonJS, Node built-ins only.
31
+ */
32
+
33
+ const fs = require('node:fs');
34
+ const path = require('node:path');
35
+
36
+ // dirs/files that are not tools - never count as drift
37
+ const SKIP_DIRS = new Set(['.archive', 'node_modules', 'scripts', 'docs', 'setup']);
38
+ const TOOL_EXTS = new Set(['.js']);
39
+ // obvious non-tool scripts (probes/tests) - surfaced separately, not as "missing"
40
+ const NONTOOL_RE = /^(test_|_)/i;
41
+
42
+ /** Every .js basename (lowercased) referenced by the register's path + invoke fields. */
43
+ function registeredBasenames(data) {
44
+ const set = new Set();
45
+ const tools = Array.isArray(data && data.tools) ? data.tools : [];
46
+ const grab = (s) => {
47
+ if (typeof s !== 'string') return;
48
+ // filename run ending in .js - NO spaces in the class (so "wraps helper.js"
49
+ // yields "helper.js", not the whole phrase). path/dir prefixes are excluded too.
50
+ const m = s.match(/[A-Za-z0-9_.\-]+\.js/g);
51
+ if (m) for (const f of m) set.add(path.basename(f).toLowerCase());
52
+ };
53
+ for (const t of tools) {
54
+ grab(t.path);
55
+ if (t.invoke && typeof t.invoke.command === 'string') grab(t.invoke.command);
56
+ // a family entry may explicitly name the member files it covers
57
+ if (Array.isArray(t.covers)) for (const f of t.covers) {
58
+ if (typeof f === 'string') set.add(path.basename(f).toLowerCase());
59
+ }
60
+ }
61
+ return set;
62
+ }
63
+
64
+ /** Recursively collect .js files under dir, skipping SKIP_DIRS and dotdirs. */
65
+ function scanTools(dir) {
66
+ const out = [];
67
+ let entries;
68
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_e) { return out; }
69
+ for (const e of entries) {
70
+ if (e.isDirectory()) {
71
+ if (SKIP_DIRS.has(e.name) || e.name.startsWith('.')) continue;
72
+ out.push(...scanTools(path.join(dir, e.name)));
73
+ } else if (e.isFile() && TOOL_EXTS.has(path.extname(e.name).toLowerCase())) {
74
+ out.push(path.join(dir, e.name));
75
+ }
76
+ }
77
+ return out;
78
+ }
79
+
80
+ function detectDrift({ registerPath, toolsDir } = {}) {
81
+ if (!registerPath || !toolsDir) {
82
+ return { ok: false, error: 'detectDrift: registerPath and toolsDir are required' };
83
+ }
84
+ let data;
85
+ try {
86
+ data = JSON.parse(fs.readFileSync(registerPath, 'utf8'));
87
+ } catch (err) {
88
+ return { ok: false, error: `cannot read/parse register: ${err.message}` };
89
+ }
90
+
91
+ const registered = registeredBasenames(data);
92
+ const onDiskFiles = scanTools(toolsDir);
93
+ const onDiskBasenames = new Set(onDiskFiles.map((f) => path.basename(f).toLowerCase()));
94
+
95
+ // on disk but not referenced by any register entry (skip obvious probes/tests)
96
+ const missingFromRegister = [];
97
+ const nonToolScripts = [];
98
+ for (const f of onDiskFiles) {
99
+ const base = path.basename(f);
100
+ const lower = base.toLowerCase();
101
+ if (registered.has(lower)) continue;
102
+ if (NONTOOL_RE.test(base)) { nonToolScripts.push(base); continue; }
103
+ missingFromRegister.push(base);
104
+ }
105
+
106
+ // entries whose script-invoke path references a file no longer present under scriptsRoot
107
+ const deadEntries = [];
108
+ const tools = Array.isArray(data.tools) ? data.tools : [];
109
+ for (const t of tools) {
110
+ const p = typeof t.path === 'string' ? t.path : '';
111
+ const cmd = t.invoke && typeof t.invoke.command === 'string' ? t.invoke.command : '';
112
+ const m = p.match(/[A-Za-z0-9_\-]+\.js/) || cmd.match(/[A-Za-z0-9_\-]+\.js/);
113
+ if (m && !onDiskBasenames.has(m[0].toLowerCase())) {
114
+ deadEntries.push({ id: t.id, path: p });
115
+ }
116
+ }
117
+
118
+ missingFromRegister.sort();
119
+ nonToolScripts.sort();
120
+
121
+ return {
122
+ ok: true,
123
+ registerPath,
124
+ toolsDir,
125
+ registeredCount: registered.size,
126
+ onDiskCount: onDiskFiles.length,
127
+ missingFromRegister,
128
+ deadEntries,
129
+ nonToolScripts,
130
+ note:
131
+ 'Scope: scans toolsDir (the scripts root) only. Scripts that live outside it ' +
132
+ 'may be referenced by the register but are not drift-checked here. Matching is ' +
133
+ 'by filename. nonToolScripts (test_*/_*) are listed separately, not counted as ' +
134
+ 'missing.',
135
+ };
136
+ }
137
+
138
+ module.exports = { detectDrift, registeredBasenames, scanTools };