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,147 +1,147 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * mcp/servers/mock-modern/server.js
6
- *
7
- * A tiny, ZERO-DEPENDENCY stdio MCP server that speaks the 2026-07-28 ("modern") protocol -
8
- * the companion to mock-upstream (which speaks the legacy 2024-11-05 protocol). It exists to
9
- * prove ToolFunnel's DUAL-ERA CLIENT: attach it as an upstream and ToolFunnel negotiates modern
10
- * (probes server/discover, skips the initialize handshake, and puts a per-request `_meta` on
11
- * every call). Over stdio the modern era needs only `_meta` - the Mcp-* headers are HTTP-only.
12
- *
13
- * Modern-only, deliberately STRICT so it proves the client is really speaking modern:
14
- * - `initialize` -> -32601 (removed in the modern era; a modern-only server has no handshake)
15
- * - `server/discover` -> { resultType, supportedVersions:['2026-07-28'], capabilities, _meta
16
- * (serverInfo), instructions, ttlMs, cacheScope }
17
- * - `tools/list` -> REQUIRES per-request `_meta` (else -32602); returns resultType + tools
18
- * + ttlMs/cacheScope
19
- * - `tools/call` -> REQUIRES per-request `_meta` (else -32602); returns resultType + content
20
- *
21
- * Tools (real upstream names): ping -> "pong", add {a,b} -> a+b, echo {text} -> text.
22
- *
23
- * JSON-RPC 2.0 over stdio, newline-delimited (one compact JSON object per line). Node built-ins
24
- * only, CommonJS. Built to the RC spec (locked 2026-05-21); reconcile minor final deltas 28 July.
25
- */
26
-
27
- const MODERN_VERSION = '2026-07-28';
28
- const SERVER_INFO = { name: 'mock-modern', version: '2.0.0' };
29
- const META_VERSION = 'io.modelcontextprotocol/protocolVersion';
30
- const META_SERVERINFO = 'io.modelcontextprotocol/serverInfo';
31
-
32
- const TOOLS = [
33
- { name: 'ping', description: 'Health check - returns "pong".', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
34
- { name: 'add', description: 'Add two numbers.', inputSchema: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'], additionalProperties: false } },
35
- { name: 'echo', description: 'Echo the given text.', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'], additionalProperties: false } },
36
- ];
37
-
38
- function writeMessage(obj) {
39
- try { process.stdout.write(JSON.stringify(obj) + '\n'); } catch (_e) { /* never crash on a write */ }
40
- }
41
-
42
- /** A modern result carries resultType + the server's serverInfo in _meta. */
43
- function modernResult(extra) {
44
- return Object.assign({ resultType: 'complete', _meta: { [META_SERVERINFO]: SERVER_INFO } }, extra);
45
- }
46
-
47
- function textResult(text, isError) {
48
- return modernResult({ content: [{ type: 'text', text: String(text) }], isError: isError === true });
49
- }
50
-
51
- /** A modern request MUST carry params._meta with BOTH the EXACT protocol version AND a
52
- * clientCapabilities object - the spec's two per-request MUSTs. Strict on purpose: this
53
- * fixture's job is to catch a client regression in EITHER field (a lax check here would let
54
- * "tools/call works with _meta" pass while the client sent the wrong version or no caps). */
55
- function hasModernMeta(params) {
56
- const meta = params && params._meta;
57
- if (!meta || typeof meta !== 'object') return false;
58
- if (meta[META_VERSION] !== MODERN_VERSION) return false;
59
- const caps = meta['io.modelcontextprotocol/clientCapabilities'];
60
- return !!(caps && typeof caps === 'object' && !Array.isArray(caps));
61
- }
62
-
63
- function handleMessage(msg) {
64
- if (!msg || typeof msg !== 'object') return;
65
- const id = msg.id;
66
- const method = msg.method;
67
- const params = msg.params || {};
68
- if (id === undefined || id === null) return; // notifications are never answered
69
-
70
- switch (method) {
71
- case 'server/discover':
72
- return writeMessage({
73
- jsonrpc: '2.0', id,
74
- result: modernResult({
75
- supportedVersions: [MODERN_VERSION],
76
- capabilities: { tools: {} },
77
- instructions: 'A modern (2026-07-28) mock MCP. Tools: ping, add, echo.',
78
- ttlMs: 3600000,
79
- cacheScope: 'private',
80
- }),
81
- });
82
-
83
- case 'initialize':
84
- // Modern-only: the initialize handshake was removed. Reject so a legacy client fails clearly.
85
- return writeMessage({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: initialize (this is a modern 2026-07-28 server - use server/discover)' } });
86
-
87
- case 'tools/list':
88
- if (!hasModernMeta(params)) {
89
- return writeMessage({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: modern requests require params._meta with a protocol version' } });
90
- }
91
- return writeMessage({ jsonrpc: '2.0', id, result: modernResult({ tools: TOOLS, ttlMs: 60000, cacheScope: 'private' }) });
92
-
93
- case 'resources/list':
94
- // A CacheableResult surface - proves non-tool forwarding under a wrap (and that the wrap adds
95
- // the required ttlMs/cacheScope when the upstream's own hints are absent).
96
- if (!hasModernMeta(params)) {
97
- return writeMessage({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: modern requests require params._meta with a protocol version' } });
98
- }
99
- return writeMessage({ jsonrpc: '2.0', id, result: modernResult({ resources: [{ uri: 'mock://modern/example', name: 'example' }] }) });
100
-
101
- case 'tools/call': {
102
- if (!hasModernMeta(params)) {
103
- return writeMessage({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: modern requests require params._meta with a protocol version' } });
104
- }
105
- const name = params.name;
106
- const args = params.arguments || {};
107
- if (name === 'ping') {
108
- // PLUMBING FIXTURE, not conformant behaviour: a real modern server emits notifications
109
- // only inside a subscriptions/listen stream, which ToolFunnel's client does not yet open
110
- // (28-July item). This unsolicited emission exists solely so BR-A2 can prove the
111
- // notification->hook plumbing; the ACK-level honesty (never counting a modern upstream as
112
- // able to honour resourceSubscriptions) is asserted by BR-A3.
113
- writeMessage({ jsonrpc: '2.0', method: 'notifications/resources/updated', params: { uri: 'mock://modern/example' } });
114
- return writeMessage({ jsonrpc: '2.0', id, result: textResult('pong') });
115
- }
116
- if (name === 'add') {
117
- const a = Number(args.a), b = Number(args.b);
118
- if (!isFinite(a) || !isFinite(b)) return writeMessage({ jsonrpc: '2.0', id, result: textResult('add requires numeric a and b', true) });
119
- return writeMessage({ jsonrpc: '2.0', id, result: textResult(String(a + b)) });
120
- }
121
- if (name === 'echo') {
122
- const text = typeof args.text === 'string' ? args.text : JSON.stringify(args.text);
123
- return writeMessage({ jsonrpc: '2.0', id, result: textResult(text) });
124
- }
125
- return writeMessage({ jsonrpc: '2.0', id, result: textResult('unknown tool: ' + name, true) });
126
- }
127
-
128
- default:
129
- return writeMessage({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: ' + method } });
130
- }
131
- }
132
-
133
- let buffer = '';
134
- process.stdin.on('data', (chunk) => {
135
- buffer += chunk.toString('utf8');
136
- let nl;
137
- while ((nl = buffer.indexOf('\n')) !== -1) {
138
- const line = buffer.slice(0, nl).replace(/\r$/, '').trim();
139
- buffer = buffer.slice(nl + 1);
140
- if (line.length === 0) continue;
141
- let msg;
142
- try { msg = JSON.parse(line); } catch (_e) { continue; }
143
- try { handleMessage(msg); } catch (_e) { /* never take the server down */ }
144
- }
145
- });
146
- process.stdin.on('end', () => process.exit(0));
147
- process.stdin.resume();
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * mcp/servers/mock-modern/server.js
6
+ *
7
+ * A tiny, ZERO-DEPENDENCY stdio MCP server that speaks the 2026-07-28 ("modern") protocol -
8
+ * the companion to mock-upstream (which speaks the legacy 2024-11-05 protocol). It exists to
9
+ * prove ToolFunnel's DUAL-ERA CLIENT: attach it as an upstream and ToolFunnel negotiates modern
10
+ * (probes server/discover, skips the initialize handshake, and puts a per-request `_meta` on
11
+ * every call). Over stdio the modern era needs only `_meta` - the Mcp-* headers are HTTP-only.
12
+ *
13
+ * Modern-only, deliberately STRICT so it proves the client is really speaking modern:
14
+ * - `initialize` -> -32601 (removed in the modern era; a modern-only server has no handshake)
15
+ * - `server/discover` -> { resultType, supportedVersions:['2026-07-28'], capabilities, _meta
16
+ * (serverInfo), instructions, ttlMs, cacheScope }
17
+ * - `tools/list` -> REQUIRES per-request `_meta` (else -32602); returns resultType + tools
18
+ * + ttlMs/cacheScope
19
+ * - `tools/call` -> REQUIRES per-request `_meta` (else -32602); returns resultType + content
20
+ *
21
+ * Tools (real upstream names): ping -> "pong", add {a,b} -> a+b, echo {text} -> text.
22
+ *
23
+ * JSON-RPC 2.0 over stdio, newline-delimited (one compact JSON object per line). Node built-ins
24
+ * only, CommonJS.
25
+ */
26
+
27
+ const MODERN_VERSION = '2026-07-28';
28
+ const SERVER_INFO = { name: 'mock-modern', version: '2.0.0' };
29
+ const META_VERSION = 'io.modelcontextprotocol/protocolVersion';
30
+ const META_SERVERINFO = 'io.modelcontextprotocol/serverInfo';
31
+
32
+ const TOOLS = [
33
+ { name: 'ping', description: 'Health check - returns "pong".', inputSchema: { type: 'object', properties: {}, additionalProperties: false } },
34
+ { name: 'add', description: 'Add two numbers.', inputSchema: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } }, required: ['a', 'b'], additionalProperties: false } },
35
+ { name: 'echo', description: 'Echo the given text.', inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'], additionalProperties: false } },
36
+ ];
37
+
38
+ function writeMessage(obj) {
39
+ try { process.stdout.write(JSON.stringify(obj) + '\n'); } catch (_e) { /* never crash on a write */ }
40
+ }
41
+
42
+ /** A modern result carries resultType + the server's serverInfo in _meta. */
43
+ function modernResult(extra) {
44
+ return Object.assign({ resultType: 'complete', _meta: { [META_SERVERINFO]: SERVER_INFO } }, extra);
45
+ }
46
+
47
+ function textResult(text, isError) {
48
+ return modernResult({ content: [{ type: 'text', text: String(text) }], isError: isError === true });
49
+ }
50
+
51
+ /** A modern request MUST carry params._meta with BOTH the EXACT protocol version AND a
52
+ * clientCapabilities object - the spec's two per-request MUSTs. Strict on purpose: this
53
+ * fixture's job is to catch a client regression in EITHER field (a lax check here would let
54
+ * "tools/call works with _meta" pass while the client sent the wrong version or no caps). */
55
+ function hasModernMeta(params) {
56
+ const meta = params && params._meta;
57
+ if (!meta || typeof meta !== 'object') return false;
58
+ if (meta[META_VERSION] !== MODERN_VERSION) return false;
59
+ const caps = meta['io.modelcontextprotocol/clientCapabilities'];
60
+ return !!(caps && typeof caps === 'object' && !Array.isArray(caps));
61
+ }
62
+
63
+ function handleMessage(msg) {
64
+ if (!msg || typeof msg !== 'object') return;
65
+ const id = msg.id;
66
+ const method = msg.method;
67
+ const params = msg.params || {};
68
+ if (id === undefined || id === null) return; // notifications are never answered
69
+
70
+ switch (method) {
71
+ case 'server/discover':
72
+ return writeMessage({
73
+ jsonrpc: '2.0', id,
74
+ result: modernResult({
75
+ supportedVersions: [MODERN_VERSION],
76
+ capabilities: { tools: {} },
77
+ instructions: 'A modern (2026-07-28) mock MCP. Tools: ping, add, echo.',
78
+ ttlMs: 3600000,
79
+ cacheScope: 'private',
80
+ }),
81
+ });
82
+
83
+ case 'initialize':
84
+ // Modern-only: the initialize handshake was removed. Reject so a legacy client fails clearly.
85
+ return writeMessage({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: initialize (this is a modern 2026-07-28 server - use server/discover)' } });
86
+
87
+ case 'tools/list':
88
+ if (!hasModernMeta(params)) {
89
+ return writeMessage({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: modern requests require params._meta with a protocol version' } });
90
+ }
91
+ return writeMessage({ jsonrpc: '2.0', id, result: modernResult({ tools: TOOLS, ttlMs: 60000, cacheScope: 'private' }) });
92
+
93
+ case 'resources/list':
94
+ // A CacheableResult surface - proves non-tool forwarding under a wrap (and that the wrap adds
95
+ // the required ttlMs/cacheScope when the upstream's own hints are absent).
96
+ if (!hasModernMeta(params)) {
97
+ return writeMessage({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: modern requests require params._meta with a protocol version' } });
98
+ }
99
+ return writeMessage({ jsonrpc: '2.0', id, result: modernResult({ resources: [{ uri: 'mock://modern/example', name: 'example' }] }) });
100
+
101
+ case 'tools/call': {
102
+ if (!hasModernMeta(params)) {
103
+ return writeMessage({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: modern requests require params._meta with a protocol version' } });
104
+ }
105
+ const name = params.name;
106
+ const args = params.arguments || {};
107
+ if (name === 'ping') {
108
+ // PLUMBING FIXTURE, not conformant behaviour: a real modern server emits notifications
109
+ // only inside a subscriptions/listen stream, which ToolFunnel's client does not yet open
110
+ // (28-July item). This unsolicited emission exists solely so BR-A2 can prove the
111
+ // notification->hook plumbing; the ACK-level honesty (never counting a modern upstream as
112
+ // able to honour resourceSubscriptions) is asserted by BR-A3.
113
+ writeMessage({ jsonrpc: '2.0', method: 'notifications/resources/updated', params: { uri: 'mock://modern/example' } });
114
+ return writeMessage({ jsonrpc: '2.0', id, result: textResult('pong') });
115
+ }
116
+ if (name === 'add') {
117
+ const a = Number(args.a), b = Number(args.b);
118
+ if (!isFinite(a) || !isFinite(b)) return writeMessage({ jsonrpc: '2.0', id, result: textResult('add requires numeric a and b', true) });
119
+ return writeMessage({ jsonrpc: '2.0', id, result: textResult(String(a + b)) });
120
+ }
121
+ if (name === 'echo') {
122
+ const text = typeof args.text === 'string' ? args.text : JSON.stringify(args.text);
123
+ return writeMessage({ jsonrpc: '2.0', id, result: textResult(text) });
124
+ }
125
+ return writeMessage({ jsonrpc: '2.0', id, result: textResult('unknown tool: ' + name, true) });
126
+ }
127
+
128
+ default:
129
+ return writeMessage({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found: ' + method } });
130
+ }
131
+ }
132
+
133
+ let buffer = '';
134
+ process.stdin.on('data', (chunk) => {
135
+ buffer += chunk.toString('utf8');
136
+ let nl;
137
+ while ((nl = buffer.indexOf('\n')) !== -1) {
138
+ const line = buffer.slice(0, nl).replace(/\r$/, '').trim();
139
+ buffer = buffer.slice(nl + 1);
140
+ if (line.length === 0) continue;
141
+ let msg;
142
+ try { msg = JSON.parse(line); } catch (_e) { continue; }
143
+ try { handleMessage(msg); } catch (_e) { /* never take the server down */ }
144
+ }
145
+ });
146
+ process.stdin.on('end', () => process.exit(0));
147
+ process.stdin.resume();