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
package/README.md CHANGED
@@ -1,130 +1,119 @@
1
1
  # ToolFunnel
2
2
 
3
- ![ToolFunnel](assets/logo.png) <!-- logo asset (assets/logo.png) is added separately -->
3
+ ![ToolFunnel](assets/logo.png)
4
4
 
5
- > A zero-dependency* MCP gateway: host your own tools, forward and curate tools from other MCP servers, expose them **leanly** to cut agent token cost, and gate every call through your own policy hooks before it runs.
5
+ > A zero-dependency MCP and Local tool gateway: host your own tools in any language, forward and curate tools from other MCP servers, expose them leanly to cut agent token cost, gate every call through your own policy hooks before it runs, create your own MCP servers easily with zero code - and **wrap any MCP server so it speaks both protocol eras, invisibly**.
6
6
 
7
- **Zero code, zero dependencies: your own MCP server in 60 seconds** — from three ordinary scripts to a named, packaged, policy-gated MCP server (everything on screen is real output; see [demo/](demo/)):
7
+ **Zero code, zero dependencies: your own MCP server in 60 seconds** - from three ordinary scripts to a named, packaged, policy-gated MCP server (everything on screen is real output; see [demo/](demo/)):
8
8
 
9
9
  ![ToolFunnel in 60 seconds: scripts → register → identity → live MCP server → npm package](demo/toolfunnel-demo.gif)
10
10
 
11
+ **📖 Full documentation: [the ToolFunnel User Manual](docs/MANUAL.pdf)** - installation, the web UI, writing tools, attaching MCP servers, the policy gate, packaging, and troubleshooting, all illustrated with screenshots and worked examples. This README is the overview; the manual is the reference.
11
12
 
12
13
  ## The problem
13
14
 
15
+ Toolfunnel is designed to solve multiple issues in one package :)
16
+
14
17
  The Model Context Protocol (MCP) lets an AI agent call tools from many servers. But every connected server dumps **all** of its tool schemas into the model's context on every turn. Connect a handful of rich MCP servers and you've spent thousands of tokens describing tools the agent won't use this turn - slower, costlier, noisier.
15
18
 
16
- Similarly, working with AI I found myself generating many many tools, some of which I wanted to use with different AI workflows, some I didnt, and I didnt want to keep setting them up, I also didnt wish to keep many different setups for different workflows. Therefore ToolFunnel was born so that all of my multi-use tools and common MCP servers can be in one place, with any workflow, and I can easily select which I want to use with whatever workflow. You can also change or add tools during a session with simple toggles in the UI. I also wanted to wire up and test an MCP *live*, in the running session, without resetting the CLI or restarting anything - and have its tools show up in the tools list straight away; ToolFunnel does exactly that.
19
+ Similarly, working with AI I found myself generating many many tools, some of which I wanted to use with different AI workflows, some I didnt, and I didnt want to keep setting them up, I also didnt wish to keep many different setups for different workflows. Therefore ToolFunnel was born so that all of my multi-use tools and common MCP servers can be in one place, with any workflow, and I can easily select which I want to use with whatever workflow and even change or add tools during a session with simple toggles in the UI. I also wanted to wire up and test MCPs *live*, in the running session, without resetting the CLI or restarting anything - and have their tools show up in the tools list straight away; ToolFunnel does exactly that.
17
20
 
18
21
  Often also, there's also no consistent way to **govern** what an agent may run: hooks and policies live in the *host* (a specific CLI or otherwise), so they don't travel when you switch clients - with ToolFunnel, this is easy because your hooks travel with the tools - it can become your one swiss-army knife for many different workflows.
19
22
 
20
- Finally, I didnt want to audit huge numbers of dependencies - a personal choice, yes - so I wanted something that could be audited quickly and easily.
23
+ Additionally, I wanted an easy and consistent way to package my own tools and create new MCP servers from other tools, whatever language they were written in. FastMCP could do that, but not everything is written in python. I also didnt want to audit huge numbers of dependencies - a personal choice, yes - so I wanted something that could be audited quickly and easily.
24
+
25
+ Finally, when the MCP changes were announced as breaking changes, it became apparent this would eventually break a lot of MCP servers and/or clients out there, many of which might not be actively maintained. Not only that but changing MCP protocol generations takes time and might take considerable effort depending on the complexity of the server - therefore I added a quick, simple single command tool that allows ToolFunnel to wrap an MCP and easily translate between new and legacy protocols - quick, low effort, maximum gain.
21
26
 
22
- At the time of writing, I have 14 MCP tools and 93 local tools all accessed through toolfunnel, gated as required.
27
+ At the time of writing, I have 14 MCP tools and 98 local tools all accessed through toolfunnel, gated as required.
23
28
 
24
29
  ## What ToolFunnel does
25
30
 
26
31
  ToolFunnel is one small MCP server that sits between your agent and everything else:
27
32
 
28
33
  1. **Hosts your own tools** - define first-party tools in a JSON register and serve them directly. Seven demo tools ship in the box.
29
- 2. **Forwards other MCP servers - leanly.** Attach an upstream MCP and its tools appear in the same lean register as your own (briefs + instructions-on-demand), runnable through `toolfunnel_run_tool` and the gate. Curate which appear, promote a chosen few to top-level "every-turn" tools, or leave them lean by default. Every forwarded call still passes the gate.
30
- 3. **Lean register** - the agent sees short tool *briefs*; the full instructions for a tool are fetched **on demand**, so context stays small. *(This is the token saver.)*
31
- 4. **Server-side policy gate** - every server-side execution path fires your PreToolUse / PostToolUse hooks **inside the gateway**, so your policy works on *any* client, not just hosts that support hooks. The gate travels with the gateway, and **fails closed**.
32
- 5. **Configure by file, UI, or in-band** - plain JSON files, an optional loopback web UI (`node bin/toolfunnel.js --ui`), or nine in-band **management functions** all add / curate / toggle tools, upstreams, and hooks - live, no restart - and package the whole setup for deployment.
33
- 6. **Audit when you want it** - a toggleable JSONL log (**default off**) records tool runs, every gate allow/deny decision, and every upstream connect / disconnect / reconnect.
34
- 7. **Live & self-healing** - attach/curate/toggle tools, upstreams, and hooks on a *running* gateway with no restart; and if an attached MCP's process dies, the gateway detects it and reconnects in the background with backoff.
34
+ 2. **Forwards other MCP servers - leanly.** Attach an upstream MCP and its tools appear in the same lean register as your own, runnable through the gate. Curate which appear, promote a chosen few to top-level "every-turn" tools, or leave them lean by default.
35
+ 3. **Lean register** - the agent sees short tool *briefs*; the full instructions for a tool are fetched on demand, so context stays small. *(This is the token saver.)*
36
+ 4. **Server-side policy gate** - every server-side execution path fires your PreToolUse / PostToolUse hooks inside the gateway, so your policy works on any client, not just hosts that support hooks. The gate travels with the gateway, and fails closed.
37
+ 5. **Configure by file, UI, or in-band** - plain JSON files, an optional loopback web UI, or ten in-band management functions (`tf_*`) all add / curate / toggle tools, upstreams, and hooks - live, no restart - and package the whole setup for deployment.
38
+ 6. **Audit when you want it** - a toggleable JSONL log (default off) records tool runs, every gate allow/deny decision, and every upstream connect / disconnect / reconnect.
39
+ 7. **Live & self-healing** - attach/curate/toggle on a running gateway with no restart; if an attached MCP's process dies, the gateway detects it and reconnects in the background with backoff.
40
+ 8. **Build your own MCP server, no code, no SDK** - assemble scripts and curated upstream tools, switch the meta-tools off, and ToolFunnel *is* your MCP server; `tf_pack` ships it as a `npx`-installable npm package.
41
+ 9. **Wrap any MCP server** - one command turns ToolFunnel into a transparent, dual-era wrapper for a single MCP server: modern clients can use legacy servers, legacy clients can use modern servers, and neither side can tell ToolFunnel is there. *(The headline of 0.6.0 - see below.)*
35
42
 
