toolfunnel 0.3.0 → 0.4.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 +24 -6
- package/bin/toolfunnel.js +39 -4
- package/docs/KNOWN_BUGS.md +225 -0
- package/docs/packaging.md +115 -0
- package/docs/releasing.md +31 -0
- package/docs/setup.md +1 -1
- package/package.json +3 -1
- package/src/auth/config.js +4 -2
- package/src/auth/resource-server.js +28 -3
- package/src/core/config-home.js +134 -0
- package/src/core/logger.js +4 -2
- package/src/core/requires.js +138 -0
- package/src/core/server-config.js +77 -0
- package/src/extend/create-tool.md +24 -4
- package/src/extend/package.md +33 -0
- package/src/mcp/aggregator.js +12 -7
- package/src/mcp/http-transport.js +40 -21
- package/src/mcp/mcp-client.js +9 -3
- package/src/mcp/server.js +84 -2
- package/src/packages/packager.js +5 -3
- package/src/tools/registry.js +43 -0
- package/src/ui/server.js +49 -3
- package/tools/scripts/tf-env.js +43 -0
- package/tools/scripts/tf-hook-add.js +4 -5
- package/tools/scripts/tf-hook-set.js +4 -5
- package/tools/scripts/tf-list.js +9 -12
- package/tools/scripts/tf-log.js +4 -1
- package/tools/scripts/tf-mcp-add.js +4 -6
- package/tools/scripts/tf-mcp-set.js +4 -6
- package/tools/scripts/tf-pack.js +237 -0
- package/tools/scripts/tf-tool-add.js +16 -16
- package/tools/scripts/tf-tool-set.js +5 -11
- package/tools/tools.register.json +46 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* tf-pack.js — a first-party MANAGEMENT tool that packages the LIVE setup for deployment.
|
|
6
|
+
*
|
|
7
|
+
* Purpose
|
|
8
|
+
* -------
|
|
9
|
+
* Everything you built in this gateway — your tools + scripts, your hot/hidden curation, your
|
|
10
|
+
* upstream selections, your policy hooks, your identity — IS a config home. This tool snapshots
|
|
11
|
+
* it into a deployment-ready artifact in a SEPARATE location (never the live tree), so "ship what
|
|
12
|
+
* I made" is one call instead of a hand-rolled copy job:
|
|
13
|
+
*
|
|
14
|
+
* format "home" (default): <home>/dist/<name>/ = a portable config home. Zip it, git-init it,
|
|
15
|
+
* or point any toolfunnel at it via --config-dir / TOOLFUNNEL_HOME.
|
|
16
|
+
* format "npm": <home>/dist/<name>/ = a PUBLISHABLE npm package: package.json (with toolfunnel
|
|
17
|
+
* as a caret DEPENDENCY — depend, never copy: your users' installs count as toolfunnel
|
|
18
|
+
* downloads and our fixes reach them via npm update), a 2-line bin launcher pointing
|
|
19
|
+
* --config-dir at the bundled home/, and a README stub. `cd dist/<name> && npm publish` is
|
|
20
|
+
* your own MCP server on npm.
|
|
21
|
+
*
|
|
22
|
+
* Args (env TOOLFUNNEL_TOOL_ARGS, mirrors the other tf-* scripts):
|
|
23
|
+
* { format?: 'home'|'npm' (default 'home'),
|
|
24
|
+
* name?: string, // pack/package name (default: the home's serverName, else 'my-mcp')
|
|
25
|
+
* version?: string, // default: the home's explicit serverVersion, else '0.1.0'
|
|
26
|
+
* description?: string, // npm format: package.json description
|
|
27
|
+
* out?: string, // FOLDER NAME under <home>/dist/ (basename only; default: name)
|
|
28
|
+
* force?: boolean } // overwrite files in an existing non-empty out dir
|
|
29
|
+
*
|
|
30
|
+
* What travels: tools/ (register + state overlay + scripts — the hot/hidden curation IS the
|
|
31
|
+
* product), mcp/ (expose.json upstream references + any vendored servers), hooks/ (THE GATE
|
|
32
|
+
* TRAVELS — the shipped pack enforces its policy on the recipient's machine regardless of
|
|
33
|
+
* client), toolfunnel.json (identity + requires; rewritten with the pack's name/version).
|
|
34
|
+
* What NEVER travels: auth/ (environment-specific OAuth config), logs/, dist/ (recursion),
|
|
35
|
+
* packages/, node_modules, .git.
|
|
36
|
+
*
|
|
37
|
+
* Output (stdout), exactly one JSON object:
|
|
38
|
+
* success: { ok:true, format, out, files, next:[…] }
|
|
39
|
+
* failure: { ok:false, error }
|
|
40
|
+
* Safety: writes ONLY under <home>/dist/<basename> (the live config is read, never touched);
|
|
41
|
+
* refuses a non-empty destination without force:true; ALWAYS exits 0.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require('node:fs');
|
|
45
|
+
const path = require('node:path');
|
|
46
|
+
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
47
|
+
const { HOME, PKG } = require('./tf-env');
|
|
48
|
+
|
|
49
|
+
const EXCLUDE_DIRS = new Set(['dist', 'logs', 'auth', 'packages', 'node_modules', '.git', '.idea', '.vscode']);
|
|
50
|
+
|
|
51
|
+
function parseStructuredArgs() {
|
|
52
|
+
const raw = process.env.TOOLFUNNEL_TOOL_ARGS;
|
|
53
|
+
if (raw === undefined || raw === null || raw === '') return { value: {} };
|
|
54
|
+
try {
|
|
55
|
+
return { value: JSON.parse(raw) };
|
|
56
|
+
} catch (_err) {
|
|
57
|
+
return { parseError: `TOOLFUNNEL_TOOL_ARGS was not valid JSON: ${String(raw)}` };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The home's toolfunnel.json as raw JSON (or {}). Read directly — we want the EXPLICIT fields,
|
|
62
|
+
* not the loader's folded-in defaults (a pack must not inherit toolfunnel's own version). */
|
|
63
|
+
function homeIdentity() {
|
|
64
|
+
try {
|
|
65
|
+
const d = JSON.parse(fs.readFileSync(path.join(HOME, 'toolfunnel.json'), 'utf8'));
|
|
66
|
+
return d && typeof d === 'object' && !Array.isArray(d) ? d : {};
|
|
67
|
+
} catch (_e) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** npm-legal name: lowercase, spaces/illegals → '-', collapsed. */
|
|
73
|
+
function sanitizeName(name) {
|
|
74
|
+
const s = String(name).toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-._]+|[-._]+$/g, '');
|
|
75
|
+
return s || 'my-mcp';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Recursively copy `srcDir` → `destDir`, skipping EXCLUDE_DIRS at the top level and dotfiles
|
|
79
|
+
* everywhere. Overwrites only when `force`. Returns the number of files written. */
|
|
80
|
+
function copyTree(srcDir, destDir, force, topLevel) {
|
|
81
|
+
let count = 0;
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
85
|
+
} catch (_e) {
|
|
86
|
+
return count;
|
|
87
|
+
}
|
|
88
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
89
|
+
for (const e of entries) {
|
|
90
|
+
if (topLevel && EXCLUDE_DIRS.has(e.name)) continue;
|
|
91
|
+
if (e.name.startsWith('.')) continue;
|
|
92
|
+
const from = path.join(srcDir, e.name);
|
|
93
|
+
const to = path.join(destDir, e.name);
|
|
94
|
+
if (e.isDirectory()) {
|
|
95
|
+
count += copyTree(from, to, force, false);
|
|
96
|
+
} else if (e.isFile()) {
|
|
97
|
+
if (!force && fs.existsSync(to)) continue;
|
|
98
|
+
fs.copyFileSync(from, to);
|
|
99
|
+
count += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return count;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The 4 config pillars → destHome. Identity is rewritten (name/version/description overlay). */
|
|
106
|
+
function snapshotHome(destHome, force, identity) {
|
|
107
|
+
let files = 0;
|
|
108
|
+
for (const dir of ['tools', 'mcp', 'hooks']) {
|
|
109
|
+
files += copyTree(path.join(HOME, dir), path.join(destHome, dir), force, true);
|
|
110
|
+
}
|
|
111
|
+
fs.writeFileSync(path.join(destHome, 'toolfunnel.json'), JSON.stringify(identity, null, 2) + '\n');
|
|
112
|
+
return files + 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function run(args) {
|
|
116
|
+
const a = args && typeof args === 'object' && !Array.isArray(args) ? args : {};
|
|
117
|
+
const format = a.format === 'npm' ? 'npm' : a.format === 'home' || a.format === undefined ? 'home' : null;
|
|
118
|
+
if (format === null) return { ok: false, error: 'format must be "home" or "npm"' };
|
|
119
|
+
const force = a.force === true;
|
|
120
|
+
|
|
121
|
+
const id = homeIdentity();
|
|
122
|
+
const name = sanitizeName(a.name || id.serverName || 'my-mcp');
|
|
123
|
+
const version = typeof a.version === 'string' && a.version.trim() ? a.version.trim()
|
|
124
|
+
: (typeof id.serverVersion === 'string' && id.serverVersion.trim() ? id.serverVersion.trim() : '0.1.0');
|
|
125
|
+
const description = typeof a.description === 'string' && a.description.trim()
|
|
126
|
+
? a.description.trim()
|
|
127
|
+
: `${name} — an MCP server built on toolfunnel.`;
|
|
128
|
+
|
|
129
|
+
// The destination: ALWAYS <home>/dist/<basename> — the separate-location guarantee. `out` is a
|
|
130
|
+
// folder NAME (basename strips traversal), never a path, so the live tree can't be targeted.
|
|
131
|
+
const outName = path.basename(String(a.out || name));
|
|
132
|
+
const outDir = path.join(HOME, 'dist', outName);
|
|
133
|
+
const rel = path.relative(path.join(HOME, 'dist'), outDir);
|
|
134
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
135
|
+
return { ok: false, error: `out "${a.out}" escapes <home>/dist` };
|
|
136
|
+
}
|
|
137
|
+
if (!force && fs.existsSync(outDir) && fs.readdirSync(outDir).length > 0) {
|
|
138
|
+
return { ok: false, error: `destination is not empty: ${outDir} — pass force:true to overwrite (files are replaced, extras are kept)` };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The identity the pack CARRIES (preserves requires + any custom fields; stamps name/version).
|
|
142
|
+
const identity = Object.assign({}, id, { serverName: name, serverVersion: version });
|
|
143
|
+
|
|
144
|
+
let files = 0;
|
|
145
|
+
const next = [];
|
|
146
|
+
if (format === 'home') {
|
|
147
|
+
files = snapshotHome(outDir, force, identity);
|
|
148
|
+
next.push(`portable config home written — zip it, git-init it, or run: toolfunnel --config-dir "${outDir}"`);
|
|
149
|
+
} else {
|
|
150
|
+
// npm: bundled home + generated package.json + 2-line bin + README stub.
|
|
151
|
+
files = snapshotHome(path.join(outDir, 'home'), force, identity);
|
|
152
|
+
|
|
153
|
+
let tfVersion = '0.4.0';
|
|
154
|
+
try { tfVersion = require(path.join(PKG, 'package.json')).version || tfVersion; } catch (_e) { /* keep fallback */ }
|
|
155
|
+
const envOverride = name.toUpperCase().replace(/[^A-Z0-9]+/g, '_') + '_HOME';
|
|
156
|
+
|
|
157
|
+
const pkgJson = {
|
|
158
|
+
name,
|
|
159
|
+
version,
|
|
160
|
+
description,
|
|
161
|
+
license: 'MIT',
|
|
162
|
+
type: 'commonjs',
|
|
163
|
+
bin: { [name]: `bin/${name}.js` },
|
|
164
|
+
files: ['bin/', 'home/'],
|
|
165
|
+
// DEPEND, never copy: installs of this package count as toolfunnel downloads, and
|
|
166
|
+
// toolfunnel's fixes reach it through a normal `npm update` instead of a stale fork.
|
|
167
|
+
dependencies: { toolfunnel: `^${tfVersion}` },
|
|
168
|
+
engines: { node: '>=18' },
|
|
169
|
+
};
|
|
170
|
+
fs.writeFileSync(path.join(outDir, 'package.json'), JSON.stringify(pkgJson, null, 2) + '\n');
|
|
171
|
+
|
|
172
|
+
const binDir = path.join(outDir, 'bin');
|
|
173
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
174
|
+
fs.writeFileSync(path.join(binDir, `${name}.js`), [
|
|
175
|
+
'#!/usr/bin/env node',
|
|
176
|
+
"'use strict';",
|
|
177
|
+
`// ${name} — an MCP server built on toolfunnel, launched against its own bundled config home.`,
|
|
178
|
+
`// Set ${envOverride} to run from an external copy of the home (survives npm updates).`,
|
|
179
|
+
"const path = require('node:path');",
|
|
180
|
+
`const home = process.env.${envOverride} || path.join(__dirname, '..', 'home');`,
|
|
181
|
+
"process.argv.splice(2, 0, '--config-dir', home);",
|
|
182
|
+
"require('toolfunnel/bin/toolfunnel.js');",
|
|
183
|
+
'',
|
|
184
|
+
].join('\n'));
|
|
185
|
+
|
|
186
|
+
const readmePath = path.join(outDir, 'README.md');
|
|
187
|
+
if (force || !fs.existsSync(readmePath)) {
|
|
188
|
+
fs.writeFileSync(readmePath, [
|
|
189
|
+
`# ${name}`,
|
|
190
|
+
'',
|
|
191
|
+
description,
|
|
192
|
+
'',
|
|
193
|
+
'An MCP server packaged with [toolfunnel](https://github.com/Rendeverance/toolfunnel) —',
|
|
194
|
+
'the bundled `home/` carries the tools, curation, upstream references, AND the policy',
|
|
195
|
+
'hooks: the gate travels with the pack and enforces itself on any MCP client.',
|
|
196
|
+
'',
|
|
197
|
+
'## Run',
|
|
198
|
+
'```bash',
|
|
199
|
+
`npx ${name}`,
|
|
200
|
+
'```',
|
|
201
|
+
'',
|
|
202
|
+
'## Use an external home (survives npm updates)',
|
|
203
|
+
`Set \`${envOverride}\` to a directory holding a copy of \`home/\`.`,
|
|
204
|
+
'',
|
|
205
|
+
'## Audit honesty',
|
|
206
|
+
'Packs spawn commands. Read `home/mcp/expose.json`, `home/tools/tools.register.json`,',
|
|
207
|
+
'and `home/toolfunnel.json` (its `requires` probes run at startup) of anything you',
|
|
208
|
+
'install — including this.',
|
|
209
|
+
'',
|
|
210
|
+
].join('\n'));
|
|
211
|
+
files += 1;
|
|
212
|
+
}
|
|
213
|
+
files += 2; // package.json + bin
|
|
214
|
+
next.push(`publishable npm package written — review it, then: cd "${outDir}" && npm publish`);
|
|
215
|
+
next.push(`try it locally first: node "${path.join(outDir, 'bin', `${name}.js`)}"`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { ok: true, format, out: outDir, files, next };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function main() {
|
|
222
|
+
const parsed = parseStructuredArgs();
|
|
223
|
+
let payload;
|
|
224
|
+
if (parsed.parseError) {
|
|
225
|
+
payload = { ok: false, error: parsed.parseError };
|
|
226
|
+
} else {
|
|
227
|
+
try {
|
|
228
|
+
payload = run(parsed.value);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
payload = { ok: false, error: (err && err.message) || String(err) };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
234
|
+
process.exit(0);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
main();
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* -------
|
|
9
9
|
* The write-side counterpart to the read-only status / inventory tools: it lets
|
|
10
10
|
* the manager surface a NEW tool in the register at runtime (no reconnect — the
|
|
11
|
-
*
|
|
12
|
-
* host-local script the new entry invokes,
|
|
13
|
-
* register entry AND its backing script
|
|
11
|
+
* running gateway watches tools.register.json and hot-reloads its register on
|
|
12
|
+
* change). Optionally it also writes the host-local script the new entry invokes,
|
|
13
|
+
* so a single call can create both the register entry AND its backing script
|
|
14
|
+
* atomically.
|
|
14
15
|
*
|
|
15
16
|
* This is a "gateway-run" tool: it mutates ToolFunnel's OWN config (the register
|
|
16
17
|
* and, optionally, a script under the scripts root). It runs through the same
|
|
@@ -20,9 +21,12 @@
|
|
|
20
21
|
* Contract with the register (registry.js `defaultRunScript`)
|
|
21
22
|
* ----------------------------------------------------------
|
|
22
23
|
* - Structured args arrive as JSON in env TOOLFUNNEL_TOOL_ARGS ({} / absent => null).
|
|
23
|
-
* - Args: { id, name, summary?, category?, instructions?,
|
|
24
|
+
* - Args: { id, name, summary?, category?, instructions?, inputSchema?,
|
|
24
25
|
* invoke: { type:'script'|'shell', path?|command? },
|
|
25
26
|
* scriptText? }
|
|
27
|
+
* inputSchema (a JSON Schema object) is what a HOT-promoted tool advertises verbatim in
|
|
28
|
+
* the top-level tools/list — dropping it here was the gap between "your own tools" and
|
|
29
|
+
* "your own tools AND schemas".
|
|
26
30
|
* When scriptText is a string AND invoke.type === 'script', the script is
|
|
27
31
|
* written first (registry.writeScript), THEN the entry is added.
|
|
28
32
|
* - Prints a SINGLE JSON object to stdout and exits 0 — ALWAYS exit 0, even on
|
|
@@ -41,17 +45,11 @@
|
|
|
41
45
|
*/
|
|
42
46
|
|
|
43
47
|
const path = require('node:path');
|
|
44
|
-
|
|
48
|
+
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
49
|
+
const { HOME, srcRequire } = require('./tf-env');
|
|
50
|
+
const { loadRegistry } = srcRequire('tools/registry');
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
* The toolfunnel root, resolved from THIS file's location so the tool is portable
|
|
48
|
-
* and stays inside the sandbox.
|
|
49
|
-
*
|
|
50
|
-
* This script lives at: <root>/tools/scripts/tf-tool-add.js
|
|
51
|
-
* so the root is two directories up from __dirname:
|
|
52
|
-
* scripts -> tools -> <root>
|
|
53
|
-
*/
|
|
54
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
52
|
+
const ROOT = HOME;
|
|
55
53
|
const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
|
|
56
54
|
const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
|
|
57
55
|
|
|
@@ -83,10 +81,10 @@ function run(args) {
|
|
|
83
81
|
if (!args || typeof args !== 'object' || Array.isArray(args)) {
|
|
84
82
|
return {
|
|
85
83
|
ok: false,
|
|
86
|
-
error: 'args must be an object { id, name, summary?, category?, instructions?, invoke, scriptText? }',
|
|
84
|
+
error: 'args must be an object { id, name, summary?, category?, instructions?, inputSchema?, invoke, scriptText? }',
|
|
87
85
|
};
|
|
88
86
|
}
|
|
89
|
-
const { id, name, summary, category, instructions, invoke, scriptText } = args;
|
|
87
|
+
const { id, name, summary, category, instructions, inputSchema, invoke, scriptText } = args;
|
|
90
88
|
|
|
91
89
|
// Build a clean register entry (only carry through the optional fields when
|
|
92
90
|
// present so the persisted entry stays tidy). registry.add validates shape.
|
|
@@ -94,6 +92,8 @@ function run(args) {
|
|
|
94
92
|
if (summary !== undefined) entry.summary = summary;
|
|
95
93
|
if (category !== undefined) entry.category = category;
|
|
96
94
|
if (instructions !== undefined) entry.instructions = instructions;
|
|
95
|
+
if (inputSchema !== undefined) entry.inputSchema = inputSchema; // validated by registry.add
|
|
96
|
+
|
|
97
97
|
|
|
98
98
|
const registry = loadRegistry(REGISTER_PATH, { scriptsRoot: SCRIPTS_ROOT });
|
|
99
99
|
|
|
@@ -45,18 +45,12 @@
|
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
const path = require('node:path');
|
|
48
|
-
|
|
49
|
-
const {
|
|
48
|
+
// Shared HOME/engine resolution (see tf-env.js): config beside us, engine from the package.
|
|
49
|
+
const { HOME, srcRequire } = require('./tf-env');
|
|
50
|
+
const { loadRegistry } = srcRequire('tools/registry');
|
|
51
|
+
const { setToolEnabled, setToolHot, setToolHidden, clearToolState } = srcRequire('tools/tool-state');
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
* The toolfunnel root, resolved from THIS file's location so the tool is portable
|
|
53
|
-
* and stays inside the sandbox.
|
|
54
|
-
*
|
|
55
|
-
* This script lives at: <root>/tools/scripts/tf-tool-set.js
|
|
56
|
-
* so the root is two directories up from __dirname:
|
|
57
|
-
* scripts -> tools -> <root>
|
|
58
|
-
*/
|
|
59
|
-
const ROOT = path.resolve(__dirname, '..', '..');
|
|
53
|
+
const ROOT = HOME;
|
|
60
54
|
const REGISTER_PATH = path.join(ROOT, 'tools', 'tools.register.json');
|
|
61
55
|
const STATE_PATH = path.join(ROOT, 'tools', 'tools.state.json');
|
|
62
56
|
const SCRIPTS_ROOT = path.join(ROOT, 'tools', 'scripts');
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"name": "tf_tool_add",
|
|
85
85
|
"summary": "Register a new tool entry in the gateway tool register.",
|
|
86
86
|
"category": "management",
|
|
87
|
-
"instructions": "Add a register entry. Args: { id, name, summary?, category?, instructions?, invoke:{type:'script',path:'scripts/<file>'}|{type:'shell',command}, scriptText? }. If scriptText is given for a script invoke, the body is written to tools/scripts/<basename> first. Change is LIVE (no reconnect). Fails if id already exists.",
|
|
87
|
+
"instructions": "Add a register entry. Args: { id, name, summary?, category?, instructions?, inputSchema?, invoke:{type:'script',path:'scripts/<file>'}|{type:'shell',command}, scriptText? }. inputSchema is a JSON Schema object for the tool's args - a HOT-promoted tool advertises it verbatim in the top-level tools/list, so author it if the tool may be promoted. If scriptText is given for a script invoke, the body is written to tools/scripts/<basename> first. Change is LIVE (no reconnect). Fails if id already exists.",
|
|
88
88
|
"invoke": {
|
|
89
89
|
"type": "script",
|
|
90
90
|
"path": "scripts/tf-tool-add.js"
|
|
@@ -166,6 +166,51 @@
|
|
|
166
166
|
"type": "script",
|
|
167
167
|
"path": "scripts/tf-log.js"
|
|
168
168
|
}
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"id": "tf_pack",
|
|
172
|
+
"name": "tf_pack",
|
|
173
|
+
"summary": "Package the LIVE setup into a deployment-ready artifact under <home>/dist/ (portable config home, or a publishable npm package wrapping toolfunnel).",
|
|
174
|
+
"category": "management",
|
|
175
|
+
"instructions": "Args: { format?:'home'|'npm' (default 'home'), name?, version?, description?, out?, force? }. Snapshots the current setup - tools + scripts, hot/hidden curation (tools.state.json), upstream references (mcp/expose.json), policy hooks (THE GATE TRAVELS), and identity (toolfunnel.json, rewritten with the pack's name/version; a declared `requires` travels too) - into <home>/dist/<out||name>/, a SEPARATE location from the live config. format 'home' = a portable config home (zip / git init / run via --config-dir). format 'npm' = a publishable package: generated package.json with toolfunnel as a caret DEPENDENCY (depend, never copy), a 2-line bin launcher pointing --config-dir at the bundled home/, and a README stub - then `cd dist/<name> && npm publish`. NEVER ships auth/ or logs/. Refuses a non-empty destination without force:true. out is a folder NAME under dist/, never a path.",
|
|
176
|
+
"inputSchema": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"properties": {
|
|
179
|
+
"format": {
|
|
180
|
+
"type": "string",
|
|
181
|
+
"enum": [
|
|
182
|
+
"home",
|
|
183
|
+
"npm"
|
|
184
|
+
],
|
|
185
|
+
"description": "Artifact type: portable config home (default) or publishable npm package."
|
|
186
|
+
},
|
|
187
|
+
"name": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "Pack/package name (default: the home's serverName)."
|
|
190
|
+
},
|
|
191
|
+
"version": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "Pack version (default: the home's serverVersion, else 0.1.0)."
|
|
194
|
+
},
|
|
195
|
+
"description": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"description": "npm format: the package.json description."
|
|
198
|
+
},
|
|
199
|
+
"out": {
|
|
200
|
+
"type": "string",
|
|
201
|
+
"description": "Folder NAME under <home>/dist/ (default: name)."
|
|
202
|
+
},
|
|
203
|
+
"force": {
|
|
204
|
+
"type": "boolean",
|
|
205
|
+
"description": "Overwrite files in an existing non-empty destination."
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
"additionalProperties": false
|
|
209
|
+
},
|
|
210
|
+
"invoke": {
|
|
211
|
+
"type": "script",
|
|
212
|
+
"path": "scripts/tf-pack.js"
|
|
213
|
+
}
|
|
169
214
|
}
|
|
170
215
|
]
|
|
171
216
|
}
|