toolfunnel 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +324 -0
- package/bin/toolfunnel.js +112 -0
- package/docs/admin-ui.md +404 -0
- package/docs/configuration.md +402 -0
- package/docs/hooks-and-gating.md +428 -0
- package/docs/logging.md +223 -0
- package/docs/management.md +429 -0
- package/docs/oauth.md +103 -0
- package/docs/setup.md +352 -0
- package/docs/tools-and-upstreams.md +469 -0
- package/hooks/hooks.manifest.json +27 -0
- package/hooks/scripts/.gitkeep +3 -0
- package/hooks/scripts/example-audit.js +21 -0
- package/hooks/scripts/example-deny-dangerous.js +31 -0
- package/mcp/expose.example.json +30 -0
- package/mcp/servers/mock-upstream/server.js +170 -0
- package/package.json +63 -0
- package/src/auth/config.js +172 -0
- package/src/auth/install.js +181 -0
- package/src/auth/resource-server.js +319 -0
- package/src/core/events.js +105 -0
- package/src/core/hook-engine.js +275 -0
- package/src/core/hook-loader.js +791 -0
- package/src/core/hook-runner.js +346 -0
- package/src/core/logger.js +169 -0
- package/src/core/matcher.js +51 -0
- package/src/core/metrics.js +75 -0
- package/src/core/tool-registry.js +180 -0
- package/src/extend/add-hook.md +142 -0
- package/src/extend/add-mcp.md +121 -0
- package/src/extend/create-tool.md +132 -0
- package/src/extend/howto.js +98 -0
- package/src/extend/package.md +154 -0
- package/src/mcp/aggregator.js +802 -0
- package/src/mcp/expose-store.js +430 -0
- package/src/mcp/gated-run.js +194 -0
- package/src/mcp/http-transport.js +962 -0
- package/src/mcp/mcp-client.js +524 -0
- package/src/mcp/protocol.js +466 -0
- package/src/mcp/server.js +1240 -0
- package/src/packages/packager.js +713 -0
- package/src/tools/drift.js +138 -0
- package/src/tools/registry.js +468 -0
- package/src/tools/tool-state.js +104 -0
- package/src/ui/public/app.js +1055 -0
- package/src/ui/public/index.html +249 -0
- package/src/ui/public/logo.jpg +0 -0
- package/src/ui/public/styles.css +535 -0
- package/src/ui/server.js +1144 -0
- package/tools/scripts/base64.js +139 -0
- package/tools/scripts/danger.js +61 -0
- package/tools/scripts/echo.js +39 -0
- package/tools/scripts/hash.js +101 -0
- package/tools/scripts/json.js +109 -0
- package/tools/scripts/text-stats.js +131 -0
- package/tools/scripts/tf-hook-add.js +152 -0
- package/tools/scripts/tf-hook-set.js +113 -0
- package/tools/scripts/tf-list.js +195 -0
- package/tools/scripts/tf-log.js +83 -0
- package/tools/scripts/tf-mcp-add.js +119 -0
- package/tools/scripts/tf-mcp-set.js +104 -0
- package/tools/scripts/tf-tool-add.js +129 -0
- package/tools/scripts/tf-tool-set.js +148 -0
- package/tools/scripts/uuid.js +102 -0
- package/tools/tools.register.json +171 -0
package/docs/setup.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# ToolFunnel — Setup
|
|
2
|
+
|
|
3
|
+
How to install ToolFunnel and register it with any MCP-capable agent. ToolFunnel is one
|
|
4
|
+
small MCP gateway: it hosts first-party tools, forwards and curates tools from upstream MCP
|
|
5
|
+
servers, exposes them **leanly** (short briefs + schema-on-demand), and gates every
|
|
6
|
+
execution through your own policy hooks.
|
|
7
|
+
|
|
8
|
+
It speaks the same protocol two ways:
|
|
9
|
+
|
|
10
|
+
- **stdio** (default) — the client spawns ToolFunnel as a child process and talks JSON-RPC
|
|
11
|
+
over stdin/stdout. Simplest; nothing to leave running.
|
|
12
|
+
- **HTTP/SSE** (`--http`) — a long-lived host process owns the server on `127.0.0.1:9998`
|
|
13
|
+
and any number of clients connect over `http://127.0.0.1:9998/mcp`.
|
|
14
|
+
|
|
15
|
+
Both serve the identical tool surface and run every call through the same gate.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Requirements
|
|
20
|
+
|
|
21
|
+
- **Node.js >= 18.** That is the only requirement. (`node --version` to check.)
|
|
22
|
+
- **Zero runtime dependencies.** ToolFunnel uses Node built-ins only — no npm packages, no
|
|
23
|
+
MCP SDK. There is nothing to `npm install` for it to run, and nothing transitive to audit.
|
|
24
|
+
- **Loopback only.** The HTTP host binds `127.0.0.1` and rejects non-loopback `Host` headers
|
|
25
|
+
(DNS-rebinding guard). It is designed for a local agent on the same machine, not remote
|
|
26
|
+
access.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Install
|
|
31
|
+
|
|
32
|
+
Clone (or copy) the repository somewhere stable. Because there are no dependencies, that is
|
|
33
|
+
the whole install:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git clone <repo-url> toolfunnel
|
|
37
|
+
cd toolfunnel
|
|
38
|
+
node bin/toolfunnel.js --help # verify it runs
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You should see the usage banner. That confirms Node can run the entry point.
|
|
42
|
+
|
|
43
|
+
> `npm install` is **optional and a no-op** here — `dependencies` is empty, so there is
|
|
44
|
+
> nothing to fetch. Running it just writes a lockfile; ToolFunnel runs straight from a clone.
|
|
45
|
+
|
|
46
|
+
### Optional: a global `toolfunnel` command
|
|
47
|
+
|
|
48
|
+
The package declares a `toolfunnel` bin, so you can put it on your `PATH` instead of typing
|
|
49
|
+
the full path every time:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# from inside the cloned repo
|
|
53
|
+
npm link # symlinks a global `toolfunnel` to this checkout
|
|
54
|
+
# …or
|
|
55
|
+
npm install -g . # installs it globally
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After either, `toolfunnel` and `toolfunnel --http` work from anywhere. (`npx toolfunnel`
|
|
59
|
+
will also work once the package is published to a registry.)
|
|
60
|
+
|
|
61
|
+
Throughout this document, **pick one invocation form** and use it consistently in your
|
|
62
|
+
client config:
|
|
63
|
+
|
|
64
|
+
| Form | stdio command | HTTP launch |
|
|
65
|
+
|------|---------------|-------------|
|
|
66
|
+
| Direct (no install) | `node /abs/path/to/toolfunnel/bin/toolfunnel.js` | `node /abs/path/to/toolfunnel/bin/toolfunnel.js --http` |
|
|
67
|
+
| Global install / link | `toolfunnel` | `toolfunnel --http` |
|
|
68
|
+
| Published package | `npx toolfunnel` | `npx toolfunnel --http` |
|
|
69
|
+
|
|
70
|
+
> **Windows paths in JSON:** use forward slashes (`F:/dev/toolfunnel/bin/toolfunnel.js`) or
|
|
71
|
+
> escaped backslashes (`F:\\dev\\toolfunnel\\bin\\toolfunnel.js`). Node accepts forward
|
|
72
|
+
> slashes on Windows, so forward slashes are the simplest choice.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. Running
|
|
77
|
+
|
|
78
|
+
### stdio (default)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node bin/toolfunnel.js
|
|
82
|
+
# or, if installed globally:
|
|
83
|
+
toolfunnel
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
It reads JSON-RPC from stdin and writes responses to stdout (diagnostics go to stderr, so
|
|
87
|
+
they never pollute the protocol channel). It accepts both `Content-Length`-framed messages
|
|
88
|
+
and newline-delimited JSON. You normally don't run this by hand — the MCP client launches it
|
|
89
|
+
for you (section 5). A quick smoke test:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | node bin/toolfunnel.js
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
You should get one JSON line back with `"serverInfo":{"name":"toolfunnel","version":"0.3.0"}`
|
|
96
|
+
and `"protocolVersion":"2024-11-05"`.
|
|
97
|
+
|
|
98
|
+
> **Working directory doesn't matter.** ToolFunnel resolves its config (`tools/`, `mcp/`,
|
|
99
|
+
> `hooks/`) relative to its own install location, not the directory it's launched from. So a
|
|
100
|
+
> client that spawns `node /abs/path/to/toolfunnel/bin/toolfunnel.js` from any `cwd` still
|
|
101
|
+
> finds the right register, expose store, and hook manifest. You never need to set a `cwd` in
|
|
102
|
+
> the client config.
|
|
103
|
+
|
|
104
|
+
### HTTP/SSE host
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
node bin/toolfunnel.js --http
|
|
108
|
+
# or:
|
|
109
|
+
toolfunnel --http
|
|
110
|
+
# npm script shortcut, from the repo:
|
|
111
|
+
npm run http
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This starts a long-lived host and logs to stderr:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
[toolfunnel] HTTP MCP host listening on http://127.0.0.1:9998
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Endpoints:
|
|
121
|
+
|
|
122
|
+
| Method + path | Purpose |
|
|
123
|
+
|---------------|---------|
|
|
124
|
+
| `POST /mcp` | One JSON-RPC request → one JSON-RPC response. This is the call channel. |
|
|
125
|
+
| `GET /mcp` | Server→client SSE stream (`Accept: text/event-stream`) — the Streamable-HTTP standard. |
|
|
126
|
+
| `GET /mcp/sse` | Working alias for the SSE stream (older HTTP+SSE shape; still supported). |
|
|
127
|
+
| `GET /health` | Synchronous JSON health snapshot. |
|
|
128
|
+
|
|
129
|
+
**The URL your client points at is `http://127.0.0.1:9998/mcp`** (note the `/mcp` path — the
|
|
130
|
+
bare origin is not the endpoint).
|
|
131
|
+
|
|
132
|
+
Bind options:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
toolfunnel --http --port 9000 # bind a specific port
|
|
136
|
+
toolfunnel --http --port 0 # OS-assigned ephemeral port (read it from the stderr log)
|
|
137
|
+
toolfunnel --http --host 127.0.0.1 # explicit loopback host (default)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Stop it with Ctrl+C (SIGINT) — it shuts down cleanly.
|
|
141
|
+
|
|
142
|
+
Smoke-test a running host:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
curl -s http://127.0.0.1:9998/health
|
|
146
|
+
curl -s -X POST http://127.0.0.1:9998/mcp \
|
|
147
|
+
-H "Content-Type: application/json" \
|
|
148
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The `tools/list` response lists the lean meta-tools the agent uses to discover everything
|
|
152
|
+
else (see section 6).
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 4. stdio vs HTTP — which to register
|
|
157
|
+
|
|
158
|
+
- **stdio** is the default and the simplest: zero processes to manage, the client starts and
|
|
159
|
+
stops ToolFunnel with its own lifecycle. Best when one client owns the gateway.
|
|
160
|
+
- **HTTP** is better when you want **one** gateway shared by several clients/sessions, want
|
|
161
|
+
it to stay up across client restarts, or want to hit `/health` and the SSE
|
|
162
|
+
`tools/list_changed` stream. You start the host yourself (or via a service manager) and
|
|
163
|
+
point clients at the URL.
|
|
164
|
+
|
|
165
|
+
Both expose the same tools and enforce the same gate, so you can switch freely.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 5. Registering with clients
|
|
170
|
+
|
|
171
|
+
### Claude Code
|
|
172
|
+
|
|
173
|
+
Claude Code reads MCP servers from a `.mcp.json` file (project root for project scope, or
|
|
174
|
+
your user config). The top-level key is `mcpServers`.
|
|
175
|
+
|
|
176
|
+
**stdio form** — Claude Code launches ToolFunnel itself:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"mcpServers": {
|
|
181
|
+
"toolfunnel": {
|
|
182
|
+
"command": "node",
|
|
183
|
+
"args": ["F:/dev/toolfunnel/bin/toolfunnel.js"]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If you installed the global bin, you can use it directly instead:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"mcpServers": {
|
|
194
|
+
"toolfunnel": {
|
|
195
|
+
"command": "toolfunnel",
|
|
196
|
+
"args": []
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**HTTP form** — point at an already-running host (`toolfunnel --http`):
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"mcpServers": {
|
|
207
|
+
"toolfunnel": {
|
|
208
|
+
"type": "http",
|
|
209
|
+
"url": "http://127.0.0.1:9998/mcp"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
You can also add either form from the CLI instead of hand-editing the file:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# stdio (everything after -- is the launch command)
|
|
219
|
+
claude mcp add toolfunnel -- node F:/dev/toolfunnel/bin/toolfunnel.js
|
|
220
|
+
|
|
221
|
+
# http (host must be running)
|
|
222
|
+
claude mcp add --transport http toolfunnel http://127.0.0.1:9998/mcp
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Generic MCP clients
|
|
226
|
+
|
|
227
|
+
Any MCP client follows one of the two shapes above. The vocabulary is near-universal:
|
|
228
|
+
|
|
229
|
+
**stdio** — give the client a command + args; it spawns the process and speaks JSON-RPC over
|
|
230
|
+
the pipes:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"command": "node",
|
|
235
|
+
"args": ["F:/dev/toolfunnel/bin/toolfunnel.js"]
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**HTTP / Streamable-HTTP** — give the client the endpoint URL; it POSTs JSON-RPC to it and
|
|
240
|
+
(optionally) opens the `GET /mcp` SSE stream:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
http://127.0.0.1:9998/mcp
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
If a client distinguishes "HTTP" from the older "SSE" transport, ToolFunnel supports both on
|
|
247
|
+
the same host: use `http://127.0.0.1:9998/mcp` for Streamable-HTTP, or
|
|
248
|
+
`http://127.0.0.1:9998/mcp/sse` for the legacy SSE alias. Protocol version is `2024-11-05`.
|
|
249
|
+
|
|
250
|
+
### Cursor
|
|
251
|
+
|
|
252
|
+
Cursor reads `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per project), also under a
|
|
253
|
+
`mcpServers` key.
|
|
254
|
+
|
|
255
|
+
**stdio:**
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"mcpServers": {
|
|
260
|
+
"toolfunnel": {
|
|
261
|
+
"command": "node",
|
|
262
|
+
"args": ["F:/dev/toolfunnel/bin/toolfunnel.js"]
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**HTTP** (point at a running `toolfunnel --http`; Cursor uses a `url` field):
|
|
269
|
+
|
|
270
|
+
```json
|
|
271
|
+
{
|
|
272
|
+
"mcpServers": {
|
|
273
|
+
"toolfunnel": {
|
|
274
|
+
"url": "http://127.0.0.1:9998/mcp"
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Windsurf
|
|
281
|
+
|
|
282
|
+
Windsurf reads `~/.codeium/windsurf/mcp_config.json`, again under `mcpServers`.
|
|
283
|
+
|
|
284
|
+
**stdio:**
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"mcpServers": {
|
|
289
|
+
"toolfunnel": {
|
|
290
|
+
"command": "node",
|
|
291
|
+
"args": ["F:/dev/toolfunnel/bin/toolfunnel.js"]
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**HTTP** (Windsurf uses a `serverUrl` field for remote/HTTP servers):
|
|
298
|
+
|
|
299
|
+
```json
|
|
300
|
+
{
|
|
301
|
+
"mcpServers": {
|
|
302
|
+
"toolfunnel": {
|
|
303
|
+
"serverUrl": "http://127.0.0.1:9998/mcp"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
> Field names for the HTTP/SSE URL vary by client (`url`, `serverUrl`, or a `type:"http"`
|
|
310
|
+
> block). The URL itself is always `http://127.0.0.1:9998/mcp`. Check your client's MCP docs
|
|
311
|
+
> if neither field name is accepted.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 6. What you'll see after connecting
|
|
316
|
+
|
|
317
|
+
ToolFunnel exposes a deliberately small surface so it costs almost no context. After
|
|
318
|
+
connecting, the agent sees these meta-tools instead of every upstream schema:
|
|
319
|
+
|
|
320
|
+
- **`toolfunnel_list_tools`** — list the available tools as short briefs (id, summary,
|
|
321
|
+
category). This is the lean catalogue.
|
|
322
|
+
- **`toolfunnel_tool_instructions`** — fetch the full instructions/schema for one tool, on
|
|
323
|
+
demand. The agent only pays for the schema of a tool it's actually about to use.
|
|
324
|
+
- **`toolfunnel_howto`** — self-extension guidance (adding tools, hooks, upstream MCPs).
|
|
325
|
+
|
|
326
|
+
That's the token saver: the long tail of tools lives behind `toolfunnel_list_tools` rather
|
|
327
|
+
than as hundreds of always-loaded `tools/list` entries.
|
|
328
|
+
|
|
329
|
+
Out of the box there are 9 demo first-party tools (echo, base64, hash, uuid, json,
|
|
330
|
+
text-stats, and a few inventory/diagnostic helpers) and **no** upstream MCPs connected
|
|
331
|
+
(`expose.json` is empty by default). Add upstreams and curate which of their tools to expose
|
|
332
|
+
via the configuration files — see the self-extension guides under `src/extend/`
|
|
333
|
+
(`add-mcp.md`, `create-tool.md`, `add-hook.md`) or call `toolfunnel_howto`.
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 7. Troubleshooting
|
|
338
|
+
|
|
339
|
+
- **Client shows no tools / connection fails (stdio):** run the exact `command` + `args`
|
|
340
|
+
from your config in a terminal and pipe the initialize smoke-test from section 3. If that
|
|
341
|
+
prints a `serverInfo` line, the launch command is correct; re-check the JSON path
|
|
342
|
+
escaping in your config.
|
|
343
|
+
- **`EADDRINUSE` on `--http`:** port 9998 is taken. Start with `--port <n>` (or `--port 0`
|
|
344
|
+
for an OS-assigned port, then read the actual port from the stderr log line) and update
|
|
345
|
+
the client URL to match.
|
|
346
|
+
- **`403 forbidden: non-loopback Host`:** you reached the host with a non-loopback `Host`
|
|
347
|
+
header (e.g. a real hostname or LAN IP). ToolFunnel is loopback-only by design — use
|
|
348
|
+
`127.0.0.1` or `localhost`.
|
|
349
|
+
- **Is it alive?** `curl -s http://127.0.0.1:9998/health` returns `{"ok":true,...}` with the
|
|
350
|
+
bound `url`, protocol version, and tool counts when the host is up.
|
|
351
|
+
- **Node too old:** `node --version` must be >= 18. Earlier versions lack APIs the transport
|
|
352
|
+
relies on.
|