@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.
- package/SKILL.md +90 -0
- package/assets/cloudflare/worker-proxy.js +89 -0
- package/assets/cloudflare/wrangler.toml.example +10 -0
- package/package.json +66 -0
- package/references/advanced-diagnostics.md +45 -0
- package/references/mcp-tool-contract.md +184 -0
- package/references/quickstart-and-compat.md +198 -0
- package/references/vdo-ninja-p2p-data-notes.md +156 -0
- package/scripts/demo-file-workflow.js +168 -0
- package/scripts/demo-message-workflow.js +167 -0
- package/scripts/install-mcp.js +302 -0
- package/scripts/lib/bridge-session.js +2331 -0
- package/scripts/lib/bridge-utils.js +185 -0
- package/scripts/lib/fake-network-sdk.js +293 -0
- package/scripts/lib/load-vdo-sdk.js +36 -0
- package/scripts/lib/mcp-stdio-parser.js +137 -0
- package/scripts/lib/tool-profiles.js +122 -0
- package/scripts/vdo-data-bridge.js +157 -0
- package/scripts/vdo-mcp-server.js +1425 -0
- package/scripts/vdo-mcp-streamable-http.js +270 -0
- package/server.json +134 -0
|
@@ -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
|
+
});
|