@vdoninja/mcp 0.4.1 → 0.4.2

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 (71) hide show
  1. package/LICENSE +21 -661
  2. package/README.md +59 -272
  3. package/assets/browser-agent/audio-worklet.js +17 -0
  4. package/assets/browser-agent/client.js +124 -0
  5. package/assets/browser-agent/index.html +50 -0
  6. package/assets/cloudflare/worker-proxy.js +3 -0
  7. package/docs/.nojekyll +0 -0
  8. package/docs/404.html +28 -0
  9. package/docs/advanced-diagnostics.html +101 -0
  10. package/docs/assets/apple-touch-icon.png +0 -0
  11. package/docs/assets/favicon.svg +7 -0
  12. package/docs/assets/how-it-works.svg +62 -0
  13. package/docs/assets/icon-192.png +0 -0
  14. package/docs/assets/icon-512.png +0 -0
  15. package/docs/assets/icon-maskable-512.png +0 -0
  16. package/docs/assets/icons.svg +42 -0
  17. package/docs/assets/logo.svg +7 -0
  18. package/docs/assets/og-image.png +0 -0
  19. package/docs/assets/site.css +735 -0
  20. package/docs/assets/site.js +289 -0
  21. package/docs/browser-agent.html +122 -0
  22. package/docs/browser-worker.html +165 -0
  23. package/docs/client-config-examples.html +254 -0
  24. package/docs/direction.html +175 -0
  25. package/docs/favicon.ico +0 -0
  26. package/docs/guide.html +872 -0
  27. package/docs/http-relay.html +154 -0
  28. package/docs/index.html +834 -0
  29. package/docs/mcp-tool-contract.html +226 -0
  30. package/docs/quickstart-and-compat.html +222 -0
  31. package/docs/site.webmanifest +16 -0
  32. package/docs/sitemap.xml +63 -0
  33. package/docs/ssapp-integration.html +594 -0
  34. package/docs/vdo-ninja-p2p-data-notes.html +225 -0
  35. package/package.json +51 -9
  36. package/references/advanced-diagnostics.md +2 -0
  37. package/references/browser-agent.md +53 -0
  38. package/references/browser-worker.md +142 -0
  39. package/references/client-config-examples.md +2 -0
  40. package/references/direction.md +88 -0
  41. package/references/http-relay.md +107 -0
  42. package/references/mcp-tool-contract.md +11 -1
  43. package/references/quickstart-and-compat.md +4 -1
  44. package/references/ssapp-integration.md +371 -0
  45. package/references/vdo-ninja-p2p-data-notes.md +2 -0
  46. package/scripts/demo-file-workflow.js +3 -0
  47. package/scripts/demo-message-workflow.js +3 -0
  48. package/scripts/install-mcp.js +5 -2
  49. package/scripts/lib/bridge-session.js +143 -26
  50. package/scripts/lib/bridge-utils.js +3 -0
  51. package/scripts/lib/browser-agent.js +255 -0
  52. package/scripts/lib/browser-media.js +79 -0
  53. package/scripts/lib/browser-routes.js +119 -0
  54. package/scripts/lib/browser-rpc.js +214 -0
  55. package/scripts/lib/browser-setup.js +133 -0
  56. package/scripts/lib/browser-worker.js +216 -0
  57. package/scripts/lib/fake-network-sdk.js +3 -0
  58. package/scripts/lib/http-relay-transport.js +294 -0
  59. package/scripts/lib/load-vdo-sdk.js +3 -0
  60. package/scripts/lib/mcp-stdio-parser.js +3 -0
  61. package/scripts/lib/ssapp-worker.js +198 -0
  62. package/scripts/lib/tool-profiles.js +3 -0
  63. package/scripts/vdo-data-bridge.js +3 -0
  64. package/scripts/vdo-mcp-browser-agent.js +132 -0
  65. package/scripts/vdo-mcp-browser.js +218 -0
  66. package/scripts/vdo-mcp-relay.js +207 -0
  67. package/scripts/vdo-mcp-server.js +50 -7
  68. package/scripts/vdo-mcp-ssapp.js +208 -0
  69. package/scripts/vdo-mcp-streamable-http.js +23 -4
  70. package/server.json +22 -4
  71. package/LICENSE-SDK-EXCEPTION +0 -54
package/README.md CHANGED
@@ -1,324 +1,111 @@
1
1
  # VDO.Ninja MCP (`@vdoninja/mcp`)
2
2
 
3
- ## TL;DR
3
+ Give an MCP client live tools for exchanging messages, files, and small shared-state updates with peers on other computers. The MCP server manages active connections while your AI client calls the tools and consumes incoming events.
4
4
 
