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/extend/wrap.md
CHANGED
|
@@ -1,99 +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" })`.
|
|
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" })`.
|