toolfunnel 0.7.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 (67) hide show
  1. package/README.md +228 -235
  2. package/SECURITY.md +84 -84
  3. package/bin/toolfunnel.js +451 -451
  4. package/docs/KNOWN_BUGS.md +80 -80
  5. package/docs/design.md +84 -84
  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 +39 -39
  10. package/mcp/expose.example.json +30 -30
  11. package/mcp/servers/mock-modern/server.js +147 -147
  12. package/mcp/servers/mock-upstream/server.js +380 -380
  13. package/package.json +67 -67
  14. package/src/auth/config.js +174 -174
  15. package/src/auth/install.js +181 -181
  16. package/src/auth/resource-server.js +353 -353
  17. package/src/core/child-reaper.js +61 -61
  18. package/src/core/config-home.js +134 -134
  19. package/src/core/events.js +105 -105
  20. package/src/core/hook-engine.js +329 -329
  21. package/src/core/hook-loader.js +824 -824
  22. package/src/core/hook-runner.js +566 -566
  23. package/src/core/logger.js +297 -297
  24. package/src/core/matcher.js +100 -100
  25. package/src/core/metrics.js +75 -75
  26. package/src/core/rename-migration.js +163 -163
  27. package/src/core/requires.js +138 -138
  28. package/src/core/server-config.js +98 -98
  29. package/src/extend/add-hook.md +152 -152
  30. package/src/extend/add-mcp.md +119 -119
  31. package/src/extend/configure.md +88 -88
  32. package/src/extend/create-tool.md +168 -168
  33. package/src/extend/howto.js +102 -102
  34. package/src/extend/package.md +190 -190
  35. package/src/extend/wrap.md +99 -99
  36. package/src/mcp/aggregator.js +1719 -1674
  37. package/src/mcp/expose-store.js +577 -577
  38. package/src/mcp/gated-run.js +210 -210
  39. package/src/mcp/http-transport.js +1563 -1563
  40. package/src/mcp/mcp-client.js +1070 -1070
  41. package/src/mcp/modern.js +406 -406
  42. package/src/mcp/protocol.js +468 -468
  43. package/src/mcp/server.js +3112 -3112
  44. package/src/tools/registry.js +661 -661
  45. package/src/tools/tool-state.js +170 -170
  46. package/src/ui/public/app.js +1187 -1187
  47. package/src/ui/public/index.html +286 -286
  48. package/src/ui/public/styles.css +535 -535
  49. package/src/ui/server.js +1616 -1616
  50. package/tools/scripts/base64.js +139 -139
  51. package/tools/scripts/danger.js +61 -61
  52. package/tools/scripts/echo.js +39 -39
  53. package/tools/scripts/hash.js +101 -101
  54. package/tools/scripts/json.js +109 -109
  55. package/tools/scripts/text-stats.js +131 -131
  56. package/tools/scripts/tf-env.js +43 -43
  57. package/tools/scripts/tf-hook-add.js +151 -151
  58. package/tools/scripts/tf-hook-set.js +120 -120
  59. package/tools/scripts/tf-list.js +192 -192
  60. package/tools/scripts/tf-log.js +86 -86
  61. package/tools/scripts/tf-mcp-add.js +117 -117
  62. package/tools/scripts/tf-mcp-set.js +121 -121
  63. package/tools/scripts/tf-pack.js +237 -237
  64. package/tools/scripts/tf-tool-add.js +134 -134
  65. package/tools/scripts/tf-tool-set.js +142 -142
  66. package/tools/scripts/tf-wrap.js +103 -103
  67. package/tools/scripts/uuid.js +102 -102
