agent-bridge-mcp 1.0.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.
Files changed (3) hide show
  1. package/README.md +138 -0
  2. package/package.json +36 -0
  3. package/server.mjs +542 -0
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # agent-bridge-mcp
2
+
3
+ MCP server that enables multiple Claude Code sessions to discover each other and exchange messages through a shared filesystem message bus.
4
+
5
+ Open multiple Claude Code tabs in the same project. Each tab can register a name, see other active tabs, and send/receive messages — no external services required.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g agent-bridge-mcp
11
+ ```
12
+
13
+ Or clone and install locally:
14
+
15
+ ```bash
16
+ git clone https://github.com/Lloydm15/agent-bridge-mcp.git
17
+ cd agent-bridge-mcp
18
+ npm install
19
+ ```
20
+
21
+ ## Setup
22
+
23
+ Add to your Claude Code MCP config:
24
+
25
+ **Global** (`~/.claude/.mcp.json`) — available in all projects:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "agent-bridge": {
31
+ "type": "stdio",
32
+ "command": "npx",
33
+ "args": ["agent-bridge-mcp"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ **Or local** (`.mcp.json` in project root) — available in one project:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "agent-bridge": {
45
+ "command": "npx",
46
+ "args": ["agent-bridge-mcp"]
47
+ }
48
+ }
49
+ }
50
+ ```
51
+
52
+ Then reload your editor window.
53
+
54
+ ## Tools
55
+
56
+ | Tool | Description |
57
+ |------|-------------|
58
+ | `register_agent` | Name yourself (e.g., "frontend-dev", "reviewer") |
59
+ | `list_agents` | See all active agents across all tabs |
60
+ | `send_message` | Message a specific agent by name or ID |
61
+ | `read_messages` | Check for unread messages |
62
+ | `broadcast` | Message all active agents |
63
+
64
+ ## How It Works
65
+
66
+ Each Claude Code tab spawns its own MCP server process. Since they can't share memory, all communication goes through the filesystem:
67
+
68
+ - **Agents** register by writing a JSON file to a shared `agents/` directory
69
+ - **Messages** are individual JSON files in a `messages/` directory (atomic creation, no corruption)
70
+ - **Heartbeat** every 15s keeps agent registrations fresh
71
+ - **Dead agents** are auto-cleaned via PID liveness checks and heartbeat staleness
72
+ - **Messages expire** after 1 hour (configurable)
73
+
74
+ ```
75
+ Data directory:
76
+ agents/
77
+ agent-a3f1c9.json # { id, name, project, pid, lastHeartbeat }
78
+ messages/
79
+ 1740524430000-x7k2f9.json # { from, to, content, timestamp }
80
+ ```
81
+
82
+ ## Example Usage
83
+
84
+ **Tab 1:**
85
+ > Register as "frontend-dev"
86
+
87
+ **Tab 2:**
88
+ > Register as "api-dev"
89
+
90
+ **Tab 1:**
91
+ > Send a message to api-dev: "I changed the auth response type, update your endpoint handlers"
92
+
93
+ **Tab 2:**
94
+ > Check messages
95
+
96
+ > Got message from frontend-dev: "I changed the auth response type, update your endpoint handlers"
97
+
98
+ ## Configuration
99
+
100
+ All settings are optional. Defaults work out of the box.
101
+
102
+ | Environment Variable | Default | Description |
103
+ |---------------------|---------|-------------|
104
+ | `AGENT_BRIDGE_DATA_DIR` | Platform-specific (see below) | Directory for agent and message files |
105
+ | `AGENT_BRIDGE_HEARTBEAT_MS` | `15000` | Heartbeat interval in milliseconds |
106
+ | `AGENT_BRIDGE_DEAD_MS` | `300000` | Time before stale agents are removed |
107
+ | `AGENT_BRIDGE_MESSAGE_TTL_MS` | `3600000` | Time before old messages are deleted |
108
+
109
+ **Default data directories:**
110
+ - **Windows:** `%LOCALAPPDATA%\agent-bridge`
111
+ - **macOS:** `~/Library/Application Support/agent-bridge`
112
+ - **Linux:** `$XDG_DATA_HOME/agent-bridge` (or `~/.local/share/agent-bridge`)
113
+
114
+ Pass env vars through your MCP config:
115
+
116
+ ```json
117
+ {
118
+ "mcpServers": {
119
+ "agent-bridge": {
120
+ "type": "stdio",
121
+ "command": "npx",
122
+ "args": ["agent-bridge-mcp"],
123
+ "env": {
124
+ "AGENT_BRIDGE_MESSAGE_TTL_MS": "7200000"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ## Requirements
132
+
133
+ - Node.js >= 18
134
+ - Any MCP-compatible client (Claude Code, etc.)
135
+
136
+ ## License
137
+
138
+ MIT
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "agent-bridge-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server enabling communication between multiple Claude Code sessions via shared filesystem message bus",
5
+ "type": "module",
6
+ "main": "server.mjs",
7
+ "bin": {
8
+ "agent-bridge-mcp": "server.mjs"
9
+ },
10
+ "keywords": [
11
+ "mcp",
12
+ "model-context-protocol",
13
+ "claude",
14
+ "claude-code",
15
+ "agent",
16
+ "multi-agent",
17
+ "communication",
18
+ "bridge"
19
+ ],
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/Lloydm15/agent-bridge-mcp.git"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "files": [
29
+ "server.mjs",
30
+ "README.md",
31
+ "LICENSE"
32
+ ],
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.0"
35
+ }
36
+ }
package/server.mjs ADDED
@@ -0,0 +1,542 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema
8
+ } from '@modelcontextprotocol/sdk/types.js';
9
+ import {
10
+ existsSync, mkdirSync, writeFileSync, readFileSync,
11
+ readdirSync, unlinkSync, renameSync
12
+ } from 'fs';
13
+ import { resolve, dirname } from 'path';
14
+ import { randomBytes } from 'crypto';
15
+ import { homedir, tmpdir, platform } from 'os';
16
+
17
+ // ============================================================
18
+ // Constants (configurable via environment variables)
19
+ // ============================================================
20
+
21
+ function getDefaultDataDir() {
22
+ // XDG_DATA_HOME on Linux, ~/Library/Application Support on macOS, LOCALAPPDATA on Windows
23
+ const p = platform();
24
+ if (p === 'win32') {
25
+ return resolve(process.env.LOCALAPPDATA || resolve(homedir(), 'AppData', 'Local'), 'agent-bridge');
26
+ }
27
+ if (p === 'darwin') {
28
+ return resolve(homedir(), 'Library', 'Application Support', 'agent-bridge');
29
+ }
30
+ // Linux / other — XDG spec
31
+ return resolve(process.env.XDG_DATA_HOME || resolve(homedir(), '.local', 'share'), 'agent-bridge');
32
+ }
33
+
34
+ const BRIDGE_DIR = process.env.AGENT_BRIDGE_DATA_DIR || getDefaultDataDir();
35
+ const AGENTS_DIR = resolve(BRIDGE_DIR, 'agents');
36
+ const MESSAGES_DIR = resolve(BRIDGE_DIR, 'messages');
37
+ const HEARTBEAT_INTERVAL_MS = parseInt(process.env.AGENT_BRIDGE_HEARTBEAT_MS || '15000', 10);
38
+ const STALE_THRESHOLD_MS = HEARTBEAT_INTERVAL_MS * 3;
39
+ const DEAD_THRESHOLD_MS = parseInt(process.env.AGENT_BRIDGE_DEAD_MS || '300000', 10);
40
+ const MESSAGE_TTL_MS = parseInt(process.env.AGENT_BRIDGE_MESSAGE_TTL_MS || '3600000', 10);
41
+
42
+ // ============================================================
43
+ // Agent State (in-memory, per-process)
44
+ // ============================================================
45
+
46
+ const agentId = `agent-${randomBytes(3).toString('hex')}`;
47
+ let agentName = null;
48
+ const startedAt = new Date().toISOString();
49
+ let lastReadTimestamp = Date.now(); // only read messages after we start
50
+ let heartbeatTimer = null;
51
+
52
+ // ============================================================
53
+ // Directory Setup
54
+ // ============================================================
55
+
56
+ function ensureDirs() {
57
+ mkdirSync(AGENTS_DIR, { recursive: true });
58
+ mkdirSync(MESSAGES_DIR, { recursive: true });
59
+ }
60
+
61
+ // ============================================================
62
+ // Process Liveness Check
63
+ // ============================================================
64
+
65
+ function isProcessAlive(pid) {
66
+ try {
67
+ process.kill(pid, 0);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ // ============================================================
75
+ // Agent File Operations
76
+ // ============================================================
77
+
78
+ function getAgentData() {
79
+ return {
80
+ id: agentId,
81
+ name: agentName,
82
+ project: process.cwd().split(/[\\/]/).pop(),
83
+ cwd: process.cwd(),
84
+ pid: process.pid,
85
+ startedAt,
86
+ lastHeartbeat: new Date().toISOString()
87
+ };
88
+ }
89
+
90
+ function writeAgentFile() {
91
+ const data = JSON.stringify(getAgentData(), null, 2);
92
+ const targetPath = resolve(AGENTS_DIR, `${agentId}.json`);
93
+ const tmpPath = targetPath + '.tmp';
94
+ try {
95
+ writeFileSync(tmpPath, data);
96
+ renameSync(tmpPath, targetPath);
97
+ } catch (err) {
98
+ // Fallback: direct write if rename fails
99
+ try { writeFileSync(targetPath, data); } catch {}
100
+ }
101
+ }
102
+
103
+ function removeAgentFile() {
104
+ try { unlinkSync(resolve(AGENTS_DIR, `${agentId}.json`)); } catch {}
105
+ try { unlinkSync(resolve(AGENTS_DIR, `${agentId}.json.tmp`)); } catch {}
106
+ }
107
+
108
+ function readAgentFile(filePath) {
109
+ try {
110
+ return JSON.parse(readFileSync(filePath, 'utf8'));
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ // ============================================================
117
+ // Get All Agents (with staleness detection + cleanup)
118
+ // ============================================================
119
+
120
+ function getAllAgents() {
121
+ const agents = [];
122
+ let files;
123
+ try {
124
+ files = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
125
+ } catch {
126
+ return agents;
127
+ }
128
+
129
+ const now = Date.now();
130
+
131
+ for (const file of files) {
132
+ const filePath = resolve(AGENTS_DIR, file);
133
+ const agent = readAgentFile(filePath);
134
+ if (!agent) continue;
135
+
136
+ const heartbeatAge = now - new Date(agent.lastHeartbeat).getTime();
137
+ const pidAlive = isProcessAlive(agent.pid);
138
+
139
+ // Dead process — clean up immediately
140
+ if (!pidAlive && agent.id !== agentId) {
141
+ try { unlinkSync(filePath); } catch {}
142
+ continue;
143
+ }
144
+
145
+ // Very stale — clean up
146
+ if (heartbeatAge > DEAD_THRESHOLD_MS && agent.id !== agentId) {
147
+ try { unlinkSync(filePath); } catch {}
148
+ continue;
149
+ }
150
+
151
+ agents.push({
152
+ ...agent,
153
+ status: heartbeatAge > STALE_THRESHOLD_MS ? 'stale' : 'active',
154
+ isMe: agent.id === agentId
155
+ });
156
+ }
157
+
158
+ return agents;
159
+ }
160
+
161
+ // ============================================================
162
+ // Cleanup
163
+ // ============================================================
164
+
165
+ function cleanOldMessages() {
166
+ let files;
167
+ try {
168
+ files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json'));
169
+ } catch {
170
+ return;
171
+ }
172
+
173
+ const cutoff = Date.now() - MESSAGE_TTL_MS;
174
+
175
+ for (const file of files) {
176
+ const timestamp = parseInt(file.split('-')[0], 10);
177
+ if (!isNaN(timestamp) && timestamp < cutoff) {
178
+ try { unlinkSync(resolve(MESSAGES_DIR, file)); } catch {}
179
+ }
180
+ }
181
+ }
182
+
183
+ // ============================================================
184
+ // Heartbeat
185
+ // ============================================================
186
+
187
+ function startHeartbeat() {
188
+ heartbeatTimer = setInterval(() => {
189
+ writeAgentFile();
190
+ cleanOldMessages();
191
+ }, HEARTBEAT_INTERVAL_MS);
192
+ heartbeatTimer.unref();
193
+ }
194
+
195
+ // ============================================================
196
+ // Message Operations
197
+ // ============================================================
198
+
199
+ function writeMessage(to, toName, content, isBroadcast) {
200
+ const now = Date.now();
201
+ const rand = randomBytes(3).toString('hex');
202
+ const msgId = `msg-${now}-${rand}`;
203
+ const filename = `${now}-${rand}.json`;
204
+
205
+ const message = {
206
+ id: msgId,
207
+ from: agentId,
208
+ fromName: agentName,
209
+ to: isBroadcast ? '*' : to,
210
+ toName: isBroadcast ? null : toName,
211
+ broadcast: isBroadcast,
212
+ content,
213
+ project: process.cwd().split(/[\\/]/).pop(),
214
+ timestamp: new Date(now).toISOString()
215
+ };
216
+
217
+ const filePath = resolve(MESSAGES_DIR, filename);
218
+ writeFileSync(filePath, JSON.stringify(message, null, 2), { flag: 'wx' });
219
+ return message;
220
+ }
221
+
222
+ function readNewMessages(includeBroadcasts = true) {
223
+ let files;
224
+ try {
225
+ files = readdirSync(MESSAGES_DIR).filter(f => f.endsWith('.json')).sort();
226
+ } catch {
227
+ return [];
228
+ }
229
+
230
+ const messages = [];
231
+
232
+ for (const file of files) {
233
+ const timestamp = parseInt(file.split('-')[0], 10);
234
+ if (isNaN(timestamp) || timestamp <= lastReadTimestamp) continue;
235
+
236
+ const filePath = resolve(MESSAGES_DIR, file);
237
+ let msg;
238
+ try {
239
+ msg = JSON.parse(readFileSync(filePath, 'utf8'));
240
+ } catch {
241
+ continue;
242
+ }
243
+
244
+ // Skip own messages
245
+ if (msg.from === agentId) continue;
246
+
247
+ // Check if addressed to us or broadcast
248
+ const isForMe = msg.to === agentId || msg.toName === agentName;
249
+ const isBroadcast = msg.broadcast && msg.to === '*';
250
+
251
+ if (isForMe || (isBroadcast && includeBroadcasts)) {
252
+ messages.push({
253
+ ...msg,
254
+ ageSeconds: Math.round((Date.now() - new Date(msg.timestamp).getTime()) / 1000)
255
+ });
256
+ }
257
+ }
258
+
259
+ // Update read cursor
260
+ if (files.length > 0) {
261
+ const lastFile = files[files.length - 1];
262
+ const lastTs = parseInt(lastFile.split('-')[0], 10);
263
+ if (!isNaN(lastTs) && lastTs > lastReadTimestamp) {
264
+ lastReadTimestamp = lastTs;
265
+ }
266
+ }
267
+
268
+ return messages;
269
+ }
270
+
271
+ // ============================================================
272
+ // Resolve Agent Target (name or ID)
273
+ // ============================================================
274
+
275
+ function resolveAgent(nameOrId) {
276
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
277
+
278
+ // Exact ID match
279
+ const byId = agents.find(a => a.id === nameOrId);
280
+ if (byId) return byId;
281
+
282
+ // Exact name match (case-insensitive)
283
+ const byName = agents.find(a => a.name && a.name.toLowerCase() === nameOrId.toLowerCase());
284
+ if (byName) return byName;
285
+
286
+ // Partial name match
287
+ const partial = agents.filter(a => a.name && a.name.toLowerCase().includes(nameOrId.toLowerCase()));
288
+ if (partial.length === 1) return partial[0];
289
+
290
+ return null;
291
+ }
292
+
293
+ // ============================================================
294
+ // Tool Handlers
295
+ // ============================================================
296
+
297
+ async function handleRegister(args) {
298
+ const name = args.name?.trim();
299
+ if (!name) return { error: 'Name is required' };
300
+
301
+ // Check uniqueness
302
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
303
+ const conflict = agents.find(a => a.name && a.name.toLowerCase() === name.toLowerCase());
304
+ if (conflict) {
305
+ return {
306
+ error: `Name "${name}" is already taken by ${conflict.id} (project: ${conflict.project})`,
307
+ suggestion: `Try "${name}-2" or a different name`
308
+ };
309
+ }
310
+
311
+ agentName = name;
312
+ writeAgentFile();
313
+
314
+ return {
315
+ registered: true,
316
+ id: agentId,
317
+ name: agentName,
318
+ project: process.cwd().split(/[\\/]/).pop(),
319
+ cwd: process.cwd(),
320
+ pid: process.pid
321
+ };
322
+ }
323
+
324
+ async function handleListAgents() {
325
+ const agents = getAllAgents();
326
+ return {
327
+ agents,
328
+ totalActive: agents.filter(a => a.status === 'active').length,
329
+ myId: agentId,
330
+ myName: agentName
331
+ };
332
+ }
333
+
334
+ async function handleSendMessage(args) {
335
+ const { to, content } = args;
336
+ if (!to) return { error: 'Target agent (to) is required' };
337
+ if (!content) return { error: 'Message content is required' };
338
+
339
+ const target = resolveAgent(to);
340
+ if (!target) {
341
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
342
+ return {
343
+ error: `No active agent found matching "${to}"`,
344
+ availableAgents: agents.map(a => ({ id: a.id, name: a.name, project: a.project }))
345
+ };
346
+ }
347
+
348
+ const msg = writeMessage(target.id, target.name, content, false);
349
+ return {
350
+ sent: true,
351
+ messageId: msg.id,
352
+ to: { id: target.id, name: target.name },
353
+ timestamp: msg.timestamp
354
+ };
355
+ }
356
+
357
+ async function handleReadMessages(args) {
358
+ const includeBroadcasts = args.include_broadcasts !== false;
359
+ const messages = readNewMessages(includeBroadcasts);
360
+ return {
361
+ messages,
362
+ count: messages.length,
363
+ myId: agentId,
364
+ myName: agentName
365
+ };
366
+ }
367
+
368
+ async function handleBroadcast(args) {
369
+ const { content } = args;
370
+ if (!content) return { error: 'Message content is required' };
371
+
372
+ const agents = getAllAgents().filter(a => !a.isMe && a.status === 'active');
373
+ const msg = writeMessage('*', null, content, true);
374
+
375
+ return {
376
+ broadcast: true,
377
+ messageId: msg.id,
378
+ activeRecipients: agents.length,
379
+ timestamp: msg.timestamp
380
+ };
381
+ }
382
+
383
+ // ============================================================
384
+ // Tool Definitions
385
+ // ============================================================
386
+
387
+ const TOOLS = [
388
+ {
389
+ name: 'register_agent',
390
+ description: 'Register this agent with a human-readable name so other agents can find and message you. Call this at the start of a session.',
391
+ inputSchema: {
392
+ type: 'object',
393
+ properties: {
394
+ name: {
395
+ type: 'string',
396
+ description: 'A short human-readable name for this agent (e.g., "frontend-dev", "reviewer", "planner")'
397
+ }
398
+ },
399
+ required: ['name']
400
+ }
401
+ },
402
+ {
403
+ name: 'list_agents',
404
+ description: 'List all active agents across all tabs. Shows agent ID, name, project, and status.',
405
+ inputSchema: {
406
+ type: 'object',
407
+ properties: {}
408
+ }
409
+ },
410
+ {
411
+ name: 'send_message',
412
+ description: 'Send a message to a specific agent by name or ID. Use list_agents first to see who is available.',
413
+ inputSchema: {
414
+ type: 'object',
415
+ properties: {
416
+ to: {
417
+ type: 'string',
418
+ description: 'The name or ID of the target agent'
419
+ },
420
+ content: {
421
+ type: 'string',
422
+ description: 'The message content to send'
423
+ }
424
+ },
425
+ required: ['to', 'content']
426
+ }
427
+ },
428
+ {
429
+ name: 'read_messages',
430
+ description: 'Check for new messages sent to you since your last read. Returns unread messages in chronological order.',
431
+ inputSchema: {
432
+ type: 'object',
433
+ properties: {
434
+ include_broadcasts: {
435
+ type: 'boolean',
436
+ description: 'Whether to include broadcast messages (default: true)'
437
+ }
438
+ }
439
+ }
440
+ },
441
+ {
442
+ name: 'broadcast',
443
+ description: 'Send a message to ALL active agents. Use sparingly — prefer direct messages when you know the recipient.',
444
+ inputSchema: {
445
+ type: 'object',
446
+ properties: {
447
+ content: {
448
+ type: 'string',
449
+ description: 'The message content to broadcast to all agents'
450
+ }
451
+ },
452
+ required: ['content']
453
+ }
454
+ }
455
+ ];
456
+
457
+ // ============================================================
458
+ // Tool Dispatcher
459
+ // ============================================================
460
+
461
+ async function handleToolCall(name, args) {
462
+ switch (name) {
463
+ case 'register_agent': return handleRegister(args);
464
+ case 'list_agents': return handleListAgents();
465
+ case 'send_message': return handleSendMessage(args);
466
+ case 'read_messages': return handleReadMessages(args);
467
+ case 'broadcast': return handleBroadcast(args);
468
+ default: return { error: `Unknown tool: ${name}` };
469
+ }
470
+ }
471
+
472
+ // ============================================================
473
+ // MCP Server
474
+ // ============================================================
475
+
476
+ const server = new Server(
477
+ { name: 'agent-bridge', version: '1.0.0' },
478
+ { capabilities: { tools: {} } }
479
+ );
480
+
481
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
482
+ tools: TOOLS
483
+ }));
484
+
485
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
486
+ const { name, arguments: args } = request.params;
487
+
488
+ try {
489
+ const result = await handleToolCall(name, args || {});
490
+ return {
491
+ content: [{
492
+ type: 'text',
493
+ text: JSON.stringify(result, null, 2)
494
+ }]
495
+ };
496
+ } catch (err) {
497
+ return {
498
+ content: [{
499
+ type: 'text',
500
+ text: JSON.stringify({
501
+ error: `Tool execution failed: ${err.message}`,
502
+ tool: name,
503
+ args
504
+ }, null, 2)
505
+ }],
506
+ isError: true
507
+ };
508
+ }
509
+ });
510
+
511
+ // ============================================================
512
+ // Main
513
+ // ============================================================
514
+
515
+ async function main() {
516
+ ensureDirs();
517
+ cleanOldMessages();
518
+ writeAgentFile();
519
+ startHeartbeat();
520
+
521
+ // Cleanup on exit
522
+ const cleanup = () => {
523
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
524
+ removeAgentFile();
525
+ };
526
+ process.on('exit', cleanup);
527
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
528
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
529
+
530
+ // Connect stdio transport
531
+ const transport = new StdioServerTransport();
532
+ await server.connect(transport);
533
+
534
+ console.error(`[AgentBridge] Server running`);
535
+ console.error(`[AgentBridge] Agent ID: ${agentId}`);
536
+ console.error(`[AgentBridge] Data dir: ${BRIDGE_DIR}`);
537
+ }
538
+
539
+ main().catch(err => {
540
+ console.error(`[AgentBridge] Fatal: ${err.message}`);
541
+ process.exit(1);
542
+ });