36
- ### The meta-tool surface
43
+ ## Under the hood
37
44
 
38
- The model never sees the long tail of tools directly. It sees a tiny, fixed surface of **meta-tools**, and reaches the real tools through them:
45
+ Three claims in this README carry the most weight, and each one has a proof you can run:
39
46
 
40
- | Meta-tool | Args | Returns |
41
- |--------------------------------|-----------------------|------------------------------------------------------|
42
- | `toolfunnel_list_tools` | `{ filter?, category? }` | Briefs only: `[{ id, name, summary, category }]` |
43
- | `toolfunnel_tool_instructions` | `{ name }` | The full usage instructions for one tool, on demand |
44
- | `toolfunnel_howto` | `{ topic }` | A self-extension guide (`create-tool`, `add-mcp`, `add-hook`, `package`) |
45
- | `toolfunnel_run_tool` | `{ name, args }` | Runs a register tool **through the gate** |
47
+ - **The gate fails closed.** A PreToolUse deny means the tool's execute path is never entered. The test suite proves it directly: a tool plants a side-effect file, the gate denies the call, and the test asserts the file never appears.
48
+ - **The wrap is tested at the wire, against servers we didn't write.** Elicitation bridging, cancel translation into the upstream's own request ids, identity mirroring and subscription replay all run over real pipes in the suite, including integration tests against the official MCP SDK client and a real npx-launched third-party server.
49
+ - **The suite runs on 3 operating systems.** CI covers Linux, macOS and Windows across Node 18, 20 and 22 (35 test files). `npm test` runs the same suite locally.
46
50
 
47
- All **four** meta-tools are advertised in `tools/list`. The flow is: list the briefs, read one tool's instructions on demand, then run it - either **yourself** in your own environment per the instructions (a *reference* tool), or via `toolfunnel_run_tool`, which executes the tool server-side **through the gate** (a *gateway* tool - see [Execution mode](#execution-mode-reference-vs-gateway)). Any **curated upstream** tools you expose are advertised alongside the meta-tools and run through the same gate.
51
+ The reasoning behind the bigger design decisions (why no SDK, why fail-closed, why zero dependencies, where the isolation boundary sits) is in [docs/design.md](docs/design.md).
48
52
 
49
- ### The visibility matrix
53
+ ## Wrap any MCP server - one command, both protocol eras, invisible
50
54
 
51
- Every tool - your own, a forwarded upstream tool, **and** the four meta-tools - has three independent visibility dials, so you decide exactly what each workflow sees:
55
+ The 2026-07-28 MCP revision is a **breaking** protocol change: it removes the `initialize` handshake and sessions that every earlier server and client is built on. When clients upgrade, unmaintained legacy servers stop working with them; older clients can't talk to new-style servers at all. A lot of good tools are going to get stranded on the wrong side of that line.
52
56
 
53
- | Axis | Surface it controls | Default |
54
- |------|---------------------|---------|
55
- | **enabled** | **Lean-visible** - appears in `toolfunnel_list_tools` and is runnable. | on |
56
- | **hot** | **Top-level / every-turn** - promoted into `tools/list`, so the agent sees its full schema on *every* turn and can call it **directly** (no `toolfunnel_run_tool` hop). | off (meta-tools: **on**) |
57
- | **hidden** | Manager-list declutter only (the UI / `tf_list`) - doesn't change what the agent sees. Hidden tools are omitted from `tf_list` and the UI Tools list by default (a "show hidden" toggle / `includeHidden` reveals them). | off |
57
+ `toolfunnel wrap <server>` un-strands them:
58
58
 
59
- The dials live per tool in `tools/tools.state.json`, keyed by the tool's **surfaced name** (a local id, an upstream's `<upstream>_<tool>` or its `as`, or a meta-tool name), and are read **fresh per call** - so a toggle is live with no restart. A disabled tool is never hot.
59
+ ```
60
+ toolfunnel wrap my-old-server
61
+ ```
60
62
 
61
- This is what lets ToolFunnel be **a lean register and a conventional MCP at the same time**:
63
+ Watch it happen (everything on screen is real output):
62
64
 
