@vdoninja/mcp 0.4.0 → 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 -0
  2. package/README.md +111 -0
  3. package/SKILL.md +8 -6
  4. package/assets/browser-agent/audio-worklet.js +17 -0
  5. package/assets/browser-agent/client.js +124 -0
  6. package/assets/browser-agent/index.html +50 -0
  7. package/assets/cloudflare/worker-proxy.js +3 -0
  8. package/docs/.nojekyll +0 -0
  9. package/docs/404.html +28 -0
  10. package/docs/advanced-diagnostics.html +101 -0
  11. package/docs/assets/apple-touch-icon.png +0 -0
  12. package/docs/assets/favicon.svg +7 -0
  13. package/docs/assets/how-it-works.svg +62 -0
  14. package/docs/assets/icon-192.png +0 -0
  15. package/docs/assets/icon-512.png +0 -0
  16. package/docs/assets/icon-maskable-512.png +0 -0
  17. package/docs/assets/icons.svg +42 -0
  18. package/docs/assets/logo.svg +7 -0
  19. package/docs/assets/og-image.png +0 -0
  20. package/docs/assets/site.css +735 -0
  21. package/docs/assets/site.js +289 -0
  22. package/docs/browser-agent.html +122 -0
  23. package/docs/browser-worker.html +165 -0
  24. package/docs/client-config-examples.html +254 -0
  25. package/docs/direction.html +175 -0
  26. package/docs/favicon.ico +0 -0
  27. package/docs/guide.html +872 -0
  28. package/docs/http-relay.html +154 -0
  29. package/docs/index.html +834 -0
  30. package/docs/mcp-tool-contract.html +226 -0
  31. package/docs/quickstart-and-compat.html +222 -0
  32. package/docs/site.webmanifest +16 -0
  33. package/docs/sitemap.xml +63 -0
  34. package/docs/ssapp-integration.html +594 -0
  35. package/docs/vdo-ninja-p2p-data-notes.html +225 -0
  36. package/package.json +67 -11
  37. package/references/advanced-diagnostics.md +6 -4
  38. package/references/browser-agent.md +53 -0
  39. package/references/browser-worker.md +142 -0
  40. package/references/client-config-examples.md +253 -0
  41. package/references/direction.md +88 -0
  42. package/references/http-relay.md +107 -0
  43. package/references/mcp-tool-contract.md +16 -6
  44. package/references/quickstart-and-compat.md +18 -6
  45. package/references/ssapp-integration.md +371 -0
  46. package/references/vdo-ninja-p2p-data-notes.md +3 -1
  47. package/scripts/demo-file-workflow.js +25 -6
  48. package/scripts/demo-message-workflow.js +25 -6
  49. package/scripts/install-mcp.js +55 -4
  50. package/scripts/lib/bridge-session.js +143 -26
  51. package/scripts/lib/bridge-utils.js +3 -0
  52. package/scripts/lib/browser-agent.js +255 -0
  53. package/scripts/lib/browser-media.js +79 -0
  54. package/scripts/lib/browser-routes.js +119 -0
  55. package/scripts/lib/browser-rpc.js +214 -0
  56. package/scripts/lib/browser-setup.js +133 -0
  57. package/scripts/lib/browser-worker.js +216 -0
  58. package/scripts/lib/fake-network-sdk.js +3 -0
  59. package/scripts/lib/http-relay-transport.js +294 -0
  60. package/scripts/lib/load-vdo-sdk.js +3 -0
  61. package/scripts/lib/mcp-stdio-parser.js +3 -0
  62. package/scripts/lib/ssapp-worker.js +198 -0
  63. package/scripts/lib/tool-profiles.js +3 -0
  64. package/scripts/vdo-data-bridge.js +3 -0
  65. package/scripts/vdo-mcp-browser-agent.js +132 -0
  66. package/scripts/vdo-mcp-browser.js +218 -0
  67. package/scripts/vdo-mcp-relay.js +207 -0
  68. package/scripts/vdo-mcp-server.js +87 -8
  69. package/scripts/vdo-mcp-ssapp.js +208 -0
  70. package/scripts/vdo-mcp-streamable-http.js +23 -4
  71. package/server.json +26 -10
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Steve Seguin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # VDO.Ninja MCP (`@vdoninja/mcp`)
2
+
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
+
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
+
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
+
9
+ HTML guides are also included in the package's `docs/` folder for offline reading.
10
+
11
+ ## Get started
12
+
13
+ Install from a stable folder; the installer registers an absolute script path with your MCP client.
14
+
15
+ ```bash
16
+ npm install @vdoninja/mcp
17
+ npx vdon-mcp-install
18
+ ```
19
+
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.
21
+
22
+ Ask the first agent to connect:
23
+
24
+ ```json
25
+ { "name": "vdo_connect", "arguments": { "room": "project_room", "stream_id": "planner" } }
26
+ ```
27
+
28
+ Ask the second agent to join and connect to the first:
29
+
30
+ ```json
31
+ { "name": "vdo_connect", "arguments": { "room": "project_room", "stream_id": "reviewer", "target_stream_id": "planner" } }
32
+ ```
33
+
34
+ Wait for the peer to appear in `vdo_sync_peers`, then send a message using the returned session ID:
35
+
36
+ ```json
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 } }
39
+ ```
40
+
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.
42
+
43
+ See the [quickstart](https://steveseguin.github.io/ninjamcp/quickstart-and-compat.html) for connection troubleshooting and secure presets.
44
+
45
+ ## What you can do
46
+
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` |
55
+
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.
57
+
58
+ ## Choose a connection
59
+
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). |
65
+
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.
67
+
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.
69
+
70
+ ## Where this fits
71
+
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) |
77
+
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.
79
+
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.
81
+
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).
83
+
84
+ ## Optional remote Chrome
85
+
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.
87
+
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.
89
+
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.
91
+
92
+ ## Optional remote SSApp
93
+
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.
97
+
98
+ ## Run from a checkout
99
+
100
+ ```bash
101
+ npm install
102
+ node scripts/vdo-mcp-server.js
103
+ ```
104
+
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.
106
+
107
+ ## License
108
+
109
+ MIT. Copyright (c) 2026 Steve Seguin.
110
+
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.
package/SKILL.md CHANGED
@@ -27,13 +27,14 @@ Use this skill to run VDO.Ninja as an MCP data bridge in Codex, Claude Code, and
27
27
  ## Fast Path
28
28
 
29
29
  1. Install MCP registration:
30
- `npm i @vdoninja/mcp @roamhq/wrtc`
30
+ `npm i @vdoninja/mcp`
31
31
 
32
32
  2. Register for Codex/Claude:
33
33
  `npx vdon-mcp-install`
34
34
 
35
35
  3. Start server:
36
36
  `npx vdon-mcp-server`
37
+ If run directly in a terminal, it prints a short running banner and a first-call hint.
37
38
 
38
39
  4. Optional least-privilege tool profile:
39
40
  `VDON_MCP_TOOL_PROFILE=core|file|state|full npx vdon-mcp-server`
@@ -80,11 +81,12 @@ For secure presets, set `VDON_MCP_JOIN_TOKEN_SECRET` before install/runtime.
80
81
 
81
82
  - Product test bar: `npm run test:mcp`
82
83
  - Full engineering suite: `npm run test:mcp:all`
83
- - Live diagnostics (optional): `MCP/references/advanced-diagnostics.md`
84
+ - Live diagnostics (optional): `references/advanced-diagnostics.md`
84
85
 
85
86
  ## References
86
87
 
87
- - Operator quickstart: `MCP/references/quickstart-and-compat.md`
88
- - Tool contract: `MCP/references/mcp-tool-contract.md`
89
- - Transport notes: `MCP/references/vdo-ninja-p2p-data-notes.md`
90
- - Registry manifest: `MCP/server.json`
88
+ - Operator quickstart: `references/quickstart-and-compat.md`
89
+ - Client config examples: `references/client-config-examples.md`
90
+ - Tool contract: `references/mcp-tool-contract.md`
91
+ - Transport notes: `references/vdo-ninja-p2p-data-notes.md`
92
+ - Registry manifest: `server.json`
@@ -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>
@@ -0,0 +1,101 @@
1
+ <!doctype html>
2
+ <html lang="en" data-icons="assets/icons.svg">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Advanced diagnostics · VDO.Ninja MCP</title>
7
+ <meta name="description" content="Check network connectivity and diagnose connection reliability.">
8
+ <meta name="color-scheme" content="light dark">
9
+ <link rel="canonical" href="https://steveseguin.github.io/ninjamcp/advanced-diagnostics.html">
10
+ <link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
11
+ <link rel="stylesheet" href="assets/site.css">
12
+ <script>document.documentElement.classList.add('js');</script>
13
+ </head>
14
+ <body class="reference-page">
15
+ <a class="skip-link" href="#main">Skip to content</a>
16
+ <header class="nav">
17
+ <div class="wrap">
18
+ <a aria-label="VDO.Ninja MCP home" class="brand" href="./">
19
+ <img alt="" height="28" src="assets/logo.svg" width="28"/>
20
+ <span>VDO.Ninja <small>MCP</small></span>
21
+ </a>
22
+ <nav aria-label="Primary" class="nav-links">
23
+ <a href="./#how">How it works</a>
24
+ <a href="./#privacy">Privacy</a>
25
+ <a href="./#tools">Tools</a>
26
+ <a href="./#faq">FAQ</a>
27
+ <a aria-current="page" href="guide.html">Docs</a>
28
+ </nav>
29
+ <div class="nav-cta">
30
+ <a class="btn btn-ghost" href="https://github.com/steveseguin/ninjamcp"><svg aria-hidden="true" class="icon-sm"><use href="assets/icons.svg#i-github"></use></svg>GitHub</a>
31
+ <a class="btn btn-dark" href="https://www.npmjs.com/package/@vdoninja/mcp"><svg aria-hidden="true" class="icon-sm"><use href="assets/icons.svg#i-package"></use></svg>npm</a>
32
+ </div>
33
+ <button aria-expanded="false" aria-label="Menu" class="nav-toggle" type="button"><svg aria-hidden="true" class="icon"><use href="assets/icons.svg#i-menu"></use></svg></button>
34
+ </div>
35
+ </header>
36
+ <div class="docs">
37
+ <aside class="sidebar" aria-label="Documentation" id="guide-contents"><h5>Documentation</h5><a href="guide.html">Getting started</a><h5>On this page</h5><a href="#overview">Overview</a><a href="#live-test-commands">Live Test Commands</a><a href="#preset-matrix-notes">Preset Matrix Notes</a><a href="#soak-matrix-knobs">Soak / Matrix Knobs</a><a href="#important">Important</a><h5>More guides</h5><a href="quickstart-and-compat.html">Quickstart</a><a href="client-config-examples.html">Client configuration</a><a href="http-relay.html">HTTP relay</a><a href="browser-worker.html">Remote Chrome</a><a href="browser-agent.html">Voice companion</a><a href="ssapp-integration.html">Remote SSApp</a><a href="mcp-tool-contract.html">Tool contract</a><a href="vdo-ninja-p2p-data-notes.html">WebRTC data notes</a><a href="direction.html">Project direction</a></aside>
38
+ <main class="docs-main" id="main">
39
+ <button class="btn docs-toggle" type="button" aria-controls="guide-contents" aria-expanded="false">Contents</button>
40
+ <header>
41
+ <p class="guide-breadcrumb"><a href="guide.html">Documentation</a> / Advanced diagnostics</p>
42
+ <h1 id="vdoninja-mcp-advanced-diagnostics">VDO.Ninja MCP Advanced Diagnostics</h1>
43
+ <p class="guide-format"><a href="https://github.com/steveseguin/ninjamcp/blob/main/references/advanced-diagnostics.md">Read in Markdown</a></p>
44
+ </header>
45
+ <section class="doc-section guide-intro" id="overview">
46
+ <p>This page is optional. Use it when you are debugging reliability or network edge cases.</p>
47
+ </section><section class="doc-section" id="live-test-commands"><h2>Live Test Commands</h2>
48
+ <pre><code class="lang-sh">LIVE_VDO_TEST=1 npm run test:mcp:live
49
+ LIVE_VDO_TEST=1 npm run test:mcp:soak
50
+ LIVE_VDO_TEST=1 npm run test:mcp:matrix
51
+ LIVE_VDO_TEST=1 npm run test:mcp:preset:matrix
52
+ LIVE_VDO_TEST=1 SOAK_MATRIX_SWEEP_RUNS=20 npm run test:mcp:matrix:sweep
53
+ </code></pre>
54
+ <p>Secure preset matrix:</p>
55
+ <pre><code class="lang-sh">LIVE_VDO_TEST=1 VDON_MCP_JOIN_TOKEN_SECRET=change_me npm run test:mcp:preset:matrix
56
+ </code></pre>
57
+ </section><section class="doc-section" id="preset-matrix-notes"><h2>Preset Matrix Notes</h2>
58
+ <ul>
59
+ <li><code>PRESET_MATRIX_ADAPTIVE_FALLBACK=1</code> enables candidate fallback.</li>
60
+ <li><code>PRESET_MATRIX_ALLOW_DIRECT_FALLBACK=1</code> allows direct fallback after TURN candidates.</li>
61
+ <li><code>PRESET_MATRIX_STRICT=1</code> fails process when any profile fails.</li>
62
+ <li><code>PRESET_CONNECT_ATTEMPTS</code>, <code>PRESET_CONNECT_TIMEOUT_MS</code>, and <code>PRESET_CHANNEL_READY_TIMEOUT_MS</code> control connect behavior.</li>
63
+ </ul>
64
+ <p>Artifacts are written to:</p>
65
+ <ul>
66
+ <li><code>tests/artifacts/latest-preset-matrix-report.json</code></li>
67
+ <li><code>tests/artifacts/latest-preset-matrix-summary.txt</code></li>
68
+ <li><code>tests/artifacts/history/preset-matrix-*.json</code></li>
69
+ <li><code>tests/artifacts/history/preset-matrix-*.txt</code></li>
70
+ </ul>
71
+ </section><section class="doc-section" id="soak-matrix-knobs"><h2>Soak / Matrix Knobs</h2>
72
+ <ul>
73
+ <li>Smoke: <code>LIVE_SMOKE_RETRIES</code>, <code>LIVE_SMOKE_TIMEOUT_MS</code></li>
74
+ <li>Soak: <code>SOAK_RETRIES</code>, <code>SOAK_TIMEOUT_MS</code></li>
75
+ <li>Matrix: <code>SOAK_MATRIX_PROFILE_RETRIES</code>, <code>SOAK_MATRIX_PROFILE_TIMEOUT_MS</code></li>
76
+ <li>Sweep: <code>SOAK_MATRIX_SWEEP_RUNS</code>, <code>SOAK_MATRIX_SWEEP_TIMEOUT_MS</code>, <code>SOAK_MATRIX_SWEEP_MIN_SUCCESS_RATE</code></li>
77
+ </ul>
78
+ </section><section class="doc-section" id="important"><h2>Important</h2>
79
+ <ul>
80
+ <li>WebRTC data channels are the transport.</li>
81
+ <li>TURN helps reachability but is not a universal bypass for all enterprise firewall policies.</li>
82
+ </ul></section>
83
+ <nav class="pager" aria-label="More documentation"><a href="guide.html#guides"><small>Keep reading</small><b>Browse all guides</b></a><a href="https://discord.vdo.ninja"><small>Need help?</small><b>Ask on Discord</b></a></nav>
84
+ </main>
85
+ </div>
86
+ <footer class="footer">
87
+ <div class="wrap">
88
+ <a class="brand" href="./"><img alt="" height="24" src="assets/logo.svg" width="24"/><span>VDO.Ninja <small>MCP</small></span></a>
89
+ <nav aria-label="Footer">
90
+ <a href="./">Home</a>
91
+ <a href="https://github.com/steveseguin/ninjamcp">GitHub</a>
92
+ <a href="https://www.npmjs.com/package/@vdoninja/mcp">npm</a>
93
+ <a href="https://discord.vdo.ninja">Discord</a>
94
+ <a href="https://vdo.ninja">VDO.Ninja</a>
95
+ </nav>
96
+ <span>MIT · © 2026 Steve Seguin</span>
97
+ </div>
98
+ </footer>
99
+ <script src="assets/site.js" defer></script>
100
+ </body>
101
+ </html>
Binary file
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="VDO.Ninja MCP">
2
+ <rect width="64" height="64" rx="16" fill="#14161b"/>
3
+ <path d="M8 27.5c7.5-4 15.5-6 24-6s16.5 2 24 6v9c-7.5 4-15.5 6-24 6s-16.5-2-24-6z" fill="#3d5afe"/>
4
+ <circle cx="21" cy="32" r="5" fill="#fff"/>
5
+ <circle cx="43" cy="32" r="5" fill="#fff"/>
6
+ <path d="M27.8 32h8.6" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-dasharray="0.1 4.1"/>
7
+ </svg>
@@ -0,0 +1,62 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 350" width="900" height="350" role="img" aria-label="Two agents, each with a local VDO.Ninja MCP server, connected by a direct encrypted WebRTC data channel. A handshake server helps them connect; a TURN relay is a fallback.">
2
+ <title>How VDO.Ninja MCP works</title>
3
+ <style>
4
+ text { font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif; fill: #4a4e58; font-size: 13px; }
5
+ .strong { fill: #111318; font-weight: 600; font-size: 14px; }
6
+ .mono { font-family: ui-monospace, "SF Mono", Consolas, monospace; font-size: 11.5px; fill: #80848e; }
7
+ .ok { fill: #16a34a; font-weight: 600; font-size: 12.5px; }
8
+ .box { fill: #ffffff; stroke: #d6d6d0; stroke-width: 1.2; }
9
+ .soft { fill: #f3f3f0; stroke: #e4e4df; stroke-width: 1.2; }
10
+ .machine { fill: none; stroke: #d6d6d0; stroke-width: 1.2; stroke-dasharray: 5 5; }
11
+ .ln { stroke: #80848e; stroke-width: 1.4; fill: none; }
12
+ .dash { stroke: #80848e; stroke-width: 1.3; stroke-dasharray: 4 5; fill: none; }
13
+ .p2p { stroke: #16a34a; stroke-width: 3; fill: none; }
14
+ </style>
15
+ <defs>
16
+ <marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 10 5 0 10z" fill="#80848e"/></marker>
17
+ <marker id="g" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0 10 5 0 10z" fill="#16a34a"/></marker>
18
+ </defs>
19
+ <rect x="0.5" y="0.5" width="899" height="349" rx="20" fill="#fafaf8" stroke="#e4e4df"/>
20
+ <g transform="translate(0 10)">
21
+ <rect class="machine" x="16" y="98" width="346" height="134" rx="16"/>
22
+ <rect class="machine" x="538" y="98" width="346" height="134" rx="16"/>
23
+ <text x="32" y="122" class="mono">MACHINE A</text>
24
+ <text x="868" y="122" class="mono" text-anchor="end">MACHINE B</text>
25
+
26
+ <rect class="box" x="32" y="136" width="122" height="80" rx="12"/>
27
+ <text x="93" y="170" class="strong" text-anchor="middle">Agent A</text>
28
+ <text x="93" y="190" text-anchor="middle">MCP client</text>
29
+ <rect class="box" x="214" y="136" width="132" height="80" rx="12"/>
30
+ <text x="280" y="170" class="strong" text-anchor="middle">MCP server</text>
31
+ <text x="280" y="190" class="mono" text-anchor="middle">vdon-mcp-server</text>
32
+ <path class="ln" d="M154 176h58" marker-end="url(#a)" marker-start="url(#a)"/>
33
+ <text x="184" y="166" class="mono" text-anchor="middle">stdio</text>
34
+
35
+ <rect class="box" x="554" y="136" width="132" height="80" rx="12"/>
36
+ <text x="620" y="170" class="strong" text-anchor="middle">MCP server</text>
37
+ <text x="620" y="190" class="mono" text-anchor="middle">vdon-mcp-server</text>
38
+ <rect class="box" x="746" y="136" width="122" height="80" rx="12"/>
39
+ <text x="807" y="170" class="strong" text-anchor="middle">Agent B</text>
40
+ <text x="807" y="190" text-anchor="middle">MCP client</text>
41
+ <path class="ln" d="M688 176h56" marker-end="url(#a)" marker-start="url(#a)"/>
42
+ <text x="716" y="166" class="mono" text-anchor="middle">stdio</text>
43
+
44
+ <path class="p2p" d="M348 176h204" marker-end="url(#g)" marker-start="url(#g)"/>
45
+ <text x="450" y="164" class="ok" text-anchor="middle">WebRTC data channel</text>
46
+ <text x="450" y="196" class="mono" text-anchor="middle">DTLS-encrypted</text>
47
+
48
+ <rect class="soft" x="370" y="8" width="160" height="56" rx="12"/>
49
+ <text x="450" y="32" class="strong" text-anchor="middle">Handshake server</text>
50
+ <text x="450" y="50" class="mono" text-anchor="middle">wss.vdo.ninja</text>
51
+ <path class="dash" d="M300 136 382 66"/>
52
+ <path class="dash" d="M600 136 518 66"/>
53
+ <text x="300" y="86" class="mono" text-anchor="middle">find peers</text>
54
+ <text x="600" y="86" class="mono" text-anchor="middle">find peers</text>
55
+
56
+ <rect class="soft" x="350" y="266" width="200" height="56" rx="12"/>
57
+ <text x="450" y="290" class="strong" text-anchor="middle">TURN relay</text>
58
+ <text x="450" y="308" class="mono" text-anchor="middle">fallback · ciphertext only</text>
59
+ <path class="dash" d="M300 216 382 266"/>
60
+ <path class="dash" d="M600 216 518 266"/>
61
+ </g>
62
+ </svg>
Binary file
Binary file