toolfunnel 0.6.0 → 0.7.1

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 (72) hide show
  1. package/README.md +228 -231
  2. package/SECURITY.md +84 -0
  3. package/bin/toolfunnel.js +451 -372
  4. package/docs/KNOWN_BUGS.md +80 -99
  5. package/docs/MANUAL.pdf +0 -0
  6. package/docs/design.md +84 -70
  7. package/docs/packaging.md +115 -115
  8. package/hooks/hooks.manifest.json +27 -27
  9. package/hooks/scripts/example-audit.js +21 -21
  10. package/hooks/scripts/example-deny-dangerous.js +39 -31
  11. package/mcp/expose.example.json +30 -30
  12. package/mcp/servers/mock-modern/server.js +147 -147
  13. package/mcp/servers/mock-upstream/server.js +380 -350
  14. package/package.json +3 -1
  15. package/src/auth/config.js +174 -174
  16. package/src/auth/install.js +181 -181
  17. package/src/auth/resource-server.js +353 -344
  18. package/src/core/child-reaper.js +61 -0
  19. package/src/core/config-home.js +134 -134
  20. package/src/core/events.js +105 -105
  21. package/src/core/hook-engine.js +329 -275
  22. package/src/core/hook-loader.js +824 -791
  23. package/src/core/hook-runner.js +566 -346
  24. package/src/core/logger.js +297 -171
  25. package/src/core/matcher.js +100 -51
  26. package/src/core/metrics.js +75 -75
  27. package/src/core/rename-migration.js +163 -0
  28. package/src/core/requires.js +138 -138
  29. package/src/core/server-config.js +98 -98
  30. package/src/extend/add-hook.md +152 -152
  31. package/src/extend/add-mcp.md +119 -122
  32. package/src/extend/configure.md +88 -87
  33. package/src/extend/create-tool.md +168 -168
  34. package/src/extend/howto.js +102 -102
  35. package/src/extend/package.md +190 -187
  36. package/src/extend/wrap.md +99 -99
  37. package/src/mcp/aggregator.js +1719 -1246
  38. package/src/mcp/expose-store.js +577 -460
  39. package/src/mcp/gated-run.js +210 -201
  40. package/src/mcp/http-transport.js +1563 -1311
  41. package/src/mcp/mcp-client.js +1070 -907
  42. package/src/mcp/modern.js +406 -372
  43. package/src/mcp/protocol.js +468 -468
  44. package/src/mcp/server.js +3112 -2688
  45. package/src/tools/registry.js +661 -513
  46. package/src/tools/tool-state.js +170 -170
  47. package/src/ui/public/app.js +1187 -1174
  48. package/src/ui/public/index.html +286 -286
  49. package/src/ui/public/styles.css +535 -535
  50. package/src/ui/server.js +1616 -1348
  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 +120 -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 -121
  64. package/tools/scripts/tf-pack.js +237 -237
  65. package/tools/scripts/tf-tool-add.js +134 -134
  66. package/tools/scripts/tf-tool-set.js +142 -142
  67. package/tools/scripts/tf-wrap.js +103 -103
  68. package/tools/scripts/uuid.js +102 -102
  69. package/tools/tools.register.json +227 -227
  70. package/src/core/tool-registry.js +0 -180
  71. package/src/packages/packager.js +0 -715
  72. package/src/tools/drift.js +0 -138
