@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,122 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_TOOL_PROFILE = 'full';
4
+
5
+ const TOOL_GROUPS = Object.freeze({
6
+ core: Object.freeze([
7
+ 'vdo_connect',
8
+ 'vdo_send',
9
+ 'vdo_receive',
10
+ 'vdo_status',
11
+ 'vdo_disconnect',
12
+ 'vdo_list_sessions',
13
+ 'vdo_capabilities',
14
+ 'vdo_sync_peers',
15
+ 'vdo_sync_announce'
16
+ ]),
17
+ file: Object.freeze([
18
+ 'vdo_file_send',
19
+ 'vdo_file_send_path',
20
+ 'vdo_file_resume',
21
+ 'vdo_file_transfers',
22
+ 'vdo_file_receive',
23
+ 'vdo_file_save'
24
+ ]),
25
+ state: Object.freeze([
26
+ 'vdo_state_set',
27
+ 'vdo_state_get',
28
+ 'vdo_state_sync'
29
+ ])
30
+ });
31
+
32
+ const TOOL_PROFILES = Object.freeze({
33
+ core: Object.freeze(['core']),
34
+ file: Object.freeze(['core', 'file']),
35
+ state: Object.freeze(['core', 'state']),
36
+ full: Object.freeze(['core', 'file', 'state'])
37
+ });
38
+
39
+ const PROFILE_NAMES = Object.freeze(Object.keys(TOOL_PROFILES));
40
+
41
+ function normalizeProfileName(value) {
42
+ if (value === undefined || value === null) return null;
43
+ const normalized = String(value).trim().toLowerCase();
44
+ return normalized || null;
45
+ }
46
+
47
+ function resolveToolProfile(value) {
48
+ const requested = normalizeProfileName(value);
49
+ const isKnown = requested && Object.prototype.hasOwnProperty.call(TOOL_PROFILES, requested);
50
+ const name = isKnown ? requested : DEFAULT_TOOL_PROFILE;
51
+ const groups = TOOL_PROFILES[name];
52
+ const toolNames = [];
53
+ const seen = new Set();
54
+
55
+ for (const groupName of groups) {
56
+ const group = TOOL_GROUPS[groupName] || [];
57
+ for (const toolName of group) {
58
+ if (!seen.has(toolName)) {
59
+ seen.add(toolName);
60
+ toolNames.push(toolName);
61
+ }
62
+ }
63
+ }
64
+
65
+ return {
66
+ requested,
67
+ name,
68
+ groups: Array.from(groups),
69
+ toolNames,
70
+ usedFallback: Boolean(requested && !isKnown)
71
+ };
72
+ }
73
+
74
+ function listAllProfileToolNames() {
75
+ const names = [];
76
+ const seen = new Set();
77
+ for (const group of Object.values(TOOL_GROUPS)) {
78
+ for (const name of group) {
79
+ if (!seen.has(name)) {
80
+ seen.add(name);
81
+ names.push(name);
82
+ }
83
+ }
84
+ }
85
+ return names;
86
+ }
87
+
88
+ function selectToolsForProfile(tools, profileName) {
89
+ const profile = resolveToolProfile(profileName);
90
+ const byName = new Map();
91
+ for (const tool of tools || []) {
92
+ if (!tool || typeof tool !== 'object') continue;
93
+ if (typeof tool.name !== 'string' || !tool.name) continue;
94
+ byName.set(tool.name, tool);
95
+ }
96
+
97
+ const missing = profile.toolNames.filter((name) => !byName.has(name));
98
+ if (missing.length > 0) {
99
+ throw new Error(`tool profile '${profile.name}' references unknown tools: ${missing.join(', ')}`);
100
+ }
101
+
102
+ const selectedTools = [];
103
+ for (const toolName of profile.toolNames) {
104
+ selectedTools.push(byName.get(toolName));
105
+ }
106
+
107
+ return {
108
+ profile,
109
+ tools: selectedTools,
110
+ allowedToolNames: new Set(selectedTools.map((tool) => tool.name))
111
+ };
112
+ }
113
+
114
+ module.exports = {
115
+ DEFAULT_TOOL_PROFILE,
116
+ TOOL_GROUPS,
117
+ TOOL_PROFILES,
118
+ PROFILE_NAMES,
119
+ resolveToolProfile,
120
+ selectToolsForProfile,
121
+ listAllProfileToolNames
122
+ };
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const readline = require('node:readline');
5
+ const { BridgeSession } = require('./lib/bridge-session');
6
+ const {
7
+ boolFromEnv,
8
+ intFromEnv,
9
+ parsePassword,
10
+ sanitizeId,
11
+ generateShortId,
12
+ nowISO
13
+ } = require('./lib/bridge-utils');
14
+
15
+ function emitStdout(message) {
16
+ process.stdout.write(`${JSON.stringify(message)}\n`);
17
+ }
18
+
19
+ function emitStderr(message) {
20
+ process.stderr.write(`${JSON.stringify(message)}\n`);
21
+ }
22
+
23
+ function shouldWriteToStderr(eventType) {
24
+ return [
25
+ 'connect_error',
26
+ 'sdk_error',
27
+ 'connection_failed',
28
+ 'disconnected',
29
+ 'reconnect_scheduled',
30
+ 'send_error'
31
+ ].includes(eventType);
32
+ }
33
+
34
+ function loadConfig() {
35
+ const room = sanitizeId(process.env.VDON_ROOM, generateShortId('mcp_room'));
36
+ const streamID = sanitizeId(process.env.VDON_STREAM_ID, generateShortId('mcp_bridge'));
37
+ const targetStreamID = process.env.VDON_TARGET_STREAM_ID
38
+ ? sanitizeId(process.env.VDON_TARGET_STREAM_ID, null)
39
+ : null;
40
+
41
+ return {
42
+ host: process.env.VDON_HOST || 'wss://wss.vdo.ninja',
43
+ room,
44
+ streamID,
45
+ targetStreamID,
46
+ password: parsePassword(process.env.VDON_PASSWORD),
47
+ label: process.env.VDON_LABEL || 'mcp_bridge',
48
+ forceTURN: boolFromEnv(process.env.VDON_FORCE_TURN, false),
49
+ debug: boolFromEnv(process.env.VDON_DEBUG, false),
50
+ reconnectDelayMs: intFromEnv(process.env.VDON_RECONNECT_MS, 3000),
51
+ maxReconnectDelayMs: intFromEnv(process.env.VDON_MAX_RECONNECT_MS, 30000),
52
+ heartbeatMs: intFromEnv(process.env.VDON_HEARTBEAT_MS, 15000)
53
+ };
54
+ }
55
+
56
+ function normalizeTarget(parsed) {
57
+ if (!parsed || typeof parsed !== 'object') return null;
58
+ if (typeof parsed.target === 'string') return parsed.target;
59
+ if (parsed.target && typeof parsed.target === 'object') return parsed.target;
60
+ if (typeof parsed.uuid === 'string') return parsed.uuid;
61
+ return null;
62
+ }
63
+
64
+ async function main() {
65
+ const config = loadConfig();
66
+ const session = new BridgeSession(config);
67
+
68
+ emitStderr({
69
+ type: 'bridge_starting',
70
+ ts: nowISO(),
71
+ config: {
72
+ host: config.host,
73
+ room: config.room,
74
+ streamID: config.streamID,
75
+ targetStreamID: config.targetStreamID,
76
+ forceTURN: config.forceTURN,
77
+ heartbeatMs: config.heartbeatMs,
78
+ reconnectDelayMs: config.reconnectDelayMs,
79
+ maxReconnectDelayMs: config.maxReconnectDelayMs
80
+ }
81
+ });
82
+
83
+ session.on('event', (event) => {
84
+ if (shouldWriteToStderr(event.type)) {
85
+ emitStderr(event);
86
+ return;
87
+ }
88
+ emitStdout(event);
89
+ });
90
+
91
+ const rl = readline.createInterface({
92
+ input: process.stdin,
93
+ terminal: false,
94
+ crlfDelay: Infinity
95
+ });
96
+
97
+ rl.on('line', async (line) => {
98
+ if (!line || !line.trim()) return;
99
+
100
+ let parsed;
101
+ try {
102
+ parsed = JSON.parse(line);
103
+ } catch (error) {
104
+ parsed = { data: line };
105
+ }
106
+
107
+ if (parsed && typeof parsed === 'object' && parsed.op === 'status') {
108
+ emitStdout({
109
+ type: 'status',
110
+ ts: nowISO(),
111
+ ...session.getStatus()
112
+ });
113
+ return;
114
+ }
115
+
116
+ if (parsed && typeof parsed === 'object' && parsed.op === 'heartbeat') {
117
+ const ok = session.sendHeartbeat(true);
118
+ emitStdout({ type: 'heartbeat_sent', ts: nowISO(), ok });
119
+ return;
120
+ }
121
+
122
+ const payload = parsed && Object.prototype.hasOwnProperty.call(parsed, 'data') ? parsed.data : parsed;
123
+ const target = normalizeTarget(parsed);
124
+ const ok = session.send(payload, target);
125
+ emitStdout({
126
+ type: 'send_result',
127
+ ts: nowISO(),
128
+ ok,
129
+ usedTarget: target || null
130
+ });
131
+ });
132
+
133
+ rl.on('close', () => {
134
+ emitStderr({ type: 'stdin_closed', ts: nowISO() });
135
+ });
136
+
137
+ process.on('SIGINT', async () => {
138
+ await session.stop();
139
+ process.exit(0);
140
+ });
141
+
142
+ process.on('SIGTERM', async () => {
143
+ await session.stop();
144
+ process.exit(0);
145
+ });
146
+
147
+ await session.start();
148
+ }
149
+
150
+ main().catch((error) => {
151
+ emitStderr({
152
+ type: 'fatal',
153
+ ts: nowISO(),
154
+ message: error.message
155
+ });
156
+ process.exit(1);
157
+ });