toolfunnel 0.4.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 +102 -213
- package/bin/toolfunnel.js +372 -147
- package/docs/KNOWN_BUGS.md +99 -225
- 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 -981
- 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 -1322
- package/src/packages/packager.js +715 -715
- package/src/tools/drift.js +138 -138
- package/src/tools/registry.js +513 -511
- 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/docs/admin-ui.md +0 -404
- package/docs/configuration.md +0 -402
- package/docs/hooks-and-gating.md +0 -428
- package/docs/logging.md +0 -223
- package/docs/management.md +0 -429
- package/docs/oauth.md +0 -103
- package/docs/releasing.md +0 -31
- package/docs/setup.md +0 -352
- package/docs/tools-and-upstreams.md +0 -469
package/src/extend/package.md
CHANGED
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
# How to package and share units (self-extension)
|
|
2
|
-
|
|
3
|
-
This is the instruction served by `toolfunnel_howto({ topic: "package" })`. It explains how to bundle
|
|
4
|
-
tools, hooks, and MCP configs into a **shareable package** and how import/export work. Packaging is a
|
|
5
|
-
**byproduct** of the gateway design: because every unit is already structured and
|
|
6
|
-
self-documented, a package is just a folder that lists its units and carries their files.
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 0. The WHOLE-SETUP path
|
|
11
|
-
|
|
12
|
-
Since 0.4.0 the fastest way to ship is not per-unit packaging but the **whole setup**: your entire
|
|
13
|
-
config home (tools + scripts, hot/hidden curation, upstream references, policy hooks, identity) is
|
|
14
|
-
already the product. One call packages it into a deployment-ready artifact under `<home>/dist/`:
|
|
15
|
-
|
|
16
|
-
- `tf_pack { format: "home" }` → a **portable config home**. Zip it, git-init it, or run it
|
|
17
|
-
directly: `toolfunnel --config-dir <dir>`. The receiving gateway seeds nothing
|
|
18
|
-
config, including the hooks: **the gate travels**, enforcing your policy on the recipient's
|
|
19
|
-
machine regardless of which MCP client they use.
|
|
20
|
-
- `tf_pack { format: "npm", name: "my-mcp" }` → a **publishable npm package**: a generated
|
|
21
|
-
`package.json` that depends on `toolfunnel` (caret range
|
|
22
|
-
2-line bin launcher pointing `--config-dir` at the bundled home, and a README stub.
|
|
23
|
-
`cd dist/my-mcp && npm publish` → your users run `npx my-mcp` and get YOUR server, under YOUR
|
|
24
|
-
name (the bundled `toolfunnel.json` identity), with YOUR gate.
|
|
25
|
-
|
|
26
|
-
Declare the runtimes your tools need in the home's `toolfunnel.json`
|
|
27
|
-
and the receiving gateway warns at startup about anything missing:
|
|
28
|
-
|
|
29
|
-
```json
|
|
30
|
-
"requires": [ { "command": "python", "min": "3.10", "why": "the pdf tools" } ]
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Audit honesty (both directions): packs spawn commands. Tell your users to read your pack's
|
|
34
|
-
`expose.json`, `tools.register.json`, AND `toolfunnel.json` (its `requires` probes execute at
|
|
35
|
-
gateway startup
|
|
36
|
-
all three yourself in anything you install.
|
|
37
|
-
|
|
38
|
-
The per-UNIT packaging below remains the right shape for sharing a single tool/hook/MCP selection
|
|
39
|
-
rather than a whole gateway.
|
|
40
|
-
|
|
41
|
-
> Phase note: packages are Phase 2 (`packages/` directory). The structure below is what the importer
|
|
42
|
-
> and exporter target; defining it now means the units authored today are already package-ready.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## 1. What a unit is (and what travels with it)
|
|
47
|
-
|
|
48
|
-
Each unit is self-contained
|
|
49
|
-
|
|
50
|
-
| Unit | Its metadata | Its files |
|
|
51
|
-
|---|---|---|
|
|
52
|
-
| **tool** | one `tools.register.json` entry (`id/name/summary/category/instructions/invoke`) | the script under `tools/scripts/` (for `invoke.type === "script"`) |
|
|
53
|
-
| **hook** | one `hooks.manifest.json` entry (`id/event/matcher/command/script/timeout/description`) + its `hooks.state.json` enabled value | the script under `hooks/scripts/` |
|
|
54
|
-
| **mcp** | one `expose.json` `upstreams[]` entry + its `expose[]` selections | any vendored server files it spawns (must live inside the package) |
|
|
55
|
-
|
|
56
|
-
A package gathers one or more of these, plus the files they reference.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## 2. Package layout
|
|
61
|
-
|
|
62
|
-
A package is a folder under `packages/` containing a manifest and the units' files in a mirrored
|
|
63
|
-
tree so import is a structured copy, not guesswork:
|
|
64
|
-
|
|
65
|
-
```
|
|
66
|
-
packages/
|
|
67
|
-
notify-pack/
|
|
68
|
-
package.json # the package manifest (below)
|
|
69
|
-
tools/
|
|
70
|
-
scripts/
|
|
71
|
-
msg-digest.sh
|
|
72
|
-
hooks/
|
|
73
|
-
scripts/
|
|
74
|
-
rate-limit.sh
|
|
75
|
-
mcp/
|
|
76
|
-
vendor/ # any vendored upstream server files
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### `package.json`
|
|
80
|
-
|
|
81
|
-
A `package.json`-style manifest listing the package's identity and its units (the metadata blocks are
|
|
82
|
-
embedded so import is self-sufficient
|
|
83
|
-
entries):
|
|
84
|
-
|
|
85
|
-
```json
|
|
86
|
-
{
|
|
87
|
-
"name": "notify-pack",
|
|
88
|
-
"version": "1.0.0",
|
|
89
|
-
"description": "Message digest tool + rate-limit gate + upstream MCP exposure.",
|
|
90
|
-
"author": "unknown",
|
|
91
|
-
"units": {
|
|
92
|
-
"tools": [
|
|
93
|
-
{
|
|
94
|
-
"entry": {
|
|
95
|
-
"id": "msg-digest",
|
|
96
|
-
"name": "Message Digest",
|
|
97
|
-
"summary": "Summarises unread chats.",
|
|
98
|
-
"category": "messaging",
|
|
99
|
-
"instructions": "Pass { since? }. Returns a per-chat unread digest.",
|
|
100
|
-
"invoke": { "type": "script", "path": "scripts/msg-digest.sh" }
|
|
101
|
-
},
|
|
102
|
-
"files": ["tools/scripts/msg-digest.sh"]
|
|
103
|
-
}
|
|
104
|
-
],
|
|
105
|
-
"hooks": [
|
|
106
|
-
{
|
|
107
|
-
"entry": {
|
|
108
|
-
"id": "pre-tool-use/rate-limit",
|
|
109
|
-
"event": "PreToolUse",
|
|
110
|
-
"matcher": "msg_send_message",
|
|
111
|
-
"type": "command",
|
|
112
|
-
"command": "bash \"${HOOKS_DIR}/scripts/rate-limit.sh\"",
|
|
113
|
-
"script": "scripts/rate-limit.sh",
|
|
114
|
-
"timeout": 5,
|
|
115
|
-
"description": "Denies sends past a per-minute cap."
|
|
116
|
-
},
|
|
117
|
-
"enabled": true,
|
|
118
|
-
"files": ["hooks/scripts/rate-limit.sh"]
|
|
119
|
-
}
|
|
120
|
-
],
|
|
121
|
-
"mcp": [
|
|
122
|
-
{
|
|
123
|
-
"upstream": {
|
|
124
|
-
"id": "notify",
|
|
125
|
-
"transport": "stdio",
|
|
126
|
-
"command": "node",
|
|
127
|
-
"args": ["${PACKAGE_DIR}/mcp/vendor/notify-mcp/server.js"],
|
|
128
|
-
"enabled": true,
|
|
129
|
-
"description": "Messaging bridge MCP."
|
|
130
|
-
},
|
|
131
|
-
"expose": [
|
|
132
|
-
{ "upstream": "notify", "tool": "list_chats", "as": "msg_list_chats", "category": "messaging", "enabled": true },
|
|
133
|
-
{ "upstream": "notify", "tool": "send_message", "as": "msg_send_message", "category": "messaging", "enabled": true }
|
|
134
|
-
],
|
|
135
|
-
"files": ["mcp/vendor/notify-mcp/server.js"]
|
|
136
|
-
}
|
|
137
|
-
]
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
| Field | Meaning |
|
|
143
|
-
|---|---|
|
|
144
|
-
| `name` / `version` / `description` / `author` | Package identity. `name` is the folder name and the import key. |
|
|
145
|
-
| `units.tools[]` | Each has the full register `entry` plus `files` (paths inside the package). |
|
|
146
|
-
| `units.hooks[]` | Each has the full manifest `entry`, its seed `enabled` (for `hooks.state.json`), and `files`. |
|
|
147
|
-
| `units.mcp[]` | Each has an `upstream` block, its `expose[]` selections, and any vendored `files`. |
|
|
148
|
-
|
|
149
|
-
`${PACKAGE_DIR}` expands to the installed package's absolute path (mirrors `${HOOKS_DIR}` /
|
|
150
|
-
`${TOOLS_DIR}`), so a package's MCP/tool commands resolve wherever it lands.
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## 3. Export
|
|
155
|
-
|
|
156
|
-
To export, gather a unit's metadata and its files into a package folder:
|
|
157
|
-
|
|
158
|
-
1. Create `packages/<name>/` with a `package.json` manifest.
|
|
159
|
-
2. For each tool: copy its `tools.register.json` entry into `units.tools[].entry`, copy its
|
|
160
|
-
`tools/scripts/<file>` into the package's `tools/scripts/`, and list it in `files`.
|
|
161
|
-
3. For each hook: copy its `hooks.manifest.json` entry into `units.hooks[].entry`, copy its enabled
|
|
162
|
-
value from `hooks.state.json` into `enabled`, copy the script into `hooks/scripts/`, list it.
|
|
163
|
-
4. For each MCP: copy the `upstreams[]` block + matching `expose[]` selections, and vendor any server
|
|
164
|
-
files into `mcp/vendor/`.
|
|
165
|
-
|
|
166
|
-
The package is then a single folder, self-describing and copy-portable. (Everything stays inside
|
|
167
|
-
`packages/`
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## 4. Import
|
|
172
|
-
|
|
173
|
-
Import is the reverse: **drop a package folder in `packages/` + register its units.**
|
|
174
|
-
|
|
175
|
-
1. Read `packages/<name>/package.json`.
|
|
176
|
-
2. **Tools:** copy each `units.tools[].files` into `tools/scripts/` and add each `entry` to
|
|
177
|
-
`tools.register.json` (atomic write via `registry.js`). Reject id collisions or prompt to rename.
|
|
178
|
-
3. **Hooks:** copy each `units.hooks[].files` into `hooks/scripts/` and add each `entry` to
|
|
179
|
-
`hooks.manifest.json`; write its `enabled` into `hooks.state.json`.
|
|
180
|
-
4. **MCP:** copy vendored `files` into the package's `mcp/vendor/`, add each `upstream` to
|
|
181
|
-
`expose.json` `upstreams[]` and its `expose[]` selections (path-rewritten to `${PACKAGE_DIR}`).
|
|
182
|
-
|
|
183
|
-
After import: tools are live immediately (the register is read fresh on every `toolfunnel_list_tools`).
|
|
184
|
-
Hooks take effect once the loader re-scans (next startup, or a reload). Curated-direct MCP tools need
|
|
185
|
-
a reconnect to reach the CLI surface (register-routed tools do not). Verify with `toolfunnel_list_tools`
|
|
186
|
-
and a `run`/run-once test that the gate still fires on the imported units
|
|
187
|
-
applies to imported tools exactly as it does to local ones.
|
|
1
|
+
# How to package and share units (self-extension)
|
|
2
|
+
|
|
3
|
+
This is the instruction served by `toolfunnel_howto({ topic: "package" })`. It explains how to bundle
|
|
4
|
+
tools, hooks, and MCP configs into a **shareable package** and how import/export work. Packaging is a
|
|
5
|
+
**byproduct** of the gateway design: because every unit is already structured and
|
|
6
|
+
self-documented, a package is just a folder that lists its units and carries their files.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 0. The WHOLE-SETUP path - `tf_pack` (start here)
|
|
11
|
+
|
|
12
|
+
Since 0.4.0 the fastest way to ship is not per-unit packaging but the **whole setup**: your entire
|
|
13
|
+
config home (tools + scripts, hot/hidden curation, upstream references, policy hooks, identity) is
|
|
14
|
+
already the product. One call packages it into a deployment-ready artifact under `<home>/dist/`:
|
|
15
|
+
|
|
16
|
+
- `tf_pack { format: "home" }` → a **portable config home**. Zip it, git-init it, or run it
|
|
17
|
+
directly: `toolfunnel --config-dir <dir>`. The receiving gateway seeds nothing - the pack IS the
|
|
18
|
+
config, including the hooks: **the gate travels**, enforcing your policy on the recipient's
|
|
19
|
+
machine regardless of which MCP client they use.
|
|
20
|
+
- `tf_pack { format: "npm", name: "my-mcp" }` → a **publishable npm package**: a generated
|
|
21
|
+
`package.json` that depends on `toolfunnel` (caret range - DEPEND, never copy the engine), a
|
|
22
|
+
2-line bin launcher pointing `--config-dir` at the bundled home, and a README stub.
|
|
23
|
+
`cd dist/my-mcp && npm publish` → your users run `npx my-mcp` and get YOUR server, under YOUR
|
|
24
|
+
name (the bundled `toolfunnel.json` identity), with YOUR gate.
|
|
25
|
+
|
|
26
|
+
Declare the runtimes your tools need in the home's `toolfunnel.json` - they travel with the pack
|
|
27
|
+
and the receiving gateway warns at startup about anything missing:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
"requires": [ { "command": "python", "min": "3.10", "why": "the pdf tools" } ]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Audit honesty (both directions): packs spawn commands. Tell your users to read your pack's
|
|
34
|
+
`expose.json`, `tools.register.json`, AND `toolfunnel.json` (its `requires` probes execute at
|
|
35
|
+
gateway startup - bare program names only, the loader rejects anything shell-shaped) - and read
|
|
36
|
+
all three yourself in anything you install.
|
|
37
|
+
|
|
38
|
+
The per-UNIT packaging below remains the right shape for sharing a single tool/hook/MCP selection
|
|
39
|
+
rather than a whole gateway.
|
|
40
|
+
|
|
41
|
+
> Phase note: packages are Phase 2 (`packages/` directory). The structure below is what the importer
|
|
42
|
+
> and exporter target; defining it now means the units authored today are already package-ready.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 1. What a unit is (and what travels with it)
|
|
47
|
+
|
|
48
|
+
Each unit is self-contained - this is the whole reason packaging is cheap:
|
|
49
|
+
|
|
50
|
+
| Unit | Its metadata | Its files |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| **tool** | one `tools.register.json` entry (`id/name/summary/category/instructions/invoke`) | the script under `tools/scripts/` (for `invoke.type === "script"`) |
|
|
53
|
+
| **hook** | one `hooks.manifest.json` entry (`id/event/matcher/command/script/timeout/description`) + its `hooks.state.json` enabled value | the script under `hooks/scripts/` |
|
|
54
|
+
| **mcp** | one `expose.json` `upstreams[]` entry + its `expose[]` selections | any vendored server files it spawns (must live inside the package) |
|
|
55
|
+
|
|
56
|
+
A package gathers one or more of these, plus the files they reference.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 2. Package layout
|
|
61
|
+
|
|
62
|
+
A package is a folder under `packages/` containing a manifest and the units' files in a mirrored
|
|
63
|
+
tree so import is a structured copy, not guesswork:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
packages/
|
|
67
|
+
notify-pack/
|
|
68
|
+
package.json # the package manifest (below)
|
|
69
|
+
tools/
|
|
70
|
+
scripts/
|
|
71
|
+
msg-digest.sh
|
|
72
|
+
hooks/
|
|
73
|
+
scripts/
|
|
74
|
+
rate-limit.sh
|
|
75
|
+
mcp/
|
|
76
|
+
vendor/ # any vendored upstream server files
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `package.json` - the package manifest
|
|
80
|
+
|
|
81
|
+
A `package.json`-style manifest listing the package's identity and its units (the metadata blocks are
|
|
82
|
+
embedded so import is self-sufficient - it does not need the source register/manifest to reconstruct
|
|
83
|
+
entries):
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"name": "notify-pack",
|
|
88
|
+
"version": "1.0.0",
|
|
89
|
+
"description": "Message digest tool + rate-limit gate + upstream MCP exposure.",
|
|
90
|
+
"author": "unknown",
|
|
91
|
+
"units": {
|
|
92
|
+
"tools": [
|
|
93
|
+
{
|
|
94
|
+
"entry": {
|
|
95
|
+
"id": "msg-digest",
|
|
96
|
+
"name": "Message Digest",
|
|
97
|
+
"summary": "Summarises unread chats.",
|
|
98
|
+
"category": "messaging",
|
|
99
|
+
"instructions": "Pass { since? }. Returns a per-chat unread digest.",
|
|
100
|
+
"invoke": { "type": "script", "path": "scripts/msg-digest.sh" }
|
|
101
|
+
},
|
|
102
|
+
"files": ["tools/scripts/msg-digest.sh"]
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
"hooks": [
|
|
106
|
+
{
|
|
107
|
+
"entry": {
|
|
108
|
+
"id": "pre-tool-use/rate-limit",
|
|
109
|
+
"event": "PreToolUse",
|
|
110
|
+
"matcher": "msg_send_message",
|
|
111
|
+
"type": "command",
|
|
112
|
+
"command": "bash \"${HOOKS_DIR}/scripts/rate-limit.sh\"",
|
|
113
|
+
"script": "scripts/rate-limit.sh",
|
|
114
|
+
"timeout": 5,
|
|
115
|
+
"description": "Denies sends past a per-minute cap."
|
|
116
|
+
},
|
|
117
|
+
"enabled": true,
|
|
118
|
+
"files": ["hooks/scripts/rate-limit.sh"]
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"mcp": [
|
|
122
|
+
{
|
|
123
|
+
"upstream": {
|
|
124
|
+
"id": "notify",
|
|
125
|
+
"transport": "stdio",
|
|
126
|
+
"command": "node",
|
|
127
|
+
"args": ["${PACKAGE_DIR}/mcp/vendor/notify-mcp/server.js"],
|
|
128
|
+
"enabled": true,
|
|
129
|
+
"description": "Messaging bridge MCP."
|
|
130
|
+
},
|
|
131
|
+
"expose": [
|
|
132
|
+
{ "upstream": "notify", "tool": "list_chats", "as": "msg_list_chats", "category": "messaging", "enabled": true },
|
|
133
|
+
{ "upstream": "notify", "tool": "send_message", "as": "msg_send_message", "category": "messaging", "enabled": true }
|
|
134
|
+
],
|
|
135
|
+
"files": ["mcp/vendor/notify-mcp/server.js"]
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
| Field | Meaning |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `name` / `version` / `description` / `author` | Package identity. `name` is the folder name and the import key. |
|
|
145
|
+
| `units.tools[]` | Each has the full register `entry` plus `files` (paths inside the package). |
|
|
146
|
+
| `units.hooks[]` | Each has the full manifest `entry`, its seed `enabled` (for `hooks.state.json`), and `files`. |
|
|
147
|
+
| `units.mcp[]` | Each has an `upstream` block, its `expose[]` selections, and any vendored `files`. |
|
|
148
|
+
|
|
149
|
+
`${PACKAGE_DIR}` expands to the installed package's absolute path (mirrors `${HOOKS_DIR}` /
|
|
150
|
+
`${TOOLS_DIR}`), so a package's MCP/tool commands resolve wherever it lands.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## 3. Export - bundling a unit (or several)
|
|
155
|
+
|
|
156
|
+
To export, gather a unit's metadata and its files into a package folder:
|
|
157
|
+
|
|
158
|
+
1. Create `packages/<name>/` with a `package.json` manifest.
|
|
159
|
+
2. For each tool: copy its `tools.register.json` entry into `units.tools[].entry`, copy its
|
|
160
|
+
`tools/scripts/<file>` into the package's `tools/scripts/`, and list it in `files`.
|
|
161
|
+
3. For each hook: copy its `hooks.manifest.json` entry into `units.hooks[].entry`, copy its enabled
|
|
162
|
+
value from `hooks.state.json` into `enabled`, copy the script into `hooks/scripts/`, list it.
|
|
163
|
+
4. For each MCP: copy the `upstreams[]` block + matching `expose[]` selections, and vendor any server
|
|
164
|
+
files into `mcp/vendor/`.
|
|
165
|
+
|
|
166
|
+
The package is then a single folder, self-describing and copy-portable. (Everything stays inside
|
|
167
|
+
`packages/` - the isolation rule still holds.)
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## 4. Import - installing a package
|
|
172
|
+
|
|
173
|
+
Import is the reverse: **drop a package folder in `packages/` + register its units.**
|
|
174
|
+
|
|
175
|
+
1. Read `packages/<name>/package.json`.
|
|
176
|
+
2. **Tools:** copy each `units.tools[].files` into `tools/scripts/` and add each `entry` to
|
|
177
|
+
`tools.register.json` (atomic write via `registry.js`). Reject id collisions or prompt to rename.
|
|
178
|
+
3. **Hooks:** copy each `units.hooks[].files` into `hooks/scripts/` and add each `entry` to
|
|
179
|
+
`hooks.manifest.json`; write its `enabled` into `hooks.state.json`.
|
|
180
|
+
4. **MCP:** copy vendored `files` into the package's `mcp/vendor/`, add each `upstream` to
|
|
181
|
+
`expose.json` `upstreams[]` and its `expose[]` selections (path-rewritten to `${PACKAGE_DIR}`).
|
|
182
|
+
|
|
183
|
+
After import: tools are live immediately (the register is read fresh on every `toolfunnel_list_tools`).
|
|
184
|
+
Hooks take effect once the loader re-scans (next startup, or a reload). Curated-direct MCP tools need
|
|
185
|
+
a reconnect to reach the CLI surface (register-routed tools do not). Verify with `toolfunnel_list_tools`
|
|
186
|
+
and a `run`/run-once test that the gate still fires on the imported units - the PreToolUse gate
|
|
187
|
+
applies to imported tools exactly as it does to local ones.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# How to wrap an MCP server (transparent passthrough)
|
|
2
|
+
|
|
3
|
+
This is the instruction served by `toolfunnel_howto({ topic: "wrap" })`. It explains the
|
|
4
|
+
**passthrough wrap**: making ONE attached upstream MCP's tools the gateway's ENTIRE surface, under
|
|
5
|
+
their original names, with the upstream's own identity presented to the client - so the client
|
|
6
|
+
cannot tell ToolFunnel is in the middle. Use it to keep a legacy server working with modern
|
|
7
|
+
clients (or vice versa - all four era combinations work), or to put a policy gate in front of a
|
|
8
|
+
server you didn't write.
|
|
9
|
+
|
|
10
|
+
**What the client sees:** the wrapped server's own `serverInfo`, capabilities, instructions,
|
|
11
|
+
tools, results, errors and notifications - verbatim. **What the wrapped server sees** (stdio,
|
|
12
|
+
legacy clients): the real downstream client's identity, mirrored automatically. Mid-call prompts
|
|
13
|
+
(elicitation), resource subscriptions, progress tokens and cancellations are all bridged across
|
|
14
|
+
protocol eras. Wrapped tool calls wait up to 120 s by default, and a tool that reports progress
|
|
15
|
+
keeps its call alive indefinitely; for silent tools that run longer, set `"timeoutMs"` on the
|
|
16
|
+
upstream entry in `mcp/expose.json`.
|
|
17
|
+
|
|
18
|
+
**What changes for YOU (the agent):** while a wrap is active, ALL ToolFunnel tools - the four
|
|
19
|
+
meta-tools included - are hidden and uncallable. That includes `tf_wrap` itself. Read §3 before
|
|
20
|
+
setting a wrap in-band.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Setting a wrap - three equivalent paths
|
|
25
|
+
|
|
26
|
+
The wrap is one persisted field (`"passthrough"` in `tools/tools.state.json`); all three paths
|
|
27
|
+
write the same state and survive restarts.
|
|
28
|
+
|
|
29
|
+
**CLI (recommended for humans):**
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
toolfunnel wrap <upstreamId> # wrap - probes the upstream's protocol era and reports it
|
|
33
|
+
toolfunnel wrap <upstreamId> --as <name> # also present that name in the handshake (serverInfo)
|
|
34
|
+
toolfunnel wrap # status - current wrap + attached upstream ids
|
|
35
|
+
toolfunnel wrap --off # clear - restore the normal funnel surface
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Global flags may sit anywhere: `toolfunnel --config-dir <dir> wrap <upstreamId>` and
|
|
39
|
+
`toolfunnel wrap <upstreamId> --config-dir <dir>` are equivalent.
|
|
40
|
+
|
|
41
|
+
The wrap persists as one field in `tools/tools.state.json` in the config home -
|
|
42
|
+
`{ "passthrough": "<upstreamId>" }` when set. Editing that file by hand is a valid last-resort
|
|
43
|
+
undo: clearing the `passthrough` key (e.g. writing `{}` if nothing else is in the file) is the
|
|
44
|
+
file-edit equivalent of `wrap --off`; a running host picks the change up live.
|
|
45
|
+
|
|
46
|
+
**UI:** the MCPs tab → the Wrap button on the upstream's row (Unwrap on the same button while
|
|
47
|
+
active). A banner shows the active wrap.
|
|
48
|
+
|
|
49
|
+
**In-band (`tf_wrap`, via `toolfunnel_run_tool`):**
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
{ } → status: { ok, wrapping, upstreams }
|
|
53
|
+
{ "off": true } → clear
|
|
54
|
+
{ "upstream": "<id>", "confirm": true } → set (confirm REQUIRED - see §3)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The upstream must already be attached and enabled (`toolfunnel_howto({ topic: "add-mcp" })` covers
|
|
58
|
+
attaching one).
|
|
59
|
+
|
|
60
|
+
## 2. Verifying a wrap
|
|
61
|
+
|
|
62
|
+
- `toolfunnel wrap` (or `tf_wrap {}` before wrapping) shows the current state.
|
|
63
|
+
- After wrapping, a client's `initialize`/`server/discover` handshake presents the WRAPPED
|
|
64
|
+
server's identity, and `tools/list` shows its tools under their original names.
|
|
65
|
+
- The CLI wrap command prints an era report: whether the upstream speaks the modern
|
|
66
|
+
(2026-07-28) protocol, the legacy family, or both.
|
|
67
|
+
|
|
68
|
+
## 3. The lockout - why `confirm: true` is required in-band
|
|
69
|
+
|
|
70
|
+
Setting a wrap HIDES every ToolFunnel tool, including `tf_wrap`. An agent that sets a wrap
|
|
71
|
+
in-band therefore loses the in-band path to undo it. Recovery is always available OUT-of-band:
|
|
72
|
+
|
|
73
|
+
- `toolfunnel wrap --off` on the CLI, or
|
|
74
|
+
- the UI's Unwrap button.
|
|
75
|
+
|
|
76
|
+
`tf_wrap` refuses to set a wrap without `confirm: true` precisely so this is a deliberate,
|
|
77
|
+
informed action. Do not set a wrap unless the operator asked for it or you can reach one of the
|
|
78
|
+
two recovery paths.
|
|
79
|
+
|
|
80
|
+
## 4. Security - what wrapping changes
|
|
81
|
+
|
|
82
|
+
Wrapping SUSPENDS the path-isolation guard for the wrapped upstream only (a wrap is an explicit
|
|
83
|
+
"this server is my whole surface" declaration - e.g. a filesystem server serving a documents
|
|
84
|
+
folder MUST reach outside the gateway root). The CLI and UI both print a security notice naming
|
|
85
|
+
the outside paths when this applies. Two things stay true regardless:
|
|
86
|
+
|
|
87
|
+
- **Every call still passes the PreToolUse gate.** Wrapping never bypasses policy.
|
|
88
|
+
- **Funnel-mode upstreams keep the guard** - the suspension is wrap-scoped, nothing else.
|
|
89
|
+
|
|
90
|
+
To restrict a wrapped server after wrapping: add a PreToolUse hook that blocks by tool/args
|
|
91
|
+
(`toolfunnel_howto({ topic: "add-hook" })`), or disable individual tools (keyed by the surfaced
|
|
92
|
+
name). The manual's "Wrapping & security" section covers the patterns.
|
|
93
|
+
|
|
94
|
+
## 5. Related settings
|
|
95
|
+
|
|
96
|
+
- `legacyPin: true` on an upstream (in `mcp/expose.json`, or the UI's "Legacy pin" toggle) pins
|
|
97
|
+
that upstream to the legacy protocol forever - the era probe is skipped. Opt-in, warns loudly.
|
|
98
|
+
- The gateway's own presented identities (funnel mode / HTTP) are configured in
|
|
99
|
+
`toolfunnel.json` - see `toolfunnel_howto({ topic: "configure" })`.
|