@@ -1,99 +1,80 @@
1
- # Known issues & roadmap
2
-
3
- This file tracks the state of the 0.6.0 dual-era work, the deliberately-deferred items, and any
4
- limitations worth knowing about.
5
-
6
- ## 0.6.0 - the 2026-07-28 MCP specification
7
-
8
- The 2026-07-28 MCP revision is a **breaking** change to the protocol: it removes the `initialize`
9
- handshake, protocol-level sessions, and the standalone GET/SSE endpoint, and adds new required
10
- behaviours - per-request protocol metadata, `Mcp-Method` / `Mcp-Name` request headers, a
11
- `server/discover` endpoint, a `subscriptions/listen` change-notification stream, and
12
- `resultType` / `ttlMs` / `cacheScope` result fields.
13
-
14
- Implemented in 0.6.0 (built against the RC spec; a reconciliation pass against the finalised
15
- spec lands after 28 July):
16
-
17
- - **Dual-era transport** (2024-11-05 + 2026-07-28) - old and new clients both work.
18
- - `server/discover`, request-header validation, and the `subscriptions/listen` stream.
19
- - A **dual-era client**: ToolFunnel speaks modern to modern upstream servers (stdio).
20
- - An **opt-in, per-upstream legacy-protocol shim** (`legacyPin`) - off by default, names the exact
21
- version it drops to, warns at startup and on every call, and is enforced (skips the era probe).
22
- - **Passthrough wrap** (`toolfunnel wrap <upstreamId>`): one command turns the gateway into a
23
- transparent, both-era wrapper for one upstream MCP - verbatim identity, tool definitions,
24
- results, errors, notifications, and per-URI resource subscriptions (forwarded, and replayed
25
- across silent upstream reconnects), with every call still passing the PreToolUse gate.
26
- - **Elicitation bridging**: a wrapped legacy server's mid-call questions are translated into the
27
- modern `input_required` + retry pattern and back - multi-round, single-use tokens, TTL'd -
28
- tested end-to-end against real published elicitation servers.
29
- - **Cancel translation** (stdio): a client's cancel reaches the upstream in the upstream's own
30
- request-id space, for forwarded methods and wrapped tool calls alike.
31
- - **Identity settings + wrap mirroring**: `toolfunnel.json` gains `clientName`/`clientVersion`
32
- (the identity upstreams see); under a wrap on stdio the real downstream client's identity is
33
- mirrored upstream automatically.
34
- - **UI**: a Settings tab (identity + ports), a per-upstream legacy-pin toggle, and the wrap
35
- security notice surfaced on the UI wrap path (parity with the CLI).
36
- - **Agent-facing docs**: `toolfunnel_howto` gains `wrap` and `configure` topics, so a plain agent
37
- can learn the wrap and the whole no-code config map from inside the protocol.
38
- - **Method-class timeouts**: tool calls (`tools/call`, `prompts/get`, `resources/read`) wait
39
- 120 s by default - configurable per upstream via `"timeoutMs"` - and a progress report from
40
- the tool re-arms the window, so a slow-but-alive tool never dies to the clock. The 10 s
41
- handshake/list window stays fixed as the dead-upstream detector.
42
- - **Async shell execution**: `shell`-invoke register tools no longer block the event loop
43
- (was `spawnSync` since 0.5.0) - concurrent HTTP clients keep being served during a long
44
- shell tool.
45
- - **Config-home visibility**: every start prints the resolved config home to stderr, with a
46
- relocation hint when it defaulted to the package root - running from a git clone no longer
47
- writes config into the repo silently.
48
- - **Bounded listen streams**: the HTTP transport caps concurrent `subscriptions/listen`
49
- streams (64) and refuses new ones past the cap with a clear error; existing streams are
50
- never evicted.
51
-
52
- ## Deliberately deferred (documented dispositions, target: the 28-July reconciliation pass)
53
-
54
- - **Legacy version family**: ToolFunnel currently speaks the oldest legacy dialect (`2024-11-05`)
55
- to legacy upstreams and clamps a wrapped handshake to what the upstream negotiated. A client and
56
- upstream that both speak a newer legacy revision (e.g. `2025-06-18`) are negotiated down.
57
- Fidelity of the *fields* is unaffected (verified against real servers - nothing version-gated is
58
- dropped), but the advertised version string is older than either end requires. DECIDED
59
- (2026-07-17): best-version negotiation in BOTH directions - newest each peer speaks, modern
60
- first, legacy fallback. Lands with the reconciliation pass, sequenced after the elicitation
61
- bridge (a newer legacy offer invites server-initiated requests the client must handle first).
62
- - **Modern-upstream subscriptions**: the client does not yet open a `subscriptions/listen` stream
63
- to modern upstreams, so their change-notifications are not received; the listen ack honestly
64
- refuses `resourceSubscriptions` when no legacy subscribe-capable upstream is in scope.
65
- - **Elicitation bridge scope**: a wrapped upstream's mid-call `elicitation/create` bridges to
66
- modern clients as MRTR (`input_required` + retry - built and wire-tested). A LEGACY client
67
- gets an automatic decline instead (relaying backwards requests to legacy clients is future
68
- work), and `sampling/createMessage` / `roots/list` from upstreams are answered -32601 for now.
69
- An elicitation can only be bound to a call when exactly ONE wrapped call is in flight for that
70
- upstream (always true on stdio); ambiguous concurrent HTTP calls decline rather than guess.
71
- - Header validation reports `-32020` where `-32022` would be more specific in one path.
72
- - Two-store wrap state read (expose.json + tools.state.json) has a benign TOCTOU window.
73
- - A meta-less `server/discover` is answered rather than refused (permissive-by-design).
74
- - The wrap's identity-mirror reconnect re-runs the full era negotiation - worst case ~3 s extra
75
- on the first handshake against an upstream that ignores `server/discover`. Reusing the
76
- already-known era on reconnect is a deferred optimisation.
77
- - HTTP cancel translation: the sessionless modern era gives a POST no connection identity, so a
78
- client `notifications/cancelled` over HTTP is dropped rather than risk cancelling another
79
- client's call. stdio (one client per pipe) translates and forwards cancels for both forwarded
80
- methods and wrapped tools/call; the remaining best-effort windows are sub-millisecond (a cancel
81
- racing the instant a forward is issued) plus the PreToolUse gate-evaluation phase of a tool
82
- call (registered at upstream-issue time, post-gate).
83
- - `io.modelcontextprotocol/logLevel` (the modern per-request log level) is stripped with the rest
84
- of the protocol `_meta` keys on wrapped forwards and not re-injected for modern upstreams -
85
- part of the modern-upstream work above.
86
- - Discover on a DISABLED upstream works (inspect its tools before enabling - an explicit
87
- allowance on the discover path only; the disabled upstream never reaches the tool surface).
88
-
89
- ## Current limitations
90
-
91
- - The OAuth 2.1 resource-server and Streamable-HTTP support are recent and less battle-tested than
92
- the core - test your setup before relying on them in a networked deployment.
93
- - No Prometheus / OpenTelemetry metrics - a toggleable JSONL audit log and in-memory `/health`
94
- call counters only.
95
- - One gateway instance per process: the bridge/cancel state is process-global, so embedding two
96
- gateway instances in one Node process is not a supported configuration (running two processes
97
- is fine, and is the normal deployment).
98
-
99
- Found a bug, or want to help with the 0.6.0 work? Issues and PRs are welcome.
1
+ # Known bugs
2
+
3
+ The **Open** list is what remains open as of 0.7.0. **Fixed in 0.7.0** marks the items from
4
+ 0.6.0's file that this release closed - kept and marked rather than silently deleted, so a
5
+ 0.6.0 reader can see their fate. The full change list is in the
6
+ [release notes](https://github.com/Rendeverance/toolfunnel/releases).
7
+
8
+ ## Open
9
+
10
+ - **OPEN - Modern-upstream subscriptions are not received.** The client does not yet open a
11
+ `subscriptions/listen` stream to modern upstreams, so their change-notifications never arrive;
12
+ the listen ack honestly refuses `resourceSubscriptions` when no legacy subscribe-capable
13
+ upstream is in scope.
14
+ - **OPEN - The elicitation bridge does not cover legacy clients.** A wrapped upstream's mid-call
15
+ `elicitation/create` bridges to MODERN clients as MRTR (`input_required` + retry), form and url
16
+ mode, gated on the caller's declared capability (0.7.0). A LEGACY client gets an automatic
17
+ decline instead - relaying backwards requests to legacy clients is unbuilt - and
18
+ `sampling/createMessage` / `roots/list` from upstreams are answered `-32601`. An elicitation can
19
+ only be bound to a call when exactly ONE wrapped call is in flight for that upstream (always
20
+ true on stdio); ambiguous concurrent HTTP calls decline rather than guess.
21
+ - **OPEN (deferred) - HTTP cancels are dropped rather than translated.** The sessionless modern
22
+ era gives a POST no connection identity, so a client `notifications/cancelled` arriving over
23
+ HTTP is dropped rather than risk cancelling another client's call. Fixing this needs design
24
+ thought - identifying the caller without reintroducing sessions - so it is deferred rather than
25
+ patched. stdio (one client per pipe) translates and forwards cancels for both forwarded methods
26
+ and wrapped tool calls; the remaining best-effort windows there are sub-millisecond (a cancel
27
+ racing the instant a forward is issued) plus the PreToolUse gate-evaluation phase of a tool
28
+ call (registered at upstream-issue time, post-gate).
29
+
30
+ ## Fixed in 0.7.0
31
+
32
+ - ***FIXED*** - **Legacy version negotiation.** 0.6.0 answered every `initialize` with
33
+ 2024-11-05 whatever the client asked for, and supported exactly one legacy revision. The
34
+ gateway now echoes the requested version when it supports it (else answers its latest legacy),
35
+ and the legacy set spans 2024-11-05 / 2025-03-26 / 2025-06-18 / 2025-11-25. Pinned by
36
+ `test/negotiation.test.js`. One direction remains conservative: the client still offers
37
+ 2024-11-05 to legacy upstreams (field fidelity is unaffected; only the advertised version
38
+ string is older than either end requires).
39
+ - ***FIXED*** - **Header validation used a generic error code for an unsupported version.** An
40
+ unsupported `MCP-Protocol-Version` header answered the generic `-32600`; it now answers
41
+ `-32022` with `data.supported` and `data.requested` - the same code and shape the `_meta`
42
+ path already used for the same condition, so a client can renegotiate without parsing prose.
43
+ Pinned by `test/header-enforcement.test.js`.
44
+ - ***FIXED*** - **The wrap's identity-mirror reconnect re-ran the full era negotiation** (worst
45
+ case roughly 3 s per respawn against an upstream that ignores `server/discover`). The
46
+ negotiated era is now remembered per upstream, and a death-driven reconnect goes straight to
47
+ the era it knows. A failed hinted connect or an explicit reconnect re-negotiates in full, so
48
+ an upstream upgraded across a restart is never pinned down an era. Pinned by
49
+ `test/reconnect-era-memo.test.js`.
50
+ - ***FIXED*** - **`io.modelcontextprotocol/logLevel` was stripped on wrapped forwards.** Now
51
+ era-keyed at the client boundary: a modern upstream gets the caller's level verbatim in
52
+ `_meta`; a legacy upstream that declared the `logging` capability gets `logging/setLevel`
53
+ issued before the call (and the key never leaks into legacy `_meta`). Pinned by
54
+ `test/loglevel.test.js`.
55
+
56
+ ## By design (not bugs)
57
+
58
+ These answer "why doesn't it do X". They are considered decisions, not defects.
59
+
60
+ - **The config home defaults to the package root.** A git clone keeps working unchanged, and
61
+ seeding protects it from `npm update`, with an every-start relocation hint. Defaulting it to a
62
+ per-user directory instead needs a migration story for existing installs, so it ships as its own
63
+ release rather than as a rider. (`preuninstall` as a rescue mechanism is tested DEAD on npm
64
+ 11.9.0 - it will not be built.)
65
+ - **Funnel-mode curated descriptions carry an `[upstream]` title prefix** that lean passthrough
66
+ does not. A toggle to reconcile the two is cosmetic.
67
+ - **The `command` slot is not path-guarded, and `PATH` is deliberately permitted.** The command
68
+ names the interpreter (`node`, a system binary), which by definition lives outside the config
69
+ home, and guarding `PATH` would break how commands resolve at all - so an upstream entry
70
+ combining `command: "node"` with a caller-supplied `env.PATH` runs whatever that PATH resolves.
71
+ This is a documented boundary, not an oversight: an upstream entry already specifies an
72
+ arbitrary command to execute, so the isolation guard is a **tripwire** for configs that quietly
73
+ reach outside an auditable config home - not a sandbox against an untrusted pack author.
74
+ Installing a pack is installing software (see [SECURITY.md](../SECURITY.md)).
75
+ - **A meta-less `server/discover` is answered rather than refused** - permissive by design.
76
+ - **Discover works on a DISABLED upstream**, so its tools can be inspected before enabling. An
77
+ explicit allowance on the discover path only; the disabled upstream never reaches the tool
78
+ surface.
79
+
80
+ Found a bug, or want to help with the open work? Issues and PRs are welcome.
package/docs/MANUAL.pdf CHANGED
Binary file
package/docs/design.md CHANGED
@@ -1,70 +1,84 @@
1
- # Design decisions
2
-
3
- Short notes on the decisions that shape ToolFunnel, so you can judge the reasoning without
4
- reading the source. Each one is also documented at the place in the code where it bites.
5
-
6
- ## 1. Zero runtime dependencies
7
-
8
- ToolFunnel sits in a privileged position: it executes tools and enforces policy. Every
9
- transitive dependency in that position is something you have to trust or audit. So the gateway
10
- uses Node built-ins only; "dependencies" in package.json is empty and stays empty. The one
11
- exception is deliberate and opt-in: OAuth token validation uses jose, installed only when you
12
- run `toolfunnel install-oauth`, version-pinned, because hand-rolled JWT crypto is how security
13
- holes are born. Dev tooling for the test suite (jose, the MCP SDK for interop tests) never
14
- ships.
15
-
16
- ## 2. No SDK
17
-
18
- The gateway hand-rolls JSON-RPC over stdio and HTTP rather than building on the official MCP
19
- SDK. Two reasons. First, the zero-dependency stance above. Second, ToolFunnel has to speak two
20
- protocol eras at once, on both sides, which the SDK does not do; owning the wire layer is what
21
- makes the dual-era serving and the wrap possible at all.
22
-
23
- ## 3. The gate fails closed
24
-
25
- Every server-side execution path goes through one function, gatedRun, and that function treats
26
- every failure as a deny: a hook that blocks, a hook engine that crashes, a malformed engine
27
- result, all of them mean the tool does not run. Denies carry a flag distinguishing operator
28
- policy ("your hook said no") from wiring failure ("the gate itself broke"), so a broken gate
29
- can never be mistaken for an approving one. The invariant has its own test.
30
-
31
- ## 4. The isolation boundary
32
-
33
- Upstream server definitions can name any executable but their path-shaped arguments must stay
34
- inside the config home. The command is the interpreter you chose; the args are what it can
35
- reach, so the args are what the guard checks. Wrapping suspends the guard for the wrapped
36
- server only, because a wrap is an explicit statement that this one server is your entire
37
- surface, and it warns you when that happens.
38
-
39
- ## 5. Two eras, served per request
40
-
41
- The 2026-07-28 revision removes the handshake that defines the older protocol, so "which era"
42
- is a property of each request, and that is how the gateway treats it: era detection per
43
- request, no mode switch, no configuration. Legacy answers stay byte-identical to what a
44
- pre-0.6.0 client saw; the gateway's own modern decoration only ever appears on modern
45
- requests. (A wrapped modern upstream's results are relayed verbatim, whatever era the
46
- caller speaks - stripping a server's own fields would be data loss and a wrapper tell.)
47
-
48
- ## 6. Config lives in one folder
49
-
50
- Everything mutable (tools, upstreams, hooks, identity, logs) lives in the config home,
51
- relocatable with `--config-dir` or `TOOLFUNNEL_HOME`, seeded once, never overwritten by
52
- updates. One folder to back up, one folder to package, one folder to hand a colleague. The
53
- gateway prints the resolved home at every start so there is never any doubt about where state
54
- lives.
55
-
56
- ## 7. Single operator by design
57
-
58
- Each ToolFunnel instance serves one operator. That is a decision, not an oversight: one
59
- instance per user gives process isolation, crash containment, per-user wrap state, and a
60
- security boundary you can reason about in one sentence. Multi-user deployment composes *in
61
- front of* the gateway rather than inside it - an authenticating proxy at the OAuth boundary
62
- (ToolFunnel is already an OAuth 2.1 resource server) routing each authenticated user to their
63
- own instance, with the per-request `_meta` lane carrying user context through to upstreams
64
- that want it. Nothing in the core couples the operator to a token subject, so that door stays
65
- open without the core ever growing user-shaped complexity.
66
-
67
- A Team edition built on this pattern - per-user identity, audit trails, quotas, per-user
68
- policy - is on the roadmap if there is interest. If your team wants that sooner, or you need a
69
- bespoke gateway built on ToolFunnel for your own requirements, open an issue on this
70
- repository titled `enquiry` - tailored builds are available.
1
+ # Design decisions
2
+
3
+ Short notes on the decisions that shape ToolFunnel, so you can judge the reasoning without
4
+ reading the source. Each one is also documented at the place in the code where it bites.
5
+
6
+ ## 1. Zero runtime dependencies
7
+
8
+ ToolFunnel sits in a privileged position: it executes tools and enforces policy. Every
9
+ transitive dependency in that position is something you have to trust or audit. So the gateway
10
+ uses Node built-ins only; "dependencies" in package.json is empty and stays empty. The one
11
+ exception is deliberate and opt-in: OAuth token validation uses jose, installed only when you
12
+ run `toolfunnel install-oauth`, version-pinned, because hand-rolled JWT crypto is how security
13
+ holes are born. Dev tooling for the test suite (jose, the MCP SDK for interop tests) never
14
+ ships.
15
+
16
+ ## 2. No SDK
17
+
18
+ The gateway hand-rolls JSON-RPC over stdio and HTTP rather than building on the official MCP
19
+ SDK. Two reasons. First, the zero-dependency stance above. Second, ToolFunnel has to speak two
20
+ protocol eras at once, on both sides, which the SDK does not do; owning the wire layer is what
21
+ makes the dual-era serving and the wrap possible at all.
22
+
23
+ ## 3. The gate: a deny is absolute, an unreadable decision is a deny
24
+
25
+ Every server-side execution path goes through one function, gatedRun, and that function treats
26
+ every failure it can *see* as a deny: a hook that blocks, a hook engine that crashes, a malformed
27
+ engine result, a decision too large or too mangled to parse - all of them mean the tool does not
28
+ run. An answer we cannot read is never treated as permission. Denies carry a flag distinguishing
29
+ operator policy ("your hook said no") from wiring failure ("the gate itself broke"), so a broken
30
+ gate can never be mistaken for an approving one.
31
+
32
+ The line is drawn at a hook that never answered at all. A hook *script* that crashes, is missing,
33
+ or hangs past its timeout is a non-blocking error and the call proceeds. That is Claude Code's
34
+ hook protocol and it is a deliberate trade: denying there would mean a single broken hook file -
35
+ a typo, a deleted script, a slow disk - silently taking every tool in the gateway offline, and an
36
+ operator debugging that has no signal to follow. So the runtime failure is reported and the call
37
+ continues, while the *decision* path stays strict. The consequence for anyone writing a gate is
38
+ concrete: deny with `exit 2`, never with a bare non-zero, and if your policy cannot decide, exit 2
39
+ deliberately. Both halves of this contract are pinned by test/gate-semantics.test.js.
40
+
41
+ ## 4. The isolation boundary
42
+
43
+ Upstream server definitions can name any executable, but every field that decides what code the
44
+ spawned process runs must stay inside the config home: path-shaped arguments (including the
45
+ value after `=` in a `--flag=value` form), the working directory (`cwd`) bare arguments resolve
46
+ against, and the small set of environment variables that load code on their own (`NODE_OPTIONS`,
47
+ `NODE_PATH`, and their equivalents for other runtimes). The command is not guarded - it is the
48
+ interpreter you chose, not the code it runs. The guard relaxes only by explicit opt-out, each
49
+ per upstream and each with a warning naming itself: wrapping (a wrap is an explicit statement
50
+ that this one server is your entire surface), `allowOutsidePaths: true` on the upstream's own
51
+ entry (its paths and path-list env), and `allowCodeLoadingEnv: true` (the env class on its own).
52
+
53
+ ## 5. Two eras, served per request
54
+
55
+ The 2026-07-28 revision removes the handshake that defines the older protocol, so "which era"
56
+ is a property of each request, and that is how the gateway treats it: era detection per
57
+ request, no mode switch, no configuration. Legacy answers stay byte-identical to what a
58
+ pre-0.6.0 client saw; the gateway's own modern decoration only ever appears on modern
59
+ requests. (A wrapped modern upstream's results are relayed verbatim, whatever era the
60
+ caller speaks - stripping a server's own fields would be data loss and a wrapper tell.)
61
+
62
+ ## 6. Config lives in one folder
63
+
64
+ Everything mutable (tools, upstreams, hooks, identity, logs) lives in the config home,
65
+ relocatable with `--config-dir` or `TOOLFUNNEL_HOME`, seeded once, never overwritten by
66
+ updates. One folder to back up, one folder to package, one folder to hand a colleague. The
67
+ gateway prints the resolved home at every start so there is never any doubt about where state
68
+ lives.
69
+
70
+ ## 7. Single operator by design
71
+
72
+ Each ToolFunnel instance serves one operator. That is a decision, not an oversight: one
73
+ instance per user gives process isolation, crash containment, per-user wrap state, and a
74
+ security boundary you can reason about in one sentence. Multi-user deployment composes *in
75
+ front of* the gateway rather than inside it - an authenticating proxy at the OAuth boundary
76
+ (ToolFunnel is already an OAuth 2.1 resource server) routing each authenticated user to their
77
+ own instance, with the per-request `_meta` lane carrying user context through to upstreams
78
+ that want it. Nothing in the core couples the operator to a token subject, so that door stays
79
+ open without the core ever growing user-shaped complexity.
80
+
81
+ A Team edition built on this pattern - per-user identity, audit trails, quotas, per-user
82
+ policy - is on the roadmap if there is interest. If your team wants that sooner, or you need a
83
+ bespoke gateway built on ToolFunnel for your own requirements, open an issue on this
84
+ repository titled `enquiry` - tailored builds are available.
package/docs/packaging.md CHANGED
@@ -1,115 +1,115 @@
1
- # Packaging: ship what you built
2
-
3
- ToolFunnel's config is a folder. That single fact is the whole packaging story: everything you
4
- build in a gateway - your tools and their scripts, which of them are promoted hot or hidden, the
5
- upstream MCPs you attached and the tools you chose to expose from them, the policy hooks that gate
6
- every call, and the identity the server introduces itself with - lives in one directory tree, the
7
- **config home**. Ship the tree and you have shipped the product.
8
-
9
- ## The config home
10
-
11
- By default the home is the package/clone root - a git clone works exactly as it always did. Point
12
- the gateway elsewhere and the config lives there instead:
13
-
14
- ```
15
- toolfunnel --config-dir D:\my-setup # flag (wins)
16
- TOOLFUNNEL_HOME=D:\my-setup toolfunnel # env var
17
- ```
18
-
19
- An empty home is **seeded** on first use from the shipped defaults (the register + management
20
- scripts, an empty `mcp/expose.json`, the default hooks manifest) and is **never overwritten**
21
- afterwards - which is the point: with the home outside the package, `npm update toolfunnel`
22
- can never eat your tools.
23
-
24
- ```
25
- <home>/
26
- toolfunnel.json # identity (serverName/serverVersion), port defaults, `requires`
27
- tools/
28
- tools.register.json # your tool entries (incl. their inputSchema)
29
- tools.state.json # the hot/hidden/enabled curation - part of the product
30
- scripts/ # the tool scripts
31
- mcp/expose.json # upstream references + curated exposure
32
- hooks/ # the policy gate: manifest + hook scripts
33
- ```
34
-
35
- ## `tf_pack` - one call to a deployment artifact
36
-
37
- The `tf_pack` management tool snapshots the LIVE setup into `<home>/dist/<name>/` - always a
38
- separate location, never the live tree. `auth/` (environment-specific) and `logs/` never ship.
39
-
40
- **`{ format: "home" }`** → a portable config home. Zip it, commit it to a repo, or run it as-is:
41
-
42
- ```
43
- toolfunnel --config-dir path/to/the-pack
44
- ```
45
-
46
- **`{ format: "npm", name: "my-mcp", version: "1.0.0" }`** → a publishable npm package:
47
-
48
- ```
49
- dist/my-mcp/
50
- package.json # name, bin, files, dependencies: { "toolfunnel": "^<current version>" }
51
- bin/my-mcp.js # 2 lines: pin --config-dir at ../home, delegate to toolfunnel
52
- home/ # the bundled config home (identity rewritten to my-mcp@1.0.0)
53
- README.md # a stub to make your own
54
- ```
55
-
56
- Review it, then `cd dist/my-mcp && npm publish`. Your users run `npx my-mcp` and get **your**
57
- server: your name in the MCP `initialize` handshake, your tools with your schemas, your curation,
58
- and - the part nobody else can say - **your gate**. The hooks travel inside the pack and enforce
59
- your policy on the recipient's machine regardless of which MCP client calls it.
60
-
61
- ### Depend, never copy
62
-
63
- The generated package **depends on** `toolfunnel` (caret range) rather than vendoring it. Your own
64
- runtime dependencies go in **your** `package.json` exactly as with any npm package - npm installs
65
- the whole tree on `npx my-mcp`; toolfunnel itself adds zero. Why depending matters:
66
-
67
- - security and bug fixes reach every wrapped MCP through a normal `npm update` (the pack pins the caret range of the version that built it, so patch releases flow automatically) - no stale forks
68
- carrying old bugs;
69
- - your package stays a few KB of config instead of a copied engine;
70
- - (and honestly) every install of your MCP counts as a toolfunnel download, which keeps the thing
71
- you depend on alive.
72
-
73
- Your users can move the bundled home out of `node_modules` (so *their* edits survive *your*
74
- updates) via the `<NAME>_HOME` env var the generated launcher supports.
75
-
76
- ## Declaring runtimes: `requires`
77
-
78
- Your tools may need runtimes the gateway doesn't (python, git, ffmpeg...). Declare them in the
79
- home's `toolfunnel.json` and they travel with every pack:
80
-
81
- ```json
82
- {
83
- "serverName": "my-mcp",
84
- "requires": [
85
- { "command": "python", "min": "3.10", "why": "the pdf-extract tools" },
86
- { "command": "git" }
87
- ]
88
- }
89
- ```
90
-
91
- The gateway probes them once at startup and prints one clear stderr line per problem - found
92
- version vs needed, plus your `why`. Advisory by design: a missing runtime breaks only the tools
93
- that need it, so the server still starts and everything else keeps working.
94
-
95
- ## Composite packs
96
-
97
- A pack is not just your own scripts. Because `mcp/expose.json` lives in the home, a pack carries
98
- **upstream references too**: "attach `@example/weather-mcp` (pin the version) and expose exactly
99
- these four tools under these names." The upstream itself travels as a *reference* - `npx` fetches
100
- it on the recipient's machine - while your **curation** and your **gate** travel as config. One
101
- pack = your tools + other people's servers, curated, renamed, and policed, as a single artifact.
102
-
103
- ## Audit honesty
104
-
105
- Packs spawn commands - that is what they are for. Before you run anyone's pack (including one of
106
- ours), read **three files** - every command it can run is declared in plain JSON:
107
-
108
- - `tools/tools.register.json` - the tool invokes (scripts and shell commands);
109
- - `mcp/expose.json` - the upstream server commands it will spawn;
110
- - `toolfunnel.json` - the `requires` probes. Note these run **at gateway startup** (each declared
111
- program is executed once with its version flag), which is why a `requires` command must be a
112
- bare program name - the gateway rejects paths, spaces, and shell characters outright.
113
-
114
- Ship your own packs expecting the same scrutiny, and say so in your README (the `tf_pack` stub
115
- already does).
1
+ # Packaging: ship what you built
2
+
3
+ ToolFunnel's config is a folder. That single fact is the whole packaging story: everything you
4
+ build in a gateway - your tools and their scripts, which of them are promoted hot or hidden, the
5
+ upstream MCPs you attached and the tools you chose to expose from them, the policy hooks that gate
6
+ every call, and the identity the server introduces itself with - lives in one directory tree, the
7
+ **config home**. Ship the tree and you have shipped the product.
8
+
9
+ ## The config home
10
+
11
+ By default the home is the package/clone root - a git clone works exactly as it always did. Point
12
+ the gateway elsewhere and the config lives there instead:
13
+
14
+ ```
15
+ toolfunnel --config-dir D:\my-setup # flag (wins)
16
+ TOOLFUNNEL_HOME=D:\my-setup toolfunnel # env var
17
+ ```
18
+
19
+ An empty home is **seeded** on first use from the shipped defaults (the register + management
20
+ scripts, an empty `mcp/expose.json`, the default hooks manifest) and is **never overwritten**
21
+ afterwards - which is the point: with the home outside the package, `npm update toolfunnel`
22
+ can never eat your tools.
23
+
24
+ ```
25
+ <home>/
26
+ toolfunnel.json # identity (serverName/serverVersion), port defaults, `requires`
27
+ tools/
28
+ tools.register.json # your tool entries (incl. their inputSchema)
29
+ tools.state.json # the hot/hidden/enabled curation - part of the product
30
+ scripts/ # the tool scripts
31
+ mcp/expose.json # upstream references + curated exposure
32
+ hooks/ # the policy gate: manifest + hook scripts
33
+ ```
34
+
35
+ ## `tf_pack` - one call to a deployment artifact
36
+
37
+ The `tf_pack` management tool snapshots the LIVE setup into `<home>/dist/<name>/` - always a
38
+ separate location, never the live tree. `auth/` (environment-specific) and `logs/` never ship.
39
+
40
+ **`{ format: "home" }`** → a portable config home. Zip it, commit it to a repo, or run it as-is:
41
+
42
+ ```
43
+ toolfunnel --config-dir path/to/the-pack
44
+ ```
45
+
46
+ **`{ format: "npm", name: "my-mcp", version: "1.0.0" }`** → a publishable npm package:
47
+
48
+ ```
49
+ dist/my-mcp/
50
+ package.json # name, bin, files, dependencies: { "toolfunnel": "^<current version>" }
51
+ bin/my-mcp.js # 2 lines: pin --config-dir at ../home, delegate to toolfunnel
52
+ home/ # the bundled config home (identity rewritten to my-mcp@1.0.0)
53
+ README.md # a stub to make your own
54
+ ```
55
+
56
+ Review it, then `cd dist/my-mcp && npm publish`. Your users run `npx my-mcp` and get **your**
57
+ server: your name in the MCP `initialize` handshake, your tools with your schemas, your curation,
58
+ and - the part nobody else can say - **your gate**. The hooks travel inside the pack and enforce
59
+ your policy on the recipient's machine regardless of which MCP client calls it.
60
+
61
+ ### Depend, never copy
62
+
63
+ The generated package **depends on** `toolfunnel` (caret range) rather than vendoring it. Your own
64
+ runtime dependencies go in **your** `package.json` exactly as with any npm package - npm installs
65
+ the whole tree on `npx my-mcp`; toolfunnel itself adds zero. Why depending matters:
66
+
67
+ - security and bug fixes reach every wrapped MCP through a normal `npm update` (the pack pins the caret range of the version that built it, so patch releases flow automatically) - no stale forks
68
+ carrying old bugs;
69
+ - your package stays a few KB of config instead of a copied engine;
70
+ - (and honestly) every install of your MCP counts as a toolfunnel download, which keeps the thing
71
+ you depend on alive.
72
+
73
+ Your users can move the bundled home out of `node_modules` (so *their* edits survive *your*
74
+ updates) via the `<NAME>_HOME` env var the generated launcher supports.
75
+
76
+ ## Declaring runtimes: `requires`
77
+
78
+ Your tools may need runtimes the gateway doesn't (python, git, ffmpeg...). Declare them in the
79
+ home's `toolfunnel.json` and they travel with every pack:
80
+
81
+ ```json
82
+ {
83
+ "serverName": "my-mcp",
84
+ "requires": [
85
+ { "command": "python", "min": "3.10", "why": "the pdf-extract tools" },
86
+ { "command": "git" }
87
+ ]
88
+ }
89
+ ```
90
+
91
+ The gateway probes them once at startup and prints one clear stderr line per problem - found
92
+ version vs needed, plus your `why`. Advisory by design: a missing runtime breaks only the tools
93
+ that need it, so the server still starts and everything else keeps working.
94
+
95
+ ## Composite packs
96
+
97
+ A pack is not just your own scripts. Because `mcp/expose.json` lives in the home, a pack carries
98
+ **upstream references too**: "attach `@example/weather-mcp` (pin the version) and expose exactly
99
+ these four tools under these names." The upstream itself travels as a *reference* - `npx` fetches
100
+ it on the recipient's machine - while your **curation** and your **gate** travel as config. One
101
+ pack = your tools + other people's servers, curated, renamed, and policed, as a single artifact.
102
+
103
+ ## Audit honesty
104
+
105
+ Packs spawn commands - that is what they are for. Before you run anyone's pack (including one of
106
+ ours), read **three files** - every command it can run is declared in plain JSON:
107
+
108
+ - `tools/tools.register.json` - the tool invokes (scripts and shell commands);
109
+ - `mcp/expose.json` - the upstream server commands it will spawn;
110
+ - `toolfunnel.json` - the `requires` probes. Note these run **at gateway startup** (each declared
111
+ program is executed once with its version flag), which is why a `requires` command must be a
112
+ bare program name - the gateway rejects paths, spaces, and shell characters outright.
113
+
114
+ Ship your own packs expecting the same scrutiny, and say so in your README (the `tf_pack` stub
115
+ already does).