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.
Files changed (78) hide show
  1. package/README.md +102 -213
  2. package/bin/toolfunnel.js +372 -147
  3. package/docs/KNOWN_BUGS.md +99 -225
  4. package/docs/MANUAL.pdf +0 -0
  5. package/docs/design.md +70 -0
  6. package/docs/packaging.md +115 -115
  7. package/hooks/hooks.manifest.json +27 -27
  8. package/hooks/scripts/example-audit.js +21 -21
  9. package/hooks/scripts/example-deny-dangerous.js +31 -31
  10. package/mcp/expose.example.json +30 -30
  11. package/mcp/servers/mock-modern/server.js +147 -0
  12. package/mcp/servers/mock-upstream/server.js +192 -12
  13. package/package.json +1 -1
  14. package/src/auth/config.js +174 -174
  15. package/src/auth/install.js +181 -181
  16. package/src/auth/resource-server.js +344 -344
  17. package/src/core/config-home.js +134 -134
  18. package/src/core/events.js +105 -105
  19. package/src/core/hook-engine.js +275 -275
  20. package/src/core/hook-loader.js +791 -791
  21. package/src/core/hook-runner.js +346 -346
  22. package/src/core/logger.js +171 -171
  23. package/src/core/matcher.js +51 -51
  24. package/src/core/metrics.js +75 -75
  25. package/src/core/requires.js +138 -138
  26. package/src/core/server-config.js +98 -77
  27. package/src/core/tool-registry.js +180 -180
  28. package/src/extend/add-hook.md +152 -142
  29. package/src/extend/add-mcp.md +122 -121
  30. package/src/extend/configure.md +87 -0
  31. package/src/extend/create-tool.md +168 -152
  32. package/src/extend/howto.js +102 -98
  33. package/src/extend/package.md +187 -187
  34. package/src/extend/wrap.md +99 -0
  35. package/src/mcp/aggregator.js +1246 -807
  36. package/src/mcp/expose-store.js +460 -430
  37. package/src/mcp/gated-run.js +201 -194
  38. package/src/mcp/http-transport.js +1311 -981
  39. package/src/mcp/mcp-client.js +907 -530
  40. package/src/mcp/modern.js +372 -0
  41. package/src/mcp/protocol.js +468 -466
  42. package/src/mcp/server.js +2688 -1322
  43. package/src/packages/packager.js +715 -715
  44. package/src/tools/drift.js +138 -138
  45. package/src/tools/registry.js +513 -511
  46. package/src/tools/tool-state.js +170 -104
  47. package/src/ui/public/app.js +1174 -1055
  48. package/src/ui/public/index.html +286 -249
  49. package/src/ui/public/styles.css +535 -535
  50. package/src/ui/server.js +1348 -1190
  51. package/tools/scripts/base64.js +139 -139
  52. package/tools/scripts/danger.js +61 -61
  53. package/tools/scripts/echo.js +39 -39
  54. package/tools/scripts/hash.js +101 -101
  55. package/tools/scripts/json.js +109 -109
  56. package/tools/scripts/text-stats.js +131 -131
  57. package/tools/scripts/tf-env.js +43 -43
  58. package/tools/scripts/tf-hook-add.js +151 -151
  59. package/tools/scripts/tf-hook-set.js +112 -112
  60. package/tools/scripts/tf-list.js +192 -192
  61. package/tools/scripts/tf-log.js +86 -86
  62. package/tools/scripts/tf-mcp-add.js +117 -117
  63. package/tools/scripts/tf-mcp-set.js +121 -102
  64. package/tools/scripts/tf-pack.js +237 -237
  65. package/tools/scripts/tf-tool-add.js +134 -129
  66. package/tools/scripts/tf-tool-set.js +142 -142
  67. package/tools/scripts/tf-wrap.js +103 -0
  68. package/tools/scripts/uuid.js +102 -102
  69. package/tools/tools.register.json +227 -216
  70. package/docs/admin-ui.md +0 -404
  71. package/docs/configuration.md +0 -402
  72. package/docs/hooks-and-gating.md +0 -428
  73. package/docs/logging.md +0 -223
  74. package/docs/management.md +0 -429
  75. package/docs/oauth.md +0 -103
  76. package/docs/releasing.md +0 -31
  77. package/docs/setup.md +0 -352
  78. package/docs/tools-and-upstreams.md +0 -469
@@ -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 — `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.
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" })`.