@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,289 @@
1
+ // VDO.Ninja MCP site behaviour: copy buttons, code highlighting, scroll story, tabs, tool profiles, docs nav.
2
+ (function () {
3
+ 'use strict';
4
+
5
+ const $ = (sel, root = document) => root.querySelector(sel);
6
+ const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
7
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
8
+ const ICONS = document.documentElement.dataset.icons || 'assets/icons.svg';
9
+
10
+ function icon(name, cls = 'icon-sm') {
11
+ return `<svg class="${cls}" aria-hidden="true"><use href="${ICONS}#${name}"></use></svg>`;
12
+ }
13
+
14
+ function escapeHtml(text) {
15
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
16
+ }
17
+
18
+ // ---------- Highlighting ----------
19
+ function highlightJson(text) {
20
+ return escapeHtml(text).replace(
21
+ /("(?:\\.|[^"\\])*")(\s*:)?|\b(true|false|null)\b|(-?\b\d+(?:\.\d+)?\b)/g,
22
+ (m, str, colon, lit, num) => {
23
+ if (str) return colon ? `<span class="c-key">${str}</span>${colon}` : `<span class="c-str">${str}</span>`;
24
+ if (lit) return `<span class="c-num">${lit}</span>`;
25
+ if (num) return `<span class="c-num">${num}</span>`;
26
+ return m;
27
+ }
28
+ );
29
+ }
30
+
31
+ function highlightShell(text) {
32
+ return text.split('\n').map((line) => {
33
+ if (!line.trim()) return '';
34
+ if (/^\s*#/.test(line)) return `<span class="c-dim">${escapeHtml(line)}</span>`;
35
+ return `<span class="ln-cmd">${escapeHtml(line)}</span>`;
36
+ }).join('\n');
37
+ }
38
+
39
+ $$('pre code.lang-json, .term-step .json').forEach((el) => { el.innerHTML = highlightJson(el.textContent); });
40
+ $$('pre code.lang-sh').forEach((el) => {
41
+ el.parentElement.classList.add('lang-sh');
42
+ el.innerHTML = highlightShell(el.textContent.replace(/\n$/, ''));
43
+ });
44
+
45
+ // ---------- Copy buttons ----------
46
+ async function copyText(text, btn) {
47
+ try {
48
+ await navigator.clipboard.writeText(text);
49
+ } catch (e) {
50
+ const ta = document.createElement('textarea');
51
+ ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
52
+ document.body.appendChild(ta); ta.select();
53
+ try { document.execCommand('copy'); } catch (err) { /* ignore */ }
54
+ ta.remove();
55
+ }
56
+ const label = btn.querySelector('.label');
57
+ btn.classList.add('done');
58
+ if (label) label.textContent = 'Copied';
59
+ clearTimeout(btn._t);
60
+ btn._t = setTimeout(() => { btn.classList.remove('done'); if (label) label.textContent = 'Copy'; }, 1600);
61
+ }
62
+
63
+ $$('pre').forEach((pre) => {
64
+ if (pre.classList.contains('no-copy') || pre.querySelector('.copy')) return;
65
+ const btn = document.createElement('button');
66
+ btn.type = 'button';
67
+ btn.className = 'copy';
68
+ btn.setAttribute('aria-label', 'Copy code');
69
+ btn.innerHTML = `${icon('i-copy', 'icon-xs')}<span class="label">Copy</span>`;
70
+ btn.addEventListener('click', () => {
71
+ const code = pre.querySelector('code') || pre;
72
+ const lines = code.textContent.split('\n').filter((l) => !/^\s*#/.test(l) || !pre.classList.contains('lang-sh'));
73
+ copyText(lines.join('\n').trim(), btn);
74
+ });
75
+ pre.appendChild(btn);
76
+ });
77
+
78
+ $$('[data-copy]').forEach((btn) => {
79
+ btn.addEventListener('click', () => copyText(btn.dataset.copy, btn));
80
+ });
81
+
82
+ // ---------- Live package version (optional; hidden if the registry can't be reached) ----------
83
+ const versionEl = $('[data-npm-version]');
84
+ if (versionEl && window.fetch) {
85
+ fetch('https://registry.npmjs.org/@vdoninja/mcp/latest')
86
+ .then((r) => (r.ok ? r.json() : null))
87
+ .then((pkg) => {
88
+ if (pkg && /^\d+\.\d+\.\d+/.test(pkg.version || '')) {
89
+ versionEl.textContent = ` · v${pkg.version}`;
90
+ versionEl.hidden = false;
91
+ }
92
+ })
93
+ .catch(() => {});
94
+ }
95
+
96
+ // ---------- Mobile nav ----------
97
+ const nav = $('.nav');
98
+ const navToggle = $('.nav-toggle');
99
+ if (nav && navToggle) {
100
+ navToggle.addEventListener('click', () => {
101
+ const open = nav.classList.toggle('open');
102
+ navToggle.setAttribute('aria-expanded', String(open));
103
+ });
104
+ $$('.nav-links a').forEach((a) => a.addEventListener('click', () => {
105
+ nav.classList.remove('open');
106
+ navToggle.setAttribute('aria-expanded', 'false');
107
+ }));
108
+ }
109
+
110
+ // ---------- Tabs ----------
111
+ $$('[data-tabs]').forEach((root) => {
112
+ const tabs = $$('[role="tab"]', root);
113
+ const select = (tab) => {
114
+ tabs.forEach((t) => {
115
+ const on = t === tab;
116
+ t.setAttribute('aria-selected', String(on));
117
+ t.tabIndex = on ? 0 : -1;
118
+ const panel = document.getElementById(t.getAttribute('aria-controls'));
119
+ if (panel) panel.hidden = !on;
120
+ });
121
+ };
122
+ tabs.forEach((tab, i) => {
123
+ tab.addEventListener('click', () => select(tab));
124
+ tab.addEventListener('keydown', (e) => {
125
+ if (e.key !== 'ArrowRight' && e.key !== 'ArrowLeft') return;
126
+ const next = tabs[(i + (e.key === 'ArrowRight' ? 1 : tabs.length - 1)) % tabs.length];
127
+ next.focus(); select(next);
128
+ });
129
+ });
130
+ });
131
+
132
+ // ---------- Tool profiles ----------
133
+ const PROFILES = {
134
+ core: { groups: ['session', 'sync'], note: '9 tools: connect, message, and peer sync. Smallest context footprint.' },
135
+ file: { groups: ['session', 'sync', 'file'], note: '15 tools: core plus chunked, resumable file transfer.' },
136
+ state: { groups: ['session', 'sync', 'state'], note: '12 tools: core plus shared room state.' },
137
+ full: { groups: ['session', 'sync', 'file', 'state'], note: 'All 18 tools. The default when no profile is set.' }
138
+ };
139
+ const profileRoot = $('[data-profiles]');
140
+ if (profileRoot) {
141
+ const buttons = $$('button', profileRoot);
142
+ const note = $('.profile-note');
143
+ const apply = (name) => {
144
+ const p = PROFILES[name];
145
+ buttons.forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.profile === name)));
146
+ $$('[data-group]').forEach((g) => g.classList.toggle('off', !p.groups.includes(g.dataset.group)));
147
+ if (note) note.innerHTML = `${p.note} <code>--tool-profile ${name}</code>`;
148
+ };
149
+ buttons.forEach((b) => b.addEventListener('click', () => apply(b.dataset.profile)));
150
+ apply('full');
151
+ }
152
+
153
+ // ---------- Scroll story ----------
154
+ const story = $('[data-story]');
155
+ if (story) {
156
+ const stage = $('.stage', story);
157
+ const items = $$('.story-steps li', story);
158
+ const pillNum = $('.caption-pill .num', story);
159
+ const pillTxt = $('.caption-pill .txt', story);
160
+ const termSteps = $$('.term-step', story);
161
+ const termTitle = $('.term-title', story);
162
+ const dashes = $('.dashes', story);
163
+ const packets = $('.packets', story);
164
+ const statusA = $('.agent-a .status', story);
165
+ const statusB = $('.agent-b .status', story);
166
+ const STATUS = [
167
+ ['not connected', ''], ['MCP ready', 'ready'], ['in room', 'ready'], ['in room', 'ready'],
168
+ ['connected', 'live'], ['connected', 'live'], ['connected', 'live'], ['connected', 'live'], ['connected', 'live']
169
+ ];
170
+ const STATUS_B = STATUS.map((s, i) => (i === 2 ? ['MCP ready', 'ready'] : s));
171
+ const PACKETS = { 5: ['go-ab', 1, false], 6: ['go-ba', 1, false], 7: ['go-ab', 6, true], 8: ['go-ba', 1, false] };
172
+
173
+ items.forEach((li, i) => {
174
+ const b = document.createElement('button');
175
+ b.type = 'button';
176
+ b.setAttribute('aria-label', `Step ${i + 1}`);
177
+ b.addEventListener('click', () => {
178
+ const top = li.getBoundingClientRect().top + window.scrollY - window.innerHeight * 0.5 + 2;
179
+ window.scrollTo({ top, behavior: reduceMotion ? 'auto' : 'smooth' });
180
+ });
181
+ dashes.appendChild(b);
182
+ });
183
+ const dashButtons = $$('button', dashes);
184
+
185
+ const setStatus = (el, [text, cls]) => {
186
+ el.textContent = text;
187
+ el.className = `status ${cls}`.trim();
188
+ };
189
+
190
+ let current = -1;
191
+ const setStep = (i) => {
192
+ if (i === current) return;
193
+ current = i;
194
+ stage.dataset.step = String(i);
195
+
196
+ $$('[data-steps]', stage).forEach((el) => {
197
+ el.hidden = !el.dataset.steps.split(' ').includes(String(i));
198
+ });
199
+ $$('.agent', stage).forEach((a) => a.classList.toggle('on', i >= 1));
200
+ setStatus(statusA, STATUS[i]);
201
+ setStatus(statusB, STATUS_B[i]);
202
+
203
+ $('.node-server', stage).classList.toggle('is-active', i === 2 || i === 3);
204
+ $('.node-server', stage).classList.toggle('is-quiet', i >= 4);
205
+ $('.l-sig-a', stage).setAttribute('class', `l-sig l-sig-a ${i === 2 || i === 3 ? 'on' : i >= 4 ? 'faded' : ''}`);
206
+ $('.l-sig-b', stage).setAttribute('class', `l-sig l-sig-b ${i === 3 ? 'on' : i >= 4 ? 'faded' : ''}`);
207
+ $('.l-p2p', stage).setAttribute('class', `l-p2p ${i >= 4 ? 'on' : ''}`);
208
+ $$('.l-relay', stage).forEach((l) => l.setAttribute('class', `l-relay ${i === 4 ? 'on' : ''}`));
209
+ $('.node-relay', stage).classList.toggle('is-quiet', i !== 4);
210
+ $('.p2p-label', stage).classList.toggle('on', i >= 4);
211
+
212
+ packets.className = 'packets';
213
+ packets.innerHTML = '';
214
+ const p = PACKETS[i];
215
+ if (p && !reduceMotion) {
216
+ const [dir, n, chunk] = p;
217
+ for (let k = 0; k < n; k += 1) {
218
+ const dot = document.createElement('i');
219
+ dot.className = chunk ? 'packet chunk' : 'packet';
220
+ dot.style.setProperty('--d', `${k * (chunk ? 0.35 : 0) + 0.2}s`);
221
+ dot.style.setProperty('--n', chunk ? '1' : '2');
222
+ packets.appendChild(dot);
223
+ }
224
+ void packets.offsetWidth;
225
+ packets.classList.add(dir);
226
+ }
227
+
228
+ const item = items[i];
229
+ pillNum.textContent = String(i).padStart(2, '0');
230
+ pillTxt.innerHTML = item.innerHTML;
231
+ termSteps.forEach((t) => t.classList.toggle('is-active', t.dataset.step === String(i)));
232
+ const active = termSteps.find((t) => t.dataset.step === String(i));
233
+ if (active && termTitle) termTitle.textContent = active.dataset.title || '';
234
+ dashButtons.forEach((b, k) => b.classList.toggle('on', k <= i));
235
+ };
236
+
237
+ let ticking = false;
238
+ const onScroll = () => {
239
+ ticking = false;
240
+ const line = window.innerHeight * 0.55;
241
+ let idx = 0;
242
+ items.forEach((li, k) => { if (li.getBoundingClientRect().top < line) idx = k; });
243
+ setStep(idx);
244
+ };
245
+ window.addEventListener('scroll', () => {
246
+ if (!ticking) { ticking = true; requestAnimationFrame(onScroll); }
247
+ }, { passive: true });
248
+ window.addEventListener('resize', onScroll);
249
+ onScroll();
250
+ }
251
+
252
+ // ---------- Docs: sidebar toggle + scrollspy ----------
253
+ const docsToggle = $('.docs-toggle');
254
+ if (docsToggle) {
255
+ const closeSidebar = () => {
256
+ document.body.classList.remove('sidebar-open');
257
+ docsToggle.setAttribute('aria-expanded', 'false');
258
+ };
259
+ docsToggle.setAttribute('aria-expanded', 'false');
260
+ docsToggle.addEventListener('click', () => {
261
+ const expanded = document.body.classList.toggle('sidebar-open');
262
+ docsToggle.setAttribute('aria-expanded', String(expanded));
263
+ });
264
+ $$('.sidebar a').forEach((a) => a.addEventListener('click', closeSidebar));
265
+ document.addEventListener('keydown', (event) => {
266
+ if (event.key === 'Escape' && document.body.classList.contains('sidebar-open')) {
267
+ closeSidebar();
268
+ docsToggle.focus();
269
+ }
270
+ });
271
+ }
272
+ const sideLinks = $$('.sidebar a[href^="#"]');
273
+ const sections = $$('.doc-section');
274
+ if (sideLinks.length && sections.length) {
275
+ const map = new Map(sideLinks.map((a) => [a.getAttribute('href').slice(1), a]));
276
+ let spyTicking = false;
277
+ const spy = () => {
278
+ spyTicking = false;
279
+ let current = sections[0];
280
+ sections.forEach((s) => { if (s.getBoundingClientRect().top < 140) current = s; });
281
+ if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 4) current = sections[sections.length - 1];
282
+ sideLinks.forEach((a) => a.classList.toggle('active', a === map.get(current.id)));
283
+ };
284
+ window.addEventListener('scroll', () => {
285
+ if (!spyTicking) { spyTicking = true; requestAnimationFrame(spy); }
286
+ }, { passive: true });
287
+ spy();
288
+ }
289
+ })();
@@ -0,0 +1,122 @@
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>Voice companion · VDO.Ninja MCP</title>
7
+ <meta name="description" content="Talk to an agent while sharing selected browser views and controls.">
8
+ <meta name="color-scheme" content="light dark">
9
+ <link rel="canonical" href="https://steveseguin.github.io/ninjamcp/browser-agent.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="#start-a-conversation">Start a conversation</a><a href="#control-what-is-shared">Control what is shared</a><a href="#watch-the-browser">Watch the browser</a><a href="#resolve-connection-problems">Resolve connection problems</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="ssapp-integration.html">Remote SSApp</a><a href="mcp-tool-contract.html">Tool contract</a><a href="advanced-diagnostics.html">Advanced diagnostics</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> / Voice companion</p>
42
+ <h1 id="talk-to-an-agent-while-sharing-a-browser">Talk to an agent while sharing a browser</h1>
43
+ <p class="guide-format"><a href="https://github.com/steveseguin/ninjamcp/blob/main/references/browser-agent.md">Read in Markdown</a></p>
44
+ </header>
45
+ <section class="doc-section guide-intro" id="overview">
46
+ <p>The browser companion lets you speak or type to an OpenAI Realtime agent, share selected views of your remote Chrome tab, and let it use the browser tools. You can also watch and hear the tab through VDO.Ninja. The browser host can be on another computer; the companion runs locally beside your microphone and speakers.</p>
47
+ </section><section class="doc-section" id="start-a-conversation"><h2>Start a conversation</h2>
48
+ <p>First <a href="browser-worker.html">start a browser worker</a> and copy one controller credential file to your computer. Use a separate credential for each agent or companion running at the same time.</p>
49
+ <p>Set <code>OPENAI_API_KEY</code> in the local companion process, then launch:</p>
50
+ <pre><code class="lang-sh">npx vdon-mcp-browser-agent --config /private/agent1.json
51
+ </code></pre>
52
+ <p>From a checkout, run <code>node scripts/vdo-mcp-browser-agent.js --config /private/agent1.json</code>. Open the private loopback URL printed in the terminal on that same computer. Keep the terminal running. The API key stays in the local server process; it is not sent to the remote browser host or the operator page.</p>
53
+ <ol>
54
+ <li>Select <strong>Check worker</strong> to confirm authenticated access.</li>
55
+ <li>Select <strong>Start agent</strong>, then choose <strong>Share my microphone</strong> to speak. You can also type a message.</li>
56
+ <li>Enable <strong>Share browser frames</strong> when you want the agent to see the tab. Frames update about every three seconds; the agent can also read page snapshots.</li>
57
+ <li>Enable <strong>Allow browser control</strong> when you want the agent to navigate, click, or type. This also enables browser sharing. The worker's controller role and tab lease still apply.</li>
58
+ </ol>
59
+ <p>The default model is <code>gpt-realtime-2.1</code>. Set <code>VDON_BROWSER_MODEL</code> to select another Realtime model that supports audio, images, and function calls. Model usage is billed to your OpenAI account. Each conversation ends after 15 minutes; start a new one to continue.</p>
60
+ </section><section class="doc-section" id="control-what-is-shared"><h2>Control what is shared</h2>
61
+ <div aria-label="Scrollable reference table" class="table-wrap" role="region" tabindex="0"><table>
62
+ <thead>
63
+ <tr>
64
+ <th scope="col">Control</th>
65
+ <th scope="col">Effect</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ <tr>
70
+ <td>Share my microphone</td>
71
+ <td>Sends microphone audio to OpenAI while the agent session is connected. Turning it off stops microphone capture and clears pending input audio.</td>
72
+ </tr>
73
+ <tr>
74
+ <td>Share browser frames</td>
75
+ <td>Sends browser images and requested page content to OpenAI. Turning it off stops new image and tool requests and releases held control. Content already sent remains part of that model conversation.</td>
76
+ </tr>
77
+ <tr>
78
+ <td>Allow browser control</td>
79
+ <td>Allows the agent to claim the shared tab and use input tools. Turning it off releases held leases and prevents queued control requests from starting.</td>
80
+ </tr>
81
+ <tr>
82
+ <td>Interrupt</td>
83
+ <td>Stops queued speech and agent actions. An action already sent to the worker may finish; inspect its result before repeating it.</td>
84
+ </tr>
85
+ <tr>
86
+ <td>End session</td>
87
+ <td>Closes the model connection, stops microphone capture and frame sharing, and releases held control.</td>
88
+ </tr>
89
+ </tbody>
90
+ </table></div>
91
+ <p>The sharing indicator identifies the selected inputs and OpenAI as their recipient. The application does not record locally. OpenAI processes the microphone audio, selected browser images, typed messages, and tool results sent during the session. The voice you hear is AI generated.</p>
92
+ <p>Other authorized observers can still see the worker's tab. To revoke another controller, use <code>revoke 1</code> in the host terminal. Stop the worker to close the browser and end access for everyone.</p>
93
+ </section><section class="doc-section" id="watch-the-browser"><h2>Watch the browser</h2>
94
+ <p>Enable <code>"media": { "enabled": true }</code> in the host's <code>worker.json</code>, or add <code>--media</code> during setup. Select <strong>Watch live browser</strong> in the companion and <strong>Hear browser audio</strong> to unmute. This uses the SDK stream directly and does not require an OpenAI key. Watching alone does not share that stream with the model.</p>
95
+ <p>The model receives selected browser frames and your shared microphone; captured tab audio is for the human viewer. Live viewing requires WebRTC. With an HTTP connection, keep using text, microphone conversation, page snapshots, screenshots, and browser controls even when the live view cannot connect. The companion's connection to OpenAI uses an outbound secure WebSocket.</p>
96
+ </section><section class="doc-section" id="resolve-connection-problems"><h2>Resolve connection problems</h2>
97
+ <ul>
98
+ <li>If <strong>Check worker</strong> fails, run <code>vdon-mcp-browser --doctor --config /private/agent1.json</code>. Confirm that the host is running with matching credentials and a common route.</li>
99
+ <li>If the model will not connect, set the API key in the companion process, check model access, and restart the companion. Viewing and worker diagnostics remain available without a key.</li>
100
+ <li>If another agent holds the tab, let it release control or wait for its lease to expire. Sharing a browser does not bypass that ownership.</li>
101
+ <li>If a tool reports an uncertain outcome, have the agent use <code>browser_result</code> and inspect the page before repeating an action. A returned tool result and completion of your requested task are separate outcomes.</li>
102
+ </ul>
103
+ <p>The page accepts one local operator connection. Reopen the original private URL after a reload or disconnect; its access token is removed from the address bar after connection. Keep that URL and the agent credential file private.</p></section>
104
+ <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>
105
+ </main>
106
+ </div>
107
+ <footer class="footer">
108
+ <div class="wrap">
109
+ <a class="brand" href="./"><img alt="" height="24" src="assets/logo.svg" width="24"/><span>VDO.Ninja <small>MCP</small></span></a>
110
+ <nav aria-label="Footer">
111
+ <a href="./">Home</a>
112
+ <a href="https://github.com/steveseguin/ninjamcp">GitHub</a>
113
+ <a href="https://www.npmjs.com/package/@vdoninja/mcp">npm</a>
114
+ <a href="https://discord.vdo.ninja">Discord</a>
115
+ <a href="https://vdo.ninja">VDO.Ninja</a>
116
+ </nav>
117
+ <span>MIT · © 2026 Steve Seguin</span>
118
+ </div>
119
+ </footer>
120
+ <script src="assets/site.js" defer></script>
121
+ </body>
122
+ </html>
@@ -0,0 +1,165 @@
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>Remote Chrome · VDO.Ninja MCP</title>
7
+ <meta name="description" content="Share a remote Chrome tab, with separate agent credentials and control handoff.">
8
+ <meta name="color-scheme" content="light dark">
9
+ <link rel="canonical" href="https://steveseguin.github.io/ninjamcp/browser-worker.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="#start-the-host">Start the host</a><a href="#connect-an-agent">Connect an agent</a><a href="#share-control-and-recover-a-connection">Share control and recover a connection</a><a href="#keep-working-when-webrtc-is-unavailable">Keep working when WebRTC is unavailable</a><a href="#watch-live-tab-audio-and-video">Watch live tab audio and video</a><a href="#choose-the-hosts-access-deliberately">Choose the host&#x27;s access deliberately</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-agent.html">Voice companion</a><a href="ssapp-integration.html">Remote SSApp</a><a href="mcp-tool-contract.html">Tool contract</a><a href="advanced-diagnostics.html">Advanced diagnostics</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> / Remote Chrome</p>
42
+ <h1 id="share-a-remote-chrome-browser">Share a remote Chrome browser</h1>
43
+ <p class="guide-format"><a href="https://github.com/steveseguin/ninjamcp/blob/main/references/browser-worker.md">Read in Markdown</a></p>
44
+ </header>
45
+ <section class="doc-section guide-intro" id="overview">
46
+ <p>Run an isolated, headless Chrome tab on one computer and let agents use it through Ninja MCP. The host connects outward using WebRTC or your HTTP relay. Chrome's debugging endpoint stays on loopback; agents need no VPN or desktop streaming software.</p>
47
+ <p>Each agent gets its own credential. Controllers take turns using a tab lease; observers can read snapshots, request screenshots, and watch optional live tab audio/video.</p>
48
+ </section><section class="doc-section" id="start-the-host"><h2>Start the host</h2>
49
+ <p>Install Chrome and Node.js 22.12 or later. In your Ninja MCP installation folder, install the optional browser packages:</p>
50
+ <pre><code class="lang-sh">npm install @vdoninja/mcp chrome-devtools-mcp@1.11.0 puppeteer-core@24.37.5
51
+ npx vdon-mcp-browser --setup ./private-browser
52
+ npx vdon-mcp-browser --worker --config ./private-browser/worker.json
53
+ </code></pre>
54
+ <p>The setup prompts for a connection and optional live media. For a direct connection without prompts, add <code>--transport webrtc</code>. From a checkout, use <code>node scripts/vdo-mcp-browser.js</code> in place of <code>npx vdon-mcp-browser</code>. Keep the generated directory outside source control and accessible only to the host account. It contains four credential files:</p>
55
+ <div aria-label="Scrollable reference table" class="table-wrap" role="region" tabindex="0"><table>
56
+ <thead>
57
+ <tr>
58
+ <th scope="col">File</th>
59
+ <th scope="col">Give it to</th>
60
+ </tr>
61
+ </thead>
62
+ <tbody>
63
+ <tr>
64
+ <td><code>worker.json</code></td>
65
+ <td>The browser host only; it contains every client key.</td>
66
+ </tr>
67
+ <tr>
68
+ <td><code>agent1.json</code>, <code>agent2.json</code></td>
69
+ <td>One controller each.</td>
70
+ </tr>
71
+ <tr>
72
+ <td><code>viewer.json</code></td>
73
+ <td>An observer who may see the shared tab but cannot control it.</td>
74
+ </tr>
75
+ </tbody>
76
+ </table></div>
77
+ <p>Copy each client file privately to its intended agent's machine. Give additional clients distinct random 32-byte hex keys and add their IDs, keys, and <code>controller</code> or <code>observer</code> roles to <code>worker.json</code>. Restart the worker to apply credential changes. Use one client credential per active agent process.</p>
78
+ <p>Setup also writes <code>mcp-client.json</code> with the local agent registration. Adjust its absolute paths when copying it to another computer. <code>--init</code> remains available for creating WebRTC credentials without prompts.</p>
79
+ <p>The worker starts with one empty shared tab and an ephemeral Chrome profile. Your ordinary Chrome windows and logged-in profile are separate. To select a Chrome executable, add an absolute <code>executable</code> path to <code>worker.json</code>. When browser packages live elsewhere, set <code>dependencies</code> to their installation directory.</p>
80
+ </section><section class="doc-section" id="connect-an-agent"><h2>Connect an agent</h2>
81
+ <p>The agent machine needs Ninja MCP, but does not need Chrome or the optional browser packages. Add this separate server to the agent's MCP configuration, using absolute paths:</p>
82
+ <pre><code class="lang-json">{
83
+ "mcpServers": {
84
+ "ninja-browser": {
85
+ "command": "node",
86
+ "args": [
87
+ "/absolute/path/node_modules/@vdoninja/mcp/scripts/vdo-mcp-browser.js",
88
+ "--config",
89
+ "/absolute/private/path/agent1.json"
90
+ ]
91
+ }
92
+ }
93
+ }
94
+ </code></pre>
95
+ <p>On Windows, use paths such as <code>C:/Ninja/node_modules/@vdoninja/mcp/scripts/vdo-mcp-browser.js</code>. Existing message, file, and state tools remain available through the ordinary Ninja MCP server.</p>
96
+ <p>Check the host packages or authenticate to the running worker from a client:</p>
97
+ <pre><code class="lang-sh">npx vdon-mcp-browser --doctor --config ./private-browser/worker.json
98
+ npx vdon-mcp-browser --doctor --config ./private-browser/agent1.json
99
+ </code></pre>
100
+ <p>The client check lists tabs without taking control. Its output distinguishes relay access from an authenticated worker response and names the route that returned the result.</p>
101
+ <p>Ask the agent to list tabs, claim the shared tab, navigate, and take a snapshot:</p>
102
+ <pre><code class="lang-json">{ "name": "browser_tabs", "arguments": {} }
103
+ { "name": "browser_claim", "arguments": { "pageId": 1 } }
104
+ { "name": "browser_call", "arguments": { "pageId": 1, "lease": "&lt;returned lease&gt;", "action": "navigate", "url": "https://example.com" } }
105
+ { "name": "browser_observe", "arguments": { "pageId": 1 } }
106
+ </code></pre>
107
+ <p>Use the returned snapshot token and element UID for input:</p>
108
+ <pre><code class="lang-json">{ "name": "browser_call", "arguments": { "pageId": 1, "lease": "&lt;returned lease&gt;", "snapshot": "&lt;snapshot token&gt;", "action": "click", "uid": "&lt;element uid&gt;" } }
109
+ { "name": "browser_observe", "arguments": { "pageId": 1, "screenshot": true } }
110
+ { "name": "browser_release", "arguments": { "pageId": 1, "lease": "&lt;returned lease&gt;" } }
111
+ </code></pre>
112
+ <p><code>fill</code> takes <code>uid</code> and <code>value</code>; <code>key</code> takes a page key such as <code>Enter</code>, <code>Tab</code>, or an arrow key. Navigation accepts HTTP and HTTPS URLs. Remote tools do not expose script evaluation, local file uploads, browser shortcuts, or the media publisher's private page. They share the initial tab; newly opened tabs are not exposed. Continue work in the shared tab by navigating it to the desired URL.</p>
113
+ </section><section class="doc-section" id="share-control-and-recover-a-connection"><h2>Share control and recover a connection</h2>
114
+ <p>A lease lasts 60 seconds by default. Set <code>ttl_ms</code> between 1,000 and 120,000 when claiming; renew by passing the same lease to <code>browser_claim</code>. Another controller can claim after release or expiry. A disconnected agent's lease expires on that same deadline.</p>
115
+ <p>Take a fresh accessibility snapshot after claiming, navigating, or acting. Snapshot tokens expire after 30 seconds. Another agent's accessibility snapshot replaces the current element references; take a fresh one if an input call returns <code>fresh_snapshot_required</code>. Screenshots do not replace those references.</p>
116
+ <p>The host can enter <code>revoke 1</code> in its worker terminal to invalidate the current lease immediately. An action already executing may finish. Stop the worker to close the browser and end all access.</p>
117
+ <p>Requests have IDs, deadlines, and bounded retries. A lost response is retried with the same ID; the running worker returns its cached result instead of repeating the action. Recent results remain available for at least two minutes after their request deadline:</p>
118
+ <pre><code class="lang-json">{ "name": "browser_result", "arguments": { "request_id": "&lt;uncertain request ID&gt;" } }
119
+ </code></pre>
120
+ <p>An <code>outcome_unknown</code> response or an unknown result does not mean the action failed to execute. Inspect the page before deciding whether to repeat a click or submission. Results are held in memory; restarting the worker creates a new browser and invalidates old commands. Results are limited to 2 MiB; screenshots cover the viewport and use JPEG compression.</p>
121
+ </section><section class="doc-section" id="keep-working-when-webrtc-is-unavailable"><h2>Keep working when WebRTC is unavailable</h2>
122
+ <p>For automatic browser routing, create a new group with a private relay configuration:</p>
123
+ <pre><code class="lang-sh">npx vdon-mcp-browser --setup ./private-browser --transport auto --relay-config /private/relay.json
124
+ </code></pre>
125
+ <p>The relay file uses <code>{ "url": "https://relay.example.com", "token": "&lt;access token&gt;" }</code>. You can reuse a ninja-p2p relay configuration and the same hosted relay service; setup creates a separate browser room and encryption key. Mailbox credentials and OAuth connections are separate and are not used here.</p>
126
+ <p><code>auto</code> prefers an available WebRTC data channel. If a request needs retrying, it tries the other available route with the same encrypted request and ID. One worker owns the result cache and control leases across both routes. After a WebRTC timeout, new requests prefer HTTP for 30 seconds while the direct connection recovers. Tool results report which route replied.</p>
127
+ <p>Use <code>--transport http</code> for HTTP only, or <code>--transport webrtc</code> for direct connections only. An automatic participant can reach a participant using either one of those routes, provided their connection and browser credentials match. Both routes carry commands, results, and screenshots; live media still needs WebRTC.</p>
128
+ <p>For an existing group, set <code>connection.transport</code> to <code>"auto"</code> or <code>"http"</code> in its files, and supply the same relay settings to each participating process. These environment variables override the generated settings:</p>
129
+ <ul>
130
+ <li><code>VDON_MCP_RELAY_URL</code>: your HTTPS relay origin.</li>
131
+ <li><code>VDON_MCP_RELAY_TOKEN</code>: relay access token.</li>
132
+ <li><code>VDON_MCP_RELAY_KEY</code>: room encryption key.</li>
133
+ </ul>
134
+ <p>Follow the <a href="http-relay.html">relay setup guide</a> to host a compatible relay. Browser credentials remain separate from relay credentials; sharing a room key does not grant browser access. Automatic routing applies to this browser adapter; ordinary Ninja MCP room sessions select their transport explicitly.</p>
135
+ </section><section class="doc-section" id="watch-live-tab-audio-and-video"><h2>Watch live tab audio and video</h2>
136
+ <p>Add this option to <code>worker.json</code>, then restart:</p>
137
+ <pre><code class="lang-json">"media": { "enabled": true }
138
+ </code></pre>
139
+ <p>Call <code>browser_media</code> to get track status and a private VDO.Ninja viewing link. The host captures only the shared tab, including its audio, through Chrome tab capture and publishes it using the VDO.Ninja SDK. The publisher uses a separate browser context and fresh media credentials on every worker start. Microphone and desktop capture are not enabled. Recording is off.</p>
140
+ <p>Anyone holding the viewing link can watch and hear that stream. Share it only with intended viewers; restart the worker to replace the link. Media access does not grant control. The capture persists when the shared tab navigates, so stop the worker before opening content you do not want viewers to see.</p>
141
+ <p>Live media requires WebRTC even when commands use HTTP. If <code>browser_media</code> reports unavailable media or an ended track, use <code>browser_observe</code> screenshots and restart after restoring capture or connectivity. An optional <code>media.host</code> selects a different SDK signaling endpoint; clients then receive connection settings instead of a default VDO.Ninja link.</p>
142
+ <p>For a speaking agent that sees selected browser frames and uses these tools, open the optional <a href="browser-agent.html">browser companion</a>. Live viewing and model sharing have separate controls.</p>
143
+ </section><section class="doc-section" id="choose-the-hosts-access-deliberately"><h2>Choose the host's access deliberately</h2>
144
+ <p>All configured viewers can see the shared tab, and controllers can interact with sites using the host's network access. Run the worker under an account or container with only the network and filesystem access you intend to share. A separate Chrome profile is not an operating-system sandbox. Avoid signing into personal accounts in this shared tab.</p>
145
+ <p>Browser requests and results use a separate AES-GCM key for each client over either transport. Keep credential files private. Removing a client requires updating the host configuration and restarting; possession of that client's file grants its configured role until then.</p>
146
+ <p>The adapter uses the pinned <a href="https://github.com/ChromeDevTools/chrome-devtools-mcp">Chrome DevTools MCP</a> release and <a href="https://sdk.vdo.ninja/">VDO.Ninja SDK</a>. Chrome runs with its sandbox enabled. The worker binds no public browser-control port and changes no ordinary Ninja MCP transport defaults.</p></section>
147
+ <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>
148
+ </main>
149
+ </div>
150
+ <footer class="footer">
151
+ <div class="wrap">
152
+ <a class="brand" href="./"><img alt="" height="24" src="assets/logo.svg" width="24"/><span>VDO.Ninja <small>MCP</small></span></a>
153
+ <nav aria-label="Footer">
154
+ <a href="./">Home</a>
155
+ <a href="https://github.com/steveseguin/ninjamcp">GitHub</a>
156
+ <a href="https://www.npmjs.com/package/@vdoninja/mcp">npm</a>
157
+ <a href="https://discord.vdo.ninja">Discord</a>
158
+ <a href="https://vdo.ninja">VDO.Ninja</a>
159
+ </nav>
160
+ <span>MIT · © 2026 Steve Seguin</span>
161
+ </div>
162
+ </footer>
163
+ <script src="assets/site.js" defer></script>
164
+ </body>
165
+ </html>