@vdoninja/mcp 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ # VDO.Ninja P2P Data Notes for MCP Workflows
2
+
3
+ This file captures operational constraints from this repository's SDK docs.
4
+
5
+ ## Hard Platform Constraints
6
+
7
+ 1. Signaling is not an app-data tunnel.
8
+ - Only handshake/signaling data should traverse WebSocket.
9
+ - Application payloads are expected over WebRTC data channels.
10
+
11
+ 2. Room scale is bounded.
12
+ - Documented guidance is about 80 peers per room (viewer limits can apply).
13
+ - For higher fanout, split rooms or use server-mediated patterns.
14
+
15
+ 3. Serverless philosophy is enforced.
16
+ - No server-side state relay for your app protocol.
17
+ - Design as peer-to-peer first.
18
+
19
+ ## Topologies That Work
20
+
21
+ 1. Two-peer data link (recommended).
22
+ - Peer A: `announce({ streamID })`
23
+ - Peer B: `view(streamID, { audio:false, video:false })`
24
+ - Result: one bidirectional data channel.
25
+
26
+ 2. Multi-peer data mesh.
27
+ - Use `autoConnect(room)` half-mesh mode to avoid duplicate pair connections.
28
+ - Move to full mesh only when media exchange actually needs two directional AV links.
29
+
30
+ 3. Targeted delivery.
31
+ - Use `sendData(data, "uuid")` or advanced target objects.
32
+ - Keep `preference: "any"` unless you explicitly need publisher-only/viewer-only/all-channel behavior.
33
+
34
+ ## Delivery Semantics
35
+
36
+ - WebRTC data channels are typically configured for reliable, ordered delivery in SDK defaults.
37
+ - Reliability applies after channel establishment; it does not guarantee every network can establish the channel.
38
+ - TURN improves establishment success in restrictive NAT/firewall environments but does not guarantee universal reachability.
39
+ - This MCP skill layers app-level reliability for file transfer:
40
+ - chunk SHA-256 validation
41
+ - ACK/NACK feedback
42
+ - resume from `next_seq` after interruption
43
+
44
+ ## Firewall/NAT Guidance
45
+
46
+ 1. Prefer TURN when needed.
47
+ - `forceTURN: true` can push connections through relay infrastructure.
48
+ - This helps restrictive networks but changes latency/cost characteristics.
49
+
50
+ 2. Treat fallback as last resort.
51
+ - `allowFallback: true` allows `sendData` fallback through signaling when P2P is unavailable.
52
+ - Use for control/survivability, not sustained throughput.
53
+
54
+ 3. Expect hard failure zones.
55
+ - Some enterprise/WSL/network setups can still block WebRTC UDP.
56
+ - Build retry and reconnection logic regardless of TURN.
57
+
58
+ ## Keep-Alive and Session Continuity
59
+
60
+ 1. Keep a heartbeat.
61
+ - Send lightweight periodic app-level heartbeats.
62
+ - Optionally send SDK pings (`sendPing`) to connected peers.
63
+
64
+ 2. Reconnect predictably.
65
+ - On disconnect/error, recreate SDK instance, reconnect, re-join room, re-announce, and re-view targets.
66
+ - Make handlers idempotent to tolerate duplicate or delayed messages around reconnection.
67
+
68
+ 3. Define canonical IDs.
69
+ - Room and stream IDs should use alphanumeric + underscore.
70
+ - Rely on sanitized IDs consistently across all peers.
71
+
72
+ ## Bridge Runtime Knobs
73
+
74
+ `MCP/scripts/vdo-data-bridge.js` supports:
75
+
76
+ - `VDON_ROOM` (required in real deployments)
77
+ - `VDON_STREAM_ID` (optional; random if omitted)
78
+ - `VDON_TARGET_STREAM_ID` (optional; auto-view peer stream for two-peer links)
79
+ - `VDON_HOST` (default `wss://wss.vdo.ninja`)
80
+ - `VDON_PASSWORD` (`false` to disable encryption; omitted uses SDK default)
81
+ - `VDON_FORCE_TURN` (`true`/`false`)
82
+ - `VDON_HEARTBEAT_MS` (default `15000`)
83
+ - `VDON_RECONNECT_MS` (default `3000`)
84
+ - `VDON_MAX_RECONNECT_MS` (default `30000`)
85
+ - `VDON_DEBUG` (`true`/`false`)
86
+
87
+ ## App Envelope Suggestions
88
+
89
+ Use explicit envelope types and IDs over raw strings:
90
+
91
+ ```json
92
+ {
93
+ "type": "mcp.request",
94
+ "id": "req_123",
95
+ "method": "tool.call",
96
+ "payload": { "name": "lookup", "args": { "q": "abc" } }
97
+ }
98
+ ```
99
+
100
+ ```json
101
+ {
102
+ "type": "mcp.response",
103
+ "id": "req_123",
104
+ "ok": true,
105
+ "payload": { "result": "..." }
106
+ }
107
+ ```
108
+
109
+ Avoid reserved SDK pub/sub types (`subscribe`, `unsubscribe`, `channelMessage`) for custom envelopes.
110
+
111
+ ## File Transfer Envelope Pattern Used Here
112
+
113
+ This MCP implementation reserves protocol envelopes with:
114
+
115
+ - `__vdo_mcp: \"vdo_mcp_bridge_v1\"`
116
+ - `kind: \"sync.*\" | \"file.*\"`
117
+
118
+ File transfer handshake:
119
+
120
+ 1. `file.offer`
121
+ 2. `file.accept`
122
+ 3. `file.chunk` + `file.ack` / `file.nack`
123
+ 4. `file.complete`
124
+ 5. `file.complete_ack`
125
+
126
+ Resume flow:
127
+
128
+ 1. `file.resume_req`
129
+ 2. `file.resume_state` (`next_seq`)
130
+
131
+ ## Disk Spooling And Large Files
132
+
133
+ - This MCP implementation can spool large incoming transfers to disk.
134
+ - Tune via `vdo_connect`:
135
+ - `file_spool_dir`
136
+ - `file_spool_threshold_bytes`
137
+ - `keep_spool_files`
138
+ - Use `vdo_file_send_path` to avoid base64 memory overhead on large local files.
139
+ - Use `vdo_file_save` to persist completed incoming transfers directly.
140
+
141
+ ## Room State Sync (CRDT-lite)
142
+
143
+ - Shared state is replicated using `state.patch` and `state.snapshot`.
144
+ - Conflict resolution is tuple-based LWW:
145
+ - higher `clock` wins
146
+ - tie breaks by `actor` lexical order
147
+ - MCP surface:
148
+ - `vdo_state_set`
149
+ - `vdo_state_get`
150
+ - `vdo_state_sync`
151
+
152
+ ## Local Source Pointers
153
+
154
+ - `README.md`
155
+ - `AI-INTEGRATION.md`
156
+ - `docs/api-reference.md`
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { VdoMcpServer } = require('./vdo-mcp-server');
5
+ const { createFakeSDKFactory } = require('./lib/fake-network-sdk');
6
+
7
+ function boolFromValue(value, fallback) {
8
+ if (value === undefined || value === null) return fallback;
9
+ if (typeof value === 'boolean') return value;
10
+ const text = String(value).trim().toLowerCase();
11
+ if (['1', 'true', 'yes', 'y', 'on'].includes(text)) return true;
12
+ if (['0', 'false', 'no', 'n', 'off'].includes(text)) return false;
13
+ return fallback;
14
+ }
15
+
16
+ async function waitUntil(predicate, timeoutMs, stepMs) {
17
+ const start = Date.now();
18
+ while (Date.now() - start < timeoutMs) {
19
+ // eslint-disable-next-line no-await-in-loop
20
+ if (await predicate()) return true;
21
+ // eslint-disable-next-line no-await-in-loop
22
+ await new Promise((resolve) => setTimeout(resolve, stepMs));
23
+ }
24
+ return false;
25
+ }
26
+
27
+ async function waitForIncomingTransfer(server, sessionId, timeoutMs) {
28
+ const start = Date.now();
29
+ while (Date.now() - start < timeoutMs) {
30
+ // eslint-disable-next-line no-await-in-loop
31
+ const transfers = await server.callTool('vdo_file_transfers', {
32
+ session_id: sessionId,
33
+ direction: 'incoming'
34
+ });
35
+ const incoming = Array.isArray(transfers.incoming) ? transfers.incoming : [];
36
+ const completed = incoming.find((transfer) => transfer && transfer.status === 'completed');
37
+ if (completed) return completed;
38
+ // eslint-disable-next-line no-await-in-loop
39
+ await new Promise((resolve) => setTimeout(resolve, 150));
40
+ }
41
+ return null;
42
+ }
43
+
44
+ async function main() {
45
+ const useFake = boolFromValue(process.env.MCP_DEMO_FAKE, true);
46
+ const forceTurn = boolFromValue(process.env.MCP_DEMO_FORCE_TURN, false);
47
+ const host = process.env.MCP_DEMO_HOST || process.env.LIVE_WSS_HOST || 'wss://wss.vdo.ninja';
48
+ const room = process.env.MCP_DEMO_ROOM || `mcp_demo_file_${Date.now()}`;
49
+ const fileName = process.env.MCP_DEMO_FILE_NAME || 'demo-note.txt';
50
+ const fileText = process.env.MCP_DEMO_FILE_TEXT || 'hello from MCP file transfer demo';
51
+ const connectTimeoutMs = Number.parseInt(process.env.MCP_DEMO_CONNECT_TIMEOUT_MS || '', 10) || 15000;
52
+ const transferTimeoutMs = Number.parseInt(process.env.MCP_DEMO_TRANSFER_TIMEOUT_MS || '', 10) || 12000;
53
+
54
+ const baseOptions = {
55
+ logger: () => {}
56
+ };
57
+ if (useFake) {
58
+ baseOptions.sdkFactory = createFakeSDKFactory();
59
+ }
60
+
61
+ const serverA = new VdoMcpServer(baseOptions);
62
+ const serverB = new VdoMcpServer(baseOptions);
63
+ let sessionA;
64
+ let sessionB;
65
+ const startedAt = new Date().toISOString();
66
+
67
+ try {
68
+ console.log(`[demo-file] mode=${useFake ? 'fake' : 'live'} host=${host} force_turn=${forceTurn}`);
69
+ console.log(`[demo-file] room=${room}`);
70
+
71
+ const connectA = await serverA.callTool('vdo_connect', {
72
+ host,
73
+ room,
74
+ stream_id: 'demo_file_a',
75
+ force_turn: forceTurn
76
+ });
77
+ sessionA = connectA.session_id;
78
+
79
+ const connectB = await serverB.callTool('vdo_connect', {
80
+ host,
81
+ room,
82
+ stream_id: 'demo_file_b',
83
+ target_stream_id: 'demo_file_a',
84
+ force_turn: forceTurn
85
+ });
86
+ sessionB = connectB.session_id;
87
+
88
+ const connected = await waitUntil(async () => {
89
+ const [statusA, statusB] = await Promise.all([
90
+ serverA.callTool('vdo_status', { session_id: sessionA }),
91
+ serverB.callTool('vdo_status', { session_id: sessionB })
92
+ ]);
93
+ return statusA.connected && statusB.connected;
94
+ }, connectTimeoutMs, 200);
95
+ if (!connected) {
96
+ throw new Error('demo-file connect timeout');
97
+ }
98
+
99
+ const payload = Buffer.from(fileText, 'utf8').toString('base64');
100
+ const sent = await serverA.callTool('vdo_file_send', {
101
+ session_id: sessionA,
102
+ data_base64: payload,
103
+ name: fileName,
104
+ mime: 'text/plain'
105
+ });
106
+ if (!sent.ok) {
107
+ throw new Error('demo-file send returned ok=false');
108
+ }
109
+
110
+ const completed = await waitForIncomingTransfer(serverB, sessionB, transferTimeoutMs);
111
+ if (!completed) {
112
+ throw new Error('demo-file receive timeout');
113
+ }
114
+
115
+ const received = await serverB.callTool('vdo_file_receive', {
116
+ session_id: sessionB,
117
+ transfer_id: completed.transfer_id,
118
+ encoding: 'utf8'
119
+ });
120
+ if (received.data_text !== fileText) {
121
+ throw new Error('demo-file content mismatch');
122
+ }
123
+
124
+ const summary = {
125
+ ok: true,
126
+ started_at: startedAt,
127
+ finished_at: new Date().toISOString(),
128
+ mode: useFake ? 'fake' : 'live',
129
+ room,
130
+ force_turn: forceTurn,
131
+ host,
132
+ session_a: sessionA,
133
+ session_b: sessionB,
134
+ transfer_id: completed.transfer_id,
135
+ file_name: fileName,
136
+ bytes: Buffer.byteLength(fileText, 'utf8')
137
+ };
138
+
139
+ console.log(JSON.stringify(summary, null, 2));
140
+ console.log('demo-file-workflow.js passed');
141
+ } finally {
142
+ try {
143
+ if (sessionA) await serverA.callTool('vdo_disconnect', { session_id: sessionA });
144
+ } catch (error) {
145
+ // Ignore cleanup failures.
146
+ }
147
+ try {
148
+ if (sessionB) await serverB.callTool('vdo_disconnect', { session_id: sessionB });
149
+ } catch (error) {
150
+ // Ignore cleanup failures.
151
+ }
152
+ try {
153
+ await serverA.shutdownAll();
154
+ } catch (error) {
155
+ // Ignore cleanup failures.
156
+ }
157
+ try {
158
+ await serverB.shutdownAll();
159
+ } catch (error) {
160
+ // Ignore cleanup failures.
161
+ }
162
+ }
163
+ }
164
+
165
+ main().catch((error) => {
166
+ console.error(error.message);
167
+ process.exit(1);
168
+ });
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { VdoMcpServer } = require('./vdo-mcp-server');
5
+ const { createFakeSDKFactory } = require('./lib/fake-network-sdk');
6
+
7
+ function boolFromValue(value, fallback) {
8
+ if (value === undefined || value === null) return fallback;
9
+ if (typeof value === 'boolean') return value;
10
+ const text = String(value).trim().toLowerCase();
11
+ if (['1', 'true', 'yes', 'y', 'on'].includes(text)) return true;
12
+ if (['0', 'false', 'no', 'n', 'off'].includes(text)) return false;
13
+ return fallback;
14
+ }
15
+
16
+ async function waitUntil(predicate, timeoutMs, stepMs) {
17
+ const start = Date.now();
18
+ while (Date.now() - start < timeoutMs) {
19
+ // eslint-disable-next-line no-await-in-loop
20
+ if (await predicate()) return true;
21
+ // eslint-disable-next-line no-await-in-loop
22
+ await new Promise((resolve) => setTimeout(resolve, stepMs));
23
+ }
24
+ return false;
25
+ }
26
+
27
+ async function waitForMessage(server, sessionId, id, timeoutMs) {
28
+ const start = Date.now();
29
+ while (Date.now() - start < timeoutMs) {
30
+ // eslint-disable-next-line no-await-in-loop
31
+ const received = await server.callTool('vdo_receive', {
32
+ session_id: sessionId,
33
+ max_events: 100,
34
+ wait_ms: 250
35
+ });
36
+ const events = Array.isArray(received.events) ? received.events : [];
37
+ const match = events.find((event) => (
38
+ event &&
39
+ event.type === 'data_received' &&
40
+ event.data &&
41
+ event.data.type === 'demo.message' &&
42
+ event.data.id === id
43
+ ));
44
+ if (match) return match;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ async function main() {
50
+ const useFake = boolFromValue(process.env.MCP_DEMO_FAKE, true);
51
+ const forceTurn = boolFromValue(process.env.MCP_DEMO_FORCE_TURN, false);
52
+ const host = process.env.MCP_DEMO_HOST || process.env.LIVE_WSS_HOST || 'wss://wss.vdo.ninja';
53
+ const room = process.env.MCP_DEMO_ROOM || `mcp_demo_message_${Date.now()}`;
54
+ const message = process.env.MCP_DEMO_MESSAGE || 'hello from MCP message demo';
55
+ const connectTimeoutMs = Number.parseInt(process.env.MCP_DEMO_CONNECT_TIMEOUT_MS || '', 10) || 15000;
56
+ const messageTimeoutMs = Number.parseInt(process.env.MCP_DEMO_MESSAGE_TIMEOUT_MS || '', 10) || 8000;
57
+
58
+ const baseOptions = {
59
+ logger: () => {}
60
+ };
61
+ if (useFake) {
62
+ baseOptions.sdkFactory = createFakeSDKFactory();
63
+ }
64
+
65
+ const serverA = new VdoMcpServer(baseOptions);
66
+ const serverB = new VdoMcpServer(baseOptions);
67
+ let sessionA;
68
+ let sessionB;
69
+ const startedAt = new Date().toISOString();
70
+
71
+ try {
72
+ console.log(`[demo-message] mode=${useFake ? 'fake' : 'live'} host=${host} force_turn=${forceTurn}`);
73
+ console.log(`[demo-message] room=${room}`);
74
+
75
+ const connectA = await serverA.callTool('vdo_connect', {
76
+ host,
77
+ room,
78
+ stream_id: 'demo_agent_a',
79
+ force_turn: forceTurn
80
+ });
81
+ sessionA = connectA.session_id;
82
+
83
+ const connectB = await serverB.callTool('vdo_connect', {
84
+ host,
85
+ room,
86
+ stream_id: 'demo_agent_b',
87
+ target_stream_id: 'demo_agent_a',
88
+ force_turn: forceTurn
89
+ });
90
+ sessionB = connectB.session_id;
91
+
92
+ const connected = await waitUntil(async () => {
93
+ const [statusA, statusB] = await Promise.all([
94
+ serverA.callTool('vdo_status', { session_id: sessionA }),
95
+ serverB.callTool('vdo_status', { session_id: sessionB })
96
+ ]);
97
+ return statusA.connected && statusB.connected;
98
+ }, connectTimeoutMs, 200);
99
+ if (!connected) {
100
+ throw new Error('demo-message connect timeout');
101
+ }
102
+
103
+ const id = `demo_msg_${Date.now()}`;
104
+ const payload = {
105
+ type: 'demo.message',
106
+ id,
107
+ text: message,
108
+ sent_at: Date.now()
109
+ };
110
+
111
+ const send = await serverA.callTool('vdo_send', {
112
+ session_id: sessionA,
113
+ data: payload
114
+ });
115
+ if (!send.ok) {
116
+ throw new Error('demo-message send returned ok=false');
117
+ }
118
+
119
+ const received = await waitForMessage(serverB, sessionB, id, messageTimeoutMs);
120
+ if (!received) {
121
+ throw new Error('demo-message receive timeout');
122
+ }
123
+
124
+ const summary = {
125
+ ok: true,
126
+ started_at: startedAt,
127
+ finished_at: new Date().toISOString(),
128
+ mode: useFake ? 'fake' : 'live',
129
+ room,
130
+ force_turn: forceTurn,
131
+ host,
132
+ session_a: sessionA,
133
+ session_b: sessionB,
134
+ sent_message: payload.text,
135
+ received_message: received.data.text
136
+ };
137
+
138
+ console.log(JSON.stringify(summary, null, 2));
139
+ console.log('demo-message-workflow.js passed');
140
+ } finally {
141
+ try {
142
+ if (sessionA) await serverA.callTool('vdo_disconnect', { session_id: sessionA });
143
+ } catch (error) {
144
+ // Ignore cleanup failures.
145
+ }
146
+ try {
147
+ if (sessionB) await serverB.callTool('vdo_disconnect', { session_id: sessionB });
148
+ } catch (error) {
149
+ // Ignore cleanup failures.
150
+ }
151
+ try {
152
+ await serverA.shutdownAll();
153
+ } catch (error) {
154
+ // Ignore cleanup failures.
155
+ }
156
+ try {
157
+ await serverB.shutdownAll();
158
+ } catch (error) {
159
+ // Ignore cleanup failures.
160
+ }
161
+ }
162
+ }
163
+
164
+ main().catch((error) => {
165
+ console.error(error.message);
166
+ process.exit(1);
167
+ });