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.
- package/README.md +238 -235
- package/SECURITY.md +84 -84
- package/bin/toolfunnel.js +530 -451
- package/docs/KNOWN_BUGS.md +100 -80
- package/docs/MANUAL.pdf +0 -0
- package/docs/design.md +84 -84
- package/docs/packaging.md +116 -115
- package/hooks/hooks.manifest.json +27 -27
- package/hooks/scripts/example-audit.js +21 -21
- package/hooks/scripts/example-deny-dangerous.js +39 -39
- package/mcp/expose.example.json +30 -30
- package/mcp/servers/mock-modern/server.js +147 -147
- package/mcp/servers/mock-upstream/server.js +379 -380
- package/package.json +67 -67
- package/src/auth/config.js +172 -174
- package/src/auth/install.js +181 -181
- package/src/auth/resource-server.js +358 -353
- package/src/core/atomic-write.js +97 -0
- package/src/core/child-reaper.js +61 -61
- package/src/core/config-home.js +226 -134
- package/src/core/events.js +105 -105
- package/src/core/hook-engine.js +304 -329
- package/src/core/hook-loader.js +766 -824
- package/src/core/hook-runner.js +615 -566
- package/src/core/logger.js +295 -297
- package/src/core/matcher.js +100 -100
- package/src/core/metrics.js +90 -75
- package/src/core/output-cap.js +31 -0
- package/src/core/rename-migration.js +178 -163
- package/src/core/requires.js +138 -138
- package/src/core/server-config.js +98 -98
- package/src/extend/add-hook.md +155 -152
- package/src/extend/add-mcp.md +119 -119
- package/src/extend/configure.md +88 -88
- package/src/extend/create-tool.md +168 -168
- package/src/extend/howto.js +102 -102
- package/src/extend/package.md +190 -190
- package/src/extend/wrap.md +99 -99
- package/src/mcp/aggregator.js +2248 -1674
- package/src/mcp/expose-store.js +755 -577
- package/src/mcp/gated-run.js +210 -210
- package/src/mcp/http-transport.js +1657 -1563
- package/src/mcp/mcp-client.js +1174 -1070
- package/src/mcp/modern.js +409 -406
- package/src/mcp/protocol.js +473 -468
- package/src/mcp/server.js +3317 -3112
- package/src/mcp/tool-approvals.js +396 -0
- package/src/tools/registry.js +607 -661
- package/src/tools/tool-state.js +170 -170
- package/src/ui/public/app.js +1246 -1187
- package/src/ui/public/index.html +286 -286
- package/src/ui/public/styles.css +535 -535
- package/src/ui/server.js +1742 -1616
- 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-accept-change.js +67 -0
- package/tools/scripts/tf-env.js +43 -43
- package/tools/scripts/tf-hook-add.js +151 -151
- package/tools/scripts/tf-hook-set.js +120 -120
- package/tools/scripts/tf-list.js +192 -192
- package/tools/scripts/tf-log.js +86 -86
- package/tools/scripts/tf-mcp-add.js +130 -117
- package/tools/scripts/tf-mcp-set.js +121 -121
- package/tools/scripts/tf-pack.js +237 -237
- package/tools/scripts/tf-tool-add.js +134 -134
- package/tools/scripts/tf-tool-set.js +142 -142
- package/tools/scripts/tf-wrap.js +103 -103
- package/tools/scripts/uuid.js +102 -102
- package/tools/tools.register.json +16 -5
package/src/mcp/protocol.js
CHANGED
|
@@ -1,468 +1,473 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* protocol.js - the lean meta-tool protocol.
|
|
5
|
-
*
|
|
6
|
-
* This is the PURE dispatch core for the gateway's MCP server. It knows nothing about the
|
|
7
|
-
* transport (no stdio, no SDK, no host, no filesystem) - every capability it needs
|
|
8
|
-
* is INJECTED as a dependency so the whole thing unit-tests under `node:test` with plain
|
|
9
|
-
* stub objects. `server.js` (the thin JSON-RPC 2.0 / stdio wrapper) is the only thing that
|
|
10
|
-
* binds this to a transport.
|
|
11
|
-
*
|
|
12
|
-
* The model's tool surface is kept to exactly FOUR meta-tools:
|
|
13
|
-
*
|
|
14
|
-
* | Tool | Args | Returns |
|
|
15
|
-
* |------------------------------|----------------------|--------------------------------------|
|
|
16
|
-
* | toolfunnel_list_tools | { filter?, category?}| [{ name, summary, category }] |
|
|
17
|
-
* | toolfunnel_tool_instructions | { name } | full usage/docs for one tool |
|
|
18
|
-
* | toolfunnel_run_tool | { name, args } | tool result { ok, output, error? } |
|
|
19
|
-
* | toolfunnel_howto | { topic } | self-extension instructions |
|
|
20
|
-
*
|
|
21
|
-
* The load-bearing one is `toolfunnel_run_tool`: it does **NOT** execute directly. It resolves
|
|
22
|
-
* the tool's executor from the register, then hands it to `gatedRun`, which fires the host's
|
|
23
|
-
* PreToolUse hooks (the gate - may DENY), executes ONLY if allowed, then fires PostToolUse
|
|
24
|
-
* (PreToolUse blocks via `hookSpecificOutput.permissionDecision`). This module therefore NEVER
|
|
25
|
-
* calls a tool's `execute` itself - it always goes through the gate. That invariant is the whole
|
|
26
|
-
* safety case.
|
|
27
|
-
*
|
|
28
|
-
* Defensive by construction: an unknown meta-tool, a malformed argument, or a thrown
|
|
29
|
-
* dependency all resolve to a clear *error result*. `dispatch()` never throws.
|
|
30
|
-
*
|
|
31
|
-
* Injected dependencies (makeProtocol({ ... })):
|
|
32
|
-
* - registry : the dynamic tool register (src/tools/registry.js). Used:
|
|
33
|
-
* registry.list({ filter, category }) -> [{ name, summary, category }]
|
|
34
|
-
* registry.instructions(name) -> full docs (string or structured)
|
|
35
|
-
* registry.resolveExecution(name, args) -> { execute, toolName?, args?, ... }
|
|
36
|
-
* where `execute` is a () => any|Promise<any>
|
|
37
|
-
* bound to run the actual tool.
|
|
38
|
-
* - gatedRun : async ({ engine, ctx, toolName, args, execute }) -> { ok, output, error? }
|
|
39
|
-
* fires PreToolUse (gate), runs `execute` iff allowed, fires PostToolUse.
|
|
40
|
-
* - engine : the HookEngine (passed straight through to gatedRun; not used directly here).
|
|
41
|
-
* - ctx : the session context object (session_id, cwd, transcript_path, ...) passed to gatedRun.
|
|
42
|
-
* - howto : (topic) -> self-extension instructions for create-tool|add-mcp|add-hook|package.
|
|
43
|
-
*
|
|
44
|
-
* CommonJS only. Node built-ins only. No transport. No SDK.
|
|
45
|
-
*/
|
|
46
|
-
|
|
47
|
-
// Activity log. Self-gating (no-op unless explicitly enabled) and contracted never to
|
|
48
|
-
// throw, so emit points cannot alter dispatch behaviour or break a tool run.
|
|
49
|
-
const logger = require('../core/logger');
|
|
50
|
-
|
|
51
|
-
/** The four meta-tool names - the entire exposed surface. */
|
|
52
|
-
const META_TOOLS = Object.freeze({
|
|
53
|
-
LIST: 'toolfunnel_list_tools',
|
|
54
|
-
INSTRUCTIONS: 'toolfunnel_tool_instructions',
|
|
55
|
-
RUN: 'toolfunnel_run_tool',
|
|
56
|
-
HOWTO: 'toolfunnel_howto',
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
/** Valid `toolfunnel_howto` topics. */
|
|
60
|
-
const HOWTO_TOPICS = Object.freeze(['create-tool', 'add-mcp', 'add-hook', 'package', 'wrap', 'configure']);
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Build a normalized error result. Meta-tool calls always resolve to a result object
|
|
64
|
-
* (never throw), so the server can surface a clean message to the model instead of a
|
|
65
|
-
* crashed JSON-RPC call.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} message human/model-facing error text
|
|
68
|
-
* @returns {{ ok: false, output: null, error: string }}
|
|
69
|
-
*/
|
|
70
|
-
function errorResult(message) {
|
|
71
|
-
return { ok: false, output: null, error: String(message) };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Build a normalized success result.
|
|
76
|
-
*
|
|
77
|
-
* @param {*} output the payload to return to the model
|
|
78
|
-
* @returns {{ ok: true, output: * }}
|
|
79
|
-
*/
|
|
80
|
-
function okResult(output) {
|
|
81
|
-
return { ok: true, output };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Normalize whatever a dependency returns into the canonical { ok, output, error? } shape.
|
|
86
|
-
* `gatedRun` is contracted to already return this shape - if it does, pass it through
|
|
87
|
-
* untouched (preserving any extra fields like a block reason). Anything else is wrapped
|
|
88
|
-
* as a success payload.
|
|
89
|
-
*
|
|
90
|
-
* @param {*} value
|
|
91
|
-
* @returns {{ ok: boolean, output?: *, error?: string }}
|
|
92
|
-
*/
|
|
93
|
-
function normalizeResult(value) {
|
|
94
|
-
if (value && typeof value === 'object' && typeof value.ok === 'boolean') {
|
|
95
|
-
return value; // already in canonical form (e.g. straight from gatedRun)
|
|
96
|
-
}
|
|
97
|
-
return okResult(value);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* makeProtocol - construct the meta-tool dispatcher.
|
|
102
|
-
*
|
|
103
|
-
* @param {object} deps
|
|
104
|
-
* @param {object} deps.registry dynamic tool register (list/instructions/resolveExecution)
|
|
105
|
-
* @param {(o: { engine: object, ctx: object, toolName: string, args: object,
|
|
106
|
-
* execute: () => (any|Promise<any>) }) => Promise<{ok:boolean, output?:any, error?:string}>}
|
|
107
|
-
* deps.gatedRun the hook-gated executor (PreToolUse -> execute -> PostToolUse)
|
|
108
|
-
* @param {object} [deps.engine] the HookEngine, forwarded to gatedRun
|
|
109
|
-
* @param {object} [deps.ctx] session context, forwarded to gatedRun
|
|
110
|
-
* @param {(topic: string) => (string|object)} deps.howto self-extension instruction provider
|
|
111
|
-
* @returns {{
|
|
112
|
-
* META_TOOLS: object,
|
|
113
|
-
* HOWTO_TOPICS: string[],
|
|
114
|
-
* toolDefinitions: () => Array<{name:string, description:string, inputSchema:object}>,
|
|
115
|
-
* dispatch: (toolName: string, args?: object) => Promise<{ok:boolean, output?:any, error?:string}>,
|
|
116
|
-
* listTools: (args?: object) => Promise<object>,
|
|
117
|
-
* toolInstructions: (args?: object) => Promise<object>,
|
|
118
|
-
* runTool: (args?: object) => Promise<object>,
|
|
119
|
-
* howtoTool: (args?: object) => Promise<object>
|
|
120
|
-
* }}
|
|
121
|
-
*/
|
|
122
|
-
function makeProtocol(deps) {
|
|
123
|
-
const d = deps || {};
|
|
124
|
-
const registry = d.registry;
|
|
125
|
-
const gatedRun = d.gatedRun;
|
|
126
|
-
const engine = d.engine;
|
|
127
|
-
const ctx = d.ctx;
|
|
128
|
-
const howto = d.howto;
|
|
129
|
-
|
|
130
|
-
// ---- toolfunnel_list_tools ----------------------------------------------
|
|
131
|
-
// Return the register, briefs only. Filtering/categorisation is the register's job
|
|
132
|
-
// (it owns the data); we just forward the optional filter/category through.
|
|
133
|
-
async function listTools(args) {
|
|
134
|
-
if (!registry || typeof registry.list !== 'function') {
|
|
135
|
-
return errorResult('toolfunnel_list_tools: registry.list is unavailable');
|
|
136
|
-
}
|
|
137
|
-
const a = args || {};
|
|
138
|
-
const opts = {};
|
|
139
|
-
if (a.filter !== undefined) opts.filter = a.filter;
|
|
140
|
-
if (a.category !== undefined) opts.category = a.category;
|
|
141
|
-
try {
|
|
142
|
-
const list = await registry.list(opts);
|
|
143
|
-
return okResult(list);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
return errorResult(`toolfunnel_list_tools failed: ${(err && err.message) || err}`);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ---- toolfunnel_tool_instructions ---------------------------------------
|
|
150
|
-
// Full usage/docs for ONE register tool, on demand (long-tail friendly).
|
|
151
|
-
async function toolInstructions(args) {
|
|
152
|
-
if (!registry || typeof registry.instructions !== 'function') {
|
|
153
|
-
return errorResult('toolfunnel_tool_instructions: registry.instructions is unavailable');
|
|
154
|
-
}
|
|
155
|
-
const a = args || {};
|
|
156
|
-
const name = a.name;
|
|
157
|
-
if (typeof name !== 'string' || name.length === 0) {
|
|
158
|
-
return errorResult('toolfunnel_tool_instructions: "name" (non-empty string) is required');
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
const docs = await registry.instructions(name);
|
|
162
|
-
if (docs === undefined || docs === null) {
|
|
163
|
-
return errorResult(`toolfunnel_tool_instructions: no tool named "${name}"`
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
//
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
pre =
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
case META_TOOLS.
|
|
354
|
-
return await
|
|
355
|
-
case META_TOOLS.
|
|
356
|
-
return await
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
},
|
|
423
|
-
{
|
|
424
|
-
name: META_TOOLS.
|
|
425
|
-
description:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* protocol.js - the lean meta-tool protocol.
|
|
5
|
+
*
|
|
6
|
+
* This is the PURE dispatch core for the gateway's MCP server. It knows nothing about the
|
|
7
|
+
* transport (no stdio, no SDK, no host, no filesystem) - every capability it needs
|
|
8
|
+
* is INJECTED as a dependency so the whole thing unit-tests under `node:test` with plain
|
|
9
|
+
* stub objects. `server.js` (the thin JSON-RPC 2.0 / stdio wrapper) is the only thing that
|
|
10
|
+
* binds this to a transport.
|
|
11
|
+
*
|
|
12
|
+
* The model's tool surface is kept to exactly FOUR meta-tools:
|
|
13
|
+
*
|
|
14
|
+
* | Tool | Args | Returns |
|
|
15
|
+
* |------------------------------|----------------------|--------------------------------------|
|
|
16
|
+
* | toolfunnel_list_tools | { filter?, category?}| [{ name, summary, category }] |
|
|
17
|
+
* | toolfunnel_tool_instructions | { name } | full usage/docs for one tool |
|
|
18
|
+
* | toolfunnel_run_tool | { name, args } | tool result { ok, output, error? } |
|
|
19
|
+
* | toolfunnel_howto | { topic } | self-extension instructions |
|
|
20
|
+
*
|
|
21
|
+
* The load-bearing one is `toolfunnel_run_tool`: it does **NOT** execute directly. It resolves
|
|
22
|
+
* the tool's executor from the register, then hands it to `gatedRun`, which fires the host's
|
|
23
|
+
* PreToolUse hooks (the gate - may DENY), executes ONLY if allowed, then fires PostToolUse
|
|
24
|
+
* (PreToolUse blocks via `hookSpecificOutput.permissionDecision`). This module therefore NEVER
|
|
25
|
+
* calls a tool's `execute` itself - it always goes through the gate. That invariant is the whole
|
|
26
|
+
* safety case.
|
|
27
|
+
*
|
|
28
|
+
* Defensive by construction: an unknown meta-tool, a malformed argument, or a thrown
|
|
29
|
+
* dependency all resolve to a clear *error result*. `dispatch()` never throws.
|
|
30
|
+
*
|
|
31
|
+
* Injected dependencies (makeProtocol({ ... })):
|
|
32
|
+
* - registry : the dynamic tool register (src/tools/registry.js). Used:
|
|
33
|
+
* registry.list({ filter, category }) -> [{ name, summary, category }]
|
|
34
|
+
* registry.instructions(name) -> full docs (string or structured)
|
|
35
|
+
* registry.resolveExecution(name, args) -> { execute, toolName?, args?, ... }
|
|
36
|
+
* where `execute` is a () => any|Promise<any>
|
|
37
|
+
* bound to run the actual tool.
|
|
38
|
+
* - gatedRun : async ({ engine, ctx, toolName, args, execute }) -> { ok, output, error? }
|
|
39
|
+
* fires PreToolUse (gate), runs `execute` iff allowed, fires PostToolUse.
|
|
40
|
+
* - engine : the HookEngine (passed straight through to gatedRun; not used directly here).
|
|
41
|
+
* - ctx : the session context object (session_id, cwd, transcript_path, ...) passed to gatedRun.
|
|
42
|
+
* - howto : (topic) -> self-extension instructions for create-tool|add-mcp|add-hook|package.
|
|
43
|
+
*
|
|
44
|
+
* CommonJS only. Node built-ins only. No transport. No SDK.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// Activity log. Self-gating (no-op unless explicitly enabled) and contracted never to
|
|
48
|
+
// throw, so emit points cannot alter dispatch behaviour or break a tool run.
|
|
49
|
+
const logger = require('../core/logger');
|
|
50
|
+
|
|
51
|
+
/** The four meta-tool names - the entire exposed surface. */
|
|
52
|
+
const META_TOOLS = Object.freeze({
|
|
53
|
+
LIST: 'toolfunnel_list_tools',
|
|
54
|
+
INSTRUCTIONS: 'toolfunnel_tool_instructions',
|
|
55
|
+
RUN: 'toolfunnel_run_tool',
|
|
56
|
+
HOWTO: 'toolfunnel_howto',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/** Valid `toolfunnel_howto` topics. */
|
|
60
|
+
const HOWTO_TOPICS = Object.freeze(['create-tool', 'add-mcp', 'add-hook', 'package', 'wrap', 'configure']);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a normalized error result. Meta-tool calls always resolve to a result object
|
|
64
|
+
* (never throw), so the server can surface a clean message to the model instead of a
|
|
65
|
+
* crashed JSON-RPC call.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} message human/model-facing error text
|
|
68
|
+
* @returns {{ ok: false, output: null, error: string }}
|
|
69
|
+
*/
|
|
70
|
+
function errorResult(message) {
|
|
71
|
+
return { ok: false, output: null, error: String(message) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build a normalized success result.
|
|
76
|
+
*
|
|
77
|
+
* @param {*} output the payload to return to the model
|
|
78
|
+
* @returns {{ ok: true, output: * }}
|
|
79
|
+
*/
|
|
80
|
+
function okResult(output) {
|
|
81
|
+
return { ok: true, output };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalize whatever a dependency returns into the canonical { ok, output, error? } shape.
|
|
86
|
+
* `gatedRun` is contracted to already return this shape - if it does, pass it through
|
|
87
|
+
* untouched (preserving any extra fields like a block reason). Anything else is wrapped
|
|
88
|
+
* as a success payload.
|
|
89
|
+
*
|
|
90
|
+
* @param {*} value
|
|
91
|
+
* @returns {{ ok: boolean, output?: *, error?: string }}
|
|
92
|
+
*/
|
|
93
|
+
function normalizeResult(value) {
|
|
94
|
+
if (value && typeof value === 'object' && typeof value.ok === 'boolean') {
|
|
95
|
+
return value; // already in canonical form (e.g. straight from gatedRun)
|
|
96
|
+
}
|
|
97
|
+
return okResult(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* makeProtocol - construct the meta-tool dispatcher.
|
|
102
|
+
*
|
|
103
|
+
* @param {object} deps
|
|
104
|
+
* @param {object} deps.registry dynamic tool register (list/instructions/resolveExecution)
|
|
105
|
+
* @param {(o: { engine: object, ctx: object, toolName: string, args: object,
|
|
106
|
+
* execute: () => (any|Promise<any>) }) => Promise<{ok:boolean, output?:any, error?:string}>}
|
|
107
|
+
* deps.gatedRun the hook-gated executor (PreToolUse -> execute -> PostToolUse)
|
|
108
|
+
* @param {object} [deps.engine] the HookEngine, forwarded to gatedRun
|
|
109
|
+
* @param {object} [deps.ctx] session context, forwarded to gatedRun
|
|
110
|
+
* @param {(topic: string) => (string|object)} deps.howto self-extension instruction provider
|
|
111
|
+
* @returns {{
|
|
112
|
+
* META_TOOLS: object,
|
|
113
|
+
* HOWTO_TOPICS: string[],
|
|
114
|
+
* toolDefinitions: () => Array<{name:string, description:string, inputSchema:object}>,
|
|
115
|
+
* dispatch: (toolName: string, args?: object) => Promise<{ok:boolean, output?:any, error?:string}>,
|
|
116
|
+
* listTools: (args?: object) => Promise<object>,
|
|
117
|
+
* toolInstructions: (args?: object) => Promise<object>,
|
|
118
|
+
* runTool: (args?: object) => Promise<object>,
|
|
119
|
+
* howtoTool: (args?: object) => Promise<object>
|
|
120
|
+
* }}
|
|
121
|
+
*/
|
|
122
|
+
function makeProtocol(deps) {
|
|
123
|
+
const d = deps || {};
|
|
124
|
+
const registry = d.registry;
|
|
125
|
+
const gatedRun = d.gatedRun;
|
|
126
|
+
const engine = d.engine;
|
|
127
|
+
const ctx = d.ctx;
|
|
128
|
+
const howto = d.howto;
|
|
129
|
+
|
|
130
|
+
// ---- toolfunnel_list_tools ----------------------------------------------
|
|
131
|
+
// Return the register, briefs only. Filtering/categorisation is the register's job
|
|
132
|
+
// (it owns the data); we just forward the optional filter/category through.
|
|
133
|
+
async function listTools(args) {
|
|
134
|
+
if (!registry || typeof registry.list !== 'function') {
|
|
135
|
+
return errorResult('toolfunnel_list_tools: registry.list is unavailable');
|
|
136
|
+
}
|
|
137
|
+
const a = args || {};
|
|
138
|
+
const opts = {};
|
|
139
|
+
if (a.filter !== undefined) opts.filter = a.filter;
|
|
140
|
+
if (a.category !== undefined) opts.category = a.category;
|
|
141
|
+
try {
|
|
142
|
+
const list = await registry.list(opts);
|
|
143
|
+
return okResult(list);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return errorResult(`toolfunnel_list_tools failed: ${(err && err.message) || err}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- toolfunnel_tool_instructions ---------------------------------------
|
|
150
|
+
// Full usage/docs for ONE register tool, on demand (long-tail friendly).
|
|
151
|
+
async function toolInstructions(args) {
|
|
152
|
+
if (!registry || typeof registry.instructions !== 'function') {
|
|
153
|
+
return errorResult('toolfunnel_tool_instructions: registry.instructions is unavailable');
|
|
154
|
+
}
|
|
155
|
+
const a = args || {};
|
|
156
|
+
const name = a.name;
|
|
157
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
158
|
+
return errorResult('toolfunnel_tool_instructions: "name" (non-empty string) is required');
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const docs = await registry.instructions(name);
|
|
162
|
+
if (docs === undefined || docs === null) {
|
|
163
|
+
return errorResult(`toolfunnel_tool_instructions: no tool named "${name}" - call toolfunnel_list_tools ` +
|
|
164
|
+
'(a filter narrows it) to find the right name.');
|
|
165
|
+
}
|
|
166
|
+
return okResult(docs);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return errorResult(`toolfunnel_tool_instructions failed for "${name}": ${(err && err.message) || err}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---- toolfunnel_run_tool ------------------------------------------------
|
|
173
|
+
// THE load-bearing meta-tool. Resolve the executor from the register, then run it
|
|
174
|
+
// ONLY through gatedRun (PreToolUse gate -> execute -> PostToolUse). This function
|
|
175
|
+
// must NEVER call `execute` itself - that would bypass the gate and break the entire
|
|
176
|
+
// safety case.
|
|
177
|
+
async function runTool(args, callOpts) {
|
|
178
|
+
const a = args || {};
|
|
179
|
+
const name = a.name;
|
|
180
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
181
|
+
return errorResult('toolfunnel_run_tool: "name" (non-empty string) is required');
|
|
182
|
+
}
|
|
183
|
+
if (!registry || typeof registry.resolveExecution !== 'function') {
|
|
184
|
+
return errorResult('toolfunnel_run_tool: registry.resolveExecution is unavailable');
|
|
185
|
+
}
|
|
186
|
+
if (typeof gatedRun !== 'function') {
|
|
187
|
+
return errorResult('toolfunnel_run_tool: gatedRun is unavailable - refusing to execute ungated');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The tool's input args (the model's payload for the underlying tool).
|
|
191
|
+
const toolArgs = a.args !== undefined && a.args !== null ? a.args : {};
|
|
192
|
+
|
|
193
|
+
// 1) Resolve how to execute this tool. The register turns (name, args) into a bound
|
|
194
|
+
// executor. A null/undefined resolution means "no such tool" - a clean error, not a throw.
|
|
195
|
+
let resolution;
|
|
196
|
+
try {
|
|
197
|
+
// callOpts { meta, progress } reach an upstream tool's forward; a local register
|
|
198
|
+
// tool ignores them (a script has no progress channel).
|
|
199
|
+
resolution = await registry.resolveExecution(name, toolArgs, callOpts);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return errorResult(`toolfunnel_run_tool: could not resolve "${name}": ${(err && err.message) || err}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 1a) REFERENCE mode. A reference tool executes NOTHING here - the connected AI performs the
|
|
205
|
+
// action in its OWN environment per the instructions, so we NEVER spawn and NEVER call
|
|
206
|
+
// gatedRun. The HANDOFF itself, however, is gated: reference fires PreToolUse ADVISORILY -
|
|
207
|
+
// a deny gates the instructions HANDOFF, not the AI's own-environment execution. A
|
|
208
|
+
// PreToolUse deny therefore REFUSES to hand over the instructions (a handoff-gate); allow /
|
|
209
|
+
// no-engine / no-ctx hands them over exactly as before. Nothing runs here in EITHER case.
|
|
210
|
+
// Gateway tools fall through to the gate below, byte-for-byte unchanged.
|
|
211
|
+
if (resolution && resolution.mode === 'reference') {
|
|
212
|
+
const refInstructions =
|
|
213
|
+
typeof resolution.instructions === 'string' ? resolution.instructions : '';
|
|
214
|
+
// The gate name is the SAME one the gateway path uses for this tool, so a PreToolUse matcher
|
|
215
|
+
// sees a stable tool_name regardless of mode.
|
|
216
|
+
const gateName = typeof resolution.toolName === 'string' ? resolution.toolName : name;
|
|
217
|
+
|
|
218
|
+
// The unconditional handoff - also the SAFE fallback when there is no engine/ctx to gate with.
|
|
219
|
+
const handoff = (additionalContext) => {
|
|
220
|
+
logger.log({ type: 'tool', name, mode: 'reference', ok: true, blocked: false });
|
|
221
|
+
const out = {
|
|
222
|
+
ok: true,
|
|
223
|
+
mode: 'reference',
|
|
224
|
+
name,
|
|
225
|
+
instructions: refInstructions,
|
|
226
|
+
message: 'reference tool - perform this in your own environment per the instructions',
|
|
227
|
+
};
|
|
228
|
+
if (typeof additionalContext === 'string' && additionalContext.length > 0) {
|
|
229
|
+
out.additionalContext = additionalContext;
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// No engine / no ctx -> cannot gate; hand the instructions over exactly as before.
|
|
235
|
+
if (!engine || typeof engine.fire !== 'function' || !ctx) {
|
|
236
|
+
return handoff();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fire PreToolUse the same way gatedRun does: engine.fire('PreToolUse', ctx,
|
|
240
|
+
// { tool_name, tool_input }). The engine has already normalised any deny into
|
|
241
|
+
// { blocked, reason } and any injected additionalContext into { injected }.
|
|
242
|
+
let pre = null;
|
|
243
|
+
try {
|
|
244
|
+
pre = await engine.fire('PreToolUse', ctx, { tool_name: gateName, tool_input: toolArgs });
|
|
245
|
+
} catch (_err) {
|
|
246
|
+
// A misbehaving engine must not break the advisory handoff - fall back to handing over.
|
|
247
|
+
pre = null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (pre && pre.blocked === true) {
|
|
251
|
+
// DENY -> refuse the HANDOFF. No instructions, no spawn.
|
|
252
|
+
logger.log({ type: 'tool', name, mode: 'reference', ok: false, blocked: true });
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
blocked: true,
|
|
256
|
+
mode: 'reference',
|
|
257
|
+
name,
|
|
258
|
+
reason: pre.reason != null ? pre.reason : 'blocked by PreToolUse',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Allowed -> hand over, carrying any hook-injected additionalContext.
|
|
263
|
+
return handoff(pre && typeof pre.injected === 'string' ? pre.injected : '');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!resolution || typeof resolution.execute !== 'function') {
|
|
267
|
+
return errorResult(
|
|
268
|
+
`toolfunnel_run_tool: tool "${name}" is not runnable (no executable resolution from the register) - ` +
|
|
269
|
+
'check the name with toolfunnel_list_tools; an upstream tool is missing while its server is disconnected or disabled.'
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Prefer the register's canonical toolName/args (it may normalize them); fall back to
|
|
274
|
+
// what the caller gave. The toolName is what PreToolUse matchers see - it must be stable.
|
|
275
|
+
const toolName = typeof resolution.toolName === 'string' ? resolution.toolName : name;
|
|
276
|
+
const execArgs = resolution.args !== undefined ? resolution.args : toolArgs;
|
|
277
|
+
|
|
278
|
+
// The execution mode for logging - the resolution's own mode if it set one, else
|
|
279
|
+
// the default gateway path.
|
|
280
|
+
const mode = typeof resolution.mode === 'string' ? resolution.mode : 'gateway';
|
|
281
|
+
|
|
282
|
+
// 2) Execute ONLY through the gate. gatedRun fires PreToolUse (may deny), runs execute
|
|
283
|
+
// iff allowed, then fires PostToolUse, and returns the canonical { ok, output, error? }.
|
|
284
|
+
const startedAt = Date.now();
|
|
285
|
+
try {
|
|
286
|
+
const result = await gatedRun({
|
|
287
|
+
engine,
|
|
288
|
+
ctx,
|
|
289
|
+
toolName,
|
|
290
|
+
args: execArgs,
|
|
291
|
+
execute: resolution.execute,
|
|
292
|
+
});
|
|
293
|
+
const normalized = normalizeResult(result);
|
|
294
|
+
// Activity log: record the run outcome. Self-gates on enabled; never throws.
|
|
295
|
+
logger.log({
|
|
296
|
+
type: 'tool',
|
|
297
|
+
name,
|
|
298
|
+
mode,
|
|
299
|
+
ok: normalized.ok === true,
|
|
300
|
+
blocked: normalized.blocked === true,
|
|
301
|
+
durationMs: Date.now() - startedAt,
|
|
302
|
+
});
|
|
303
|
+
return normalized;
|
|
304
|
+
} catch (err) {
|
|
305
|
+
// gatedRun is contracted not to throw, but if it does we still return a clean result.
|
|
306
|
+
logger.log({
|
|
307
|
+
type: 'tool',
|
|
308
|
+
name,
|
|
309
|
+
mode,
|
|
310
|
+
ok: false,
|
|
311
|
+
blocked: false,
|
|
312
|
+
durationMs: Date.now() - startedAt,
|
|
313
|
+
});
|
|
314
|
+
return errorResult(`toolfunnel_run_tool: gated execution of "${name}" failed: ${(err && err.message) || err}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---- toolfunnel_howto ---------------------------------------------------
|
|
319
|
+
// Self-extension instructions for a known topic (makes the system self-extending).
|
|
320
|
+
async function howtoTool(args) {
|
|
321
|
+
if (typeof howto !== 'function') {
|
|
322
|
+
return errorResult('toolfunnel_howto: howto provider is unavailable');
|
|
323
|
+
}
|
|
324
|
+
const a = args || {};
|
|
325
|
+
const topic = a.topic;
|
|
326
|
+
if (typeof topic !== 'string' || topic.length === 0) {
|
|
327
|
+
return errorResult(
|
|
328
|
+
`toolfunnel_howto: "topic" is required (one of ${HOWTO_TOPICS.join(' | ')})`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (!HOWTO_TOPICS.includes(topic)) {
|
|
332
|
+
return errorResult(
|
|
333
|
+
`toolfunnel_howto: unknown topic "${topic}" (expected one of ${HOWTO_TOPICS.join(' | ')})`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const content = await howto(topic);
|
|
338
|
+
if (content === undefined || content === null) {
|
|
339
|
+
return errorResult(`toolfunnel_howto: no instructions available for topic "${topic}"`);
|
|
340
|
+
}
|
|
341
|
+
return okResult(content);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return errorResult(`toolfunnel_howto failed for "${topic}": ${(err && err.message) || err}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---- dispatch -----------------------------------------------------------
|
|
348
|
+
// The server's single entry point. Pure routing; never throws - an unknown tool or any
|
|
349
|
+
// internal failure becomes a clear error result (defensive by construction).
|
|
350
|
+
async function dispatch(toolName, args, callOpts) {
|
|
351
|
+
try {
|
|
352
|
+
switch (toolName) {
|
|
353
|
+
case META_TOOLS.LIST:
|
|
354
|
+
return await listTools(args);
|
|
355
|
+
case META_TOOLS.INSTRUCTIONS:
|
|
356
|
+
return await toolInstructions(args);
|
|
357
|
+
case META_TOOLS.RUN:
|
|
358
|
+
return await runTool(args, callOpts);
|
|
359
|
+
case META_TOOLS.HOWTO:
|
|
360
|
+
return await howtoTool(args);
|
|
361
|
+
default: {
|
|
362
|
+
const known = Object.values(META_TOOLS).join(', ');
|
|
363
|
+
return errorResult(
|
|
364
|
+
`Unknown tool "${toolName}". This MCP's own tools are: ${known}. Register and upstream tools are ` +
|
|
365
|
+
'listed by toolfunnel_list_tools and run with toolfunnel_run_tool.'
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
} catch (err) {
|
|
370
|
+
// Last line of defence: dispatch itself must never throw.
|
|
371
|
+
return errorResult(
|
|
372
|
+
`dispatch("${toolName}") failed unexpectedly: ${(err && err.message) || err}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---- toolDefinitions ----------------------------------------------------
|
|
378
|
+
// The JSON-Schema input shapes for the ADVERTISED meta-tools, for `tools/list`.
|
|
379
|
+
// All FOUR are advertised: list, instructions, run, howto. toolfunnel_run_tool executes
|
|
380
|
+
// a register tool ONLY through gatedRun (PreToolUse gate -> execute -> PostToolUse), so a
|
|
381
|
+
// connected agent can run tools while every call still passes through the gate - the gate,
|
|
382
|
+
// not the absence of the meta-tool, is the safety boundary.
|
|
383
|
+
function toolDefinitions() {
|
|
384
|
+
return [
|
|
385
|
+
{
|
|
386
|
+
name: META_TOOLS.LIST,
|
|
387
|
+
description:
|
|
388
|
+
'List the available tools (briefs only). Optionally filter by a free-text ' +
|
|
389
|
+
'string and/or a category. Returns [{ name, summary, category }]. ' +
|
|
390
|
+
'Call toolfunnel_tool_instructions for a specific tool, then run it yourself per its instructions.',
|
|
391
|
+
inputSchema: {
|
|
392
|
+
type: 'object',
|
|
393
|
+
properties: {
|
|
394
|
+
filter: {
|
|
395
|
+
type: 'string',
|
|
396
|
+
description: 'Optional free-text filter matched against tool name/summary.',
|
|
397
|
+
},
|
|
398
|
+
category: {
|
|
399
|
+
type: 'string',
|
|
400
|
+
description: 'Optional category to restrict the listing to.',
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
},
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: META_TOOLS.INSTRUCTIONS,
|
|
408
|
+
description:
|
|
409
|
+
'Get the full usage instructions / documentation for one tool by name (venv, the ' +
|
|
410
|
+
'reliable invoke command, safety notes). Read this before running the tool yourself.',
|
|
411
|
+
inputSchema: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
properties: {
|
|
414
|
+
name: {
|
|
415
|
+
type: 'string',
|
|
416
|
+
description: 'The exact tool name (as returned by toolfunnel_list_tools).',
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
required: ['name'],
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: META_TOOLS.RUN,
|
|
425
|
+
description: 'Execute one register tool by its name through the gateway PreToolUse gate. Args: { name, args? }. Call toolfunnel_tool_instructions first to learn the tool\'s arg shape. Returns { ok, output, error? }. Every call is gated (a PreToolUse hook may deny it).',
|
|
426
|
+
inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Exact tool name from toolfunnel_list_tools.' }, args: { type: 'object', description: 'Structured arguments forwarded to the tool. Optional.' } }, required: ['name'], additionalProperties: false },
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: META_TOOLS.HOWTO,
|
|
430
|
+
description:
|
|
431
|
+
'Get self-extension instructions: how to author a new tool, register an upstream ' +
|
|
432
|
+
'MCP, add a hook, build a shareable package, WRAP one MCP server as the entire ' +
|
|
433
|
+
'surface (transparent passthrough), or configure the gateway with JSON files only. ' +
|
|
434
|
+
'The system documents how to extend itself.',
|
|
435
|
+
inputSchema: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
properties: {
|
|
438
|
+
topic: {
|
|
439
|
+
type: 'string',
|
|
440
|
+
enum: HOWTO_TOPICS.slice(),
|
|
441
|
+
description:
|
|
442
|
+
'Which guide to return: create-tool | add-mcp | add-hook | package | wrap | configure.',
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
required: ['topic'],
|
|
446
|
+
additionalProperties: false,
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
META_TOOLS,
|
|
454
|
+
HOWTO_TOPICS,
|
|
455
|
+
toolDefinitions,
|
|
456
|
+
dispatch,
|
|
457
|
+
// Individual handlers exported for direct unit testing / reuse.
|
|
458
|
+
listTools,
|
|
459
|
+
toolInstructions,
|
|
460
|
+
runTool,
|
|
461
|
+
howtoTool,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
module.exports = {
|
|
466
|
+
makeProtocol,
|
|
467
|
+
META_TOOLS,
|
|
468
|
+
HOWTO_TOPICS,
|
|
469
|
+
// Exported for tests / reuse:
|
|
470
|
+
errorResult,
|
|
471
|
+
okResult,
|
|
472
|
+
normalizeResult,
|
|
473
|
+
};
|