xmux-bridge 1.0.39

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,486 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * xmux-lead-mcp-server.js
4
+ * Stdio-only MCP server exposing XMux lead/team mailbox tools.
5
+ *
6
+ * Mailbox persistence is delegated to the Node mailbox CLI.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const { spawnSync } = require('child_process');
15
+
16
+ const SERVER_NAME = 'xmux-lead';
17
+ const SERVER_VERSION = '0.1.0';
18
+ const XMUX_INSTALL_DIR = process.env.XMUX_INSTALL_DIR
19
+ ? path.resolve(process.env.XMUX_INSTALL_DIR)
20
+ : __dirname;
21
+ const MAILBOX_BACKEND = resolveMailboxBackend();
22
+
23
+ const INTERNAL_ERRORS = new Set([
24
+ 'mailbox_cli_missing',
25
+ 'mailbox_cli_spawn_failed',
26
+ 'mailbox_cli_empty_output',
27
+ 'mailbox_cli_invalid_json',
28
+ 'mailbox_cli_failed',
29
+ ]);
30
+
31
+ const TOOL_SCHEMAS = [
32
+ {
33
+ name: 'send_to_teammate',
34
+ description: 'Send a request from the Codex lead to an XMux teammate.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ team: { type: 'string', description: 'XMux team name.' },
39
+ to: { type: 'string', description: 'Teammate name or id.' },
40
+ message: { type: 'string', description: 'Message body for the teammate.' },
41
+ from: { type: 'string', description: 'Sender name. Defaults to codex-lead.' },
42
+ request_id: { type: 'string', description: 'Optional caller-provided request id.' },
43
+ },
44
+ required: ['team', 'to', 'message'],
45
+ },
46
+ },
47
+ {
48
+ name: 'wait_teammate_response',
49
+ description: 'Wait for a teammate response to a request id.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ team: { type: 'string', description: 'XMux team name.' },
54
+ request_id: { type: 'string', description: 'Request id to wait for.' },
55
+ timeout_sec: { type: 'number', description: 'Optional wait timeout in seconds.' },
56
+ interval_sec: { type: 'number', description: 'Optional polling interval in seconds.' },
57
+ mark_read: { type: 'boolean', description: 'Mark the response as read when returned.' },
58
+ },
59
+ required: ['team', 'request_id'],
60
+ },
61
+ },
62
+ {
63
+ name: 'read_teammate_response',
64
+ description: 'Read a teammate response for a request id if one is available.',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ team: { type: 'string', description: 'XMux team name.' },
69
+ request_id: { type: 'string', description: 'Request id to read.' },
70
+ mark_read: { type: 'boolean', description: 'Mark the response as read when returned.' },
71
+ },
72
+ required: ['team', 'request_id'],
73
+ },
74
+ },
75
+ {
76
+ name: 'list_teammate_events',
77
+ description: 'List lightweight teammate events or requests when the mailbox CLI supports it.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ team: { type: 'string', description: 'XMux team name.' },
82
+ status: { type: 'string', description: 'Optional status filter.' },
83
+ },
84
+ required: ['team'],
85
+ },
86
+ },
87
+ {
88
+ name: 'team_status',
89
+ description: 'Return mailbox/team status for an XMux team.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ team: { type: 'string', description: 'XMux team name.' },
94
+ },
95
+ required: ['team'],
96
+ },
97
+ },
98
+ ];
99
+
100
+ function requiredArgs(args, names) {
101
+ const missing = [];
102
+ for (const name of names) {
103
+ if (args[name] === undefined || args[name] === null || String(args[name]).length === 0) {
104
+ missing.push(name);
105
+ }
106
+ }
107
+ if (missing.length === 0) return null;
108
+ return { ok: false, error: 'invalid_arguments', missing };
109
+ }
110
+
111
+ function addFlag(argv, flag, value) {
112
+ if (value === undefined || value === null || value === '') return;
113
+ argv.push(flag, String(value));
114
+ }
115
+
116
+ function addBoolFlag(argv, flag, value) {
117
+ if (value === true || value === 'true' || value === 1) argv.push(flag);
118
+ }
119
+
120
+ function parseJsonOutput(stdout) {
121
+ const trimmed = String(stdout || '').trim();
122
+ if (!trimmed) return { ok: false, error: 'mailbox_cli_empty_output' };
123
+
124
+ try {
125
+ return { ok: true, value: JSON.parse(trimmed) };
126
+ } catch (_) {
127
+ const lines = trimmed.split(/\r?\n/).map(s => s.trim()).filter(Boolean).reverse();
128
+ for (const line of lines) {
129
+ try {
130
+ return { ok: true, value: JSON.parse(line) };
131
+ } catch (_) {}
132
+ }
133
+ }
134
+
135
+ return { ok: false, error: 'mailbox_cli_invalid_json' };
136
+ }
137
+
138
+ function mailboxInstallBases() {
139
+ const seen = new Set();
140
+ const bases = [];
141
+ for (const candidate of [XMUX_INSTALL_DIR, __dirname]) {
142
+ if (!candidate) continue;
143
+ const resolved = path.resolve(candidate);
144
+ if (seen.has(resolved)) continue;
145
+ seen.add(resolved);
146
+ bases.push(resolved);
147
+ }
148
+ return bases;
149
+ }
150
+
151
+ function mailboxCandidates() {
152
+ const candidates = [];
153
+ for (const base of mailboxInstallBases()) {
154
+ candidates.push({
155
+ kind: 'node',
156
+ command: process.execPath || 'node',
157
+ prefixArgs: [path.join(base, 'dist', 'bin', 'xmux-mailbox.js')],
158
+ });
159
+ }
160
+ return candidates;
161
+ }
162
+
163
+ function resolveMailboxBackend() {
164
+ for (const candidate of mailboxCandidates()) {
165
+ if (fs.existsSync(candidate.prefixArgs[0])) return candidate;
166
+ }
167
+ return null;
168
+ }
169
+
170
+ function safeTeamName(value) {
171
+ if (value === undefined || value === null) return '';
172
+ const text = String(value).trim();
173
+ if (!text || text === '.' || text === '..') return '';
174
+ if (text.includes('/') || text.includes('\\')) return '';
175
+ return text;
176
+ }
177
+
178
+ function activeTeamRegistryFile(team) {
179
+ if (process.env.XMUX_ACTIVE_TEAM_REGISTRY_DIR) {
180
+ return path.join(path.resolve(process.env.XMUX_ACTIVE_TEAM_REGISTRY_DIR), `${team}.json`);
181
+ }
182
+ const home = process.env.HOME || os.homedir();
183
+ if (!home) return '';
184
+ return path.join(home, '.codex', 'xmux', 'active-teams', `${team}.json`);
185
+ }
186
+
187
+ function readJsonFile(file) {
188
+ try {
189
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
190
+ } catch (_) {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ function validateRegistryState(team, registry) {
196
+ if (!registry || typeof registry !== 'object') return null;
197
+ if (registry.team !== team || registry.status !== 'active') return null;
198
+ if (typeof registry.state_dir !== 'string' || registry.state_dir.length === 0) return null;
199
+
200
+ const stateDir = path.resolve(registry.state_dir);
201
+ const teamJsonPath = path.join(stateDir, 'teams', team, 'team.json');
202
+ const teamJson = readJsonFile(teamJsonPath);
203
+ if (!teamJson || typeof teamJson !== 'object') return null;
204
+ if (teamJson.name && teamJson.name !== team) return null;
205
+
206
+ const env = { XMUX_STATE_DIR: stateDir };
207
+ if (typeof registry.project_dir === 'string' && registry.project_dir.length > 0) {
208
+ env.XMUX_PROJECT_DIR = path.resolve(registry.project_dir);
209
+ }
210
+ return {
211
+ env,
212
+ project_dir: env.XMUX_PROJECT_DIR || '',
213
+ state_dir: env.XMUX_STATE_DIR,
214
+ registry_team_dir: typeof registry.team_dir === 'string' ? path.resolve(registry.team_dir) : '',
215
+ };
216
+ }
217
+
218
+ function resolveMailboxEnv(argv) {
219
+ const team = safeTeamName(argv && argv[0]);
220
+ const context = {
221
+ env: {},
222
+ source: 'default',
223
+ team,
224
+ registry_file: '',
225
+ project_dir: process.env.XMUX_PROJECT_DIR ? path.resolve(process.env.XMUX_PROJECT_DIR) : '',
226
+ state_dir: process.env.XMUX_STATE_DIR ? path.resolve(process.env.XMUX_STATE_DIR) : '',
227
+ install_dir: XMUX_INSTALL_DIR,
228
+ mailbox_cli: MAILBOX_BACKEND ? MAILBOX_BACKEND.prefixArgs[0] : '',
229
+ };
230
+
231
+ if (process.env.XMUX_STATE_DIR) {
232
+ context.source = 'process-env';
233
+ return context;
234
+ }
235
+
236
+ if (!team) return context;
237
+
238
+ const file = activeTeamRegistryFile(team);
239
+ context.registry_file = file;
240
+ if (!file) return context;
241
+
242
+ const registry = readJsonFile(file);
243
+ const resolved = validateRegistryState(team, registry);
244
+ if (!resolved) return context;
245
+ return {
246
+ ...context,
247
+ ...resolved,
248
+ source: 'active-registry',
249
+ };
250
+ }
251
+
252
+ function mailboxContextPayload(context) {
253
+ const payload = {
254
+ install_dir: XMUX_INSTALL_DIR,
255
+ mailbox_cli: MAILBOX_BACKEND ? MAILBOX_BACKEND.prefixArgs[0] : '',
256
+ resolution_source: context && context.source ? context.source : 'default',
257
+ };
258
+ if (context && context.team) payload.team = context.team;
259
+ if (context && context.registry_file) payload.registry_file = context.registry_file;
260
+ if (context && context.project_dir) payload.resolved_project_dir = context.project_dir;
261
+ if (context && context.state_dir) payload.resolved_state_dir = context.state_dir;
262
+ return payload;
263
+ }
264
+
265
+ function runMailbox(subcommand, argv) {
266
+ const resolved = resolveMailboxEnv(argv);
267
+ const context = mailboxContextPayload(resolved);
268
+ if (!MAILBOX_BACKEND) {
269
+ return {
270
+ ok: false,
271
+ error: 'mailbox_cli_missing',
272
+ message: 'dist/bin/xmux-mailbox.js is not available yet',
273
+ candidates: mailboxCandidates().map((candidate) => candidate.prefixArgs[0]),
274
+ command: subcommand,
275
+ ...context,
276
+ };
277
+ }
278
+
279
+ const args = [...MAILBOX_BACKEND.prefixArgs, subcommand, ...argv];
280
+ const result = spawnSync(MAILBOX_BACKEND.command, args, {
281
+ env: { ...process.env, ...resolved.env },
282
+ encoding: 'utf8',
283
+ maxBuffer: 1024 * 1024,
284
+ });
285
+
286
+ if (result.error) {
287
+ return {
288
+ ok: false,
289
+ error: 'mailbox_cli_spawn_failed',
290
+ message: String(result.error.message || result.error),
291
+ command: subcommand,
292
+ ...context,
293
+ };
294
+ }
295
+
296
+ const parsed = parseJsonOutput(result.stdout);
297
+ if (parsed.ok) return parsed.value;
298
+
299
+ const payload = {
300
+ ok: false,
301
+ error: result.status === 0 ? parsed.error : 'mailbox_cli_failed',
302
+ command: subcommand,
303
+ exit_code: result.status,
304
+ ...context,
305
+ };
306
+ if (parsed.error && payload.error !== parsed.error) payload.parse_error = parsed.error;
307
+ if (String(result.stderr || '').trim()) payload.stderr = String(result.stderr).trim();
308
+ if (String(result.stdout || '').trim()) payload.stdout = String(result.stdout).trim();
309
+ return payload;
310
+ }
311
+
312
+ function notImplemented(command, detail) {
313
+ const payload = {
314
+ ok: false,
315
+ error: 'not_implemented',
316
+ status: 'not_implemented',
317
+ command,
318
+ message: `mailbox CLI does not expose ${command} JSON output yet`,
319
+ };
320
+ if (detail) payload.detail = detail;
321
+ return payload;
322
+ }
323
+
324
+ function looksUnsupported(result) {
325
+ if (!result || typeof result !== 'object') return false;
326
+ if (INTERNAL_ERRORS.has(result.error)) return true;
327
+ const text = `${result.error || ''} ${result.message || ''} ${result.stderr || ''}`;
328
+ return /unknown|invalid choice|not implemented|unsupported/i.test(text);
329
+ }
330
+
331
+ function sendToTeammate(args) {
332
+ const invalid = requiredArgs(args, ['team', 'to', 'message']);
333
+ if (invalid) return invalid;
334
+
335
+ const argv = [];
336
+ argv.push(String(args.team), String(args.to));
337
+ addFlag(argv, '--message', args.message);
338
+ addFlag(argv, '--from', args.from || 'codex-lead');
339
+ addFlag(argv, '--request-id', args.request_id);
340
+ return runMailbox('enqueue-request', argv);
341
+ }
342
+
343
+ function waitTeammateResponse(args) {
344
+ const invalid = requiredArgs(args, ['team', 'request_id']);
345
+ if (invalid) return invalid;
346
+
347
+ const argv = [];
348
+ argv.push(String(args.team), String(args.request_id));
349
+ addFlag(argv, '--timeout', args.timeout_sec);
350
+ addFlag(argv, '--interval', args.interval_sec);
351
+ addBoolFlag(argv, '--mark-read', args.mark_read);
352
+ return runMailbox('wait-response', argv);
353
+ }
354
+
355
+ function readTeammateResponse(args) {
356
+ const invalid = requiredArgs(args, ['team', 'request_id']);
357
+ if (invalid) return invalid;
358
+
359
+ const argv = [];
360
+ argv.push(String(args.team), String(args.request_id));
361
+ addBoolFlag(argv, '--mark-read', args.mark_read);
362
+ return runMailbox('read-response', argv);
363
+ }
364
+
365
+ function listTeammateEvents(args) {
366
+ const invalid = requiredArgs(args, ['team']);
367
+ if (invalid) return invalid;
368
+
369
+ const argv = [];
370
+ argv.push(String(args.team));
371
+ addFlag(argv, '--status', args.status);
372
+
373
+ const listEvents = runMailbox('list-events', argv);
374
+ if (!looksUnsupported(listEvents)) return listEvents;
375
+
376
+ const listRequests = runMailbox('list-requests', argv);
377
+ if (!looksUnsupported(listRequests)) return listRequests;
378
+
379
+ return notImplemented('list-events', { attempts: [listEvents, listRequests] });
380
+ }
381
+
382
+ function teamStatus(args) {
383
+ const invalid = requiredArgs(args, ['team']);
384
+ if (invalid) return invalid;
385
+
386
+ const argv = [];
387
+ argv.push(String(args.team));
388
+ return runMailbox('team-status', argv);
389
+ }
390
+
391
+ function toolResult(id, payload) {
392
+ return {
393
+ jsonrpc: '2.0',
394
+ id,
395
+ result: {
396
+ content: [{ type: 'text', text: JSON.stringify(payload) }],
397
+ },
398
+ };
399
+ }
400
+
401
+ function buildResponse(msg) {
402
+ const method = msg.method || '';
403
+ const id = msg.id !== undefined ? msg.id : null;
404
+
405
+ if (method === 'initialize') {
406
+ const params = msg.params || {};
407
+ return {
408
+ jsonrpc: '2.0',
409
+ id,
410
+ result: {
411
+ protocolVersion: params.protocolVersion || '2024-11-05',
412
+ capabilities: { tools: {} },
413
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
414
+ },
415
+ };
416
+ }
417
+
418
+ if (method === 'notifications/initialized' || method === 'initialized') {
419
+ return null;
420
+ }
421
+
422
+ if (method === 'tools/list') {
423
+ return { jsonrpc: '2.0', id, result: { tools: TOOL_SCHEMAS } };
424
+ }
425
+
426
+ if (method === 'resources/list') {
427
+ return { jsonrpc: '2.0', id, result: { resources: [] } };
428
+ }
429
+
430
+ if (method === 'resources/templates/list') {
431
+ return { jsonrpc: '2.0', id, result: { resourceTemplates: [] } };
432
+ }
433
+
434
+ if (method === 'tools/call') {
435
+ const params = msg.params || {};
436
+ const args = params.arguments || {};
437
+
438
+ if (params.name === 'send_to_teammate') return toolResult(id, sendToTeammate(args));
439
+ if (params.name === 'wait_teammate_response') return toolResult(id, waitTeammateResponse(args));
440
+ if (params.name === 'read_teammate_response') return toolResult(id, readTeammateResponse(args));
441
+ if (params.name === 'list_teammate_events') return toolResult(id, listTeammateEvents(args));
442
+ if (params.name === 'team_status') return toolResult(id, teamStatus(args));
443
+
444
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Unknown tool' } };
445
+ }
446
+
447
+ if (id !== null) {
448
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${method}` } };
449
+ }
450
+
451
+ return null;
452
+ }
453
+
454
+ function startStdio() {
455
+ const readline = require('readline');
456
+ const messageQueue = [];
457
+ let ready = false;
458
+
459
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
460
+ rl.on('line', (line) => {
461
+ if (!line.trim()) return;
462
+ let msg;
463
+ try {
464
+ msg = JSON.parse(line);
465
+ } catch (_) {
466
+ return;
467
+ }
468
+
469
+ if (ready) {
470
+ const resp = buildResponse(msg);
471
+ if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
472
+ } else {
473
+ messageQueue.push(msg);
474
+ }
475
+ });
476
+ rl.on('close', () => process.exit(0));
477
+
478
+ ready = true;
479
+ for (const msg of messageQueue) {
480
+ const resp = buildResponse(msg);
481
+ if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
482
+ }
483
+ messageQueue.length = 0;
484
+ }
485
+
486
+ startStdio();