5
- This package gives an AI app a small set of tools for talking to another AI app. Two Codex, Claude, Cursor, or other MCP sessions can join the same VDO.Ninja room, send messages, exchange files, and keep shared task state without you building a new messaging server.
5
+ WebRTC is the default peer transport. An optional [self-hosted HTTP relay](https://steveseguin.github.io/ninjamcp/http-relay.html) works when WebRTC is unavailable.
6
6
 
7
- If your AI client supports MCP and you want **tools it can call**, use this package. If you want an **always-on agent room with a local inbox**, use [`ninja-p2p`](https://github.com/steveseguin/ninja-p2p) instead.
7
+ [Package](https://www.npmjs.com/package/@vdoninja/mcp) | [HTML guides](https://steveseguin.github.io/ninjamcp/guide.html#guides) | [Client setup](https://steveseguin.github.io/ninjamcp/client-config-examples.html) | [Tool reference](https://steveseguin.github.io/ninjamcp/mcp-tool-contract.html) | [Support](https://discord.vdo.ninja)
8
8
 
9
- **Best for:** MCP-native agent handoffs, file delivery, and small shared-state workflows.
9
+ HTML guides are also included in the package's `docs/` folder for offline reading.
10
10
 
11
- **Not for:** a general VPN, permanent storage, or a large public chat network.
11
+ ## Get started
12
12
 
13
- Package: [`@vdoninja/mcp`](https://www.npmjs.com/package/@vdoninja/mcp) | [Quickstart](references/quickstart-and-compat.md) | [Client setup examples](references/client-config-examples.md) | [Support](https://discord.vdo.ninja)
13
+ Install from a stable folder; the installer registers an absolute script path with your MCP client.
14
14
 
15
- ## What It Looks Like
16
-
17
- One agent joins a room:
18
-
19
- ```json
20
- { "name": "vdo_connect", "arguments": { "room": "family_calendar", "stream_id": "planner" } }
21
- ```
22
-
23
- A second agent joins the same room, then sends it a message:
24
-
25
- ```json
26
- { "name": "vdo_connect", "arguments": { "room": "family_calendar", "stream_id": "reviewer", "target_stream_id": "planner" } }
27
- { "name": "vdo_send", "arguments": { "session_id": "<reviewer-session>", "data": { "text": "I checked the plan. Tuesday has a conflict." } } }
15
+ ```bash
16
+ npm install @vdoninja/mcp
17
+ npx vdon-mcp-install
28
18
  ```
29
19
 
30
- Those are normal MCP tool calls. This server translates them into VDO.Ninja SDK operations over WebRTC data channels.
31
-
32
- ## Which VDO.Ninja Package Do I Need?
33
-
34
- | Your goal | Use |
35
- | --- | --- |
36
- | Add connect/send/file/state tools to an MCP client | **`@vdoninja/mcp`** |
37
- | Give agents a persistent room and local inbox | [`@vdoninja/ninja-p2p`](https://github.com/steveseguin/ninja-p2p) |
38
- | Build directly with WebRTC media or data channels | [`@vdoninja/sdk`](https://github.com/steveseguin/ninjasdk) |
39
-
40
- This package uses VDO.Ninja's existing room, discovery, and WebRTC behavior. It does not add signaling-server commands. `@vdoninja/sdk` and the Node WebRTC runtime install as package dependencies.
41
-
42
- ## Why This MCP
43
-
44
- - Open source (`AGPL-3.0-only` with additional unmodified-distribution exception) and free to use.
45
- - Peer-to-peer first: no central relay required for normal data flow.
46
- - End-to-end encrypted WebRTC data transport (DTLS + SCTP data channels).
47
- - Reliable data channel mode available for ordered/loss-recovered delivery.
48
- - Low infrastructure overhead for bot-to-bot messaging and file sync.
49
- - Works for local LAN, remote internet peers, and hybrid topologies.
50
- - TURN support improves connectivity when direct P2P is blocked.
51
- - Good fit for multi-agent coordination without exposing plaintext payloads to a middlebox.
52
-
53
- ## Common and Interesting Use Cases
54
-
55
- - AI swarm coordination: multiple agents in a private room exchanging tasks, state, and results.
56
- - Bot-to-bot handoff: one agent collects context, another executes, another validates, all over P2P.
57
- - Private file exchange: send logs, artifacts, prompts, and generated outputs directly between agents.
58
- - Human + AI war room: operator plus several bots sharing near-real-time status updates.
59
- - Edge + cloud hybrid: local on-device agent syncs with remote agents without standing up a custom tunnel.
60
- - Temporary secure collaboration spaces: ad-hoc rooms with join tokens and peer allowlists.
61
- - Cross-tool interop: Codex and Claude instances sharing updates through the same bridge contract.
62
-
63
- ## Fun Agent-to-Agent Rooms
64
-
65
- These are real patterns teams are starting to use:
66
-
67
- - Claude Code to Codex CLI pair mode where one agent asks for a second opinion and gets patch/test feedback.
68
- - OpenClaw-to-OpenClaw specialist rooms where one local assistant asks another for tool-specific help.
69
- - Small LLM idea circles (3-5 agents) sharing proposals, critiques, and final votes.
70
- - Private "night shift" automation where bots sync status/events/files without broad network exposure.
71
-
72
- OpenClaw project link:
20
+ The installer supports Codex and Claude Code. Use `--codex-only` or `--claude-only` to choose one, or follow the [manual setup examples](https://steveseguin.github.io/ninjamcp/client-config-examples.html) for other MCP clients. Your client launches the server; `npx vdon-mcp-server` starts it directly for manual use.
73
21
 
74
- - https://github.com/openclaw/openclaw
22
+ Ask the first agent to connect:
75
23
 
76
- ## Quick Example: Claude + Codex Helper Loop
77
-
78
- 1. Claude-side session:
79
- ```json
80
- {
81
- "name": "vdo_connect",
82
- "arguments": {
83
- "room": "agent_help_room",
84
- "stream_id": "claude_agent"
85
- }
86
- }
87
- ```
88
-
89
- 2. Codex-side session:
90
24
  ```json
91
- {
92
- "name": "vdo_connect",
93
- "arguments": {
94
- "room": "agent_help_room",
95
- "stream_id": "codex_agent",
96
- "target_stream_id": "claude_agent"
97
- }
98
- }
25
+ { "name": "vdo_connect", "arguments": { "room": "project_room", "stream_id": "planner" } }
99
26
  ```
100
27
 
101
- 3. Claude sends task:
102
- ```json
103
- {
104
- "name": "vdo_send",
105
- "arguments": {
106
- "session_id": "<claude_session>",
107
- "data": {
108
- "type": "help.request",
109
- "task": "review patch for race condition"
110
- }
111
- }
112
- }
113
- ```
28
+ Ask the second agent to join and connect to the first:
114
29
 
115
- 4. Codex replies:
116
30
  ```json
117
- {
118
- "name": "vdo_send",
119
- "arguments": {
120
- "session_id": "<codex_session>",
121
- "data": {
122
- "type": "help.reply",
123
- "summary": "found 2 edge cases",
124
- "next_steps": ["add timeout guard", "add retry test"]
125
- }
126
- }
127
- }
128
- ```
129
-
130
- ## High-Value AI Advantages
131
-
132
- - Agents share messages/files/state directly, without opening broad machine-to-machine network access.
133
- - You can limit tool access with profiles (`core|file|state|full`) so each agent only gets what it needs.
134
- - Rooms can be short-lived: create for a task, then close when done.
135
- - Easy to start: no private subnet setup, no custom tunnel setup.
136
- - Works between browser peers and Node peers.
137
- - Data channels can be reliable and ordered, which is good for control messages.
138
- - Security options are built in: join tokens, stream allowlists, and session MAC checks.
139
-
140
- ## Clever Real-World Patterns
141
-
142
- - Reviewer team: one writer agent, several reviewer agents, and one decider agent sharing feedback in the same room.
143
- - State recovery: agents can re-sync shared state with `vdo_state_sync` after reconnects.
144
- - File pipeline: one agent sends artifacts, another indexes them, another summarizes them.
145
- - Human override: a person can join the room to inspect progress and step in when needed.
146
- - Local-first flow: keep sensitive work local and only send required outputs to remote agents.
147
- - Burst jobs: create short-lived agent groups per task instead of running a permanent private network.
148
-
149
- ## Compared with VPN Overlays (e.g., Tailscale)
150
-
151
- Where this MCP is often better for AI workflows:
152
-
153
- - You need agent-to-agent messaging/file/state, not full network access between hosts.
154
- - You want tool-level permissions instead of giving every machine broad private-network reach.
155
- - You want browser and Node agents working together quickly.
156
- - You want temporary collaboration sessions without managing a permanent virtual network.
157
-
158
- Where overlays are often better:
159
-
160
- - You need always-on private IP connectivity across many services.
161
- - You need many protocols (databases, SSH, internal web apps) over one virtual network.
162
- - You need centralized network policy and audit at the infrastructure layer.
163
-
164
- Practical framing:
165
-
166
- - This MCP is not a full replacement for Tailscale.
167
- - It is a strong complement for AI-native, data-channel-first collaboration.
168
-
169
- ## Transport Truth
170
-
171
- - This is **WebRTC data transport**, not a generic TCP/SSH tunnel.
172
- - Data channels can run in reliable/ordered mode (SCTP over DTLS).
173
- - Default behavior is direct P2P when possible (great for LAN/local-network speed and low latency).
174
- - Privacy mode is opt-in via `force_turn=true`: this can reduce direct IP exposure between peers, but may reduce connection success in some environments.
175
- - TURN improves success on restrictive networks, but it is not a guaranteed firewall bypass for every enterprise policy.
176
-
177
- ## Quickstart (No Clone)
178
-
179
- Install from a stable folder. The installer stores an absolute script path in Codex/Claude config.
180
-
181
- ```bash
182
- npm i @vdoninja/mcp
183
- npx vdon-mcp-install
184
- npx vdon-mcp-server
31
+ { "name": "vdo_connect", "arguments": { "room": "project_room", "stream_id": "reviewer", "target_stream_id": "planner" } }
185
32
  ```
186
33
 
187
- When started directly in a terminal, `vdon-mcp-server` now prints a short "server running" banner and a first-call hint.
188
-
189
- First MCP call (recommended):
34
+ Wait for the peer to appear in `vdo_sync_peers`, then send a message using the returned session ID:
190
35
 
191
36
  ```json
192
- { "name": "vdo_capabilities", "arguments": {} }
37
+ { "name": "vdo_send", "arguments": { "session_id": "<reviewer-session>", "target": "planner", "data": { "text": "Please send me the plan to review." } } }
38
+ { "name": "vdo_receive", "arguments": { "session_id": "<planner-session>", "wait_ms": 1000 } }
193
39
  ```
194
40
 
195
- Useful install variants:
196
-
197
- - `npx vdon-mcp-install --codex-only`
198
- - `npx vdon-mcp-install --claude-only`
199
- - `npx vdon-mcp-install --preset core|file|state|secure-core|secure-full`
200
- - `npx vdon-mcp-install --uninstall`
201
-
202
- ## Onboarding Guides
203
-
204
- - Operator quickstart: `references/quickstart-and-compat.md`
205
- - Client setup examples (Cursor, Gemini CLI, OpenCode): `references/client-config-examples.md`
206
- - First functional workflows (Codex-to-Codex and Claude-to-Claude): `references/client-config-examples.md`
207
-
208
- ## Typical Workflows
209
-
210
- Message demo:
211
-
212
- ```bash
213
- npx vdon-mcp-demo-message
214
- ```
41
+ These are MCP tool calls made by your AI client. A successful send means the transport accepted the payload; ask the recipient to reply when you need confirmation. Clients use `vdo_receive` to retrieve incoming events.
215
42
 
216
- Purpose: prove end-to-end room connect + message send/receive over data channels.
43
+ See the [quickstart](https://steveseguin.github.io/ninjamcp/quickstart-and-compat.html) for connection troubleshooting and secure presets.
217
44
 
218
- File demo:
45
+ ## What you can do
219
46
 
220
- ```bash
221
- npx vdon-mcp-demo-file
222
- ```
47
+ | Goal | Tools |
48
+ | --- | --- |
49
+ | Connect, discover peers, and inspect a room | `vdo_connect`, `vdo_sync_peers`, `vdo_status` |
50
+ | Exchange messages and hand off work | `vdo_send`, `vdo_receive` |
51
+ | Send and save artifacts with integrity checks and retries | `vdo_file_send_path`, `vdo_file_receive`, `vdo_file_save` |
52
+ | Share small task state and recover a snapshot | `vdo_state_set`, `vdo_state_get`, `vdo_state_sync` |
53
+ | Inspect available tools and transport behavior | `vdo_capabilities` |
54
+ | End a session | `vdo_disconnect` |
223
55
 
224
- Purpose: prove reliable chunked file transfer and receive path.
56
+ Choose a tool profile with `npx vdon-mcp-install --preset core`, `file`, or `state`. The `full` profile includes all tools. Use `secure-core` or `secure-full` with a configured `VDON_MCP_JOIN_TOKEN_SECRET` for join-token enforcement and session MACs. The [tool reference](https://steveseguin.github.io/ninjamcp/mcp-tool-contract.html) describes peer allowlists and other connection controls.
225
57
 
226
- Live network mode (instead of local fake mode):
58
+ ## Choose a connection
227
59
 
228
- ```bash
229
- MCP_DEMO_FAKE=0 npx vdon-mcp-demo-message
230
- MCP_DEMO_FAKE=0 npx vdon-mcp-demo-file
231
- ```
60
+ | Path | When to use it |
61
+ | --- | --- |
62
+ | WebRTC | Default for direct peer messaging and files. Payloads travel over encrypted data channels. |
63
+ | WebRTC with `force_turn: true` | Route through TURN when you need it. TURN still requires a WebRTC runtime and network support. |
64
+ | HTTP relay with `transport: "http"` | Use a relay you operate when WebRTC cannot run or connect. Follow the [relay setup guide](https://steveseguin.github.io/ninjamcp/http-relay.html). |
232
65
 
233
- Privacy-first relay mode (opt-in):
66
+ The HTTP relay uses encrypted room messages, POST, and long polling. Both peers must select HTTP and share the relay settings. It has bounded in-memory replay, with visible events for lost history; use an application inbox when work must survive process restarts or long periods offline. Room-key holders can decrypt the room's traffic, so use separate rooms for separate trust groups.
234
67
 
235
- ```bash
236
- MCP_DEMO_FAKE=0 MCP_DEMO_FORCE_TURN=1 npx vdon-mcp-demo-message
237
- MCP_DEMO_FAKE=0 MCP_DEMO_FORCE_TURN=1 npx vdon-mcp-demo-file
238
- ```
68
+ The native WebRTC dependency installs by default. For HTTP-only machines, install with `npm install @vdoninja/mcp --omit=optional` and configure a relay. Ordinary room sessions select their transport explicitly; the optional browser adapter also supports automatic routing.
239
69
 
240
- ## Security and Least Privilege
70
+ ## Where this fits
241
71
 
242
- Use secure controls where needed:
72
+ | Your goal | Use |
73
+ | --- | --- |
74
+ | Connect an MCP client to live peers and exchange messages, files, and state | **This package** |
75
+ | Keep a team connected between agent turns, with local inboxes, handoffs, wake hooks, and a dashboard | [ninja-p2p](https://github.com/steveseguin/ninja-p2p) |
76
+ | Build directly with WebRTC audio, video, or data | [VDO.Ninja SDK](https://github.com/steveseguin/ninjasdk) |
243
77
 
244
- - `join_token` and `join_token_secret`
245
- - `enforce_join_token: true`
246
- - `allow_peer_stream_ids: [...]`
247
- - `require_session_mac: true`
78
+ Choose Ninja MCP when your client should manage an active interaction through MCP tools. Choose ninja-p2p when a background sidecar should retain incoming work and notify an agent between turns. Ninja MCP's ordinary event queues and shared state live in the server process; use ninja-p2p for persistent agent inbox workflows.
248
79
 
249
- Server profile scoping:
80
+ Both use the VDO.Ninja SDK, but their message and file protocols differ. A common room name alone does not connect their higher-level workflows. A dedicated adapter is planned; until then, use matching packages on both ends of each workflow.
250
81
 
251
- ```bash
252
- VDON_MCP_TOOL_PROFILE=core npx vdon-mcp-server
253
- ```
82
+ Ninja MCP's direction is live access to remote peers, tools, and perception through MCP. ninja-p2p's direction is the continuing team workspace and its handoffs. See [Ninja MCP's direction](https://steveseguin.github.io/ninjamcp/direction.html) and [ninja-p2p's direction](https://github.com/steveseguin/ninja-p2p/blob/main/docs/north-star.md).
254
83
 
255
- Set `VDON_MCP_JOIN_TOKEN_SECRET` before secure preset installs/runtime.
84
+ ## Optional remote Chrome
256
85
 
257
- ## Compatibility
86
+ The [browser worker](https://steveseguin.github.io/ninjamcp/browser-worker.html) lets agents share an isolated, headless Chrome tab over WebRTC or your HTTP relay. Each agent has a separate credential; one controller holds the tab while others observe. Tools return page snapshots and screenshots. Hosts can also enable live tab audio/video through the VDO.Ninja SDK.
258
87
 
259
- - Codex MCP CLI
260
- - Claude Code MCP CLI
261
- - Other MCP clients that support stdio command servers
88
+ Install the optional browser packages on the host, run `vdon-mcp-browser --setup`, and register it as a separate MCP server. The [setup guide](https://steveseguin.github.io/ninjamcp/browser-worker.html) covers connection checks, automatic WebRTC/HTTP routing, control handoff, and private viewing links.
262
89
 
263
- Manual command wiring example:
90
+ The optional [browser companion](https://steveseguin.github.io/ninjamcp/browser-agent.html) adds a speaking agent with microphone, browser-frame sharing, interruption, and control permissions. It uses an OpenAI API key; ordinary MCP tools and live browser viewing do not require a model account.
264
91
 
265
- ```json
266
- {
267
- "name": "vdo-ninja-mcp",
268
- "command": "node",
269
- "args": ["node_modules/@vdoninja/mcp/scripts/vdo-mcp-server.js"]
270
- }
271
- ```
92
+ ## Optional remote SSApp
272
93
 
273
- ## Local Development (This Repo)
94
+ For an existing Social Stream Ninja Standalone instance, the optional [SSApp bridge](https://steveseguin.github.io/ninjamcp/ssapp-integration.html)
95
+ forwards its MCP tools, screenshots, and input over WebRTC or the HTTP relay. Agents use separate credentials
96
+ and take turns controlling the app; SSApp keeps its existing sessions and windows.
274
97
 
275
- Start server:
98
+ ## Run from a checkout
276
99
 
277
100
  ```bash
101
+ npm install
278
102
  node scripts/vdo-mcp-server.js
279
103
  ```
280
104
 
281
- Install registration from local checkout:
282
-
283
- ```bash
284
- node scripts/install-mcp.js
285
- ```
286
-
287
- ## Testing
288
-
289
- Fast product-focused suite:
290
-
291
- ```bash
292
- node tests/run-mcp-tests-minimal.js
293
- ```
294
-
295
- Full suite:
296
-
297
- ```bash
298
- node tests/run-mcp-tests.js
299
- ```
300
-
301
- Live diagnostics:
302
-
303
- ```bash
304
- node tests/live-turn-smoke.js
305
- node tests/live-turn-soak.js
306
- node tests/live-turn-matrix.js
307
- node tests/live-turn-preset-matrix.js
308
- ```
309
-
310
- ## Key Files
311
-
312
- - `scripts/vdo-mcp-server.js` (stdio MCP server)
313
- - `scripts/vdo-mcp-streamable-http.js` (streamable HTTP bridge)
314
- - `scripts/install-mcp.js` (Codex/Claude installer automation)
315
- - `references/quickstart-and-compat.md` (operator quickstart)
316
- - `references/mcp-tool-contract.md` (tool semantics and contracts)
317
- - `server.json` (MCP discovery manifest metadata)
105
+ `npm run check` runs the existing suite. `npm run demo:message` and `npm run demo:file` run local simulated workflows; set `MCP_DEMO_FAKE=0` to use live WebRTC peers. See [advanced diagnostics](https://steveseguin.github.io/ninjamcp/advanced-diagnostics.html) for live network checks.
318
106
 
319
107
  ## License
320
108
 
321
- AGPL-3.0-only with additional unmodified-distribution exception.
109
+ MIT. Copyright (c) 2026 Steve Seguin.
322
110
 
323
- - Main license text: `LICENSE`
324
- - Additional permission: `LICENSE-SDK-EXCEPTION`
111
+ The `@vdoninja/sdk` dependency has its own terms: MPL-2.0, with some separately marked wrappers and adapters under MIT. Preserve its copyright and license notices when redistributing it and comply with the MPL-2.0 for covered SDK files. Those terms do not require the rest of an application to use the MPL-2.0.
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+ class NinjaMicrophone extends AudioWorkletProcessor {
3
+ constructor() { super(); this.samples = new Int16Array(2400); this.offset = 0; }
4
+ process(inputs) {
5
+ const input = inputs[0] && inputs[0][0];
6
+ if (!input) return true;
7
+ for (const value of input) {
8
+ this.samples[this.offset++] = Math.round(Math.max(-1, Math.min(1, value)) * 32767);
9
+ if (this.offset === this.samples.length) {
10
+ this.port.postMessage(this.samples.buffer, [this.samples.buffer]);
11
+ this.samples = new Int16Array(2400); this.offset = 0;
12
+ }
13
+ }
14
+ return true;
15
+ }
16
+ }
17
+ registerProcessor('ninja-microphone', NinjaMicrophone);
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+ const $ = (id) => document.getElementById(id);
3
+ const token = location.hash.slice(1);
4
+ history.replaceState(null, '', location.pathname);
5
+ const socket = new WebSocket(`ws://${location.host}/operator?token=${encodeURIComponent(token)}`);
6
+ let connected = false, micStream, micContext, micNode, outputContext, sdk, mediaStream;
7
+ let micEpoch = 0, mediaEpoch = 0, playbackEpoch = 0, playbackPlayed = 0, playbackEnd = 0, playbackItem = null;
8
+ const sources = new Map();
9
+ const send = (event) => { if (socket.readyState === WebSocket.OPEN && socket.bufferedAmount < 1000000) socket.send(JSON.stringify(event)); };
10
+ const log = (text) => { $('log').textContent = ($('log').textContent + text).slice(-20000); $('log').scrollTop = $('log').scrollHeight; };
11
+ function sharing() {
12
+ send({ type: 'permissions', microphone: connected && $('microphone').checked, share: $('frames').checked, control: $('control').checked });
13
+ const parts = [];
14
+ if (connected && $('microphone').checked) parts.push('microphone');
15
+ if (connected && $('frames').checked) parts.push('browser frames and page content');
16
+ $('sharing').textContent = parts.length ? `Sending ${parts.join(' + ')} to OpenAI.${$('control').checked ? ' Browser control enabled.' : ''}` : 'Microphone and browser sharing are off.';
17
+ }
18
+ async function stopMic() {
19
+ micEpoch++;
20
+ if (micNode) micNode.disconnect(); micNode = null;
21
+ if (micStream) micStream.getTracks().forEach((track) => track.stop()); micStream = null;
22
+ const context = micContext; micContext = null;
23
+ if (context) await context.close().catch(() => {});
24
+ }
25
+ async function startMic() {
26
+ if (!connected || !$('microphone').checked || micStream) return;
27
+ const epoch = ++micEpoch;
28
+ try {
29
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, noiseSuppression: true }, video: false });
30
+ if (epoch !== micEpoch || !connected || !$('microphone').checked) { stream.getTracks().forEach((track) => track.stop()); return; }
31
+ micStream = stream;
32
+ const context = new AudioContext({ sampleRate: 24000 });
33
+ micContext = context;
34
+ await context.audioWorklet.addModule('/audio-worklet.js');
35
+ if (epoch !== micEpoch) return;
36
+ micNode = new AudioWorkletNode(micContext, 'ninja-microphone');
37
+ micNode.port.onmessage = (event) => {
38
+ if (!connected || !$('microphone').checked) return;
39
+ send({ type: 'audio', data: btoa(String.fromCharCode(...new Uint8Array(event.data))) });
40
+ };
41
+ const silent = micContext.createGain(); silent.gain.value = 0;
42
+ micContext.createMediaStreamSource(micStream).connect(micNode); micNode.connect(silent); silent.connect(micContext.destination);
43
+ await micContext.resume();
44
+ } catch (error) { if (epoch !== micEpoch) return; $('error').textContent = `Microphone: ${error.message}`; $('microphone').checked = false; await stopMic(); sharing(); }
45
+ }
46
+ function stopPlayback(report = true) {
47
+ playbackEpoch++;
48
+ let played = playbackPlayed;
49
+ if (outputContext) for (const segment of sources.values()) played += Math.max(0, Math.min(segment.duration, outputContext.currentTime - segment.start));
50
+ if (playbackItem && report) send({ type: 'playback', item_id: playbackItem, played_ms: played * 1000 });
51
+ for (const source of sources.keys()) { source.onended = null; try { source.stop(); } catch (_) {} }
52
+ sources.clear(); playbackItem = null; playbackEnd = 0; playbackPlayed = 0;
53
+ }
54
+ async function audio(event) {
55
+ if (!connected) return;
56
+ const epoch = playbackEpoch;
57
+ outputContext = outputContext || new AudioContext({ sampleRate: 24000 });
58
+ await outputContext.resume();
59
+ if (epoch !== playbackEpoch || !connected) return;
60
+ const bytes = Uint8Array.from(atob(event.data), (value) => value.charCodeAt(0));
61
+ const samples = new Int16Array(bytes.buffer);
62
+ const buffer = outputContext.createBuffer(1, samples.length, 24000);
63
+ const data = buffer.getChannelData(0); for (let i = 0; i < samples.length; i++) data[i] = samples[i] / 32768;
64
+ if (playbackItem !== event.item_id) { stopPlayback(false); playbackItem = event.item_id; playbackEnd = outputContext.currentTime; }
65
+ if (playbackEnd - outputContext.currentTime > 30) { send({ type: 'interrupt' }); stopPlayback(); return; }
66
+ const source = outputContext.createBufferSource(); source.buffer = buffer; source.connect(outputContext.destination);
67
+ const start = Math.max(outputContext.currentTime, playbackEnd); source.start(start); playbackEnd = start + buffer.duration;
68
+ sources.set(source, { start, duration: buffer.duration }); source.onended = () => { if (sources.delete(source)) playbackPlayed += buffer.duration; };
69
+ }
70
+ async function stopWatching() {
71
+ mediaEpoch++;
72
+ const viewer = sdk; sdk = null;
73
+ if (mediaStream) mediaStream.getTracks().forEach((track) => track.stop()); mediaStream = null;
74
+ $('video').srcObject = null; $('video').hidden = true; $('video').muted = true;
75
+ $('watch').disabled = false; $('unwatch').hidden = true; $('sound').hidden = true;
76
+ $('placeholder').hidden = !$('preview').hidden;
77
+ if (viewer) await viewer.disconnect().catch(() => {});
78
+ }
79
+ async function watch(media) {
80
+ if (!media.enabled) throw new Error('Live browser media is off or unavailable. Enable media on the host, or share browser frames for HTTP-compatible vision.');
81
+ await stopWatching();
82
+ const epoch = mediaEpoch;
83
+ $('watch').disabled = true; $('unwatch').hidden = false; $('sound').hidden = false;
84
+ const viewer = new VDONinjaSDK({ password: media.password, ...(media.signaling_host ? { host: media.signaling_host } : {}) });
85
+ sdk = viewer;
86
+ const stream = new MediaStream(); mediaStream = stream; $('video').srcObject = stream;
87
+ viewer.addEventListener('track', (event) => { if (epoch !== mediaEpoch) { event.detail.track.stop(); return; } stream.addTrack(event.detail.track); $('video').hidden = false; $('preview').hidden = true; $('placeholder').hidden = true; });
88
+ await viewer.connect(); if (epoch !== mediaEpoch) { await viewer.disconnect(); return; }
89
+ await viewer.joinRoom({ room: media.room, password: media.password }); if (epoch !== mediaEpoch) { await viewer.disconnect(); return; }
90
+ await viewer.view(media.stream_id, { audio: true, video: true });
91
+ }
92
+ socket.onmessage = async ({ data }) => {
93
+ try {
94
+ const event = JSON.parse(data);
95
+ if (event.type === 'hello') { $('identity').textContent = `${event.worker} · ${event.transport} · ${event.provider} / ${event.model}`; if (!event.key_available) $('error').textContent = 'Set OPENAI_API_KEY before starting this operator server to enable the agent. Browser checks and live viewing still work.'; }
96
+ else if (event.type === 'status') {
97
+ connected = !!event.connected; $('status').textContent = connected ? 'Agent connected' : 'Stopped'; $('start').disabled = connected;
98
+ if (connected) { sharing(); await startMic(); } else { await stopMic(); stopPlayback(); $('microphone').checked = false; $('frames').checked = false; $('control').checked = false; sharing(); }
99
+ if (event.message) log(event.message + '\n');
100
+ } else if (event.type === 'audio') await audio(event);
101
+ else if (event.type === 'interrupt') stopPlayback();
102
+ else if (event.type === 'text_delta') log(event.text);
103
+ else if (event.type === 'turn_done') log('\n');
104
+ else if (event.type === 'tool') log(`\n${event.name}: ${event.stage}${event.route ? ` (${event.route})` : ''}\n`);
105
+ else if (event.type === 'error') { $('error').textContent = event.message; $('start').disabled = connected; }
106
+ else if (event.type === 'worker') log(`Worker authenticated · ${event.role} · ${event.route}\n`);
107
+ else if (event.type === 'preview') { if ($('video').hidden) { $('preview').src = `data:${event.mime};base64,${event.data}`; $('preview').hidden = false; $('placeholder').hidden = true; } $('frameTime').textContent = `Frame sent to OpenAI · ${event.captured_at}`; }
108
+ else if (event.type === 'media') await watch(event);
109
+ } catch (error) { $('error').textContent = error.message; }
110
+ };
111
+ socket.onclose = () => { connected = false; stopMic(); stopPlayback(false); stopWatching(); $('status').textContent = 'Disconnected'; $('error').textContent = 'The local operator disconnected. Reopen its private URL to reconnect.'; $('sharing').textContent = 'Sharing stopped.'; };
112
+ $('start').onclick = async () => { outputContext = outputContext || new AudioContext({ sampleRate: 24000 }); await outputContext.resume(); $('error').textContent = ''; $('start').disabled = true; send({ type: 'start' }); };
113
+ $('stop').onclick = () => { stopPlayback(); send({ type: 'stop' }); stopMic(); };
114
+ $('interrupt').onclick = () => { stopPlayback(); send({ type: 'interrupt' }); };
115
+ $('microphone').onchange = async () => { sharing(); if ($('microphone').checked) await startMic(); else await stopMic(); };
116
+ $('frames').onchange = () => { if (!$('frames').checked) $('control').checked = false; sharing(); if (connected && $('frames').checked) send({ type: 'frame' }); };
117
+ $('control').onchange = () => { if ($('control').checked) $('frames').checked = true; sharing(); };
118
+ $('check').onclick = () => send({ type: 'check' });
119
+ $('watch').onclick = () => send({ type: 'watch' });
120
+ $('unwatch').onclick = stopWatching;
121
+ $('sound').onclick = () => { $('video').muted = !$('video').muted; $('sound').textContent = $('video').muted ? 'Hear browser audio' : 'Mute browser audio'; };
122
+ $('message').onsubmit = (event) => { event.preventDefault(); if (!connected) { $('error').textContent = 'Start the agent before sending a message.'; return; } const text = $('text').value.trim(); if (!text) return; stopPlayback(); log(`\nYou: ${text}\n`); send({ type: 'text', text }); $('text').value = ''; };
123
+ setInterval(() => { if (connected && $('frames').checked) send({ type: 'frame' }); }, 3000);
124
+ window.addEventListener('pagehide', () => { stopMic(); if (sdk) sdk.disconnect(); socket.close(); });
@@ -0,0 +1,50 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <meta charset="utf-8">
4
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <title>Ninja · Browser companion</title>
6
+ <style>
7
+ :root { color-scheme: dark; font: 16px/1.5 system-ui, sans-serif; background: #10151e; color: #e9eef7; }
8
+ body { margin: 0 auto; padding: 28px; max-width: 1160px; }
9
+ header { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
10
+ h1 { font-size: 26px; margin: 0; } h2 { font-size: 17px; margin-top: 0; }
11
+ .muted { color: #a9b7ca; } .status { border: 1px solid #455369; border-radius: 20px; padding: 5px 12px; }
12
+ .layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; margin-top: 24px; }
13
+ .panel { background: #182130; border: 1px solid #2f4058; border-radius: 14px; padding: 20px; }
14
+ .stage { display: grid; place-items: center; min-height: 300px; background: #090d13; border-radius: 9px; overflow: hidden; }
15
+ .stage img, .stage video { width: 100%; max-height: 450px; object-fit: contain; }
16
+ [hidden] { display: none !important; }
17
+ label { display: block; margin: 16px 0; } input[type=checkbox] { width: 18px; height: 18px; vertical-align: middle; margin-right: 8px; accent-color: #6dcfbc; }
18
+ button, input[type=text] { font: inherit; border-radius: 7px; border: 1px solid #50647e; padding: 9px 12px; }
19
+ button { background: #283b53; color: white; cursor: pointer; } button:hover { background: #345471; }
20
+ button:disabled { opacity: .45; cursor: default; } button.primary { background: #257c6f; } button.stop { background: #773c48; }
21
+ .row { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
22
+ form { display: flex; gap: 8px; margin-top: 16px; } input[type=text] { min-width: 0; flex: 1; background: #111923; color: white; }
23
+ #log { white-space: pre-wrap; max-height: 260px; overflow: auto; font-size: 14px; }
24
+ #sharing { color: #8ee5ce; min-height: 48px; } #error { color: #ffc4bb; min-height: 24px; }
25
+ small { display: block; color: #a9b7ca; }
26
+ @media(max-width: 780px) { .layout { grid-template-columns: 1fr; } body { padding: 16px; } header { align-items: start; } }
27
+ </style>
28
+ <header><div><h1>Ninja browser companion</h1><div class="muted" id="identity">Connecting to the local operator…</div></div><span class="status" id="status">Stopped</span></header>
29
+ <div class="layout">
30
+ <main class="panel">
31
+ <div class="stage"><span id="placeholder" class="muted">Choose a browser view to begin.</span><img id="preview" alt="Latest shared browser frame" hidden><video id="video" autoplay muted playsinline hidden></video></div>
32
+ <div class="row"><button id="watch">Watch live browser</button><button id="unwatch" hidden>Stop watching</button><button id="sound" hidden>Hear browser audio</button><button id="check">Check worker</button></div>
33
+ <small id="frameTime">Live viewing uses VDO.Ninja. Model sharing is controlled separately.</small>
34
+ <form id="message"><input id="text" type="text" maxlength="8000" placeholder="Ask your browser companion…" aria-label="Message to the agent"><button id="send">Send</button></form>
35
+ <p id="error" role="alert"></p><div id="log" aria-live="polite"></div>
36
+ </main>
37
+ <aside class="panel">
38
+ <h2>Talk and work together</h2>
39
+ <p class="muted">Start an OpenAI voice session, then choose what the agent can hear, see, and control.</p>
40
+ <label><input id="microphone" type="checkbox">Share my microphone</label>
41
+ <label><input id="frames" type="checkbox">Share browser frames</label>
42
+ <label><input id="control" type="checkbox">Allow browser control</label>
43
+ <div id="sharing">Microphone and browser sharing are off.</div>
44
+ <div class="row"><button id="start" class="primary">Start agent</button><button id="interrupt">Interrupt</button><button id="stop" class="stop">End session</button></div>
45
+ <small>Shared microphone audio, browser images, and tool results go to OpenAI. Browser frames update about every three seconds. Control follows the worker's tab lease. Interrupt stops speech and queued actions; an action already sent may finish.</small>
46
+ <p><small>AI-generated voice. No local recording. Sessions end after 15 minutes. Watching the SDK stream alone does not send it to OpenAI.</small></p>
47
+ </aside>
48
+ </div>
49
+ <script src="/sdk.js"></script><script src="/client.js"></script>
50
+ </html>
@@ -1,3 +1,6 @@
1
+ // Copyright (c) 2026 Steve Seguin
2
+ // SPDX-License-Identifier: MIT
3
+
1
4
  /**
2
5
  * Cloudflare Worker proxy template for a remote MCP HTTP endpoint.
3
6
  *
package/docs/.nojekyll ADDED
File without changes
package/docs/404.html ADDED
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html lang="en" data-icons="/ninjamcp/assets/icons.svg">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Page not found · VDO.Ninja MCP</title>
7
+ <meta name="robots" content="noindex">
8
+ <meta name="color-scheme" content="light dark">
9
+ <link rel="icon" href="/ninjamcp/favicon.ico" sizes="32x32">
10
+ <link rel="icon" href="/ninjamcp/assets/favicon.svg" type="image/svg+xml">
11
+ <link rel="apple-touch-icon" href="/ninjamcp/assets/apple-touch-icon.png">
12
+ <link rel="stylesheet" href="/ninjamcp/assets/site.css">
13
+ </head>
14
+ <body>
15
+ <main class="hero" style="min-height:100vh;display:grid;place-items:center;padding:24px">
16
+ <div class="wrap">
17
+ <a href="/ninjamcp/" aria-label="VDO.Ninja MCP home"><img src="/ninjamcp/assets/logo.svg" alt="" width="64" height="64" style="margin:0 auto"></a>
18
+ <h1 style="font-size:clamp(36px,6vw,56px);max-width:none">This line is dead.</h1>
19
+ <p class="lede">The page you asked for isn't here. It may have moved when the docs were reorganized.</p>
20
+ <div class="hero-actions">
21
+ <a class="btn btn-dark btn-lg" href="/ninjamcp/">Home</a>
22
+ <a class="btn btn-lg" href="/ninjamcp/guide.html">Documentation</a>
23
+ <a class="btn btn-lg" href="/ninjamcp/#faq">FAQ</a>
24
+ </div>
25
+ </div>
26
+ </main>
27
+ </body>
28
+ </html>