63
- - **Default (lean).** The four meta-tools are top-level; everything else is lean - short briefs, schemas on demand. Token cost stays flat as you add tools.
64
- - **Promote the few you call constantly.** Flip `hot` on a tool and it joins the every-turn surface, directly callable like any native MCP tool - while the long tail stays lean. A promoted call still passes the gate (a hot local tool through `toolfunnel_run_tool`'s gated path; a hot upstream tool forwarded through the same gate).
65
- - **Turn ordinary tools into a plain MCP.** Promote a set of local/upstream tools **and** hide the four meta-tools (`hot:false`), and ToolFunnel presents exactly those tools as a normal top-level MCP server - no meta-tools, no `toolfunnel_run_tool` hop - assembled from scripts and *other* MCPs with no SDK and no Python.
65
+ ![toolfunnel wrap in 22 seconds: one command, then the client sees the wrapped server's own identity, tools, and results](demo/toolfunnel-wrap-demo.gif)
66
66
 
67
- Two footguns the UI warns about: hiding the meta-tools leaves the agent unable to *discover* tools by name (intended for the "ordinary MCP" pattern, a mistake otherwise), and promoting *many* tools re-introduces the context bloat the lean register exists to avoid. Promote/demote from the **Tools** tab (a per-tool **Hot** toggle plus the top-level **surface panel**), from the **MCPs** tab (per discovered upstream tool), or with `tf_tool_set { id, action: "hot" | "unhot" | "hide" | "unhide" }`. (A hidden meta-tool is also un-callable, not just unlisted - the lockdown is real.)
67
+ That's it - zero configuration. The name is an attached upstream's id (not attached yet? that's one `tf_mcp_add` call or UI row first). The command probes the server and tells you which protocol era(s) it speaks; if the server needs paths outside the gateway root (a filesystem server serving your documents folder, say) you get a clear security notice up front explaining exactly what that means. Changed your mind, or just looking?
68
68
 
69
- There's a secondary use, too: because the four ToolFunnel meta-tools are themselves switchable, you can set up your own tools and schemas, promote the ones you want hot, then switch the meta-tools off so only your tools are visible over MCP. That makes ToolFunnel a viable alternative to MCP-Anything for assembling a mixed toolset into a single server :)
69
+ ```
70
+ toolfunnel wrap # show what's currently wrapped
71
+ toolfunnel wrap --off # undo - restore the normal ToolFunnel surface
72
+ toolfunnel wrap my-old-server --as legacy-tools # present a name of your choosing
73
+ ```
70
74
 
71
- ### The demo tools
75
+ The wrap survives restarts, and the web UI has the same controls (Wrap / Unwrap on the MCPs tab). ToolFunnel becomes that server, for both eras at once:
72
76
 
73
- Seven first-party demo tools ship in `tools/scripts/`, declared in `tools/tools.register.json` (which also declares the eight [management functions](#management-functions) below):
77
+ - **Any client, any server, any combination.** Legacy client → modern server, modern client → legacy server, or matched pairs - all four combinations work through the same wrap. The gateway speaks both dialects natively and translates between them.
78
+ - **Invisible from both sides.** The client sees the wrapped server's own identity, tools, results, errors, and notifications - byte-for-byte, verified against real published servers. The server sees a normal client with your client's identity. No renamed tools, no injected prefixes, no "via toolfunnel" tells - the wrap presents the server *as itself*. Tool calls wait up to 120 s by default and a progress-reporting tool keeps its call alive indefinitely; for silent tools that run longer, set `"timeoutMs"` on the upstream in `expose.json`, and for a server that needs longer than 10 s to boot before it can answer its handshake, set `"requestTimeoutMs"` the same way.
79
+ - **The hard parts are bridged, not dropped.** Mid-call user prompts (elicitation) from a legacy server are translated into the modern retry pattern and back - for modern clients; a legacy client gets a clean decline rather than a hang (the bridge targets the era gap, and legacy-to-legacy relay is on the roadmap). Resource subscriptions survive - and are silently re-established if the wrapped server crashes and reconnects. Progress tokens flow through. Cancellations are translated into the server's own request ids.
80
+ - **Still governable when you want it.** Wrapping doesn't switch off the gate: every call can still pass your PreToolUse hooks, and every tool still has its visibility dials - hide a dangerous tool, and it vanishes from the wrapped surface too. Transparency is the default; the levers are opt-in.
74
81
 
75
- | Tool | Category | What it does |
76
- |-------------------|-------------|----------------------------------------------------|
77
- | `echo` | demo | Return the provided args back unchanged |
78
- | `base64` | encoding | Encode text to / decode text from Base64 |
79
- | `hash` | crypto | Compute a digest (default sha256) of input text |
80
- | `uuid` | generators | Generate one or more RFC 4122 v4 UUIDs |
81
- | `json` | data | Validate and pretty-print a JSON string |
82
- | `text-stats` | text | Count characters, words, and lines |
83
- | `danger` | demo | A deliberately "destructive" demo to prove the gate (its only side effect is appending a line to the file named by `TOOLFUNNEL_DANGER_LOG`) |
82
+ Use it to keep a favourite unmaintained server alive past the cutover, to give a modern-only server to your older tooling, or just to put a policy gate in front of a server you didn't write - without its client ever knowing. One related dial: if an upstream should *stay* on the old protocol forever, set `legacyPin` on it (a toggle in the UI, or one field in `mcp/expose.json`) and the gateway will never auto-upgrade it - opt-in, and it warns loudly so nobody forgets it's there. Full details (including the security model for wrapped servers with filesystem access): the [manual](docs/MANUAL.pdf).
84
83
 
85
- **Script contract.** Each tool is a standalone Node script that reads its structured args from the environment variable `TOOLFUNNEL_TOOL_ARGS` (a JSON string), prints **exactly one JSON line** to stdout, and exits `0`. The register resolves a tool's `invoke: { type: "script", path: "scripts/<file>" }` to a `node <script>` spawn, and that spawn only happens after the PreToolUse gate allows it.
84
+ ## Build your own MCP server - no code, no SDK
86
85
 
87
- They're just here to get you started - turn any of them off or delete them as you like :)
86
+ ToolFunnel is a no-code MCP builder. Point it at a few scripts (any language), curate a set of tools from other MCP servers, or mix both, then switch its own four meta-tools off - and what your agent connects to is a plain, top-level MCP server presenting exactly your chosen tools. No decorators, no framework, no `@tool` boilerplate, no Python. One `tf_pack` call turns it into a publishable npm package your users install with `npx your-mcp`.
88
87
 
89
- ### Management functions
88
+ That puts ToolFunnel head to head with a framework like FastMCP for the "I just want to stand up an MCP server" job, from the opposite direction:
90
89
 
91
- The gateway can manage **itself**. Nine first-party tools in the `management` category let an agent (or the web UI) add, curate, toggle, and package the gateway's own configuration. They are ordinary register tools - reached through `toolfunnel_run_tool`, so every call passes the same gate as any other gateway tool (a single PreToolUse matcher such as `tf_.*` locks the whole management surface down, since each is named `tf_*`).
90
+ - **FastMCP:** write Python, decorate your functions, ship a framework dependency.
91
+ - **ToolFunnel:** declare your tools in JSON (or let the AI author them for you), and you have a gated MCP server - assembled from scripts *and* other people's MCP servers - with zero runtime dependencies and a policy gate built in.
92
92
 
93
- | Function | What it does |
94
- |----------------|------------------------------------------------------------------------------|
95
- | `tf_tool_add` | Register a new tool (optionally authoring its script body under `tools/scripts/`). Live, no reconnect. |
96
- | `tf_tool_set` | Enable, disable, **promote/demote on the every-turn surface (`hot`/`unhot`)**, **hide/unhide from the manager views (`hide`/`unhide`)**, or remove a register tool (the enable/disable overlay is default-ON; `hot`/`hidden` are default-OFF). |
97
- | `tf_mcp_add` | Register an upstream MCP and optionally curate some of its tools to expose. |
98
- | `tf_mcp_set` | Enable, disable, or remove an upstream MCP (remove cascades its expose entries). |
99
- | `tf_hook_add` | Add a hook manifest entry (e.g. a PreToolUse gate), optionally authoring the script under `hooks/scripts/`. |
100
- | `tf_hook_set` | Enable, disable, or remove a hook (live toggles via the `hooks.state.json` overlay). |
101
- | `tf_list` | List register tools, upstream MCPs, or hooks with their live active state (read-only). Hidden tools are omitted by default - pass `{ includeHidden: true }` to show them. |
102
- | `tf_log` | Enable, disable, or check the [audit log](#activity--audit-log) (`{ action: enable\|disable\|status }`). |
103
- | `tf_pack` | Package the live setup into a deployment artifact under `dist/` - a portable config home, or a publishable npm package wrapping toolfunnel. See [Packaging](#packaging-ship-your-own-mcp). |
93
+ If you can write a script, you can ship an MCP. The full walkthrough is in the [manual](docs/MANUAL.pdf); the short version is `toolfunnel_howto({ topic: "create-tool" })` and `tf_pack`.
104
94
 
105
- Tool / hook register edits are **live** (re-read per call, no reconnect). Upstream attach/curate changes are picked up live too: the gateway watches its config and reloads - reconnecting upstreams and emitting `notifications/tools/list_changed` - with no restart.
95
+ ### The meta-tool surface
106
96
 
107
- ### Execution mode: reference vs gateway
97
+ The model never sees your long tail of tools directly. It sees four fixed **meta-tools** - `toolfunnel_list_tools`, `toolfunnel_tool_instructions`, `toolfunnel_howto`, `toolfunnel_run_tool` - and reaches the real tools through them: list the briefs, read one tool's instructions on demand, then run it through the gate. Any curated upstream tools you expose are advertised alongside them and run through the same gate.
108
98
 
109
- Each register tool resolves to one of two execution modes (set the optional `mode` field, or let it be **inferred backward-compatibly**):
99
+ ### The visibility matrix
110
100
 
111
- - **`reference`** - ToolFunnel only **describes** the tool. `toolfunnel_run_tool` returns the tool's instructions and the connected AI performs the action in **its own environment**. Nothing runs server-side, so **the gate does not fire** (there is nothing to gate). A reference tool needs no `invoke`. This is the lean default for a tool that carries no executable body.
112
- - **`gateway`** - ToolFunnel **executes** the tool server-side via the gated run path (`toolfunnel_run_tool` → PreToolUse gate → run → PostToolUse). This is the opt-in mode for tools the gateway should actually run.
101
+ Every tool - your own, a forwarded upstream tool, and the four meta-tools - has three independent visibility dials: **enabled** (lean-visible), **hot** (promoted to the every-turn `tools/list` surface, directly callable), and **hidden** (manager-list declutter). Toggles are live, no restart.
113
102
 
114
- When `mode` is omitted it is inferred so existing tools keep working unchanged: a `script` or `shell` invoke ⇒ `gateway`; anything else ⇒ `reference`. The seven demo tools and eight management functions all carry script invokes, so they resolve to `gateway`. The resolved mode is shown (and switchable) per tool in the web UI.
103
+ This is what lets ToolFunnel be a lean register and a conventional MCP at the same time: keep the long tail lean, promote the few tools you call constantly, or promote your chosen set and switch the meta-tools off to present a plain top-level MCP server (the no-code-MCP posture above). Two footguns the UI warns about: hiding the meta-tools removes the agent's ability to discover tools by name, and promoting many tools reintroduces the context bloat the lean register exists to avoid.
115
104
 
116
105
  ### The gate
117
106
 
118
- Every server-side run-path - a `gateway` tool run via `toolfunnel_run_tool` (including the management functions) **and** every forwarded curated-upstream call - funnels through `src/mcp/gated-run.js`. (`reference` tools execute nothing here, so they short-circuit before the gate.)
107
+ Every server-side run path - your tools, the management functions, and every forwarded upstream call - funnels through one gate:
119
108
 
120
109
  ```
121
110
  gatedRun({ engine, ctx, toolName, args, execute })
122
- → fire PreToolUse (may BLOCK - fails CLOSED if the engine errors)
111
+ → fire PreToolUse (may BLOCK; fails CLOSED if the engine errors)
123
112
  → execute() (ONLY if allowed)
124
- → fire PostToolUse (advisory - cannot un-run the tool)
113
+ → fire PostToolUse (advisory; cannot un-run the tool)
125
114
  ```
126
115
 
127
- Hooks live in `hooks/hooks.manifest.json` (empty manifest = allow-all). They're matched against the tool name (`src/core/matcher.js`) and run by `src/core/hook-runner.js`, which speaks the Claude-Code hook protocol: a hook command reads the event JSON on stdin and either exits `2` to block (with a reason on stderr), or exits `0` and optionally returns JSON (`{ "decision": "block", ... }` or `{ "hookSpecificOutput": { "permissionDecision": "deny" | "allow", ... } }`). The load-bearing invariant, proven by test: **a PreToolUse deny means `execute()` is never called.**
116
+ Hooks speak the Claude-Code hook protocol (event JSON on stdin, exit `2` to block), so an existing hook travels here unchanged. The load-bearing invariant, proven by test: a PreToolUse deny means `execute()` is never called. The shipped example hooks are **disabled by default** - enable one in `hooks/hooks.manifest.json` (or the UI's Hooks tab) to turn policy on. Writing hooks, matchers, and worked examples: [manual](docs/MANUAL.pdf).
128
117
 
129
118
  ## Why it's different
130
119
 
@@ -143,19 +132,21 @@ A quick feature comparison as of June 2026:
143
132
  | Zero runtime dependencies\* | ✓ (Node, no SDK) | ✗ (framework) | ✓ (Go) | ✗ (Docker) | ✓ (Go) |
144
133
  | Runtime dependencies (installed / bundled) | **0\*** | many | 40+ (bundled) | many | bundled (Go) |
145
134
  | Config web UI | ✓ | ✗ | ✓ | ✓ | ✗ |
135
+ | Dual-era protocol (2026-07-28 + legacy), client & server side | ✓ | ✗ | ✗ | ✗ | ✗ |
136
+ | Transparent single-server wrap (invisible, era-bridging) | ✓ | ✗ | ✗ | ✗ | ✗ |
146
137
 
147
- **Beyond the dependency count, four capability choices set ToolFunnel apart from a pure proxy like mcpproxy-go:**
138
+ Beyond the dependency count, four capability choices set ToolFunnel apart from a pure proxy like mcpproxy-go:
148
139
 
149
- 1. **It HOSTS, not just proxies.** A proxy/aggregator forwards tools from *existing* MCP servers. ToolFunnel also turns an arbitrary local command or script - any language - into a first-class **gated** MCP tool from one JSON entry, with no SDK and no pre-existing server.
150
- 2. **Reference mode.** A tool ToolFunnel only *describes*: `toolfunnel_run_tool` hands back the instructions and the connected AI performs the action in **its own** environment - and the **handoff itself is gated** (a PreToolUse deny withholds the instructions). Nothing executes server-side.
151
- 3. **A portable policy gate, not a content scanner.** The gate speaks the **Claude-Code hook protocol** (PreToolUse/PostToolUse, exit-2-to-block) *inside* the gateway, so an existing hook/policy travels to **any** client unchanged - an easily adaptable (to CODEX or whatever you are using) programmable policy that ports, rather than a built-in scanner.
152
- 4. **The visibility matrix.** Not on/off filtering - a lean default you can selectively **promote to the every-turn surface**, **hide** from the manager view, or collapse entirely (turn the meta-tools off and present your tools as a plain top-level MCP).
140
+ 1. **It HOSTS, not just proxies.** A proxy forwards tools from existing MCP servers. ToolFunnel also turns an arbitrary local command or script, any language, into a first-class gated MCP tool from one JSON entry, with no SDK and no pre-existing server.
141
+ 2. **Reference mode.** A tool ToolFunnel only *describes*: `toolfunnel_run_tool` hands back the instructions and the connected AI performs the action in its own environment - and the handoff itself is gated (a PreToolUse deny withholds the instructions). Nothing executes server-side.
142
+ 3. **A portable policy gate, not a content scanner.** The gate speaks the Claude-Code hook protocol (PreToolUse/PostToolUse, exit-2-to-block) inside the gateway, so an existing hook/policy travels to any client unchanged - an easily adaptable (to CODEX or whatever you are using) programmable policy that ports, rather than a built-in scanner.
143
+ 4. **The visibility matrix.** Not on/off filtering: a lean default you can selectively promote to the every-turn surface, hide from the manager view, or collapse entirely (turn the meta-tools off and present your tools as a plain top-level MCP).
153
144
 
154
- Underneath it all: a **lean** surface (briefs + instructions-on-demand) that keeps token cost flat as you add tools, a **fail-closed** gate on every server-side path, and **zero runtime dependencies\*** in a component that sits in a privileged position. The wedge is the whole package in one small, auditable server - not any single trick.
145
+ Underneath it all: a lean surface (briefs + instructions-on-demand) that keeps token cost flat as you add tools, a fail-closed gate on every server-side path, and zero runtime dependencies\* in a component that sits in a privileged position. The wedge is the whole package in one small, auditable server, not any single trick.
155
146
 
156
147
  ### State of development and honest limitations
157
148
 
158
- ToolFunnel is a **focused solo build**. I've tested it thoroughly for my own use case, but it may still contain bugs outside of that - and there are features I've leaned on less than others. The OAuth 2.1 and Streamable-HTTP implementations in particular are recent, added in line with the latest MCP SDK capabilities, and haven't been tested as extensively, so your mileage may vary.
149
+ ToolFunnel is a focused solo build. I've tested it thoroughly for my own use case, but it may still contain bugs outside of that - and there are features I've leaned on less than others. The OAuth 2.1 and Streamable-HTTP implementations in particular are recent, added in line with the latest MCP SDK capabilities, and haven't been tested as extensively, so your mileage may vary.
159
150
 
160
151
  There are no Prometheus/OpenTelemetry metrics either - instead there's a toggleable JSONL audit log plus in-memory call counters on `/health`. I haven't needed full metrics for my own use; if it's something you'd want, let me know and I'll look at adding it.
161
152
 
@@ -166,176 +157,74 @@ And because it's hand-rolled rather than built on the MCP SDK, I'll need to keep
166
157
  ## Use cases
167
158
 
168
159
  - **Tame context bloat.** Several rich MCP servers connected at once drown the agent's context in tool schemas it won't use this turn. Behind ToolFunnel the agent sees short briefs and loads a tool's full schema only when it actually reaches for it.
169
- - **One toolbox, many workflows.** Keep all your multi-use tools and common MCP servers in one place and pick which to surface per workflow - instead of re-wiring a different setup for every client.
170
- - **Govern what an agent may run - on any client.** Put a fail-closed PreToolUse policy *in the gateway* so it travels with your tools, even to clients that have no hook system of their own.
171
- - **Turn a script into an MCP tool without building an MCP.** Have a useful CLI, a bash one-liner, or a script in any language? Declare it in a JSON entry and it's a gated MCP tool - no protocol code, no SDK (`toolfunnel_howto({ topic: "create-tool" })` shows the shapes).
172
- - **Add a tool mid-session.** Need a tool while you're working? Drop it in and it's live on the next turn - no restart.
173
- - **Survive an upstream crash.** If an attached MCP server dies mid-session, ToolFunnel notices, reconnects it in the background with backoff, and re-advertises its tools - no restart, no dropped session. Most reconnect logic elsewhere is Docker-scoped; this isn't.
160
+ - **One toolbox, many workflows.** Keep all your multi-use tools and common MCP servers in one place and pick which to surface per workflow, instead of re-wiring a different setup for every client.
161
+ - **Govern what an agent may run, on any client.** Put a fail-closed PreToolUse policy in the gateway so it travels with your tools, even to clients that have no hook system of their own.
162
+ - **Turn a script into an MCP tool without building an MCP.** Have a useful CLI, a bash one-liner, or a script in any language? Declare it in a JSON entry and it's a gated MCP tool - no protocol code, no SDK.
163
+ - **Keep a legacy internal MCP working.** Front an ageing in-house MCP server with ToolFunnel and keep the workflows that depend on it running as the ecosystem moves on.
164
+ - **Add a tool mid-session.** Need a tool while you're working? Drop it in and it's live on the next turn, no restart.
165
+ - **Survive an upstream crash.** If an attached MCP server dies mid-session, ToolFunnel notices, reconnects it in the background with backoff, and re-advertises its tools. Most reconnect logic elsewhere is Docker-scoped; this isn't.
174
166
 
175
167
  ## Zero dependencies, on purpose
176
168
 
177
- ToolFunnel has **no runtime npm dependencies** (`"dependencies": {}`) and does **not** use an MCP SDK - the JSON-RPC 2.0 wire protocol is hand-rolled on top of Node built-ins (`node:http`, `node:child_process`, `node:fs`, …). Requires Node **>= 18**. `npm install toolfunnel` pulls **zero** packages.
169
+ ToolFunnel has no runtime npm dependencies (`"dependencies": {}`) and does not use an MCP SDK - the JSON-RPC 2.0 wire protocol is hand-rolled on top of Node built-ins (`node:http`, `node:child_process`, `node:fs`, ...). Requires Node >= 18. `npm install toolfunnel` pulls zero packages.
178
170
 
179
- > **\* The one asterisk: OAuth is opt-in.** Enabling OAuth 2.1 (off by default) adds exactly **one** dependency - [`jose`](https://www.npmjs.com/package/jose), which is itself **zero-dependency**, audited, and the same library the official MCP SDK uses. It is **not** a runtime dependency of the core: it appears only as a `devDependency` (for the OAuth test suite) and is **installed on demand** for users who turn auth on - `toolfunnel install-oauth` or the Install button in the UI's Auth tab. So the default footprint is genuinely zero, and the most security-sensitive code in the project - token validation - is delegated to an audited library rather than hand-rolled. See [Authentication](#authentication---optional-oauth-21).
171
+ > **\* The one asterisk: OAuth is opt-in.** Enabling OAuth 2.1 (off by default) adds exactly one dependency - [`jose`](https://www.npmjs.com/package/jose), which is itself zero-dependency, audited, and the same library the official MCP SDK uses. It is not a runtime dependency of the core: it appears only as a `devDependency` (for the OAuth test suite) and is installed on demand for users who turn auth on. So the default footprint is genuinely zero, and the most security-sensitive code in the project - token validation - is delegated to an audited library rather than hand-rolled.
180
172
 
181
- This is a deliberate **security** decision, not minimalism for its own sake. ToolFunnel sits in a privileged position - it **gates tool execution** - which is exactly where you don't want an unaudited dependency tree. And `dependencies: {}` in npm is a stronger claim than a "single binary" elsewhere: a Go gateway that ships as one binary still *statically links* its dependency tree (mcpproxy-go's, for example, runs to 40+ direct deps - goja, esbuild, gRPC, an observability stack). That's dependency-**bundled**, not dependency-free. In a year when npm itself saw large packages compromised (Axios, the 140-package Mastra incident), a tool-execution gate with **nothing transitive to audit** is a defensible engineering stance - you audit *your* code, not forty supply chains. (It is not a silver bullet: a zero-dep posture shrinks the audit surface, it doesn't remove the burden of getting the hand-rolled wire + gate correct - which I have tried to do.)
182
-
183
- The transport reads **both** LSP-style `Content-Length:` framing **and** newline-delimited JSON, and writes newline-delimited JSON, so it interoperates with simple and framed clients alike.
173
+ This is a deliberate security decision, not minimalism for its own sake. ToolFunnel sits in a privileged position - it gates tool execution - which is exactly where you don't want an unaudited dependency tree. And `dependencies: {}` in npm is a stronger claim than a "single binary" elsewhere: a Go gateway that ships as one binary still statically links its dependency tree (mcpproxy-go's, for example, runs to 40+ direct deps - goja, esbuild, gRPC, an observability stack). That's dependency-bundled, not dependency-free. In a year of high-profile npm supply-chain compromises, a tool-execution gate with nothing transitive to audit is a defensible engineering stance - you audit your code, not forty supply chains. (It is not a silver bullet: a zero-dep posture shrinks the audit surface, it doesn't remove the burden of getting the hand-rolled wire + gate correct - which I have tried to do.)
184
174
 
185
175
  ## Authentication - optional OAuth 2.1
186
176
 
187
- By default the gateway is **loopback-only and unauthenticated** - the right posture for a single operator on `localhost`. For a networked or multi-user deployment, ToolFunnel can act as an **OAuth 2.1 resource server**: it validates the bearer token on every request before any tool runs.
188
-
189
- - **Opt-in, one dependency.** Off until you enable it. Enabling installs `jose` on demand (`toolfunnel install-oauth` or the UI Auth tab) and the core stays zero-runtime-dependency for everyone else.
190
- - **Delegated, not hand-rolled.** Token validation goes through `jose` with a **pinned algorithm allowlist** (which defeats the RS256↔HS256 confusion attack and `alg:none`), enforced **issuer**, and an enforced **audience** bound to this gateway's resource URI - the RFC 8707 confused-deputy defence. JWKS fetch, caching, and key rotation are the library's job.
191
- - **Discovery.** With auth on, the gateway serves the RFC 9728 Protected Resource Metadata document at `GET /.well-known/oauth-protected-resource` (unauthenticated), and a `401` carries a `WWW-Authenticate: Bearer resource_metadata="…"` hint.
192
- - **Safe exposure.** Auth is what unlocks a non-loopback bind: the HTTP host **refuses to bind off-localhost unless OAuth is enabled**, and refuses to start if auth is on but the dependency is missing or the config is incoherent (fail-fast).
193
- - **Configure** in the UI Auth tab or `auth/auth.config.json` (`enabled`, `issuer`, `audience`, optional `jwksUri` - else derived from the issuer via OIDC discovery - `algorithms`, `requiredScopes`, `clockToleranceSec`). Requires Node **>= 18** (`jose` v5, the CommonJS build).
177
+ By default the gateway is loopback-only and unauthenticated - the right posture for a single operator on `localhost`. For a networked or multi-user deployment, ToolFunnel can act as an OAuth 2.1 resource server: it validates the bearer token on every request before any tool runs, through `jose` with a pinned algorithm allowlist, enforced issuer, and an enforced audience bound to this gateway's resource URI (the RFC 8707 confused-deputy defence). Auth is what unlocks a non-loopback bind: the HTTP host refuses to bind off-localhost unless OAuth is enabled. Setup, config fields, and discovery details: [manual](docs/MANUAL.pdf).
194
178
 
195
- The OAuth **client** leg (Dynamic Client Registration, authorization-server-metadata discovery, step-up auth) and the 2026 spec hardening (RFC 9207 issuer validation, credential binding) are **planned, not yet shipped** - the resource-server slice is the shipped, tested MVP.
179
+ The OAuth client leg (Dynamic Client Registration, authorization-server-metadata discovery, step-up auth) and the 2026 spec hardening are planned, not yet shipped - the resource-server slice is the shipped, tested MVP.
196
180
 
197
- ## Token efficiency
181
+ ## Teams and bespoke builds
198
182
 
199
- The mechanism is the lean register: a connected upstream normally injects every tool's full JSON schema into context on every turn. ToolFunnel replaces that with short briefs (`name`, `summary`, `category`) and serves the full instructions for a single tool only when the agent asks via `toolfunnel_tool_instructions`. The more tools you connect, the larger the saving.
183
+ One ToolFunnel instance serves one operator - a deliberate design ([why](docs/design.md)). A Team edition (per-user identity, audit trails, quotas) is on the roadmap if there is interest - and custom gateways built on ToolFunnel are available today. Open an issue titled `enquiry`.
200
184
 
201
185
  ## Quickstart
202
186
 
203
- ```bash
204
- # 1. Get the code
205
- git clone <your-fork-or-clone-url> toolfunnel
206
- cd toolfunnel
207
-
208
- # 2. "Install" - zero RUNTIME deps; a clone's npm install pulls only the dev/test
209
- # tooling (jose, for the OAuth test suite). Installed as a dependency it pulls nothing.
210
- npm install # runtime dependencies: none
211
-
212
- # 3a. Run as a stdio MCP server (the default - what most clients spawn)
213
- node bin/toolfunnel.js
187
+ [![ToolFunnel MCP server on Glama](https://glama.ai/mcp/servers/Rendeverance/toolfunnel/badge)](https://glama.ai/mcp/servers/Rendeverance/toolfunnel)
214
188
 
215
- # 3b. Or run as an HTTP/SSE host on 127.0.0.1:9998 (long-lived; many clients can connect)
216
- node bin/toolfunnel.js --http
217
- node bin/toolfunnel.js --http --port 0 # 0 = OS-assigned port
218
- node bin/toolfunnel.js --http --host 127.0.0.1 --port 9998
219
-
220
- # 3c. Or open the OPTIONAL config web UI on 127.0.0.1:9777 (loopback only)
221
- node bin/toolfunnel.js --ui
222
- node bin/toolfunnel.js --ui --port 0 # 0 = OS-assigned port
223
-
224
- # Help
225
- node bin/toolfunnel.js --help
189
+ ```bash
190
+ npm install toolfunnel # runtime dependencies: none
191
+ # or from source:
192
+ git clone https://github.com/Rendeverance/toolfunnel.git && cd toolfunnel && npm install
226
193
 
227
- # Smoke-test the wiring (build → initialize → tools/list → call a meta-tool)
228
- node test/smoke.js
194
+ node bin/toolfunnel.js # stdio MCP server (what most clients spawn)
195
+ node bin/toolfunnel.js --ui # optional config web UI on 127.0.0.1:9777
229
196
  ```
230
197
 
231
- The HTTP host binds loopback only and exposes a single Streamable-HTTP endpoint at `POST/GET /mcp` (plus a deprecated `GET /mcp/sse` alias and a `GET /health` JSON snapshot). The npm scripts mirror the above: `npm start` (stdio) and `npm run http`.
232
-
233
- ### Registering with an MCP client
234
-
235
- Point your client at ToolFunnel via `.mcp.json`. For a **stdio** server the client spawns the process:
198
+ Point your client at it via `.mcp.json`:
236
199
 
237
200
  ```json
238
201
  { "mcpServers": { "toolfunnel": { "command": "node", "args": ["/path/to/toolfunnel/bin/toolfunnel.js"] } } }
239
202
  ```
240
203
 
241
- For the **HTTP** host (start it first with `node bin/toolfunnel.js --http`), point the client at the `/mcp` endpoint:
204
+ **Or skip the config entirely - just ask your AI.** Point any MCP-aware client at ToolFunnel and say *"attach this tool server, expose these tools, and block anything destructive."* The AI reads ToolFunnel's own built-in instructions and wires it all up for you - upstreams, exposed tools, a PreToolUse safety gate, even wrapping a server - in plain language, no JSON editing. Every feature is documented from the inside (`toolfunnel_howto` covers creating tools, attaching MCPs, hooks, packaging, wrapping, and configuration), so a plain agent with no special prompt can drive the whole gateway. Prefer to click? The web UI does the same. No coding experience required :)
242
205
 
243
- ```json
244
- { "mcpServers": { "toolfunnel": { "type": "http", "url": "http://127.0.0.1:9998/mcp" } } }
245
- ```
246
-
247
- Once connected, call `toolfunnel_list_tools` to see the briefs, `toolfunnel_tool_instructions` with a tool `name` to get its full usage, and `toolfunnel_howto` to learn how to extend the gateway.
248
-
249
- **Or skip the config entirely - just ask your AI.** Point any MCP-aware client at ToolFunnel and say *"attach this tool server, expose these tools, and block anything destructive."* The AI reads ToolFunnel's own built-in instructions and wires it all up for you - upstreams, exposed tools, and a PreToolUse safety gate - in plain language, no JSON editing. Prefer to click? The web UI below does the same. **No coding experience required :)**
250
-
251
- ## The config web UI
252
-
253
- An **optional** local admin console lets a human view, search, and configure the gateway without hand-editing JSON:
254
-
255
- ```bash
256
- node bin/toolfunnel.js --ui # http://127.0.0.1:9777
257
- node bin/toolfunnel.js --ui --port 0 # OS-assigned port
258
- ```
259
-
260
- It binds **loopback only**, rejects a non-loopback `Host` header (DNS-rebind guard), and has zero front-end dependencies (vanilla HTML/CSS/JS, dark theme, offline - no CDN, no framework). Five tabs:
206
+ The HTTP host (`--http`), registering over HTTP, the web UI tabs, the audit log, configuration files, project layout, and the smoke test are all walked through step-by-step, with screenshots, in the [manual](docs/MANUAL.pdf).
261
207
 
262
- - **Tools** - every register tool with live search and a **show-hidden** filter; a top-level **surface panel** (the four meta-tools' every-turn toggles + a promotion count and footgun warnings); add a tool (script body, shell, or invoke-less reference); toggle **Enabled** (lean), **Hot** (every-turn promotion), and **Hidden** (declutter this manager list); flip per-tool **execution mode** (reference ↔ gateway); toggle per-tool **Pre/Post hook** gates; remove.
263
- - **MCPs** - upstream MCP servers and their curated-direct expose selections; add an upstream with exposed tools; enable / disable / remove; **Discover** - a live connect-and-list of an upstream's tools, each with its own **lean** (enabled) and **Hot** toggle keyed by the surfaced name.
264
- - **Hooks** - the hook manifest with live enabled state; add a lifecycle hook (optionally authoring its script body); enable / disable / remove.
265
- - **Logs** - the on/off switch for the [audit log](#activity--audit-log) and a newest-first view of recent records.
266
- - **Auth** - configure the optional OAuth 2.1 resource-server validation (off by default) and install its single dependency on demand.
208
+ ## Identity & settings - one small file
267
209
 
268
- Every UI write goes through the **same stores** the MCP server reads, so an edit is byte-identical to a hand edit (and a CLI edit) and is visible to the running server with no restart.
210
+ Everything the gateway calls itself lives in `toolfunnel.json` at the config home - every field optional, an absent file is simply the default identity:
269
211
 
270
- ## Activity & audit log
271
-
272
- ToolFunnel can keep a **JSONL** activity/audit log - **off by default** (a fresh checkout writes nothing and creates no log file). When enabled, it records tool runs and **every gate allow/deny decision**. Toggle it three ways, all equivalent:
273
-
274
- - the `tf_log` management function - `{ action: "enable" | "disable" | "status" }`;
275
- - the web UI **Logs** tab switch;
276
- - editing `logs/log.config.json` directly (`{ "enabled": bool, "path": "logs/toolfunnel.log.jsonl" }`).
277
-
278
- Config is read **fresh per event**, so a toggle takes effect immediately. Logging is wrapped so it can **never** throw into a tool call or the gate, and `tf_log` can itself be disabled to revoke logging control. (The `logs/` directory is git-ignored.)
279
-
280
- ## Project layout
281
-
282
- ToolFunnel keeps a deliberate split between **the engine** (`src/`, which you never edit) and **what you manage** (`tools/`, `mcp/`, `hooks/` - plain config and scripts at the top level):
283
-
284
- ```
285
- toolfunnel/
286
- ├─ bin/toolfunnel.js # entry point - stdio by default, --http for the host, --ui for the console
287
- ├─ src/ # ToolFunnel's own engine (you never edit this)
288
- │ ├─ mcp/ # server, protocol, transports, mcp-client, aggregator, gated-run
289
- │ ├─ core/ # hook-engine, hook-loader, hook-runner, matcher, events, logger
290
- │ ├─ tools/ # the tool SYSTEM (registry, tool-state, drift)
291
- │ ├─ ui/ # the optional config web UI (loopback server + vanilla assets)
292
- │ └─ extend/ · packages/
293
- ├─ tools/ # YOUR tools
294
- │ ├─ tools.register.json # first-party tool register (7 demos + 8 management functions)
295
- │ ├─ tools.state.json # enable/disable overlay (default-ON)
296
- │ └─ scripts/ # the tool scripts
297
- ├─ mcp/ # YOUR upstreams
298
- │ ├─ expose.json # upstream servers + curated tools to expose (empty default)
299
- │ └─ expose.example.json # annotated sample
300
- ├─ hooks/ # YOUR policy gate
301
- │ ├─ hooks.manifest.json # the gate (empty = allow-all)
302
- │ ├─ hooks.state.json # live enable/disable overlay (optional; wins over the manifest seed)
303
- │ └─ scripts/ # hook command scripts
304
- └─ logs/ # audit log (git-ignored; created only when logging is enabled)
305
- ├─ log.config.json # { enabled, path } - default OFF
306
- └─ toolfunnel.log.jsonl # the JSONL records (default path)
212
+ ```json
213
+ { "serverName": "my-mcp", "serverVersion": "1.0.0", "clientName": "my-client", "httpPort": 9998, "uiPort": 9777 }
307
214
  ```
308
215
 
309
- Rule of thumb: `src/` is the gateway machinery; `tools/`, `mcp/`, and `hooks/` are where your configuration lives. By default these resolve relative to the repo root — or point them anywhere with a **config home** (`--config-dir` / `TOOLFUNNEL_HOME`): an empty home is seeded from the shipped defaults on first use and never overwritten after, so an `npm update` can never eat your tools. See [docs/packaging.md](docs/packaging.md).
310
-
311
- ## Configuration
312
-
313
- All config is plain JSON under the repo - edit it by hand, through the [config web UI](#the-config-web-ui), or via the [management functions](#management-functions):
216
+ `serverName`/`serverVersion` are what connected *clients* see in the handshake; `clientName`/`clientVersion` are what upstream *servers* see from the gateway. (Under a wrap on stdio you don't need any of it - the wrapped server is shown your real client's identity automatically.) The web UI's **Settings** tab edits the same file.
314
217
 
315
- | File | Purpose |
316
- |-----------------------------------|-------------------------------------------------------------------------|
317
- | `tools/tools.register.json` | The first-party tool register (`{ version, description, tools: [...] }`) - 7 demos + 9 management functions. Entries may carry an `inputSchema`, advertised verbatim when the tool is promoted hot |
318
- | `tools/tools.state.json` | The visibility-matrix overlay, keyed by surfaced name → `{ enabled?, hidden?, hot? }`. Default-ON for `enabled` (empty `{}` enables everything), default-OFF for `hot`/`hidden` (meta-tools default `hot`-ON). Read fresh per call - no restart needed |
319
- | `hooks/hooks.manifest.json` | The policy gate (`{ version, hooks: [] }`). An empty `hooks` array = allow-all |
320
- | `hooks/hooks.state.json` | Optional live enable/disable overlay for hooks; an entry here wins over the manifest's seed `enabled` |
321
- | `mcp/expose.json` | Upstream MCP servers + the curated set of their tools to expose (`{ version, upstreams: [], expose: [] }`). **Empty by default**, so the gateway connects to nothing |
322
- | `mcp/expose.example.json` | An annotated sample showing how to wire an upstream and expose two of its tools |
323
- | `logs/log.config.json` | The audit-log toggle (`{ enabled, path }`). **Default OFF**; created only when logging is enabled |
324
- | `toolfunnel.json` | Optional identity + defaults: `{ serverName, serverVersion, httpPort, uiPort, requires }`. Absent = the gateway is plain `toolfunnel`. This is how a packaged MCP introduces itself as ITSELF |
218
+ Prefer to configure everything by hand? The whole gateway is driven by a handful of plain JSON files - no code anywhere. The complete map (every file, every field, a worked five-file example) is one call away for your agent - `toolfunnel_howto({ topic: "configure" })` - and in the manual's configuration chapter for you.
325
219
 
326
220
  ## Packaging: ship your own MCP
327
221
 
328
- Everything you build here — tools, curation, upstream selections, policy hooks, identity — lives
329
- in one folder (the config home), and one management call packages it:
222
+ Everything you build here - tools, curation, upstream selections, policy hooks, identity - lives in one folder (the config home), and one management call packages it:
330
223
 
331
- - `tf_pack { format: "home" }` → a portable setup: zip it, git it, or run it with `--config-dir`.
332
- - `tf_pack { format: "npm", name: "my-mcp" }` → a **publishable npm package** that depends on
333
- toolfunnel (never forks it), bundles your setup, and gives your users `npx my-mcp` — your
334
- server, your name in the handshake, your tools and schemas, **your gate enforced on their
335
- machine regardless of client**.
224
+ - `tf_pack { format: "home" }` produces a portable setup: zip it, git it, or run it with `--config-dir`.
225
+ - `tf_pack { format: "npm", name: "my-mcp" }` produces a publishable npm package that depends on toolfunnel (never forks it), bundles your setup, and gives your users `npx my-mcp` - your server, your name in the handshake, your tools and schemas, your gate enforced on their machine regardless of client.
336
226
 
337
- Declare runtime needs (`python`, `git`, …) in `toolfunnel.json` `requires` and the gateway warns
338
- at startup about anything missing. Full story: [docs/packaging.md](docs/packaging.md).
227
+ Full story: [docs/packaging.md](docs/packaging.md).
339
228
 
340
229
  ## License
341
230