@@ -1,119 +1,119 @@
1
- # How to register an upstream MCP (self-extension)
2
-
3
- This is the instruction served by `toolfunnel_howto({ topic: "add-mcp" })`. It explains how to connect an
4
- **upstream MCP server** (a browser-automation MCP, a chat bridge, ...) to the aggregator and choose **which of its
5
- tools to expose downstream** to the CLI. All of this is configured in one persisted file:
6
- `mcp/expose.json`.
7
-
8
- The architecture: many upstream MCPs each expose many tools; the
9
- aggregator connects to them and the **MCP Manager picks which tools to surface** to the CLI as
10
- *curated-direct* tools - alongside the four lean meta-tools (`toolfunnel_list_tools`,
11
- `toolfunnel_tool_instructions`, `toolfunnel_run_tool`, `toolfunnel_howto`). The CLI sees a small surface; everything
12
- else stays reachable through the register.
13
-
14
-
15
- ---
16
-
17
- ## 1. `expose.json` - the two blocks
18
-
19
- `expose.json` persists (a) the upstream MCP connections and (b) the curated-direct selection drawn
20
- from them:
21
-
22
- ```json
23
- {
24
- "version": 1,
25
- "upstreams": [
26
- {
27
- "id": "chat",
28
- "transport": "stdio",
29
- "command": "node",
30
- "args": ["vendor/chat-mcp/server.js"],
31
- "env": { "CHAT_DB": "..." },
32
- "enabled": true,
33
- "description": "Chat bridge MCP - chats, messages, send."
34
- },
35
- {
36
- "id": "browser",
37
- "transport": "stdio",
38
- "command": "npx",
39
- "args": ["@example/browser-mcp"],
40
- "enabled": false,
41
- "description": "Browser automation MCP."
42
- }
43
- ],
44
- "expose": [
45
- {
46
- "upstream": "chat",
47
- "tool": "list_chats",
48
- "as": "chat_list_chats",
49
- "category": "chat",
50
- "enabled": true
51
- },
52
- {
53
- "upstream": "chat",
54
- "tool": "send_message",
55
- "as": "chat_send_message",
56
- "category": "chat",
57
- "enabled": true
58
- }
59
- ]
60
- }
61
- ```
62
-
63
- ### `upstreams[]` - one entry per upstream MCP
64
-
65
- | Field | Type | Meaning |
66
- |---|---|---|
67
- | `id` | string | Stable key for this upstream. Referenced by `expose[].upstream`. Unique. |
68
- | `transport` | string | How the aggregator connects. `"stdio"` (spawn `command`+`args`) is the baseline. |
69
- | `command` / `args` / `env` | string / string[] / object | The process the aggregator spawns for a `stdio` upstream. The isolation guard refuses to connect an upstream whose path-shaped **args** (including `--flag=value` values), **cwd**, or code-loading **env** vars (`NODE_OPTIONS`, `NODE_PATH`, and their equivalents) reach outside the config home. (The `command` itself, an executable, is not guarded. Opt-outs, each warned loudly by name: the currently-WRAPPED upstream is exempt; `allowOutsidePaths: true` on the entry exempts its paths; `allowCodeLoadingEnv: true` exempts the env class.) |
70
- | `enabled` | boolean | Whether the aggregator connects to this upstream at startup. |
71
- | `description` | string | Human-facing note for the MCP Manager UI. |
72
-
73
- ### `expose[]` - which upstream tools become curated-direct
74
-
75
- | Field | Type | Meaning |
76
- |---|---|---|
77
- | `upstream` | string | Must match an `upstreams[].id`. |
78
- | `tool` | string | The upstream tool's own name (as the upstream's `tools/list` reports it). |
79
- | `as` | string | The downstream name the CLI sees (namespaced so two upstreams can't collide, e.g. `chat_send_message`). Optional - defaults to `<upstream>_<tool>`. |
80
- | `category` | string | Grouping label, same convention as register entries. |
81
- | `enabled` | boolean | Whether this tool is actually exposed downstream right now. |
82
-
83
- Only the tools listed in `expose[]` with `enabled: true` are surfaced to the CLI as curated-direct
84
- tools. Everything else on the upstream stays connected but hidden - reachable, if needed, by adding
85
- an `expose` entry (no upstream reconnect required to *list*, though see §3 on the CLI surface).
86
-
87
- ---
88
-
89
- ## 2. Steps to register an upstream and expose a tool
90
-
91
- 1. Add an entry to `upstreams[]` with a unique `id`, the `stdio` `command`/`args`, and
92
- `enabled: true`. Keep the server's path-shaped args **inside the config home** (vendor the
93
- server in if it isn't already) - the isolation guard refuses args that escape it.
94
- 2. Let the aggregator connect and run the upstream's `tools/list` (the MCP Manager UI surfaces the
95
- discovered tools).
96
- 3. For each tool you want hot, add an `expose[]` entry: `{ upstream, tool, as, category, enabled:true }`.
97
- 4. Leave the long tail unexposed - the model can still reach those via the register/meta-tools if you
98
- later mirror them, but the *curated-direct* surface stays lean by design (the dial in §1 of the
99
- architecture: directly-exposed = the HOT tools; the register meta-tools = the long tail).
100
-
101
- ---
102
-
103
- ## 3. Curated-direct vs. the register (update cost)
104
-
105
- This matters because the two layers refresh differently:
106
-
107
- - **Curated-direct tools** (what `expose[]` produces) are negotiated with the CLI **at connect time**.
108
- Changing the exposed set therefore needs a **reconnect**, or a `notifications/tools/list_changed`
109
- message *if the CLI honours it* (VERIFY per host; restart is the safe baseline).
110
- - **Register tools** (the `toolfunnel_list_tools`/`run` path) are **always live** - the model re-reads the
111
- register on every `toolfunnel_list_tools` call, so no reconnect is needed.
112
-
113
- Rule of thumb: expose a handful of HOT, high-reliability tools curated-direct; everything else lives
114
- in the register and is reached through the meta-tools.
115
-
116
- > Safety: a curated-direct upstream tool is still ultimately invoked by the model. When you wire
117
- > upstream execution through the host, route it through the same PreToolUse gate the register's `run`
118
- > uses (see `toolfunnel_howto({ topic: "add-hook" })`) so the safety invariant holds uniformly. The gate
119
- > firing on every execution path is a tested invariant, not an assumption.
1
+ # How to register an upstream MCP (self-extension)
2
+
3
+ This is the instruction served by `toolfunnel_howto({ topic: "add-mcp" })`. It explains how to connect an
4
+ **upstream MCP server** (a browser-automation MCP, a chat bridge, ...) to the aggregator and choose **which of its
5
+ tools to expose downstream** to the CLI. All of this is configured in one persisted file:
6
+ `mcp/expose.json`.
7
+
8
+ The architecture: many upstream MCPs each expose many tools; the
9
+ aggregator connects to them and the **MCP Manager picks which tools to surface** to the CLI as
10
+ *curated-direct* tools - alongside the four lean meta-tools (`toolfunnel_list_tools`,
11
+ `toolfunnel_tool_instructions`, `toolfunnel_run_tool`, `toolfunnel_howto`). The CLI sees a small surface; everything
12
+ else stays reachable through the register.
13
+
14
+
15
+ ---
16
+
17
+ ## 1. `expose.json` - the two blocks
18
+
19
+ `expose.json` persists (a) the upstream MCP connections and (b) the curated-direct selection drawn
20
+ from them:
21
+
22
+ ```json
23
+ {
24
+ "version": 1,
25
+ "upstreams": [
26
+ {
27
+ "id": "chat",
28
+ "transport": "stdio",
29
+ "command": "node",
30
+ "args": ["vendor/chat-mcp/server.js"],
31
+ "env": { "CHAT_DB": "..." },
32
+ "enabled": true,
33
+ "description": "Chat bridge MCP - chats, messages, send."
34
+ },
35
+ {
36
+ "id": "browser",
37
+ "transport": "stdio",
38
+ "command": "npx",
39
+ "args": ["@example/browser-mcp"],
40
+ "enabled": false,
41
+ "description": "Browser automation MCP."
42
+ }
43
+ ],
44
+ "expose": [
45
+ {
46
+ "upstream": "chat",
47
+ "tool": "list_chats",
48
+ "as": "chat_list_chats",
49
+ "category": "chat",
50
+ "enabled": true
51
+ },
52
+ {
53
+ "upstream": "chat",
54
+ "tool": "send_message",
55
+ "as": "chat_send_message",
56
+ "category": "chat",
57
+ "enabled": true
58
+ }
59
+ ]
60
+ }
61
+ ```
62
+
63
+ ### `upstreams[]` - one entry per upstream MCP
64
+
65
+ | Field | Type | Meaning |
66
+ |---|---|---|
67
+ | `id` | string | Stable key for this upstream. Referenced by `expose[].upstream`. Unique. |
68
+ | `transport` | string | How the aggregator connects. `"stdio"` (spawn `command`+`args`) is the baseline. |
69
+ | `command` / `args` / `env` | string / string[] / object | The process the aggregator spawns for a `stdio` upstream. The isolation guard refuses to connect an upstream whose path-shaped **args** (including `--flag=value` values), **cwd**, or code-loading **env** vars (`NODE_OPTIONS`, `NODE_PATH`, and their equivalents) reach outside the config home. (The `command` itself, an executable, is not guarded. Opt-outs, each warned loudly by name: the currently-WRAPPED upstream is exempt; `allowOutsidePaths: true` on the entry exempts its paths; `allowCodeLoadingEnv: true` exempts the env class.) |
70
+ | `enabled` | boolean | Whether the aggregator connects to this upstream at startup. |
71
+ | `description` | string | Human-facing note for the MCP Manager UI. |
72
+
73
+ ### `expose[]` - which upstream tools become curated-direct
74
+
75
+ | Field | Type | Meaning |
76
+ |---|---|---|
77
+ | `upstream` | string | Must match an `upstreams[].id`. |
78
+ | `tool` | string | The upstream tool's own name (as the upstream's `tools/list` reports it). |
79
+ | `as` | string | The downstream name the CLI sees (namespaced so two upstreams can't collide, e.g. `chat_send_message`). Optional - defaults to `<upstream>_<tool>`. |
80
+ | `category` | string | Grouping label, same convention as register entries. |
81
+ | `enabled` | boolean | Whether this tool is actually exposed downstream right now. |
82
+
83
+ Only the tools listed in `expose[]` with `enabled: true` are surfaced to the CLI as curated-direct
84
+ tools. Everything else on the upstream stays connected but hidden - reachable, if needed, by adding
85
+ an `expose` entry (no upstream reconnect required to *list*, though see §3 on the CLI surface).
86
+
87
+ ---
88
+
89
+ ## 2. Steps to register an upstream and expose a tool
90
+
91
+ 1. Add an entry to `upstreams[]` with a unique `id`, the `stdio` `command`/`args`, and
92
+ `enabled: true`. Keep the server's path-shaped args **inside the config home** (vendor the
93
+ server in if it isn't already) - the isolation guard refuses args that escape it.
94
+ 2. Let the aggregator connect and run the upstream's `tools/list` (the MCP Manager UI surfaces the
95
+ discovered tools).
96
+ 3. For each tool you want hot, add an `expose[]` entry: `{ upstream, tool, as, category, enabled:true }`.
97
+ 4. Leave the long tail unexposed - the model can still reach those via the register/meta-tools if you
98
+ later mirror them, but the *curated-direct* surface stays lean by design (the dial in §1 of the
99
+ architecture: directly-exposed = the HOT tools; the register meta-tools = the long tail).
100
+
101
+ ---
102
+
103
+ ## 3. Curated-direct vs. the register (update cost)
104
+
105
+ This matters because the two layers refresh differently:
106
+
107
+ - **Curated-direct tools** (what `expose[]` produces) are negotiated with the CLI **at connect time**.
108
+ Changing the exposed set therefore needs a **reconnect**, or a `notifications/tools/list_changed`
109
+ message *if the CLI honours it* (VERIFY per host; restart is the safe baseline).
110
+ - **Register tools** (the `toolfunnel_list_tools`/`run` path) are **always live** - the model re-reads the
111
+ register on every `toolfunnel_list_tools` call, so no reconnect is needed.
112
+
113
+ Rule of thumb: expose a handful of HOT, high-reliability tools curated-direct; everything else lives
114
+ in the register and is reached through the meta-tools.
115
+
116
+ > Safety: a curated-direct upstream tool is still ultimately invoked by the model. When you wire
117
+ > upstream execution through the host, route it through the same PreToolUse gate the register's `run`
118
+ > uses (see `toolfunnel_howto({ topic: "add-hook" })`) so the safety invariant holds uniformly. The gate
119
+ > firing on every execution path is a tested invariant, not an assumption.
@@ -1,88 +1,88 @@
1
- # How to configure ToolFunnel - config files only, no code
2
-
3
- This is the instruction served by `toolfunnel_howto({ topic: "configure" })`. It is the complete
4
- no-code setup map: every behaviour of the gateway is driven by a small set of JSON files at the
5
- **config home**. Editing these files (or driving the UI / the `tf_*` management tools, which write
6
- the same files) is the entire configuration story - there is nothing to program.
7
-
8
- ## 1. The config home
9
-
10
- All files live under one directory, resolved in this order:
11
-
12
- 1. `--config-dir <dir>` on the command line
13
- 2. the `TOOLFUNNEL_HOME` environment variable
14
- 3. the package/repo root (the default for a git clone)
15
-
16
- On first start with a fresh `--config-dir`, the home is seeded with the standard layout. Every
17
- file below is read FRESH per request - edits are live without a restart, except `toolfunnel.json`
18
- (read at boot; restart to apply).
19
-
20
- ## 2. The files
21
-
22
- | File | What it controls | Live? |
23
- |---|---|---|
24
- | `toolfunnel.json` | Identity + default ports (see §3) | restart |
25
- | `mcp/expose.json` | Attached upstream MCPs + which of their tools are curated-direct | live |
26
- | `tools/tools.register.json` | The local tool register (the gateway's own tools) | live |
27
- | `tools/tools.state.json` | Per-tool overlays: enabled / hot / hidden, and the active wrap (`"passthrough"`). Wrapped example: `{ "passthrough": "acme-notes" }`; cleared: `{}` (the file-edit equivalent of `wrap --off`) | live |
28
- | `hooks/hooks.manifest.json` | PreToolUse / PostToolUse policy hooks (the gate) | live |
29
- | `hooks/hooks.state.json` | Per-hook enabled overlay | live |
30
- | `logs/log.config.json` | Activity log on/off + path | live |
31
- | `auth/auth.config.json` | OAuth 2.1 resource-server validation (opt-in; needs `toolfunnel install-oauth` once) | live |
32
-
33
- Shapes: `expose.json` → `toolfunnel_howto({ topic: "add-mcp" })`; register entries →
34
- `toolfunnel_howto({ topic: "create-tool" })`; hook entries → `toolfunnel_howto({ topic: "add-hook" })`;
35
- the wrap field → `toolfunnel_howto({ topic: "wrap" })`.
36
-
37
- ## 3. `toolfunnel.json` - identity & ports
38
-
39
- Every field optional; an absent or empty file IS the default identity. Blank/omitted fields fall
40
- back per-field (a broken file never stops the gateway starting).
41
-
42
- ```json
43
- {
44
- "serverName": "my-mcp", // what CLIENTS see in the initialize handshake (default "toolfunnel")
45
- "serverVersion": "1.0.0", // ...its version (default: package version)
46
- "clientName": "my-client", // what UPSTREAM servers see from the gateway (default "toolfunnel")
47
- "clientVersion": "1.0.0", // ...its version
48
- "httpPort": 9998, // --http default port (default 9998)
49
- "uiPort": 9777 // --ui default port (default 9777)
50
- }
51
- ```
52
-
53
- Precedence: CLI flag > this file > built-in default. Under a WRAP on stdio, the wrapped upstream
54
- is shown the real downstream client's identity automatically - `clientName`/`clientVersion` cover
55
- funnel mode, HTTP, and the boot connection. The UI's **Settings** tab edits this file.
56
-
57
- ## 4. Worked example - a complete gateway from five files
58
-
59
- A gateway that attaches one upstream, exposes two of its tools, gates everything, logs activity,
60
- and introduces itself as `acme-tools`:
61
-
62
- 1. `toolfunnel.json` → `{ "serverName": "acme-tools", "clientName": "acme-tools" }`
63
- 2. `mcp/expose.json` → one `upstreams[]` entry (id, `"transport": "stdio"`, command, args,
64
- `"enabled": true`) + two `expose[]` entries (`{ "upstream", "tool", "enabled": true }`)
65
- 3. `hooks/hooks.manifest.json` → one PreToolUse entry matching the tools you want policy on
66
- 4. `logs/log.config.json` → `{ "enabled": true }`
67
- 5. Start it: `node bin/toolfunnel.js` (stdio) or `--http` / `--ui` as needed.
68
-
69
- Optional per-upstream flags in `expose.json`: `"legacyPin": true` pins that upstream to the
70
- legacy MCP protocol permanently (opt-in, warns loudly); `"env"` passes environment variables to
71
- the spawned server; `"cwd"` sets its working directory; `"timeoutMs"` raises the payload
72
- timeout for that upstream (default 120000 ms - applies to `tools/call`, `prompts/get` and
73
- `resources/read`; under a wrap, a tool that reports progress keeps its call alive regardless.
74
- the separate 10 s handshake/list window detects dead servers, and `"requestTimeoutMs"` raises
75
- it for a server that needs longer to boot); `"modernOnly": true`
76
- requires that upstream to speak the modern (2026-07-28) protocol - the connect fails with a
77
- clear error instead of falling back to legacy (the mirror of `legacyPin`; setting both on one
78
- upstream is refused). Server side, `"serveLegacy": false` in `toolfunnel.json` makes the
79
- gateway itself modern-only: legacy clients are refused with a clear error (default: serve
80
- both eras - only an explicit `false` flips it).
81
-
82
- ## 5. Where the AI-driven and human paths meet
83
-
84
- The UI (all five tabs + Settings), the `tf_*` management tools, and hand-editing these files are
85
- three views of the SAME state - every write goes through the same validated, atomic store code.
86
- Pick whichever fits: config files for repeatable/scripted setups, the UI for humans, the
87
- management tools for in-band agent-driven changes (each documented via
88
- `toolfunnel_tool_instructions`).
1
+ # How to configure ToolFunnel - config files only, no code
2
+
3
+ This is the instruction served by `toolfunnel_howto({ topic: "configure" })`. It is the complete
4
+ no-code setup map: every behaviour of the gateway is driven by a small set of JSON files at the
5
+ **config home**. Editing these files (or driving the UI / the `tf_*` management tools, which write
6
+ the same files) is the entire configuration story - there is nothing to program.
7
+
8
+ ## 1. The config home
9
+
10
+ All files live under one directory, resolved in this order:
11
+
12
+ 1. `--config-dir <dir>` on the command line
13
+ 2. the `TOOLFUNNEL_HOME` environment variable
14
+ 3. the package/repo root (the default for a git clone)
15
+
16
+ On first start with a fresh `--config-dir`, the home is seeded with the standard layout. Every
17
+ file below is read FRESH per request - edits are live without a restart, except `toolfunnel.json`
18
+ (read at boot; restart to apply).
19
+
20
+ ## 2. The files
21
+
22
+ | File | What it controls | Live? |
23
+ |---|---|---|
24
+ | `toolfunnel.json` | Identity + default ports (see §3) | restart |
25
+ | `mcp/expose.json` | Attached upstream MCPs + which of their tools are curated-direct | live |
26
+ | `tools/tools.register.json` | The local tool register (the gateway's own tools) | live |
27
+ | `tools/tools.state.json` | Per-tool overlays: enabled / hot / hidden, and the active wrap (`"passthrough"`). Wrapped example: `{ "passthrough": "acme-notes" }`; cleared: `{}` (the file-edit equivalent of `wrap --off`) | live |
28
+ | `hooks/hooks.manifest.json` | PreToolUse / PostToolUse policy hooks (the gate) | live |
29
+ | `hooks/hooks.state.json` | Per-hook enabled overlay | live |
30
+ | `logs/log.config.json` | Activity log on/off + path | live |
31
+ | `auth/auth.config.json` | OAuth 2.1 resource-server validation (opt-in; needs `toolfunnel install-oauth` once) | live |
32
+
33
+ Shapes: `expose.json` → `toolfunnel_howto({ topic: "add-mcp" })`; register entries →
34
+ `toolfunnel_howto({ topic: "create-tool" })`; hook entries → `toolfunnel_howto({ topic: "add-hook" })`;
35
+ the wrap field → `toolfunnel_howto({ topic: "wrap" })`.
36
+
37
+ ## 3. `toolfunnel.json` - identity & ports
38
+
39
+ Every field optional; an absent or empty file IS the default identity. Blank/omitted fields fall
40
+ back per-field (a broken file never stops the gateway starting).
41
+
42
+ ```json
43
+ {
44
+ "serverName": "my-mcp", // what CLIENTS see in the initialize handshake (default "toolfunnel")
45
+ "serverVersion": "1.0.0", // ...its version (default: package version)
46
+ "clientName": "my-client", // what UPSTREAM servers see from the gateway (default "toolfunnel")
47
+ "clientVersion": "1.0.0", // ...its version
48
+ "httpPort": 9998, // --http default port (default 9998)
49
+ "uiPort": 9777 // --ui default port (default 9777)
50
+ }
51
+ ```
52
+
53
+ Precedence: CLI flag > this file > built-in default. Under a WRAP on stdio, the wrapped upstream
54
+ is shown the real downstream client's identity automatically - `clientName`/`clientVersion` cover
55
+ funnel mode, HTTP, and the boot connection. The UI's **Settings** tab edits this file.
56
+
57
+ ## 4. Worked example - a complete gateway from five files
58
+
59
+ A gateway that attaches one upstream, exposes two of its tools, gates everything, logs activity,
60
+ and introduces itself as `acme-tools`:
61
+
62
+ 1. `toolfunnel.json` → `{ "serverName": "acme-tools", "clientName": "acme-tools" }`
63
+ 2. `mcp/expose.json` → one `upstreams[]` entry (id, `"transport": "stdio"`, command, args,
64
+ `"enabled": true`) + two `expose[]` entries (`{ "upstream", "tool", "enabled": true }`)
65
+ 3. `hooks/hooks.manifest.json` → one PreToolUse entry matching the tools you want policy on
66
+ 4. `logs/log.config.json` → `{ "enabled": true }`
67
+ 5. Start it: `node bin/toolfunnel.js` (stdio) or `--http` / `--ui` as needed.
68
+
69
+ Optional per-upstream flags in `expose.json`: `"legacyPin": true` pins that upstream to the
70
+ legacy MCP protocol permanently (opt-in, warns loudly); `"env"` passes environment variables to
71
+ the spawned server; `"cwd"` sets its working directory; `"timeoutMs"` raises the payload
72
+ timeout for that upstream (default 120000 ms - applies to `tools/call`, `prompts/get` and
73
+ `resources/read`; under a wrap, a tool that reports progress keeps its call alive regardless.
74
+ the separate 10 s handshake/list window detects dead servers, and `"requestTimeoutMs"` raises
75
+ it for a server that needs longer to boot); `"modernOnly": true`
76
+ requires that upstream to speak the modern (2026-07-28) protocol - the connect fails with a
77
+ clear error instead of falling back to legacy (the mirror of `legacyPin`; setting both on one
78
+ upstream is refused). Server side, `"serveLegacy": false` in `toolfunnel.json` makes the
79
+ gateway itself modern-only: legacy clients are refused with a clear error (default: serve
80
+ both eras - only an explicit `false` flips it).
81
+
82
+ ## 5. Where the AI-driven and human paths meet
83
+
84
+ The UI (all five tabs + Settings), the `tf_*` management tools, and hand-editing these files are
85
+ three views of the SAME state - every write goes through the same validated, atomic store code.
86
+ Pick whichever fits: config files for repeatable/scripted setups, the UI for humans, the
87
+ management tools for in-band agent-driven changes (each documented via
88
+ `toolfunnel_tool_instructions`).