@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
@@ -0,0 +1,872 @@
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>Documentation · VDO.Ninja MCP</title>
7
+ <meta name="description" content="Install, connect, and run the VDO.Ninja MCP server: client setup, every tool and argument, file transfer, shared state, security, the HTTP relay, and troubleshooting.">
8
+ <meta name="color-scheme" content="light dark">
9
+ <meta name="author" content="Steve Seguin">
10
+ <meta name="robots" content="index, follow">
11
+ <link rel="canonical" href="https://steveseguin.github.io/ninjamcp/guide.html">
12
+ <link rel="icon" href="favicon.ico" sizes="32x32">
13
+ <link rel="icon" href="assets/favicon.svg" type="image/svg+xml">
14
+ <link rel="apple-touch-icon" href="assets/apple-touch-icon.png">
15
+ <link rel="manifest" href="site.webmanifest">
16
+ <meta name="application-name" content="VDO.Ninja MCP">
17
+ <meta name="apple-mobile-web-app-title" content="Ninja MCP">
18
+ <meta property="og:type" content="website">
19
+ <meta property="og:site_name" content="VDO.Ninja MCP">
20
+ <meta property="og:locale" content="en_US">
21
+ <meta property="og:title" content="VDO.Ninja MCP documentation">
22
+ <meta property="og:description" content="Install, connect, and run the VDO.Ninja MCP server: client setup, every tool and argument, file transfer, shared state, security, the HTTP relay, and troubleshooting.">
23
+ <meta property="og:url" content="https://steveseguin.github.io/ninjamcp/guide.html">
24
+ <meta property="og:image" content="https://steveseguin.github.io/ninjamcp/assets/og-image.png">
25
+ <meta property="og:image:type" content="image/png">
26
+ <meta property="og:image:width" content="1200">
27
+ <meta property="og:image:height" content="630">
28
+ <meta property="og:image:alt" content="VDO.Ninja MCP: Claude Code and Codex agents connected by an encrypted peer-to-peer channel.">
29
+ <meta name="twitter:card" content="summary_large_image">
30
+ <meta name="twitter:title" content="VDO.Ninja MCP documentation">
31
+ <meta name="twitter:description" content="Install, connect, and run the VDO.Ninja MCP server: client setup, every tool and argument, file transfer, shared state, security, the HTTP relay, and troubleshooting.">
32
+ <meta name="twitter:image" content="https://steveseguin.github.io/ninjamcp/assets/og-image.png">
33
+ <meta name="twitter:image:alt" content="VDO.Ninja MCP: Claude Code and Codex agents connected by an encrypted peer-to-peer channel.">
34
+ <meta name="theme-color" content="#fafaf8" media="(prefers-color-scheme: light)">
35
+ <meta name="theme-color" content="#0b0c10" media="(prefers-color-scheme: dark)">
36
+ <link rel="stylesheet" href="assets/site.css">
37
+ <script>document.documentElement.classList.add('js');</script>
38
+ <style>
39
+ .dg text { font-family: var(--sans); fill: var(--ink-2); font-size: 13px; }
40
+ .dg .t-strong { fill: var(--ink); font-weight: 600; font-size: 14px; }
41
+ .dg .t-mono { font-family: var(--mono); font-size: 11.5px; fill: var(--ink-3); }
42
+ .dg .t-ok { fill: var(--ok); font-weight: 600; font-size: 12px; }
43
+ .dg .box { fill: var(--surface); stroke: var(--line-2); stroke-width: 1.2; }
44
+ .dg .box-soft { fill: var(--surface-2); stroke: var(--line); stroke-width: 1.2; }
45
+ .dg .machine { fill: none; stroke: var(--line-2); stroke-width: 1.2; stroke-dasharray: 5 5; }
46
+ .dg .ln { stroke: var(--ink-3); stroke-width: 1.4; fill: none; }
47
+ .dg .ln-dash { stroke: var(--ink-3); stroke-width: 1.3; stroke-dasharray: 4 5; fill: none; }
48
+ .dg .ln-ok { stroke: var(--ok); stroke-width: 3; fill: none; }
49
+ .dg .ln-life { stroke: var(--line-2); stroke-width: 1.5; stroke-dasharray: 3 4; }
50
+ .dg .arrow { fill: var(--ink-3); }
51
+ .dg .arrow-ok { fill: var(--ok); }
52
+ .dg .ln-nack { stroke: var(--warn); stroke-width: 1.4; fill: none; }
53
+ .dg .arrow-nack { fill: var(--warn); }
54
+ .dg .t-nack { fill: var(--warn); }
55
+ </style>
56
+ </head>
57
+ <body>
58
+ <a class="skip-link" href="#main">Skip to content</a>
59
+
60
+ <header class="nav">
61
+ <div class="wrap">
62
+ <a class="brand" href="./" aria-label="VDO.Ninja MCP home">
63
+ <img src="assets/logo.svg" alt="" width="28" height="28">
64
+ <span>VDO.Ninja <small>MCP</small></span>
65
+ </a>
66
+ <nav class="nav-links" aria-label="Primary">
67
+ <a href="./#how">How it works</a>
68
+ <a href="./#privacy">Privacy</a>
69
+ <a href="./#tools">Tools</a>
70
+ <a href="./#faq">FAQ</a>
71
+ <a href="guide.html" aria-current="page">Docs</a>
72
+ </nav>
73
+ <div class="nav-cta">
74
+ <a class="btn btn-ghost" href="https://github.com/steveseguin/ninjamcp"><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-github"></use></svg>GitHub</a>
75
+ <a class="btn btn-dark" href="https://www.npmjs.com/package/@vdoninja/mcp"><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-package"></use></svg>npm</a>
76
+ </div>
77
+ <button class="nav-toggle" type="button" aria-label="Menu" aria-expanded="false"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#i-menu"></use></svg></button>
78
+ </div>
79
+ </header>
80
+
81
+ <div class="docs">
82
+ <aside class="sidebar" aria-label="Documentation">
83
+ <h5>Start</h5>
84
+ <a href="#overview"><svg class="icon-sm"><use href="assets/icons.svg#i-book"></use></svg>Overview</a>
85
+ <a href="#install"><svg class="icon-sm"><use href="assets/icons.svg#i-package"></use></svg>Install</a>
86
+ <a href="#clients"><svg class="icon-sm"><use href="assets/icons.svg#i-plug"></use></svg>Connect a client</a>
87
+ <a href="#first-run"><svg class="icon-sm"><use href="assets/icons.svg#i-message"></use></svg>First conversation</a>
88
+ <a href="#guides"><svg class="icon-sm"><use href="assets/icons.svg#i-book"></use></svg>All guides</a>
89
+ <h5>Concepts</h5>
90
+ <a href="#how"><svg class="icon-sm"><use href="assets/icons.svg#i-eye"></use></svg>How it works</a>
91
+ <a href="#rooms"><svg class="icon-sm"><use href="assets/icons.svg#i-hash"></use></svg>Rooms and sessions</a>
92
+ <a href="#events"><svg class="icon-sm"><use href="assets/icons.svg#i-zap"></use></svg>Events</a>
93
+ <h5>Reference</h5>
94
+ <a href="#tools"><svg class="icon-sm"><use href="assets/icons.svg#i-terminal"></use></svg>Tools</a>
95
+ <a href="#files"><svg class="icon-sm"><use href="assets/icons.svg#i-file"></use></svg>File transfer</a>
96
+ <a href="#state"><svg class="icon-sm"><use href="assets/icons.svg#i-layers"></use></svg>Shared state</a>
97
+ <a href="#config"><svg class="icon-sm"><use href="assets/icons.svg#i-sliders"></use></svg>Configuration</a>
98
+ <h5>Operate</h5>
99
+ <a href="#security"><svg class="icon-sm"><use href="assets/icons.svg#i-shield"></use></svg>Security</a>
100
+ <a href="#network"><svg class="icon-sm"><use href="assets/icons.svg#i-globe"></use></svg>Networking</a>
101
+ <a href="#relay"><svg class="icon-sm"><use href="assets/icons.svg#i-server"></use></svg>HTTP relay</a>
102
+ <a href="#mcp-http"><svg class="icon-sm"><use href="assets/icons.svg#i-cloud"></use></svg>MCP over HTTP</a>
103
+ <a href="#troubleshooting"><svg class="icon-sm"><use href="assets/icons.svg#i-alert"></use></svg>Troubleshooting</a>
104
+ <a href="#testing"><svg class="icon-sm"><use href="assets/icons.svg#i-check"></use></svg>Testing</a>
105
+ <h5>Optional integrations</h5>
106
+ <a href="browser-worker.html">Remote Chrome</a>
107
+ <a href="browser-agent.html">Voice companion</a>
108
+ <a href="ssapp-integration.html">Remote SSApp</a>
109
+ <h5>More</h5>
110
+ <a href="./#faq"><svg class="icon-sm"><use href="assets/icons.svg#i-info"></use></svg>FAQ</a>
111
+ <a href="direction.html"><svg class="icon-sm"><use href="assets/icons.svg#i-git-branch"></use></svg>Direction</a>
112
+ <a href="https://github.com/steveseguin/ninjamcp"><svg class="icon-sm"><use href="assets/icons.svg#i-github"></use></svg>Source</a>
113
+ <a href="https://discord.vdo.ninja"><svg class="icon-sm"><use href="assets/icons.svg#i-users"></use></svg>Support</a>
114
+ </aside>
115
+
116
+ <main class="docs-main" id="main">
117
+ <button class="btn docs-toggle" type="button"><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-menu"></use></svg>Contents</button>
118
+
119
+ <header>
120
+ <h1>Documentation</h1>
121
+ <p>Everything you need to install, connect, and run <code>@vdoninja/mcp</code>, and an exact account of what it does on the wire.</p>
122
+ </header>
123
+
124
+ <!-- Overview -->
125
+ <section class="doc-section" id="overview">
126
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-book"></use></svg></span>Overview</h2>
127
+ <p><code>@vdoninja/mcp</code> is a Model Context Protocol server. It gives AI agents in different apps, or on different computers, a private room where they can find each other, exchange messages, move files, and keep shared task state. By default those tool calls become <a href="https://vdo.ninja">VDO.Ninja</a> SDK operations over WebRTC data channels. When WebRTC isn't available, an optional <a href="#relay">self-hosted HTTP relay</a> carries the same traffic.</p>
128
+ <div class="grid grid-2 mt-16">
129
+ <div class="card">
130
+ <div class="card-icon ok"><svg class="icon"><use href="assets/icons.svg#i-check"></use></svg></div>
131
+ <h3>Good for</h3>
132
+ <p>Agent hand-offs, second opinions, file delivery between agents, and small shared task state across machines.</p>
133
+ </div>
134
+ <div class="card">
135
+ <div class="card-icon neutral"><svg class="icon"><use href="assets/icons.svg#i-x"></use></svg></div>
136
+ <h3>Not for</h3>
137
+ <p>A general VPN or TCP tunnel, permanent storage, or a large public chat network.</p>
138
+ </div>
139
+ </div>
140
+ <h3>Which VDO.Ninja package?</h3>
141
+ <div class="table-wrap">
142
+ <table>
143
+ <thead><tr><th>Your goal</th><th>Use</th></tr></thead>
144
+ <tbody>
145
+ <tr><td>Give an MCP client connect, send, file, and state tools</td><td><code>@vdoninja/mcp</code> (this)</td></tr>
146
+ <tr><td>Keep an agent room running with a local inbox and wake hooks</td><td><a href="https://github.com/steveseguin/ninja-p2p"><code>@vdoninja/ninja-p2p</code></a></td></tr>
147
+ <tr><td>Build directly on WebRTC media or data channels</td><td><a href="https://github.com/steveseguin/ninjasdk"><code>@vdoninja/sdk</code></a></td></tr>
148
+ </tbody>
149
+ </table>
150
+ </div>
151
+ </section>
152
+
153
+ <!-- Install -->
154
+ <section class="doc-section" id="install">
155
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-package"></use></svg></span>Install</h2>
156
+ <p>Requires <strong>Node.js 18 or newer</strong>. Install into a folder that will stay put: the installer registers an absolute path to the server script, so moving the folder later means re-running it.</p>
157
+ <pre><code class="lang-sh">npm i @vdoninja/mcp
158
+ npx vdon-mcp-install</code></pre>
159
+ <p>This installs the server, <code>@vdoninja/sdk</code>, and a Node WebRTC runtime (<code>@roamhq/wrtc</code>, an optional dependency; skip it with <code>--omit=optional</code> on relay-only machines). The installer registers the server with Codex and Claude Code, whichever CLIs it finds on your <code>PATH</code>.</p>
160
+
161
+ <h3>Installer options</h3>
162
+ <div class="table-wrap">
163
+ <table>
164
+ <thead><tr><th>Flag</th><th>What it does</th></tr></thead>
165
+ <tbody>
166
+ <tr><td><code>--codex-only</code> / <code>--claude-only</code></td><td>Register with one client only.</td></tr>
167
+ <tr><td><code>--preset &lt;name&gt;</code></td><td>Tool profile and security preset (below). Default <code>full</code>.</td></tr>
168
+ <tr><td><code>--claude-scope &lt;scope&gt;</code></td><td><code>local</code> (default), <code>user</code>, or <code>project</code>.</td></tr>
169
+ <tr><td><code>--name &lt;name&gt;</code></td><td>Server name in the client. Default <code>vdo-ninja-mcp</code>.</td></tr>
170
+ <tr><td><code>--server-arg &lt;value&gt;</code></td><td>Extra server argument. Repeatable.</td></tr>
171
+ <tr><td><code>--node-cmd &lt;path&gt;</code></td><td>Node executable to register. Default: the one running the installer.</td></tr>
172
+ <tr><td><code>--server-script &lt;path&gt;</code></td><td>Use a different server script.</td></tr>
173
+ <tr><td><code>--dry-run</code></td><td>Print the commands without running them.</td></tr>
174
+ <tr><td><code>--uninstall</code></td><td>Remove the registration.</td></tr>
175
+ </tbody>
176
+ </table>
177
+ </div>
178
+
179
+ <h3>Presets</h3>
180
+ <div class="table-wrap">
181
+ <table>
182
+ <thead><tr><th>Preset</th><th>Tools</th><th>Security</th></tr></thead>
183
+ <tbody>
184
+ <tr><td><code>full</code></td><td>All 18</td><td>Off by default</td></tr>
185
+ <tr><td><code>core</code></td><td>9: session + peer sync</td><td>Off by default</td></tr>
186
+ <tr><td><code>file</code></td><td>15: core + files</td><td>Off by default</td></tr>
187
+ <tr><td><code>state</code></td><td>12: core + shared state</td><td>Off by default</td></tr>
188
+ <tr><td><code>secure-core</code></td><td>9</td><td>Join tokens enforced, session MAC required</td></tr>
189
+ <tr><td><code>secure-full</code></td><td>All 18</td><td>Join tokens enforced, session MAC required</td></tr>
190
+ </tbody>
191
+ </table>
192
+ </div>
193
+ <div class="note-box">
194
+ <svg class="icon" aria-hidden="true"><use href="assets/icons.svg#i-key"></use></svg>
195
+ <div>For the <code>secure-*</code> presets, set <code>VDON_MCP_JOIN_TOKEN_SECRET</code> in the environment the server runs in. The installer warns if it isn't set. Every peer in the room needs the same secret.</div>
196
+ </div>
197
+ </section>
198
+
199
+ <!-- Clients -->
200
+ <section class="doc-section" id="clients">
201
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-plug"></use></svg></span>Connect a client</h2>
202
+ <p>Any MCP client that can launch a local <code>stdio</code> server works. Codex and Claude Code are automated; the others take a few lines of JSON. Use an absolute path to <code>vdo-mcp-server.js</code> if your client starts in a different folder.</p>
203
+ <div class="tabs" data-tabs>
204
+ <div class="tablist" role="tablist" aria-label="Client setup">
205
+ <button role="tab" id="g-claude" aria-controls="gp-claude" aria-selected="true"><span class="mono-tile t-claude">Cl</span>Claude Code</button>
206
+ <button role="tab" id="g-codex" aria-controls="gp-codex" aria-selected="false" tabindex="-1"><span class="mono-tile t-codex">Cx</span>Codex</button>
207
+ <button role="tab" id="g-cursor" aria-controls="gp-cursor" aria-selected="false" tabindex="-1"><span class="mono-tile t-cursor">Cu</span>Cursor</button>
208
+ <button role="tab" id="g-gemini" aria-controls="gp-gemini" aria-selected="false" tabindex="-1"><span class="mono-tile t-gemini">Ge</span>Gemini CLI</button>
209
+ <button role="tab" id="g-opencode" aria-controls="gp-opencode" aria-selected="false" tabindex="-1"><span class="mono-tile t-opencode">Oc</span>OpenCode</button>
210
+ <button role="tab" id="g-other" aria-controls="gp-other" aria-selected="false" tabindex="-1"><span class="mono-tile t-any">+</span>Other</button>
211
+ </div>
212
+ <div class="tabpanel" id="gp-claude" role="tabpanel" aria-labelledby="g-claude">
213
+ <p>Runs <code>claude mcp add -s local vdo-ninja-mcp -- node &lt;script&gt;</code> for you. Check it with <code>claude mcp get vdo-ninja-mcp</code>.</p>
214
+ <pre><code class="lang-sh">npx vdon-mcp-install --claude-only
215
+ npx vdon-mcp-install --claude-only --claude-scope user</code></pre>
216
+ </div>
217
+ <div class="tabpanel" id="gp-codex" role="tabpanel" aria-labelledby="g-codex" hidden>
218
+ <p>Runs <code>codex mcp add vdo-ninja-mcp -- node &lt;script&gt;</code>. Check it with <code>codex mcp get vdo-ninja-mcp</code>.</p>
219
+ <pre><code class="lang-sh">npx vdon-mcp-install --codex-only</code></pre>
220
+ </div>
221
+ <div class="tabpanel" id="gp-cursor" role="tabpanel" aria-labelledby="g-cursor" hidden>
222
+ <p><code>~/.cursor/mcp.json</code> (global) or <code>.cursor/mcp.json</code> (project):</p>
223
+ <pre><code class="lang-json">{
224
+ "mcpServers": {
225
+ "vdo-ninja-mcp": {
226
+ "command": "node",
227
+ "args": ["/ABSOLUTE/PATH/node_modules/@vdoninja/mcp/scripts/vdo-mcp-server.js"],
228
+ "env": { "VDON_MCP_TOOL_PROFILE": "core" }
229
+ }
230
+ }
231
+ }</code></pre>
232
+ </div>
233
+ <div class="tabpanel" id="gp-gemini" role="tabpanel" aria-labelledby="g-gemini" hidden>
234
+ <p><code>.gemini/settings.json</code> (project), <code>~/.gemini/settings.json</code>, or <code>%USERPROFILE%\.gemini\settings.json</code> on Windows. Restart, then <code>/mcp</code> shows the server.</p>
235
+ <pre><code class="lang-json">{
236
+ "mcpServers": {
237
+ "vdo-ninja-mcp": {
238
+ "command": "node",
239
+ "args": ["/ABSOLUTE/PATH/node_modules/@vdoninja/mcp/scripts/vdo-mcp-server.js"],
240
+ "env": { "VDON_MCP_TOOL_PROFILE": "core" }
241
+ }
242
+ }
243
+ }</code></pre>
244
+ </div>
245
+ <div class="tabpanel" id="gp-opencode" role="tabpanel" aria-labelledby="g-opencode" hidden>
246
+ <p><code>opencode.json</code> in the project root, or <code>~/.config/opencode/opencode.json</code>:</p>
247
+ <pre><code class="lang-json">{
248
+ "$schema": "https://opencode.ai/config.json",
249
+ "mcp": {
250
+ "vdo-ninja-mcp": {
251
+ "type": "local",
252
+ "command": ["node", "/ABSOLUTE/PATH/node_modules/@vdoninja/mcp/scripts/vdo-mcp-server.js",
253
+ "--tool-profile", "core"],
254
+ "enabled": true
255
+ }
256
+ }
257
+ }</code></pre>
258
+ </div>
259
+ <div class="tabpanel" id="gp-other" role="tabpanel" aria-labelledby="g-other" hidden>
260
+ <p>Every stdio client needs a command, the script path, and optional flags or env:</p>
261
+ <pre><code class="lang-json">{
262
+ "command": "node",
263
+ "args": ["/ABSOLUTE/PATH/node_modules/@vdoninja/mcp/scripts/vdo-mcp-server.js",
264
+ "--tool-profile", "core"],
265
+ "env": { "VDON_MCP_TOOL_PROFILE": "core" }
266
+ }</code></pre>
267
+ <p>On Windows the path looks like <code>C:\\path\\node_modules\\@vdoninja\\mcp\\scripts\\vdo-mcp-server.js</code>.</p>
268
+ </div>
269
+ </div>
270
+ <h3>Check the wiring</h3>
271
+ <p>Ask your agent to call <code>vdo_capabilities</code>. If you get back the server's limits and active profile, it's connected.</p>
272
+ <pre><code class="lang-json">{ "name": "vdo_capabilities", "arguments": {} }</code></pre>
273
+ <div class="note-box">
274
+ <svg class="icon" aria-hidden="true"><use href="assets/icons.svg#i-info"></use></svg>
275
+ <div>Every MCP server adds tool descriptions to your agent's context. Start with the <code>core</code> profile and widen it only when you need files or state.</div>
276
+ </div>
277
+ </section>
278
+
279
+ <!-- First run -->
280
+ <section class="doc-section" id="first-run">
281
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-message"></use></svg></span>First conversation</h2>
282
+ <p>Open two agent sessions, on one machine or two. Pick a room name nobody else will guess.</p>
283
+ <h3>1. Agent A joins</h3>
284
+ <pre><code class="lang-json">{ "name": "vdo_connect", "arguments": { "room": "lab_7f3k2", "stream_id": "agent_a" } }</code></pre>
285
+ <p>The result includes a <code>session_id</code> like <code>vdo_3f1c…</code>. Every other tool needs it.</p>
286
+ <h3>2. Agent B joins and points at A</h3>
287
+ <pre><code class="lang-json">{ "name": "vdo_connect",
288
+ "arguments": { "room": "lab_7f3k2", "stream_id": "agent_b", "target_stream_id": "agent_a" } }</code></pre>
289
+ <h3>3. Wait for the peer, then A sends</h3>
290
+ <p>Call <code>vdo_sync_peers</code> on A until <code>agent_b</code> shows up. Then address it by stream ID:</p>
291
+ <pre><code class="lang-json">{ "name": "vdo_send",
292
+ "arguments": { "session_id": "&lt;agent_a_session&gt;", "target": "agent_b",
293
+ "data": { "type": "chat.message", "text": "hello from agent_a" } } }</code></pre>
294
+ <p><code>ok: true</code> means the transport accepted the message, not that B has read it. Ask B to reply when you need confirmation.</p>
295
+ <h3>4. B receives</h3>
296
+ <pre><code class="lang-json">{ "name": "vdo_receive",
297
+ "arguments": { "session_id": "&lt;agent_b_session&gt;", "wait_ms": 1000 } }</code></pre>
298
+ <p>Expect a <code>data_received</code> event whose <code>data</code> is the object A sent. If you only see lifecycle events, call <code>vdo_receive</code> again: the channel may still be opening.</p>
299
+ <h3>No agents handy?</h3>
300
+ <p>Two scripts run the full exchange for you on a simulated local network, then exit with pass or fail:</p>
301
+ <pre><code class="lang-sh">npx vdon-mcp-demo-message
302
+ npx vdon-mcp-demo-file
303
+ # use the real network
304
+ MCP_DEMO_FAKE=0 npx vdon-mcp-demo-message
305
+ # real network, forced through TURN
306
+ MCP_DEMO_FAKE=0 MCP_DEMO_FORCE_TURN=1 npx vdon-mcp-demo-file</code></pre>
307
+ </section>
308
+
309
+ <!-- How it works -->
310
+ <section class="doc-section" id="how">
311
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-eye"></use></svg></span>How it works</h2>
312
+ <p>Each agent runs its own copy of the MCP server as a local process. The servers find each other through VDO.Ninja's handshake server, then open a WebRTC data channel between them. From then on, data goes peer to peer.</p>
313
+ <figure>
314
+ <svg class="dg" viewBox="0 0 900 330" role="img" aria-labelledby="dg1t">
315
+ <title id="dg1t">Two agents, each with a local MCP server, connected by a direct WebRTC data channel. A handshake server helps them connect; a TURN relay is a fallback.</title>
316
+ <defs>
317
+ <marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="arrow" d="M0 0 10 5 0 10z"/></marker>
318
+ <marker id="ah-ok" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path class="arrow-ok" d="M0 0 10 5 0 10z"/></marker>
319
+ </defs>
320
+ <rect class="machine" x="6" y="98" width="356" height="134" rx="16"/>
321
+ <rect class="machine" x="538" y="98" width="356" height="134" rx="16"/>
322
+ <text x="22" y="122" class="t-mono">MACHINE A</text>
323
+ <text x="878" y="122" class="t-mono" text-anchor="end">MACHINE B</text>
324
+
325
+ <rect class="box" x="22" y="136" width="130" height="80" rx="12"/>
326
+ <text x="87" y="170" class="t-strong" text-anchor="middle">Agent A</text>
327
+ <text x="87" y="190" text-anchor="middle">MCP client</text>
328
+ <rect class="box" x="214" y="136" width="132" height="80" rx="12"/>
329
+ <text x="280" y="170" class="t-strong" text-anchor="middle">MCP server</text>
330
+ <text x="280" y="190" class="t-mono" text-anchor="middle">vdon-mcp-server</text>
331
+ <path class="ln" d="M152 176h60" marker-end="url(#ah)" marker-start="url(#ah)"/>
332
+ <text x="183" y="166" class="t-mono" text-anchor="middle">stdio</text>
333
+
334
+ <rect class="box" x="554" y="136" width="132" height="80" rx="12"/>
335
+ <text x="620" y="170" class="t-strong" text-anchor="middle">MCP server</text>
336
+ <text x="620" y="190" class="t-mono" text-anchor="middle">vdon-mcp-server</text>
337
+ <rect class="box" x="748" y="136" width="130" height="80" rx="12"/>
338
+ <text x="813" y="170" class="t-strong" text-anchor="middle">Agent B</text>
339
+ <text x="813" y="190" text-anchor="middle">MCP client</text>
340
+ <path class="ln" d="M688 176h58" marker-end="url(#ah)" marker-start="url(#ah)"/>
341
+ <text x="717" y="166" class="t-mono" text-anchor="middle">stdio</text>
342
+
343
+ <path class="ln-ok" d="M348 176h204" marker-end="url(#ah-ok)" marker-start="url(#ah-ok)"/>
344
+ <text x="450" y="164" class="t-ok" text-anchor="middle">WebRTC data channel</text>
345
+ <text x="450" y="196" class="t-mono" text-anchor="middle">DTLS-encrypted</text>
346
+
347
+ <rect class="box-soft" x="370" y="8" width="160" height="56" rx="12"/>
348
+ <text x="450" y="32" class="t-strong" text-anchor="middle">Handshake server</text>
349
+ <text x="450" y="50" class="t-mono" text-anchor="middle">wss.vdo.ninja</text>
350
+ <path class="ln-dash" d="M300 136 382 66"/>
351
+ <path class="ln-dash" d="M600 136 518 66"/>
352
+ <text x="300" y="86" class="t-mono" text-anchor="middle">find peers</text>
353
+ <text x="600" y="86" class="t-mono" text-anchor="middle">find peers</text>
354
+
355
+ <rect class="box-soft" x="350" y="266" width="200" height="56" rx="12"/>
356
+ <text x="450" y="290" class="t-strong" text-anchor="middle">TURN relay</text>
357
+ <text x="450" y="308" class="t-mono" text-anchor="middle">fallback · ciphertext only</text>
358
+ <path class="ln-dash" d="M300 216 382 266"/>
359
+ <path class="ln-dash" d="M600 216 518 266"/>
360
+ </svg>
361
+ <figcaption>Dashed lines are only used to connect, or when a direct path is impossible.</figcaption>
362
+ </figure>
363
+
364
+ <h3>Step by step</h3>
365
+ <ol>
366
+ <li><strong>Tool call.</strong> Your agent calls <code>vdo_connect</code>. The client passes it to the local server as JSON-RPC over stdio.</li>
367
+ <li><strong>Handshake.</strong> The server opens a WebSocket to <code>wss://wss.vdo.ninja</code> and joins the room. The room name is hashed with the room password, and the WebRTC offer and ICE candidates are encrypted with it before they leave your machine. Stream IDs are sent as-is with a short hash suffix, so don't put anything sensitive in them.</li>
368
+ <li><strong>Route discovery.</strong> WebRTC asks public STUN servers (Google, Cloudflare) for your public address and tries to connect directly. If that fails, it uses a VDO.Ninja TURN relay.</li>
369
+ <li><strong>Data channel.</strong> A DTLS-encrypted, reliable, ordered data channel opens between the two servers.</li>
370
+ <li><strong>Traffic.</strong> <code>vdo_send</code>, file chunks, and state updates go over that channel. Incoming data is queued locally until your agent calls <code>vdo_receive</code>.</li>
371
+ </ol>
372
+
373
+ <h3>Who sees what</h3>
374
+ <div class="table-wrap">
375
+ <table>
376
+ <thead><tr><th>Party</th><th>When</th><th>Sees</th><th>Never sees</th></tr></thead>
377
+ <tbody>
378
+ <tr><td>Handshake server</td><td>While connecting</td><td>Your IP, hashed room ID, stream IDs, encrypted offers</td><td>Messages, files, state</td></tr>
379
+ <tr><td>STUN servers</td><td>While connecting</td><td>Your public IP and port</td><td>Anything you send</td></tr>
380
+ <tr><td>TURN relay</td><td>Only if direct fails, or <code>force_turn</code></td><td>Both IPs, encrypted packets</td><td>Contents</td></tr>
381
+ <tr><td>Your HTTP relay</td><td>Only with <code>transport: "http"</code></td><td>Addresses, opaque room IDs, timing, sizes</td><td>The room key, plaintext</td></tr>
382
+ <tr><td>The other agent</td><td>Once connected</td><td>What you send it; your IP when direct</td><td>Anything tools don't send</td></tr>
383
+ <tr><td>Your AI provider</td><td>Every tool call</td><td>Tool calls and results the agent reads</td><td>Files moved disk to disk</td></tr>
384
+ </tbody>
385
+ </table>
386
+ </div>
387
+ <div class="note-box warn">
388
+ <svg class="icon" aria-hidden="true"><use href="assets/icons.svg#i-alert"></use></svg>
389
+ <div><strong>The default room password is public.</strong> If you don't pass <code>password</code>, the SDK uses VDO.Ninja's well-known default key. Set your own for private rooms. <code>password: false</code> turns handshake encryption off entirely; the data channel itself is always DTLS-encrypted.</div>
390
+ </div>
391
+ </section>
392
+
393
+ <!-- Rooms -->
394
+ <section class="doc-section" id="rooms">
395
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-hash"></use></svg></span>Rooms and sessions</h2>
396
+ <ul>
397
+ <li><strong>Room</strong>: a meeting point. Everyone with the same room name and password can find each other. Stick to letters, numbers, and underscores.</li>
398
+ <li><strong>Stream ID</strong>: an agent's name in the room, like <code>planner</code>. Random if you omit it. Peers use it in <code>target_stream_id</code> and allowlists.</li>
399
+ <li><strong>Target stream</strong>: set <code>target_stream_id</code> to connect straight to one peer. That's the simplest, most reliable two-agent setup.</li>
400
+ <li><strong>Session</strong>: one <code>vdo_connect</code> call. Returns a <code>session_id</code>. One server can hold several sessions, in different rooms, at once.</li>
401
+ <li><strong>Addressing</strong>: pass a peer's stream ID (or its UUID from <code>vdo_sync_peers</code>) as <code>target</code> to reach one peer. Omit it to send to everyone connected.</li>
402
+ <li><strong>Transport</strong>: <code>webrtc</code> (default) or <code>http</code> for the <a href="#relay">relay</a>. Everyone in a room must use the same one.</li>
403
+ </ul>
404
+ <p>Two to five agents per room works best. VDO.Ninja's guidance is about 80 peers per room at most.</p>
405
+ </section>
406
+
407
+ <!-- Events -->
408
+ <section class="doc-section" id="events">
409
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-zap"></use></svg></span>Events</h2>
410
+ <p><code>vdo_receive</code> returns everything that happened since the last call, oldest first. Pass <code>wait_ms</code> (up to 30,000) to wait for something to arrive. Up to 2,000 events are held per session; older ones are dropped first.</p>
411
+ <div class="table-wrap">
412
+ <table>
413
+ <thead><tr><th>Group</th><th>Event types</th></tr></thead>
414
+ <tbody>
415
+ <tr><td>Lifecycle</td><td><code>ready</code> <code>connected</code> <code>disconnected</code> <code>connection_failed</code> <code>sdk_error</code> <code>reconnect_scheduled</code></td></tr>
416
+ <tr><td>Peers</td><td><code>peer_connected</code> <code>peer_disconnected</code> <code>data_channel_open</code> <code>data_channel_close</code> <code>heartbeat_sent</code></td></tr>
417
+ <tr><td>Data</td><td><code>data_received</code> <code>send_error</code> <code>send_rejected</code></td></tr>
418
+ <tr><td>Sync and auth</td><td><code>sync_hello_sent</code> <code>sync_peer_updated</code> <code>sync_peer_rejected</code> <code>protocol_auth_failed</code></td></tr>
419
+ <tr><td>Files</td><td><code>file_offer_received</code> <code>file_chunk_received</code> <code>file_chunk_rejected</code> <code>file_received</code> <code>file_transfer_complete</code> <code>file_transfer_cancelled</code></td></tr>
420
+ <tr><td>State</td><td><code>state_updated</code> <code>state_snapshot_merged</code> <code>state_protocol_unhandled</code></td></tr>
421
+ <tr><td>HTTP relay</td><td><code>relay_receipt</code> <code>relay_connection</code> <code>relay_history_lost</code> <code>relay_message_rejected</code></td></tr>
422
+ </tbody>
423
+ </table>
424
+ </div>
425
+ <h3>Errors</h3>
426
+ <p>Failed tool calls return <code>isError: true</code> and this shape. <code>validation_error</code> means bad arguments or a tool outside the active profile; fix and retry.</p>
427
+ <pre><code class="lang-json">{ "ok": false,
428
+ "error": { "type": "validation_error", "message": "…", "tool": "vdo_send" } }</code></pre>
429
+ </section>
430
+
431
+ <!-- Tools -->
432
+ <section class="doc-section" id="tools">
433
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-terminal"></use></svg></span>Tools</h2>
434
+ <p>18 tools in four groups. The profile column shows the smallest profile that includes each one. <span class="req" style="color:var(--warn);font-weight:600;font-size:12px">required</span> marks required arguments.</p>
435
+
436
+ <h3>Session</h3>
437
+ <details class="tool-ref" open>
438
+ <summary><code>vdo_connect</code><span>Join a room and open a session.</span><span class="profile-tag">core</span></summary>
439
+ <div class="tool-body">
440
+ <table class="args">
441
+ <thead><tr><th>Argument</th><th>Default</th><th>Notes</th></tr></thead>
442
+ <tbody>
443
+ <tr><td><code>room</code><span class="req">required</span></td><td></td><td>Room name.</td></tr>
444
+ <tr><td><code>stream_id</code></td><td>random</td><td>This agent's name in the room.</td></tr>
445
+ <tr><td><code>target_stream_id</code></td><td></td><td>Peer to connect to directly.</td></tr>
446
+ <tr><td><code>transport</code></td><td><code>webrtc</code></td><td><code>http</code> uses the <a href="#relay">relay</a>. <code>host</code>, <code>password</code>, and <code>force_turn</code> are WebRTC-only.</td></tr>
447
+ <tr><td><code>relay_url</code>, <code>relay_token</code>, <code>relay_key</code></td><td>from env</td><td>HTTP only. Prefer the <code>VDON_MCP_RELAY_*</code> env vars.</td></tr>
448
+ <tr><td><code>password</code></td><td>SDK default</td><td>Room secret. <code>false</code> disables handshake encryption.</td></tr>
449
+ <tr><td><code>host</code></td><td><code>wss://wss.vdo.ninja</code></td><td>Handshake server.</td></tr>
450
+ <tr><td><code>force_turn</code></td><td><code>false</code></td><td>Always relay through TURN. Hides your IP from peers; may connect less often.</td></tr>
451
+ <tr><td><code>heartbeat_ms</code></td><td>15000</td><td>Keep-alive interval.</td></tr>
452
+ <tr><td><code>reconnect_ms</code> / <code>max_reconnect_ms</code></td><td>3000 / 30000</td><td>Reconnect backoff, doubling each try.</td></tr>
453
+ <tr><td><code>join_token</code> / <code>join_token_secret</code></td><td></td><td>Present a token, or a secret to sign and verify them.</td></tr>
454
+ <tr><td><code>join_token_ttl_ms</code></td><td>600000</td><td>Token lifetime (10 minutes).</td></tr>
455
+ <tr><td><code>enforce_join_token</code></td><td><code>false</code></td><td>Reject peers without a valid token.</td></tr>
456
+ <tr><td><code>allow_peer_stream_ids</code></td><td></td><td>Only these stream IDs may talk to you.</td></tr>
457
+ <tr><td><code>require_session_mac</code></td><td><code>false</code></td><td>Reject unsigned control and file messages.</td></tr>
458
+ <tr><td><code>file_chunk_bytes</code></td><td>65536</td><td>1 KB to 256 KB.</td></tr>
459
+ <tr><td><code>file_max_bytes</code></td><td>64 MB</td><td>Largest file accepted.</td></tr>
460
+ <tr><td><code>file_ack_timeout_ms</code> / <code>file_max_retries</code></td><td>1200 / 6</td><td>Per-chunk retry policy.</td></tr>
461
+ <tr><td><code>file_spool_dir</code></td><td>OS temp</td><td>Where large incoming files are written.</td></tr>
462
+ <tr><td><code>file_spool_threshold_bytes</code></td><td>4 MB</td><td>Spool to disk at or above this size.</td></tr>
463
+ <tr><td><code>keep_spool_files</code></td><td><code>false</code></td><td>Keep spooled files after cleanup.</td></tr>
464
+ <tr><td><code>state_max_keys</code> / <code>state_max_snapshot_entries</code></td><td>2048 / 4096</td><td>Shared state limits.</td></tr>
465
+ <tr><td><code>label</code>, <code>debug</code></td><td></td><td>Display label; verbose SDK logging.</td></tr>
466
+ </tbody>
467
+ </table>
468
+ </div>
469
+ </details>
470
+ <details class="tool-ref">
471
+ <summary><code>vdo_send</code><span>Send JSON or text over the data channel.</span><span class="profile-tag">core</span></summary>
472
+ <div class="tool-body">
473
+ <table class="args">
474
+ <thead><tr><th>Argument</th><th>Notes</th></tr></thead>
475
+ <tbody>
476
+ <tr><td><code>session_id</code><span class="req">required</span></td><td></td></tr>
477
+ <tr><td><code>data</code><span class="req">required</span></td><td>Any JSON value. Use an envelope like <code>{"type": "help.request", …}</code>.</td></tr>
478
+ <tr><td><code>target</code> or <code>uuid</code></td><td>One peer, by stream ID or UUID. Omit for everyone connected. Not both. On WebRTC, a target object can set <code>allowFallback: true</code> to permit sending through the handshake server when no channel is open.</td></tr>
479
+ </tbody>
480
+ </table>
481
+ <p><code>ok: true</code> means the transport accepted the payload. On WebRTC, <code>ok: false</code> plus a <code>send_rejected</code> event means no data channel is open yet. Over the relay, results also carry <code>message_id</code> and <code>delivery_stage</code>.</p>
482
+ </div>
483
+ </details>
484
+ <details class="tool-ref">
485
+ <summary><code>vdo_receive</code><span>Read queued messages and events.</span><span class="profile-tag">core</span></summary>
486
+ <div class="tool-body">
487
+ <table class="args">
488
+ <thead><tr><th>Argument</th><th>Notes</th></tr></thead>
489
+ <tbody>
490
+ <tr><td><code>session_id</code><span class="req">required</span></td><td></td></tr>
491
+ <tr><td><code>max_events</code></td><td>Default 50, max 500.</td></tr>
492
+ <tr><td><code>wait_ms</code></td><td>0 to 30000. Waits for the first event if the queue is empty.</td></tr>
493
+ </tbody>
494
+ </table>
495
+ </div>
496
+ </details>
497
+ <details class="tool-ref">
498
+ <summary><code>vdo_status</code> · <code>vdo_disconnect</code><span>Inspect or close one session.</span><span class="profile-tag">core</span></summary>
499
+ <div class="tool-body"><p>Both take <code>session_id</code>. Disconnecting stops reconnects and frees the session.</p></div>
500
+ </details>
501
+ <details class="tool-ref">
502
+ <summary><code>vdo_list_sessions</code> · <code>vdo_capabilities</code><span>Server-wide views.</span><span class="profile-tag">core</span></summary>
503
+ <div class="tool-body"><p>No arguments. <code>vdo_capabilities</code> reports the active profile, limits, and transport constraints. Have agents call it first.</p></div>
504
+ </details>
505
+
506
+ <h3>Peer sync</h3>
507
+ <details class="tool-ref">
508
+ <summary><code>vdo_sync_peers</code><span>Per-peer handshake, auth, and capability state.</span><span class="profile-tag">core</span></summary>
509
+ <div class="tool-body"><p>Takes <code>session_id</code>. Shows whether each peer passed token checks and has a session key.</p></div>
510
+ </details>
511
+ <details class="tool-ref">
512
+ <summary><code>vdo_sync_announce</code><span>Send a sync hello now.</span><span class="profile-tag">core</span></summary>
513
+ <div class="tool-body"><p>Takes <code>session_id</code>, and optionally <code>uuid</code> or <code>target</code> to announce to one peer.</p></div>
514
+ </details>
515
+
516
+ <h3>Files</h3>
517
+ <details class="tool-ref">
518
+ <summary><code>vdo_file_send_path</code> · <code>vdo_file_send</code><span>Send a file.</span><span class="profile-tag">file</span></summary>
519
+ <div class="tool-body">
520
+ <table class="args">
521
+ <thead><tr><th>Argument</th><th>Notes</th></tr></thead>
522
+ <tbody>
523
+ <tr><td><code>session_id</code><span class="req">required</span></td><td></td></tr>
524
+ <tr><td><code>file_path</code><span class="req">required</span></td><td><code>vdo_file_send_path</code> only. Streams from disk; contents never enter the model's context.</td></tr>
525
+ <tr><td><code>data_base64</code><span class="req">required</span></td><td><code>vdo_file_send</code> only. The whole file, base64-encoded.</td></tr>
526
+ <tr><td><code>name</code>, <code>mime</code></td><td>Metadata for the receiver.</td></tr>
527
+ <tr><td><code>uuid</code> or <code>target</code></td><td>Recipient.</td></tr>
528
+ <tr><td><code>transfer_id</code></td><td>Your own ID. Reuse it only to retry the same file to the same peer.</td></tr>
529
+ <tr><td><code>chunk_bytes</code>, <code>timeout_ms</code>, <code>ack_timeout_ms</code>, <code>max_retries</code></td><td>Override session defaults for this transfer.</td></tr>
530
+ </tbody>
531
+ </table>
532
+ </div>
533
+ </details>
534
+ <details class="tool-ref">
535
+ <summary><code>vdo_file_resume</code><span>Continue an interrupted send.</span><span class="profile-tag">file</span></summary>
536
+ <div class="tool-body"><p>Takes <code>session_id</code> and <code>transfer_id</code>; optional <code>target</code>, <code>start_seq</code>, <code>ack_timeout_ms</code>, <code>max_retries</code>. Asks the receiver where it left off, then sends the rest.</p></div>
537
+ </details>
538
+ <details class="tool-ref">
539
+ <summary><code>vdo_file_transfers</code><span>Transfer progress.</span><span class="profile-tag">file</span></summary>
540
+ <div class="tool-body"><p>Takes <code>session_id</code> and <code>direction</code>: <code>incoming</code>, <code>outgoing</code>, or <code>all</code>.</p></div>
541
+ </details>
542
+ <details class="tool-ref">
543
+ <summary><code>vdo_file_receive</code> · <code>vdo_file_save</code><span>Read or save a finished file.</span><span class="profile-tag">file</span></summary>
544
+ <div class="tool-body"><p>Both take <code>session_id</code> and <code>transfer_id</code>. <code>vdo_file_receive</code> returns the contents as <code>base64</code>, <code>utf8</code>, or <code>json</code>. <code>vdo_file_save</code> writes to <code>output_path</code> (set <code>overwrite</code> to replace an existing file) and keeps the contents out of context.</p></div>
545
+ </details>
546
+
547
+ <h3>Shared state</h3>
548
+ <details class="tool-ref">
549
+ <summary><code>vdo_state_set</code><span>Set a key and share it.</span><span class="profile-tag">state</span></summary>
550
+ <div class="tool-body"><p>Takes <code>session_id</code>, <code>key</code>, and <code>value</code> (any JSON). <code>broadcast</code> defaults to <code>true</code>; pass <code>uuid</code> or <code>target</code> to send to one peer. Returns the stored entry with its <code>clock</code> and <code>actor</code>.</p></div>
551
+ </details>
552
+ <details class="tool-ref">
553
+ <summary><code>vdo_state_get</code><span>Read one key or everything.</span><span class="profile-tag">state</span></summary>
554
+ <div class="tool-body"><p>Takes <code>session_id</code>; omit <code>key</code> for the full snapshot. <code>include_meta</code> adds clocks and actors.</p></div>
555
+ </details>
556
+ <details class="tool-ref">
557
+ <summary><code>vdo_state_sync</code><span>Exchange full snapshots.</span><span class="profile-tag">state</span></summary>
558
+ <div class="tool-body"><p>Takes <code>session_id</code> and <code>mode</code>: <code>request</code> asks peers for theirs, <code>send</code> pushes yours. Use after a reconnect.</p></div>
559
+ </details>
560
+ </section>
561
+
562
+ <!-- Files -->
563
+ <section class="doc-section" id="files">
564
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-file"></use></svg></span>File transfer</h2>
565
+ <p>The data channel is already reliable and ordered. On top of that, file transfer adds its own checks so a bad chunk or a dropped connection never leaves you with a corrupt file.</p>
566
+ <figure>
567
+ <svg class="dg" viewBox="0 0 640 430" role="img" aria-labelledby="dg2t">
568
+ <title id="dg2t">File transfer sequence: offer, accept, chunks with acknowledgements, a negative acknowledgement and retry, completion, and a resume handshake.</title>
569
+ <defs>
570
+ <marker id="sa" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="arrow" d="M0 0 10 5 0 10z"/></marker>
571
+ <marker id="sn" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path class="arrow-nack" d="M0 0 10 5 0 10z"/></marker>
572
+ </defs>
573
+ <rect class="box" x="70" y="6" width="140" height="40" rx="10"/>
574
+ <text x="140" y="31" class="t-strong" text-anchor="middle">Sender</text>
575
+ <rect class="box" x="430" y="6" width="140" height="40" rx="10"/>
576
+ <text x="500" y="31" class="t-strong" text-anchor="middle">Receiver</text>
577
+ <line class="ln-life" x1="140" y1="46" x2="140" y2="424"/>
578
+ <line class="ln-life" x1="500" y1="46" x2="500" y2="424"/>
579
+
580
+ <path class="ln" d="M140 74H498" marker-end="url(#sa)"/><text x="320" y="68" text-anchor="middle">file.offer · name, size, SHA-256, chunk hashes</text>
581
+ <path class="ln" d="M500 106H142" marker-end="url(#sa)"/><text x="320" y="100" text-anchor="middle">file.accept</text>
582
+ <path class="ln" d="M140 138H498" marker-end="url(#sa)"/><text x="320" y="132" text-anchor="middle">file.chunk seq 0</text>
583
+ <path class="ln" d="M500 170H142" marker-end="url(#sa)"/><text x="320" y="164" text-anchor="middle">file.ack 0 · hash checked</text>
584
+ <path class="ln" d="M140 202H498" marker-end="url(#sa)"/><text x="320" y="196" text-anchor="middle">file.chunk seq 1</text>
585
+ <path class="ln-nack" d="M500 234H142" marker-end="url(#sn)"/><text x="320" y="228" text-anchor="middle" class="t-nack">file.nack 1 · bad hash, send again</text>
586
+ <path class="ln" d="M140 266H498" marker-end="url(#sa)"/><text x="320" y="260" text-anchor="middle">file.chunk seq 1 (retry) … seq n</text>
587
+ <path class="ln" d="M140 298H498" marker-end="url(#sa)"/><text x="320" y="292" text-anchor="middle">file.complete · whole-file SHA-256</text>
588
+ <path class="ln" d="M500 330H142" marker-end="url(#sa)"/><text x="320" y="324" text-anchor="middle">file.complete_ack · hash and size match</text>
589
+ <rect class="box-soft" x="160" y="346" width="320" height="22" rx="6"/>
590
+ <text x="320" y="361" text-anchor="middle" class="t-mono">connection dropped? vdo_file_resume</text>
591
+ <path class="ln" d="M140 390H498" marker-end="url(#sa)"/><text x="320" y="384" text-anchor="middle">file.resume_req</text>
592
+ <path class="ln" d="M500 418H142" marker-end="url(#sa)"/><text x="320" y="412" text-anchor="middle">file.resume_state · next_seq</text>
593
+ </svg>
594
+ <figcaption>Each chunk waits for an ack (1.2 s default) and is retried up to 6 times.</figcaption>
595
+ </figure>
596
+ <ul>
597
+ <li><strong>Integrity</strong>: every chunk and the whole file are checked with SHA-256.</li>
598
+ <li><strong>Resume</strong>: a transfer ID is tied to the sending peer and the exact file. Retrying the same file continues where it stopped; different content under the same ID is rejected.</li>
599
+ <li><strong>Memory</strong>: incoming files of 4 MB or more spool to disk. Prefer <code>vdo_file_send_path</code> and <code>vdo_file_save</code> for anything large.</li>
600
+ <li><strong>Limits</strong>: 64 MB per file by default (<code>file_max_bytes</code>). The server keeps the 12 most recent completed incoming files.</li>
601
+ </ul>
602
+ </section>
603
+
604
+ <!-- State -->
605
+ <section class="doc-section" id="state">
606
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-layers"></use></svg></span>Shared state</h2>
607
+ <p>A small key/value store every peer in the room keeps a copy of. Each write carries a <code>clock</code> (a counter that only goes up) and an <code>actor</code> (the writer's stream ID). When two copies disagree:</p>
608
+ <ol>
609
+ <li>The higher <code>clock</code> wins.</li>
610
+ <li>On a tie, the alphabetically later <code>actor</code> wins.</li>
611
+ </ol>
612
+ <p>Every peer applies the same rule, so they all end up with the same value without a server deciding. Good for task status, assignments, and small flags. Not a database: keep values small and keys under the 2,048 limit.</p>
613
+ <pre><code class="lang-json">{ "name": "vdo_state_set",
614
+ "arguments": { "session_id": "vdo_9a0e…", "key": "task.status", "value": "approved" } }</code></pre>
615
+ </section>
616
+
617
+ <!-- Config -->
618
+ <section class="doc-section" id="config">
619
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-sliders"></use></svg></span>Configuration</h2>
620
+ <p>Server-wide settings come from environment variables or CLI flags. Security settings here become defaults for every <code>vdo_connect</code>; per-call arguments override them.</p>
621
+ <div class="table-wrap">
622
+ <table>
623
+ <thead><tr><th>Environment variable</th><th>CLI flag</th><th>Default</th></tr></thead>
624
+ <tbody>
625
+ <tr><td><code>VDON_MCP_TOOL_PROFILE</code></td><td><code>--tool-profile</code></td><td><code>full</code></td></tr>
626
+ <tr><td><code>VDON_MCP_MAX_MESSAGE_BYTES</code></td><td><code>--max-message-bytes</code></td><td>1048576</td></tr>
627
+ <tr><td><code>VDON_MCP_JOIN_TOKEN_SECRET</code></td><td><code>--join-token-secret</code></td><td></td></tr>
628
+ <tr><td><code>VDON_MCP_ENFORCE_JOIN_TOKEN</code></td><td><code>--enforce-join-token</code></td><td><code>false</code></td></tr>
629
+ <tr><td><code>VDON_MCP_ALLOW_PEER_STREAM_IDS</code></td><td><code>--allow-peer-stream-ids</code></td><td>comma list</td></tr>
630
+ <tr><td><code>VDON_MCP_REQUIRE_SESSION_MAC</code></td><td><code>--require-session-mac</code></td><td><code>false</code></td></tr>
631
+ <tr><td><code>VDON_MCP_RELAY_URL</code></td><td></td><td>HTTP relay origin</td></tr>
632
+ <tr><td><code>VDON_MCP_RELAY_TOKEN</code></td><td></td><td>relay access token</td></tr>
633
+ <tr><td><code>VDON_MCP_RELAY_KEY</code></td><td></td><td>room key, 64 hex chars</td></tr>
634
+ </tbody>
635
+ </table>
636
+ </div>
637
+ <pre><code class="lang-sh">VDON_MCP_TOOL_PROFILE=core npx vdon-mcp-server
638
+ npx vdon-mcp-server --tool-profile file --require-session-mac true</code></pre>
639
+ <p>Started directly in a terminal, the server prints a short "Server running (stdio)" banner. Started by an MCP client, it stays quiet, which is normal.</p>
640
+ </section>
641
+
642
+ <!-- Security -->
643
+ <section class="doc-section" id="security">
644
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-shield"></use></svg></span>Security</h2>
645
+ <p>Out of the box, data is encrypted in transit but anyone who knows your room name can join. Add layers as the work gets more sensitive.</p>
646
+ <div class="table-wrap">
647
+ <table>
648
+ <thead><tr><th>Control</th><th>How it works</th><th>Turn it on</th></tr></thead>
649
+ <tbody>
650
+ <tr><td>Room password</td><td>Hashes the room name and encrypts the handshake. Peers without it can't find you.</td><td><code>password</code></td></tr>
651
+ <tr><td>Join tokens</td><td>HMAC-SHA256-signed token naming the room and stream, with an expiry. Checked during the sync handshake.</td><td><code>join_token_secret</code> + <code>enforce_join_token</code></td></tr>
652
+ <tr><td>Peer allowlist</td><td>Ignores room and file actions from stream IDs not on the list.</td><td><code>allow_peer_stream_ids</code></td></tr>
653
+ <tr><td>Session MAC</td><td>Each pair of peers agrees on a key (X25519). Control and file messages carry an HMAC-SHA256 signature.</td><td><code>require_session_mac</code></td></tr>
654
+ <tr><td>Tool profile</td><td>Hides tools an agent doesn't need. Calls to hidden tools fail.</td><td><code>--tool-profile</code></td></tr>
655
+ <tr><td>Relay only</td><td>Routes through TURN so peers don't see your IP.</td><td><code>force_turn</code></td></tr>
656
+ </tbody>
657
+ </table>
658
+ </div>
659
+ <h3>A locked-down room</h3>
660
+ <pre><code class="lang-json">{ "name": "vdo_connect",
661
+ "arguments": {
662
+ "room": "acme_release_q3",
663
+ "stream_id": "planner",
664
+ "password": "a-long-random-string",
665
+ "join_token_secret": "shared-secret-from-your-vault",
666
+ "enforce_join_token": true,
667
+ "allow_peer_stream_ids": ["reviewer", "decider"],
668
+ "require_session_mac": true } }</code></pre>
669
+ <div class="note-box">
670
+ <svg class="icon" aria-hidden="true"><use href="assets/icons.svg#i-info"></use></svg>
671
+ <div>Secrets passed as tool arguments pass through your agent's context. To keep them out, set <code>VDON_MCP_JOIN_TOKEN_SECRET</code> in the server's environment instead.</div>
672
+ </div>
673
+ </section>
674
+
675
+ <!-- Network -->
676
+ <section class="doc-section" id="network">
677
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-globe"></use></svg></span>Networking</h2>
678
+ <ul>
679
+ <li><strong>Direct first.</strong> WebRTC tries a direct path, which is fastest on a LAN and often works across the internet through NAT.</li>
680
+ <li><strong>TURN fallback.</strong> If direct fails, traffic relays through VDO.Ninja TURN servers. Still end-to-end encrypted; a little more latency.</li>
681
+ <li><strong>Privacy mode.</strong> <code>force_turn: true</code> always relays, so peers never see your IP. Some networks connect less reliably this way.</li>
682
+ <li><strong>Hard blocks.</strong> Some corporate firewalls and WSL network setups block WebRTC's UDP traffic. TURN helps but isn't a guaranteed bypass.</li>
683
+ <li><strong>No WebRTC at all?</strong> Use the <a href="#relay">HTTP relay</a>: plain HTTPS POST and long polling through a server you run.</li>
684
+ <li><strong>Not a tunnel.</strong> Only tool payloads cross the channel. No TCP, SSH, or port forwarding.</li>
685
+ <li><strong>Reconnects.</strong> Heartbeats every 15 s. On a drop the session rebuilds itself with backoff, re-joins, and re-announces. Make your message handlers safe to run twice.</li>
686
+ </ul>
687
+ </section>
688
+
689
+
690
+ <!-- Relay -->
691
+ <section class="doc-section" id="relay">
692
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-server"></use></svg></span>HTTP relay</h2>
693
+ <p>For machines that can't run the native WebRTC library, or networks that block it. Both agents join the same encrypted room through a relay you operate. Messages, files, and shared state use the same tools.</p>
694
+ <div class="note-box">
695
+ <svg class="icon" aria-hidden="true"><use href="assets/icons.svg#i-info"></use></svg>
696
+ <div>WebRTC stays the default. Both participants must choose <code>transport: "http"</code> with the same relay settings; sessions on different transports don't see each other. There's no automatic switching yet.</div>
697
+ </div>
698
+
699
+ <h3>1. Run the relay</h3>
700
+ <p>Node 18+, no native dependencies. Generate two secrets: an access token for the relay and a separate room key for the clients.</p>
701
+ <pre><code class="lang-sh"># run twice: once for the token, once for the room key
702
+ node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
703
+ VDON_RELAY_TOKEN=&lt;token&gt; npx vdon-mcp-relay
704
+ # listens on 127.0.0.1:8790 (VDON_RELAY_HOST / VDON_RELAY_PORT to change)</code></pre>
705
+ <p>For other computers, put it behind an HTTPS reverse proxy: forward <code>/v1/rooms/</code> unchanged, keep the <code>Authorization</code> header, disable caching, allow 768 KiB request bodies and 30-second responses. Remote clients must use HTTPS; plain HTTP is only accepted on loopback.</p>
706
+
707
+ <h3>2. Point each MCP server at it</h3>
708
+ <p>Machines that only use the relay can install without WebRTC: <code>npm install @vdoninja/mcp --omit=optional</code>.</p>
709
+ <pre><code class="lang-json">{
710
+ "command": "node",
711
+ "args": ["/absolute/path/node_modules/@vdoninja/mcp/scripts/vdo-mcp-server.js"],
712
+ "env": {
713
+ "VDON_MCP_RELAY_URL": "https://relay.example.com",
714
+ "VDON_MCP_RELAY_TOKEN": "&lt;relay access token&gt;",
715
+ "VDON_MCP_RELAY_KEY": "&lt;64 hex characters&gt;"
716
+ }
717
+ }</code></pre>
718
+ <p>The relay only ever gets the access token. Keep the room key on the clients. <code>vdo_connect</code> also takes <code>relay_url</code>, <code>relay_token</code>, and <code>relay_key</code>, but environment variables keep secrets out of the agent's context.</p>
719
+
720
+ <h3>3. Join and send</h3>
721
+ <pre><code class="lang-json">{ "name": "vdo_connect", "arguments": { "transport": "http", "room": "project_room", "stream_id": "planner" } }
722
+ { "name": "vdo_connect", "arguments": { "transport": "http", "room": "project_room", "stream_id": "reviewer" } }
723
+ { "name": "vdo_send", "arguments": { "session_id": "&lt;planner-session&gt;", "target": "reviewer", "data": { "text": "Please review the plan." } } }</code></pre>
724
+
725
+ <h3>What the results mean</h3>
726
+ <div class="table-wrap">
727
+ <table>
728
+ <thead><tr><th>You see</th><th>It means</th></tr></thead>
729
+ <tbody>
730
+ <tr><td><code>delivery_stage: "queued"</code></td><td>Your local transport accepted the message. Keep the <code>message_id</code> to track it.</td></tr>
731
+ <tr><td><code>relay_receipt</code> · <code>relay_accepted</code></td><td>The relay has it. Not proof the other agent read it; ask for a reply.</td></tr>
732
+ <tr><td><code>relay_receipt</code> · <code>delivery_unknown</code></td><td>Retries failed; the relay may or may not have it. Check before repeating anything consequential.</td></tr>
733
+ <tr><td><code>relay_connection</code></td><td>The reader lost or regained the relay. <code>vdo_status</code> shows the current state.</td></tr>
734
+ <tr><td><code>relay_history_lost</code></td><td>Your cursor fell out of the replay window. Reconcile tasks, re-sync state, check unfinished transfers.</td></tr>
735
+ <tr><td><code>relay_message_rejected</code></td><td>An event failed decryption or validation. Check room keys match and clocks are in sync.</td></tr>
736
+ </tbody>
737
+ </table>
738
+ </div>
739
+
740
+ <h3>Limits and privacy</h3>
741
+ <ul>
742
+ <li><strong>Encryption</strong>: AES-256-GCM, key derived from the shared room key. The relay sees addresses, opaque room IDs, timing, and sizes, never the key or plaintext.</li>
743
+ <li><strong>Shared key, shared access</strong>: anyone with the room key can read all of that room's traffic and claim any identity in it. A <code>target</code> picks delivery; it isn't a private channel. Use separate rooms for separate trust groups.</li>
744
+ <li><strong>No forward secrecy</strong>: a leaked key decrypts previously captured traffic. Rotate keys when membership changes. Prefer WebRTC when that matters.</li>
745
+ <li><strong>Replay, not storage</strong>: up to 512 events or 8 MiB per room for 5 minutes, 16 rooms per relay, memory only. Long polls return within 25 seconds.</li>
746
+ </ul>
747
+ </section>
748
+
749
+ <!-- MCP over HTTP -->
750
+ <section class="doc-section" id="mcp-http">
751
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-cloud"></use></svg></span>MCP over HTTP</h2>
752
+ <p>For AI clients that connect to remote MCP servers over HTTP instead of launching a local process. This changes how your client reaches the MCP server, not how agents reach each other. It's a different service from the <a href="#relay">HTTP relay</a>.</p>
753
+ <pre><code class="lang-sh">VDON_MCP_HTTP_BEARER_TOKEN=change-me npx vdon-mcp-http
754
+ # listens on http://127.0.0.1:8787/mcp</code></pre>
755
+ <div class="table-wrap">
756
+ <table>
757
+ <thead><tr><th>Variable</th><th>Default</th></tr></thead>
758
+ <tbody>
759
+ <tr><td><code>VDON_MCP_HTTP_HOST</code></td><td><code>127.0.0.1</code></td></tr>
760
+ <tr><td><code>VDON_MCP_HTTP_PORT</code></td><td>8787</td></tr>
761
+ <tr><td><code>VDON_MCP_HTTP_PATH</code></td><td><code>/mcp</code></td></tr>
762
+ <tr><td><code>VDON_MCP_HTTP_BEARER_TOKEN</code></td><td>none. Set it before exposing the port.</td></tr>
763
+ <tr><td><code>VDON_MCP_HTTP_ALLOW_ORIGIN</code></td><td>none (no CORS)</td></tr>
764
+ <tr><td><code>VDON_MCP_HTTP_MAX_BODY_BYTES</code></td><td>1048576</td></tr>
765
+ </tbody>
766
+ </table>
767
+ </div>
768
+ <p>The package also ships a Cloudflare Worker template (<code>assets/cloudflare/worker-proxy.js</code>) that forwards MCP requests to an upstream HTTP server. It doesn't run WebRTC itself.</p>
769
+ </section>
770
+
771
+ <!-- Troubleshooting -->
772
+ <section class="doc-section" id="troubleshooting">
773
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-alert"></use></svg></span>Troubleshooting</h2>
774
+ <div class="faq">
775
+ <details>
776
+ <summary><span>My agent doesn't see any <code>vdo_</code> tools</span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
777
+ <div class="answer"><p>Restart the client after installing. Check the registration with <code>claude mcp get vdo-ninja-mcp</code> or <code>codex mcp get vdo-ninja-mcp</code>. For JSON-configured clients, make sure the script path is absolute and exists. If you moved the install folder, run <code>npx vdon-mcp-install</code> again.</p></div>
778
+ </details>
779
+ <details>
780
+ <summary><span>The installer says a CLI wasn't found</span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
781
+ <div class="answer"><p>It only registers with CLIs on your <code>PATH</code>. Install the missing one, use <code>--codex-only</code> or <code>--claude-only</code>, or configure the client by hand.</p></div>
782
+ </details>
783
+ <details>
784
+ <summary><span>Both agents connected, but nothing arrives</span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
785
+ <div class="answer"><p>Check both used the same <code>room</code> and <code>password</code>. Have one side set <code>target_stream_id</code> to the other's <code>stream_id</code>. Call <code>vdo_receive</code> with <code>wait_ms: 1000</code> and look for <code>data_channel_open</code>. A <code>send_rejected</code> event with <code>no_open_data_channel</code> means the channel isn't up yet: wait and retry.</p></div>
786
+ </details>
787
+ <details>
788
+ <summary><span><code>connection_failed</code>, or it never connects</span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
789
+ <div class="answer"><p>The network is probably blocking direct WebRTC. Try <code>force_turn: true</code>. Run <code>MCP_DEMO_FAKE=0 npx vdon-mcp-demo-message</code> on the same machine to separate network problems from agent problems.</p></div>
790
+ </details>
791
+ <details>
792
+ <summary><span><code>sync_peer_rejected</code> or <code>protocol_auth_failed</code></span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
793
+ <div class="answer"><p>A security check failed. Common causes: different join token secrets, an expired token, a stream ID missing from the allowlist, or one side requiring session MAC while the other can't provide it. <code>vdo_sync_peers</code> shows the reason per peer.</p></div>
794
+ </details>
795
+ <details>
796
+ <summary><span>One agent is on the relay and the other isn't</span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
797
+ <div class="answer"><p>Sessions on different transports never discover each other. Disconnect both and reconnect with the same <code>transport</code>, room, relay URL, token, and room key.</p></div>
798
+ </details>
799
+ <details>
800
+ <summary><span><code>relay_history_lost</code> or <code>relay_message_rejected</code></span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
801
+ <div class="answer"><p>History lost: you were away longer than the relay's replay window. Check unfinished work with your peers, run <code>vdo_state_sync</code>, and look at <code>vdo_file_transfers</code>. Rejected: room keys differ, or a clock is far off. Sync clocks and compare keys.</p></div>
802
+ </details>
803
+ <details>
804
+ <summary><span><code>validation_error</code> on a tool call</span><svg class="icon-sm" aria-hidden="true"><use href="assets/icons.svg#i-plus"></use></svg></summary>
805
+ <div class="answer"><p>An argument is missing or misspelled (unknown keys are rejected), or the tool isn't in the active profile. <code>vdo_capabilities</code> lists what's enabled.</p></div>
806
+ </details>
807
+ </div>
808
+ </section>
809
+
810
+ <section class="doc-section" id="guides">
811
+ <h2>Guides</h2>
812
+ <p>Choose the task you want to complete. Each guide is also available in Markdown.</p>
813
+ <h3>Setup and connections</h3>
814
+ <div class="guide-list">
815
+ <a href="quickstart-and-compat.html"><strong>Quickstart</strong><span>Connect two clients and exchange a first message.</span></a>
816
+ <a href="client-config-examples.html"><strong>Client configuration</strong><span>Ready-to-copy setup for your MCP client.</span></a>
817
+ <a href="http-relay.html"><strong>Connect without WebRTC</strong><span>Run and use an encrypted HTTP relay.</span></a>
818
+ </div>
819
+ <h3>Optional remote tools</h3>
820
+ <div class="guide-list">
821
+ <a href="browser-worker.html"><strong>Remote Chrome</strong><span>Share a browser with agent control handoff and optional live media.</span></a>
822
+ <a href="browser-agent.html"><strong>Voice companion</strong><span>Speak to an agent and share selected browser views.</span></a>
823
+ <a href="ssapp-integration.html"><strong>Remote SSApp</strong><span>Use Social Stream Ninja screenshots, input, and app controls remotely.</span></a>
824
+ </div>
825
+ <h3>Reference and direction</h3>
826
+ <div class="guide-list">
827
+ <a href="mcp-tool-contract.html"><strong>Tool contract</strong><span>Arguments, permissions, and transport limits.</span></a>
828
+ <a href="advanced-diagnostics.html"><strong>Advanced diagnostics</strong><span>Check connectivity and troubleshoot reliability.</span></a>
829
+ <a href="vdo-ninja-p2p-data-notes.html"><strong>WebRTC data notes</strong><span>SDK constraints for peer data connections.</span></a>
830
+ <a href="direction.html"><strong>Project direction</strong><span>How Ninja MCP, ninja-p2p, and the SDK fit together.</span></a>
831
+ </div>
832
+ </section>
833
+
834
+ <!-- Testing -->
835
+ <section class="doc-section" id="testing">
836
+ <h2><span class="card-icon"><svg class="icon"><use href="assets/icons.svg#i-check"></use></svg></span>Testing</h2>
837
+ <pre><code class="lang-sh"># fast product checks: conformance, server smoke, sync security
838
+ npm run test:mcp
839
+ # full engineering suite
840
+ npm run test:mcp:all
841
+ # live network diagnostics
842
+ node tests/live-turn-smoke.js
843
+ node tests/live-turn-soak.js
844
+ node tests/live-turn-matrix.js</code></pre>
845
+ <p>Soak, matrix, and tuning knobs are covered in <a href="advanced-diagnostics.html">advanced diagnostics</a>. See the <a href="mcp-tool-contract.html">tool contract</a> for arguments and protocol limits.</p>
846
+
847
+ <p class="muted" style="margin-top:40px;font-size:14px"><a class="link" href="https://github.com/steveseguin/ninjamcp/edit/main/docs/guide.html">Edit this page on GitHub</a> · Found a mistake? <a class="link" href="https://github.com/steveseguin/ninjamcp/issues">Open an issue</a>.</p>
848
+ <nav class="pager" aria-label="More">
849
+ <a href="./#faq"><small>Still curious?</small><b>Read the FAQ</b></a>
850
+ <a href="https://discord.vdo.ninja"><small>Stuck?</small><b>Ask on Discord</b></a>
851
+ </nav>
852
+ </section>
853
+ </main>
854
+ </div>
855
+
856
+ <footer class="footer">
857
+ <div class="wrap">
858
+ <a class="brand" href="./"><img src="assets/logo.svg" alt="" width="24" height="24"><span>VDO.Ninja <small>MCP</small></span></a>
859
+ <nav aria-label="Footer">
860
+ <a href="./">Home</a>
861
+ <a href="https://github.com/steveseguin/ninjamcp">GitHub</a>
862
+ <a href="https://www.npmjs.com/package/@vdoninja/mcp">npm</a>
863
+ <a href="https://discord.vdo.ninja">Discord</a>
864
+ <a href="https://vdo.ninja">VDO.Ninja</a>
865
+ </nav>
866
+ <span>MIT · © 2026 Steve Seguin</span>
867
+ </div>
868
+ </footer>
869
+
870
+ <script src="assets/site.js" defer></script>
871
+ </body>
872
+ </html>