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.
- package/README.md +231 -167
- package/bin/toolfunnel.js +372 -147
- package/docs/KNOWN_BUGS.md +99 -33
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +70 -0
- package/docs/packaging.md +115 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +31 -31
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -0
- package/mcp/servers/mock-upstream/server.js +192 -12
- package/package.json +1 -1
- package/src/auth/config.js +174 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +344 -344
- package/src/core/config-home.js +134 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +275 -275
- package/src/core/hook-loader.js +791 -791
- package/src/core/hook-runner.js +346 -346
- package/src/core/logger.js +171 -171
- package/src/core/matcher.js +51 -51
- package/src/core/metrics.js +75 -75
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -77
- package/src/core/tool-registry.js +180 -180
- package/src/extend/add-hook.md +152 -142
- package/src/extend/add-mcp.md +122 -121
- package/src/extend/configure.md +87 -0
- package/src/extend/create-tool.md +168 -152
- package/src/extend/howto.js +102 -98
- package/src/extend/package.md +187 -187
- package/src/extend/wrap.md +99 -0
- package/src/mcp/aggregator.js +1246 -807
- package/src/mcp/expose-store.js +460 -430
- package/src/mcp/gated-run.js +201 -194
- package/src/mcp/http-transport.js +1311 -1012
- package/src/mcp/mcp-client.js +907 -530
- package/src/mcp/modern.js +372 -0
- package/src/mcp/protocol.js +468 -466
- package/src/mcp/server.js +2688 -1405
- package/src/packages/packager.js +715 -715
- package/src/tools/drift.js +138 -138
- package/src/tools/registry.js +513 -513
- package/src/tools/tool-state.js +170 -104
- package/src/ui/public/app.js +1174 -1055
- package/src/ui/public/index.html +286 -249
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1348 -1190
- package/tools/scripts/base64.js +139 -139
- package/tools/scripts/danger.js +61 -61
- package/tools/scripts/echo.js +39 -39
- package/tools/scripts/hash.js +101 -101
- package/tools/scripts/json.js +109 -109
- package/tools/scripts/text-stats.js +131 -131
- package/tools/scripts/tf-env.js +43 -43
- package/tools/scripts/tf-hook-add.js +151 -151
- package/tools/scripts/tf-hook-set.js +112 -112
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +117 -117
- package/tools/scripts/tf-mcp-set.js +121 -102
- package/tools/scripts/tf-pack.js +237 -237
- package/tools/scripts/tf-tool-add.js +134 -129
- package/tools/scripts/tf-tool-set.js +142 -142
- package/tools/scripts/tf-wrap.js +103 -0
- package/tools/scripts/uuid.js +102 -102
- package/tools/tools.register.json +227 -216
package/tools/scripts/tf-log.js
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* tf-log.js
|
|
6
|
-
*
|
|
7
|
-
* A first-party "management" script-tool (discovered via list_tools, executed via
|
|
8
|
-
* run_tool through the PreToolUse gate). It toggles ToolFunnel's own JSONL audit log
|
|
9
|
-
* (tool runs + gate allow/deny decisions) via src/core/logger. Logging is DEFAULT OFF.
|
|
10
|
-
*
|
|
11
|
-
* Contract with the register (`defaultRunScript`)
|
|
12
|
-
* ----------------------------------------------------------
|
|
13
|
-
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
|
|
14
|
-
* absent). Shape: { action: 'enable'|'disable'|'status', path? }.
|
|
15
|
-
* - Prints EXACTLY ONE JSON line to stdout and exits 0
|
|
16
|
-
* (bad args) is reported as { ok:false, error }.
|
|
17
|
-
*
|
|
18
|
-
* Output (stdout), exactly one JSON object:
|
|
19
|
-
* enable/disable: { ok:true, action, enabled, path }
|
|
20
|
-
* status: { ok:true, action, enabled, path, count } // count = log entries
|
|
21
|
-
* failure: { ok:false, error }
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
// Shared HOME/engine resolution (see tf-env.js): the logger module itself resolves the config
|
|
25
|
-
// home from the env the gateway set, so requiring it from the right PACKAGE is all we need.
|
|
26
|
-
const { srcRequire } = require('./tf-env');
|
|
27
|
-
const logger = srcRequire('core/logger');
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed JSON
|
|
31
|
-
* or a non-object payload so the caller reports { ok:false, error }.
|
|
32
|
-
* @returns {object}
|
|
33
|
-
*/
|
|
34
|
-
function parseArgs() {
|
|
35
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
36
|
-
if (raw === undefined || raw === null || raw === '') return {};
|
|
37
|
-
const parsed = JSON.parse(raw);
|
|
38
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
39
|
-
throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
|
|
40
|
-
}
|
|
41
|
-
return parsed;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Perform the action. May throw
|
|
46
|
-
* @returns {object} the success payload
|
|
47
|
-
*/
|
|
48
|
-
function run() {
|
|
49
|
-
const args = parseArgs();
|
|
50
|
-
const action = args.action;
|
|
51
|
-
|
|
52
|
-
switch (action) {
|
|
53
|
-
case 'enable': {
|
|
54
|
-
const patch = { enabled: true };
|
|
55
|
-
if (typeof args.path === 'string' && args.path.length > 0) patch.path = args.path;
|
|
56
|
-
const cfg = logger.setConfig(patch);
|
|
57
|
-
return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
|
|
58
|
-
}
|
|
59
|
-
case 'disable': {
|
|
60
|
-
const cfg = logger.setConfig({ enabled: false });
|
|
61
|
-
return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
|
|
62
|
-
}
|
|
63
|
-
case 'status': {
|
|
64
|
-
const cfg = logger.getConfig();
|
|
65
|
-
const entries = logger.tail();
|
|
66
|
-
return { ok: true, action, enabled: cfg.enabled, path: cfg.path, count: entries.length };
|
|
67
|
-
}
|
|
68
|
-
default:
|
|
69
|
-
throw new Error(
|
|
70
|
-
`args.action must be one of enable|disable|status (got ${JSON.stringify(action)})`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function main() {
|
|
76
|
-
let payload;
|
|
77
|
-
try {
|
|
78
|
-
payload = run();
|
|
79
|
-
} catch (err) {
|
|
80
|
-
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
81
|
-
}
|
|
82
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
83
|
-
process.exit(0);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tf-log.js - gateway-run MANAGEMENT tool: enable, disable, or check the activity log.
|
|
6
|
+
*
|
|
7
|
+
* A first-party "management" script-tool (discovered via list_tools, executed via
|
|
8
|
+
* run_tool through the PreToolUse gate). It toggles ToolFunnel's own JSONL audit log
|
|
9
|
+
* (tool runs + gate allow/deny decisions) via src/core/logger. Logging is DEFAULT OFF.
|
|
10
|
+
*
|
|
11
|
+
* Contract with the register (`defaultRunScript`)
|
|
12
|
+
* ----------------------------------------------------------
|
|
13
|
+
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
|
|
14
|
+
* absent). Shape: { action: 'enable'|'disable'|'status', path? }.
|
|
15
|
+
* - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical failure
|
|
16
|
+
* (bad args) is reported as { ok:false, error }.
|
|
17
|
+
*
|
|
18
|
+
* Output (stdout), exactly one JSON object:
|
|
19
|
+
* enable/disable: { ok:true, action, enabled, path }
|
|
20
|
+
* status: { ok:true, action, enabled, path, count } // count = log entries
|
|
21
|
+
* failure: { ok:false, error }
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Shared HOME/engine resolution (see tf-env.js): the logger module itself resolves the config
|
|
25
|
+
// home from the env the gateway set, so requiring it from the right PACKAGE is all we need.
|
|
26
|
+
const { srcRequire } = require('./tf-env');
|
|
27
|
+
const logger = srcRequire('core/logger');
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed JSON
|
|
31
|
+
* or a non-object payload so the caller reports { ok:false, error }.
|
|
32
|
+
* @returns {object}
|
|
33
|
+
*/
|
|
34
|
+
function parseArgs() {
|
|
35
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
36
|
+
if (raw === undefined || raw === null || raw === '') return {};
|
|
37
|
+
const parsed = JSON.parse(raw);
|
|
38
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
39
|
+
throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Perform the action. May throw - main() turns any throw into { ok:false, error }.
|
|
46
|
+
* @returns {object} the success payload
|
|
47
|
+
*/
|
|
48
|
+
function run() {
|
|
49
|
+
const args = parseArgs();
|
|
50
|
+
const action = args.action;
|
|
51
|
+
|
|
52
|
+
switch (action) {
|
|
53
|
+
case 'enable': {
|
|
54
|
+
const patch = { enabled: true };
|
|
55
|
+
if (typeof args.path === 'string' && args.path.length > 0) patch.path = args.path;
|
|
56
|
+
const cfg = logger.setConfig(patch);
|
|
57
|
+
return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
|
|
58
|
+
}
|
|
59
|
+
case 'disable': {
|
|
60
|
+
const cfg = logger.setConfig({ enabled: false });
|
|
61
|
+
return { ok: true, action, enabled: cfg.enabled, path: cfg.path };
|
|
62
|
+
}
|
|
63
|
+
case 'status': {
|
|
64
|
+
const cfg = logger.getConfig();
|
|
65
|
+
const entries = logger.tail();
|
|
66
|
+
return { ok: true, action, enabled: cfg.enabled, path: cfg.path, count: entries.length };
|
|
67
|
+
}
|
|
68
|
+
default:
|
|
69
|
+
throw new Error(
|
|
70
|
+
`args.action must be one of enable|disable|status (got ${JSON.stringify(action)})`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function main() {
|
|
76
|
+
let payload;
|
|
77
|
+
try {
|
|
78
|
+
payload = run();
|
|
79
|
+
} catch (err) {
|
|
80
|
+
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main();
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* tf-mcp-add.js
|
|
6
|
-
* ExposeStore and (optionally) curate which of its tools are exposed downstream.
|
|
7
|
-
*
|
|
8
|
-
* This is a first-party "management" script-tool: it is discovered via list_tools
|
|
9
|
-
* and executed via run_tool through the PreToolUse gate. It does not add new
|
|
10
|
-
* MCP-protocol commands
|
|
11
|
-
* by way of the ExposeStore, the on-disk source of truth the aggregator consumes.
|
|
12
|
-
*
|
|
13
|
-
* Contract with the register (`defaultRunScript`)
|
|
14
|
-
* ----------------------------------------------------------
|
|
15
|
-
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
|
|
16
|
-
* absent). Shape:
|
|
17
|
-
* {
|
|
18
|
-
* id, command,
|
|
19
|
-
* args?:[], env?:{}, transport?='stdio', enabled?=true, description?,
|
|
20
|
-
* expose?:[ { tool, as?, category?, enabled? } ]
|
|
21
|
-
* }
|
|
22
|
-
* - Prints EXACTLY ONE JSON line to stdout and exits 0
|
|
23
|
-
* failure (bad args, validation error from the store) is reported as
|
|
24
|
-
* { ok:false, error }, never a non-zero exit or a crash.
|
|
25
|
-
*
|
|
26
|
-
* Output (stdout), exactly one JSON object:
|
|
27
|
-
* success: { ok:true, upstream:{...}, exposed:[{...}, ...] }
|
|
28
|
-
* failure: { ok:false, error }
|
|
29
|
-
*
|
|
30
|
-
* Behaviour
|
|
31
|
-
* - addUpstream({ id, command, args, env, transport, enabled, description })
|
|
32
|
-
* the store validates (unique non-empty id, transport==='stdio', non-empty
|
|
33
|
-
* command) and persists atomically.
|
|
34
|
-
* - For each expose[] item: addExpose({ upstream:id, tool, as, category, enabled }).
|
|
35
|
-
* The store requires the upstream to exist (it does
|
|
36
|
-
* non-empty tool, and a unique (upstream,tool) pair.
|
|
37
|
-
*
|
|
38
|
-
* Note: there is no cross-step transaction. If the upstream is added but a later
|
|
39
|
-
* expose[] item fails validation, the upstream (and any earlier exposes) remain
|
|
40
|
-
* persisted; the failure is reported as { ok:false, error }.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
const path = require('node:path');
|
|
44
|
-
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
45
|
-
const { HOME, srcRequire } = require('./tf-env');
|
|
46
|
-
const { loadExposeStore } = srcRequire('mcp/expose-store');
|
|
47
|
-
|
|
48
|
-
const ROOT = HOME;
|
|
49
|
-
|
|
50
|
-
/** The MCP config (upstreams + expose), per the host path contract. */
|
|
51
|
-
const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed
|
|
55
|
-
* JSON or a non-object payload so the caller reports { ok:false, error }.
|
|
56
|
-
* @returns {object}
|
|
57
|
-
*/
|
|
58
|
-
function parseArgs() {
|
|
59
|
-
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
60
|
-
if (raw === undefined || raw === null || raw === '') return {};
|
|
61
|
-
const parsed = JSON.parse(raw);
|
|
62
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
63
|
-
throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
|
|
64
|
-
}
|
|
65
|
-
return parsed;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Perform the add. May throw
|
|
70
|
-
* @returns {object} the success payload
|
|
71
|
-
*/
|
|
72
|
-
function run() {
|
|
73
|
-
const args = parseArgs();
|
|
74
|
-
const store = loadExposeStore(EXPOSE_PATH);
|
|
75
|
-
|
|
76
|
-
// The store's normaliseUpstream already defaults transport/enabled/args/env, but
|
|
77
|
-
// apply the documented defaults explicitly so the intent is visible here too.
|
|
78
|
-
const upstream = store.addUpstream({
|
|
79
|
-
id: args.id,
|
|
80
|
-
command: args.command,
|
|
81
|
-
args: args.args,
|
|
82
|
-
env: args.env,
|
|
83
|
-
transport: typeof args.transport === 'string' ? args.transport : 'stdio',
|
|
84
|
-
enabled: typeof args.enabled === 'boolean' ? args.enabled : true,
|
|
85
|
-
description: args.description,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
const exposed = [];
|
|
89
|
-
const exposeList = Array.isArray(args.expose) ? args.expose : [];
|
|
90
|
-
for (const item of exposeList) {
|
|
91
|
-
const e = item && typeof item === 'object' && !Array.isArray(item) ? item : {};
|
|
92
|
-
exposed.push(
|
|
93
|
-
store.addExpose({
|
|
94
|
-
upstream: upstream.id,
|
|
95
|
-
tool: e.tool,
|
|
96
|
-
as: e.as,
|
|
97
|
-
category: e.category,
|
|
98
|
-
enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
|
|
99
|
-
})
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { ok: true, upstream, exposed };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function main() {
|
|
107
|
-
let payload;
|
|
108
|
-
try {
|
|
109
|
-
payload = run();
|
|
110
|
-
} catch (err) {
|
|
111
|
-
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
112
|
-
}
|
|
113
|
-
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
114
|
-
process.exit(0);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tf-mcp-add.js - gateway-run MANAGEMENT tool: register an upstream MCP in the
|
|
6
|
+
* ExposeStore and (optionally) curate which of its tools are exposed downstream.
|
|
7
|
+
*
|
|
8
|
+
* This is a first-party "management" script-tool: it is discovered via list_tools
|
|
9
|
+
* and executed via run_tool through the PreToolUse gate. It does not add new
|
|
10
|
+
* MCP-protocol commands - it edits ToolFunnel's own MCP config (mcp/expose.json)
|
|
11
|
+
* by way of the ExposeStore, the on-disk source of truth the aggregator consumes.
|
|
12
|
+
*
|
|
13
|
+
* Contract with the register (`defaultRunScript`)
|
|
14
|
+
* ----------------------------------------------------------
|
|
15
|
+
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
|
|
16
|
+
* absent). Shape:
|
|
17
|
+
* {
|
|
18
|
+
* id, command,
|
|
19
|
+
* args?:[], env?:{}, transport?='stdio', enabled?=true, description?,
|
|
20
|
+
* expose?:[ { tool, as?, category?, enabled? } ]
|
|
21
|
+
* }
|
|
22
|
+
* - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical
|
|
23
|
+
* failure (bad args, validation error from the store) is reported as
|
|
24
|
+
* { ok:false, error }, never a non-zero exit or a crash.
|
|
25
|
+
*
|
|
26
|
+
* Output (stdout), exactly one JSON object:
|
|
27
|
+
* success: { ok:true, upstream:{...}, exposed:[{...}, ...] }
|
|
28
|
+
* failure: { ok:false, error }
|
|
29
|
+
*
|
|
30
|
+
* Behaviour
|
|
31
|
+
* - addUpstream({ id, command, args, env, transport, enabled, description }) -
|
|
32
|
+
* the store validates (unique non-empty id, transport==='stdio', non-empty
|
|
33
|
+
* command) and persists atomically.
|
|
34
|
+
* - For each expose[] item: addExpose({ upstream:id, tool, as, category, enabled }).
|
|
35
|
+
* The store requires the upstream to exist (it does - we just added it), a
|
|
36
|
+
* non-empty tool, and a unique (upstream,tool) pair.
|
|
37
|
+
*
|
|
38
|
+
* Note: there is no cross-step transaction. If the upstream is added but a later
|
|
39
|
+
* expose[] item fails validation, the upstream (and any earlier exposes) remain
|
|
40
|
+
* persisted; the failure is reported as { ok:false, error }.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const path = require('node:path');
|
|
44
|
+
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
45
|
+
const { HOME, srcRequire } = require('./tf-env');
|
|
46
|
+
const { loadExposeStore } = srcRequire('mcp/expose-store');
|
|
47
|
+
|
|
48
|
+
const ROOT = HOME;
|
|
49
|
+
|
|
50
|
+
/** The MCP config (upstreams + expose), per the host path contract. */
|
|
51
|
+
const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed
|
|
55
|
+
* JSON or a non-object payload so the caller reports { ok:false, error }.
|
|
56
|
+
* @returns {object}
|
|
57
|
+
*/
|
|
58
|
+
function parseArgs() {
|
|
59
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
60
|
+
if (raw === undefined || raw === null || raw === '') return {};
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
63
|
+
throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
|
|
64
|
+
}
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Perform the add. May throw - main() turns any throw into { ok:false, error }.
|
|
70
|
+
* @returns {object} the success payload
|
|
71
|
+
*/
|
|
72
|
+
function run() {
|
|
73
|
+
const args = parseArgs();
|
|
74
|
+
const store = loadExposeStore(EXPOSE_PATH);
|
|
75
|
+
|
|
76
|
+
// The store's normaliseUpstream already defaults transport/enabled/args/env, but
|
|
77
|
+
// apply the documented defaults explicitly so the intent is visible here too.
|
|
78
|
+
const upstream = store.addUpstream({
|
|
79
|
+
id: args.id,
|
|
80
|
+
command: args.command,
|
|
81
|
+
args: args.args,
|
|
82
|
+
env: args.env,
|
|
83
|
+
transport: typeof args.transport === 'string' ? args.transport : 'stdio',
|
|
84
|
+
enabled: typeof args.enabled === 'boolean' ? args.enabled : true,
|
|
85
|
+
description: args.description,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const exposed = [];
|
|
89
|
+
const exposeList = Array.isArray(args.expose) ? args.expose : [];
|
|
90
|
+
for (const item of exposeList) {
|
|
91
|
+
const e = item && typeof item === 'object' && !Array.isArray(item) ? item : {};
|
|
92
|
+
exposed.push(
|
|
93
|
+
store.addExpose({
|
|
94
|
+
upstream: upstream.id,
|
|
95
|
+
tool: e.tool,
|
|
96
|
+
as: e.as,
|
|
97
|
+
category: e.category,
|
|
98
|
+
enabled: typeof e.enabled === 'boolean' ? e.enabled : true,
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { ok: true, upstream, exposed };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function main() {
|
|
107
|
+
let payload;
|
|
108
|
+
try {
|
|
109
|
+
payload = run();
|
|
110
|
+
} catch (err) {
|
|
111
|
+
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
112
|
+
}
|
|
113
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
main();
|
|
@@ -1,102 +1,121 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* tf-mcp-set.js
|
|
6
|
-
* upstream MCP in the ExposeStore.
|
|
7
|
-
*
|
|
8
|
-
* A first-party "management" script-tool (discovered via list_tools, executed via
|
|
9
|
-
* run_tool through the PreToolUse gate). It edits ToolFunnel's own MCP config
|
|
10
|
-
* (mcp/expose.json) via the ExposeStore
|
|
11
|
-
*
|
|
12
|
-
* Contract with the register (`defaultRunScript`)
|
|
13
|
-
* ----------------------------------------------------------
|
|
14
|
-
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
|
|
15
|
-
* absent). Shape: { id, action: 'enable'|'disable'|'remove' }.
|
|
16
|
-
* - Prints EXACTLY ONE JSON line to stdout and exits 0
|
|
17
|
-
* failure (bad args, unknown id) is reported as { ok:false, error }.
|
|
18
|
-
*
|
|
19
|
-
* Output (stdout), exactly one JSON object:
|
|
20
|
-
* enable/disable: { ok:true, action, id, upstream:{...} } // the updated entry
|
|
21
|
-
* remove: { ok:true, action, id, removed:true } // cascades expose[]
|
|
22
|
-
* failure: { ok:false, error }
|
|
23
|
-
*
|
|
24
|
-
* Behaviour
|
|
25
|
-
* - enable -> setUpstreamEnabled(id, true)
|
|
26
|
-
* - disable -> setUpstreamEnabled(id, false)
|
|
27
|
-
* - remove -> removeUpstream(id) (the store cascade-removes its expose entries)
|
|
28
|
-
* All three throw on an unknown id; the throw is reported as { ok:false, error }.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
const path = require('node:path');
|
|
32
|
-
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
33
|
-
const { HOME, srcRequire } = require('./tf-env');
|
|
34
|
-
const { loadExposeStore } = srcRequire('mcp/expose-store');
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tf-mcp-set.js - gateway-run MANAGEMENT tool: enable, disable, or remove an
|
|
6
|
+
* upstream MCP in the ExposeStore.
|
|
7
|
+
*
|
|
8
|
+
* A first-party "management" script-tool (discovered via list_tools, executed via
|
|
9
|
+
* run_tool through the PreToolUse gate). It edits ToolFunnel's own MCP config
|
|
10
|
+
* (mcp/expose.json) via the ExposeStore - it does not add MCP-protocol commands.
|
|
11
|
+
*
|
|
12
|
+
* Contract with the register (`defaultRunScript`)
|
|
13
|
+
* ----------------------------------------------------------
|
|
14
|
+
* - Structured args arrive as a JSON string in env TOOLFUNNEL_TOOL_ARGS ({} if
|
|
15
|
+
* absent). Shape: { id, action: 'enable'|'disable'|'remove' }.
|
|
16
|
+
* - Prints EXACTLY ONE JSON line to stdout and exits 0 - ALWAYS. A logical
|
|
17
|
+
* failure (bad args, unknown id) is reported as { ok:false, error }.
|
|
18
|
+
*
|
|
19
|
+
* Output (stdout), exactly one JSON object:
|
|
20
|
+
* enable/disable: { ok:true, action, id, upstream:{...} } // the updated entry
|
|
21
|
+
* remove: { ok:true, action, id, removed:true } // cascades expose[]
|
|
22
|
+
* failure: { ok:false, error }
|
|
23
|
+
*
|
|
24
|
+
* Behaviour
|
|
25
|
+
* - enable -> setUpstreamEnabled(id, true)
|
|
26
|
+
* - disable -> setUpstreamEnabled(id, false)
|
|
27
|
+
* - remove -> removeUpstream(id) (the store cascade-removes its expose entries)
|
|
28
|
+
* All three throw on an unknown id; the throw is reported as { ok:false, error }.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
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 ROOT = HOME;
|
|
38
|
+
|
|
39
|
+
/** The MCP config (upstreams + expose), per the host path contract. */
|
|
40
|
+
const EXPOSE_PATH = path.join(ROOT, 'mcp', 'expose.json');
|
|
41
|
+
const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* If `id` is the currently-wrapped upstream, clear the wrap - a disable/remove of the wrapped
|
|
45
|
+
* upstream would otherwise BRICK the whole surface (empty tool list, tf_wrap itself gone) with
|
|
46
|
+
* only out-of-band recovery. Returns true if a wrap was cleared.
|
|
47
|
+
*/
|
|
48
|
+
function clearWrapIfTarget(id) {
|
|
49
|
+
try {
|
|
50
|
+
if (getPassthrough(loadToolState(STATE_PATH)) === id) {
|
|
51
|
+
setPassthrough(STATE_PATH, null);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
} catch (_e) { /* never let wrap-cleanup sink the primary action */ }
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse TOOLFUNNEL_TOOL_ARGS (a JSON string; {} if absent). Throws on malformed
|
|
60
|
+
* JSON or a non-object payload so the caller reports { ok:false, error }.
|
|
61
|
+
* @returns {object}
|
|
62
|
+
*/
|
|
63
|
+
function parseArgs() {
|
|
64
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
65
|
+
if (raw === undefined || raw === null || raw === '') return {};
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
68
|
+
throw new Error('TOOLFUNNEL_TOOL_ARGS must be a JSON object');
|
|
69
|
+
}
|
|
70
|
+
return parsed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Perform the action. May throw - main() turns any throw into { ok:false, error }.
|
|
75
|
+
* @returns {object} the success payload
|
|
76
|
+
*/
|
|
77
|
+
function run() {
|
|
78
|
+
const args = parseArgs();
|
|
79
|
+
const id = args.id;
|
|
80
|
+
const action = args.action;
|
|
81
|
+
|
|
82
|
+
if (typeof id !== 'string' || id.length === 0) {
|
|
83
|
+
throw new Error('args.id must be a non-empty string');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const store = loadExposeStore(EXPOSE_PATH);
|
|
87
|
+
|
|
88
|
+
switch (action) {
|
|
89
|
+
case 'enable': {
|
|
90
|
+
const upstream = store.setUpstreamEnabled(id, true);
|
|
91
|
+
return { ok: true, action, id, upstream };
|
|
92
|
+
}
|
|
93
|
+
case 'disable': {
|
|
94
|
+
const upstream = store.setUpstreamEnabled(id, false);
|
|
95
|
+
const unwrapped = clearWrapIfTarget(id);
|
|
96
|
+
return { ok: true, action, id, upstream, ...(unwrapped ? { wrapCleared: true } : {}) };
|
|
97
|
+
}
|
|
98
|
+
case 'remove': {
|
|
99
|
+
store.removeUpstream(id);
|
|
100
|
+
const unwrapped = clearWrapIfTarget(id);
|
|
101
|
+
return { ok: true, action, id, removed: true, ...(unwrapped ? { wrapCleared: true } : {}) };
|
|
102
|
+
}
|
|
103
|
+
default:
|
|
104
|
+
throw new Error(
|
|
105
|
+
`args.action must be one of enable|disable|remove (got ${JSON.stringify(action)})`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function main() {
|
|
111
|
+
let payload;
|
|
112
|
+
try {
|
|
113
|
+
payload = run();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main();
|