agent-bridge-mcp 1.0.0 → 1.2.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 +77 -7
  2. package/package.json +1 -1
  3. package/server.mjs +282 -25
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # agent-bridge-mcp
2
2
 
3
- MCP server that enables multiple Claude Code sessions to discover each other and exchange messages through a shared filesystem message bus.
3
+ MCP server that enables multiple Claude Code sessions to discover each other, exchange messages, and share working context through a shared filesystem bus.
4
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.
5
+ Open multiple Claude Code tabs across different projects. Each tab can register a name, see other active tabs, send/receive messages, and share accumulated knowledge — no external services required. Context persists after sessions end.
6
6
 
7
7
  ## Install
8
8
 
@@ -36,30 +36,37 @@ Add to your Claude Code MCP config:
36
36
  }
37
37
  ```
38
38
 
39
- **Or local** (`.mcp.json` in project root) — available in one project:
39
+ **Per-project** (`.mcp.json` in project root) — with auto-registration:
40
40
 
41
41
  ```json
42
42
  {
43
43
  "mcpServers": {
44
44
  "agent-bridge": {
45
45
  "command": "npx",
46
- "args": ["agent-bridge-mcp"]
46
+ "args": ["agent-bridge-mcp"],
47
+ "env": {
48
+ "AGENT_BRIDGE_NAME": "my-agent-name"
49
+ }
47
50
  }
48
51
  }
49
52
  }
50
53
  ```
51
54
 
55
+ Setting `AGENT_BRIDGE_NAME` automatically registers the agent on startup — no need to call `register_agent` manually.
56
+
52
57
  Then reload your editor window.
53
58
 
54
59
  ## Tools
55
60
 
56
61
  | Tool | Description |
57
62
  |------|-------------|
58
- | `register_agent` | Name yourself (e.g., "frontend-dev", "reviewer") |
63
+ | `register_agent` | Name yourself (e.g., "frontend-dev", "reviewer"). Not needed if `AGENT_BRIDGE_NAME` is set. |
59
64
  | `list_agents` | See all active agents across all tabs |
60
65
  | `send_message` | Message a specific agent by name or ID |
61
66
  | `read_messages` | Check for unread messages |
62
67
  | `broadcast` | Message all active agents |
68
+ | `share_context` | Dump your accumulated working knowledge so other agents can read it. Persists after session ends. |
69
+ | `get_context` | Read shared context from another agent by name, or get all shared contexts. Works even if that agent is dead. |
63
70
 
64
71
  ## How It Works
65
72
 
@@ -67,9 +74,11 @@ Each Claude Code tab spawns its own MCP server process. Since they can't share m
67
74
 
68
75
  - **Agents** register by writing a JSON file to a shared `agents/` directory
69
76
  - **Messages** are individual JSON files in a `messages/` directory (atomic creation, no corruption)
77
+ - **Context** is stored per-agent in a `context/` directory, keyed by agent name (not session ID), so knowledge survives session restarts
70
78
  - **Heartbeat** every 15s keeps agent registrations fresh
71
79
  - **Dead agents** are auto-cleaned via PID liveness checks and heartbeat staleness
72
80
  - **Messages expire** after 1 hour (configurable)
81
+ - **Context persists** indefinitely until overwritten by the same agent name
73
82
 
74
83
  ```
75
84
  Data directory:
@@ -77,10 +86,14 @@ Data directory:
77
86
  agent-a3f1c9.json # { id, name, project, pid, lastHeartbeat }
78
87
  messages/
79
88
  1740524430000-x7k2f9.json # { from, to, content, timestamp }
89
+ context/
90
+ nova-main.json # { agentName, agentId, project, updatedAt, content }
80
91
  ```
81
92
 
82
93
  ## Example Usage
83
94
 
95
+ ### Messaging
96
+
84
97
  **Tab 1:**
85
98
  > Register as "frontend-dev"
86
99
 
@@ -92,16 +105,28 @@ Data directory:
92
105
 
93
106
  **Tab 2:**
94
107
  > Check messages
95
-
96
108
  > Got message from frontend-dev: "I changed the auth response type, update your endpoint handlers"
97
109
 
110
+ ### Context Sharing
111
+
112
+ **Project A (nova-main):**
113
+ > Share context: "Working on auth flow. Modified auth.ts and login.tsx. Using JWT with refresh tokens. API endpoint is /api/auth/login."
114
+
115
+ **Project B (emergence-main):**
116
+ > Get context from nova-main
117
+
118
+ > Returns: "Working on auth flow. Modified auth.ts and login.tsx. Using JWT with refresh tokens. API endpoint is /api/auth/login."
119
+
120
+ Context persists even after nova-main's session ends — emergence-main can read it anytime.
121
+
98
122
  ## Configuration
99
123
 
100
124
  All settings are optional. Defaults work out of the box.
101
125
 
102
126
  | Environment Variable | Default | Description |
103
127
  |---------------------|---------|-------------|
104
- | `AGENT_BRIDGE_DATA_DIR` | Platform-specific (see below) | Directory for agent and message files |
128
+ | `AGENT_BRIDGE_NAME` | *(none)* | Auto-register with this name on startup |
129
+ | `AGENT_BRIDGE_DATA_DIR` | Platform-specific (see below) | Directory for agent, message, and context files |
105
130
  | `AGENT_BRIDGE_HEARTBEAT_MS` | `15000` | Heartbeat interval in milliseconds |
106
131
  | `AGENT_BRIDGE_DEAD_MS` | `300000` | Time before stale agents are removed |
107
132
  | `AGENT_BRIDGE_MESSAGE_TTL_MS` | `3600000` | Time before old messages are deleted |
@@ -121,6 +146,7 @@ Pass env vars through your MCP config:
121
146
  "command": "npx",
122
147
  "args": ["agent-bridge-mcp"],
123
148
  "env": {
149
+ "AGENT_BRIDGE_NAME": "my-agent",
124
150
  "AGENT_BRIDGE_MESSAGE_TTL_MS": "7200000"
125
151
  }
126
152
  }
@@ -128,6 +154,50 @@ Pass env vars through your MCP config:
128
154
  }
129
155
  ```
130
156
 
157
+ ## Automatic Context Sharing (Stop Hook)
158
+
159
+ The `--ingest` flag lets you auto-capture every conversation exchange into the context directory — no manual `share_context` calls needed.
160
+
161
+ **How it works:** Claude Code fires a [Stop hook](https://docs.anthropic.com/en/docs/claude-code/hooks) after every agent response. The hook pipes `session_id` and `last_assistant_message` via stdin. `agent-bridge-mcp --ingest` reads this, pairs it with the user prompt (saved by a `UserPromptSubmit` hook), strips system tags, and appends the exchange to the agent's context file.
162
+
163
+ **Setup:**
164
+
165
+ 1. Add a `UserPromptSubmit` hook to save user prompts (required for `--ingest` to capture both sides):
166
+
167
+ Add to `~/.claude/settings.json`:
168
+ ```json
169
+ {
170
+ "hooks": {
171
+ "UserPromptSubmit": [
172
+ {
173
+ "hooks": [
174
+ {
175
+ "type": "command",
176
+ "command": "npx agent-bridge-mcp --capture-prompt",
177
+ "timeout": 5
178
+ }
179
+ ]
180
+ }
181
+ ],
182
+ "Stop": [
183
+ {
184
+ "hooks": [
185
+ {
186
+ "type": "command",
187
+ "command": "npx agent-bridge-mcp --ingest",
188
+ "timeout": 10
189
+ }
190
+ ]
191
+ }
192
+ ]
193
+ }
194
+ }
195
+ ```
196
+
197
+ Set `AGENT_BRIDGE_NAME` in your project's `.mcp.json` so the ingest knows which agent name to write as.
198
+
199
+ Context is stored as the last 20 exchanges per agent, accessible via `get_context`.
200
+
131
201
  ## Requirements
132
202
 
133
203
  - Node.js >= 18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "MCP server enabling communication between multiple Claude Code sessions via shared filesystem message bus",
5
5
  "type": "module",
6
6
  "main": "server.mjs",
package/server.mjs CHANGED
@@ -34,6 +34,7 @@ function getDefaultDataDir() {
34
34
  const BRIDGE_DIR = process.env.AGENT_BRIDGE_DATA_DIR || getDefaultDataDir();
35
35
  const AGENTS_DIR = resolve(BRIDGE_DIR, 'agents');
36
36
  const MESSAGES_DIR = resolve(BRIDGE_DIR, 'messages');
37
+ const CONTEXT_DIR = resolve(BRIDGE_DIR, 'context');
37
38
  const HEARTBEAT_INTERVAL_MS = parseInt(process.env.AGENT_BRIDGE_HEARTBEAT_MS || '15000', 10);
38
39
  const STALE_THRESHOLD_MS = HEARTBEAT_INTERVAL_MS * 3;
39
40
  const DEAD_THRESHOLD_MS = parseInt(process.env.AGENT_BRIDGE_DEAD_MS || '300000', 10);
@@ -44,7 +45,7 @@ const MESSAGE_TTL_MS = parseInt(process.env.AGENT_BRIDGE_MESSAGE_TTL_MS || '3600
44
45
  // ============================================================
45
46
 
46
47
  const agentId = `agent-${randomBytes(3).toString('hex')}`;
47
- let agentName = null;
48
+ let agentName = process.env.AGENT_BRIDGE_NAME || null;
48
49
  const startedAt = new Date().toISOString();
49
50
  let lastReadTimestamp = Date.now(); // only read messages after we start
50
51
  let heartbeatTimer = null;
@@ -56,6 +57,7 @@ let heartbeatTimer = null;
56
57
  function ensureDirs() {
57
58
  mkdirSync(AGENTS_DIR, { recursive: true });
58
59
  mkdirSync(MESSAGES_DIR, { recursive: true });
60
+ mkdirSync(CONTEXT_DIR, { recursive: true });
59
61
  }
60
62
 
61
63
  // ============================================================
@@ -380,6 +382,87 @@ async function handleBroadcast(args) {
380
382
  };
381
383
  }
382
384
 
385
+ // ============================================================
386
+ // Context Operations
387
+ // ============================================================
388
+
389
+ function writeContextFile(name, content) {
390
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
391
+ const data = JSON.stringify({
392
+ agentName: name,
393
+ agentId,
394
+ project: process.cwd().split(/[\\/]/).pop(),
395
+ updatedAt: new Date().toISOString(),
396
+ content
397
+ }, null, 2);
398
+ const targetPath = resolve(CONTEXT_DIR, `${safeName}.json`);
399
+ const tmpPath = targetPath + '.tmp';
400
+ try {
401
+ writeFileSync(tmpPath, data);
402
+ renameSync(tmpPath, targetPath);
403
+ } catch (err) {
404
+ try { writeFileSync(targetPath, data); } catch {}
405
+ }
406
+ }
407
+
408
+ function readAllContextFiles() {
409
+ let files;
410
+ try {
411
+ files = readdirSync(CONTEXT_DIR).filter(f => f.endsWith('.json'));
412
+ } catch {
413
+ return [];
414
+ }
415
+ const contexts = [];
416
+ for (const file of files) {
417
+ try {
418
+ const ctx = JSON.parse(readFileSync(resolve(CONTEXT_DIR, file), 'utf8'));
419
+ contexts.push(ctx);
420
+ } catch {
421
+ continue;
422
+ }
423
+ }
424
+ return contexts;
425
+ }
426
+
427
+ async function handleShareContext(args) {
428
+ const { content } = args;
429
+ if (!content) return { error: 'Content is required — dump what you know' };
430
+ if (!agentName) return { error: 'Register with a name first (register_agent) before sharing context' };
431
+
432
+ writeContextFile(agentName, content);
433
+
434
+ return {
435
+ shared: true,
436
+ agentName,
437
+ project: process.cwd().split(/[\\/]/).pop(),
438
+ updatedAt: new Date().toISOString(),
439
+ contentLength: content.length
440
+ };
441
+ }
442
+
443
+ async function handleGetContext(args) {
444
+ const { from } = args;
445
+
446
+ if (from) {
447
+ // Get context from a specific agent
448
+ const safeName = from.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
449
+ const filePath = resolve(CONTEXT_DIR, `${safeName}.json`);
450
+ try {
451
+ const ctx = JSON.parse(readFileSync(filePath, 'utf8'));
452
+ return { found: true, context: ctx };
453
+ } catch {
454
+ return { found: false, error: `No shared context found for "${from}"` };
455
+ }
456
+ }
457
+
458
+ // Get all shared contexts
459
+ const contexts = readAllContextFiles();
460
+ return {
461
+ contexts,
462
+ count: contexts.length
463
+ };
464
+ }
465
+
383
466
  // ============================================================
384
467
  // Tool Definitions
385
468
  // ============================================================
@@ -451,6 +534,33 @@ const TOOLS = [
451
534
  },
452
535
  required: ['content']
453
536
  }
537
+ },
538
+ {
539
+ name: 'share_context',
540
+ description: 'Share your accumulated working knowledge so other agents (even in other projects) can read it. Write a comprehensive dump of everything you know — files read, decisions made, key facts, current state. Context persists after your session ends.',
541
+ inputSchema: {
542
+ type: 'object',
543
+ properties: {
544
+ content: {
545
+ type: 'string',
546
+ description: 'Freeform text dump of everything you know that would be useful to another agent picking up where you left off'
547
+ }
548
+ },
549
+ required: ['content']
550
+ }
551
+ },
552
+ {
553
+ name: 'get_context',
554
+ description: 'Read shared context from another agent. Works even if that agent is no longer active — context persists across sessions. Call with no arguments to see all available contexts.',
555
+ inputSchema: {
556
+ type: 'object',
557
+ properties: {
558
+ from: {
559
+ type: 'string',
560
+ description: 'Name of the agent whose context you want to read (e.g., "nova-main"). Omit to get all shared contexts.'
561
+ }
562
+ }
563
+ }
454
564
  }
455
565
  ];
456
566
 
@@ -465,6 +575,8 @@ async function handleToolCall(name, args) {
465
575
  case 'send_message': return handleSendMessage(args);
466
576
  case 'read_messages': return handleReadMessages(args);
467
577
  case 'broadcast': return handleBroadcast(args);
578
+ case 'share_context': return handleShareContext(args);
579
+ case 'get_context': return handleGetContext(args);
468
580
  default: return { error: `Unknown tool: ${name}` };
469
581
  }
470
582
  }
@@ -474,7 +586,7 @@ async function handleToolCall(name, args) {
474
586
  // ============================================================
475
587
 
476
588
  const server = new Server(
477
- { name: 'agent-bridge', version: '1.0.0' },
589
+ { name: 'agent-bridge', version: '1.2.0' },
478
590
  { capabilities: { tools: {} } }
479
591
  );
480
592
 
@@ -509,34 +621,179 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
509
621
  });
510
622
 
511
623
  // ============================================================
512
- // Main
624
+ // CLI: --capture-prompt (UserPromptSubmit hook mode)
513
625
  // ============================================================
626
+ // Saves user prompt to temp file for --ingest to pair with.
627
+ // Usage: node server.mjs --capture-prompt
628
+
629
+ async function runCapturePrompt() {
630
+ const chunks = [];
631
+ for await (const chunk of process.stdin) chunks.push(chunk);
632
+ const raw = Buffer.concat(chunks).toString('utf8');
633
+
634
+ let data;
635
+ try { data = JSON.parse(raw); } catch { process.exit(0); }
636
+
637
+ const sessionId = data.session_id || '';
638
+ const prompt = data.prompt || '';
639
+ if (!sessionId || !prompt) process.exit(0);
640
+
641
+ // Strip system tags
642
+ const tagPatterns = [
643
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
644
+ /<ide_opened_file>[\s\S]*?<\/ide_opened_file>/g,
645
+ /<ide_selection>[\s\S]*?<\/ide_selection>/g,
646
+ /<task-notification>[\s\S]*?<\/task-notification>/g,
647
+ ];
648
+ let clean = prompt;
649
+ for (const pat of tagPatterns) clean = clean.replace(pat, '');
650
+ clean = clean.replace(/\n\s*\n/g, '\n').trim();
651
+
652
+ if (clean.length < 5) process.exit(0);
653
+
654
+ const tmp = tmpdir();
655
+ writeFileSync(resolve(tmp, `agent-bridge-prompt-${sessionId}`), clean, 'utf8');
656
+ process.exit(0);
657
+ }
658
+
659
+ // ============================================================
660
+ // CLI: --ingest (Stop hook mode)
661
+ // ============================================================
662
+ // Called by Claude Code Stop hook. Reads stdin JSON with
663
+ // session_id + last_assistant_message, writes to context dir.
664
+ // Usage: node server.mjs --ingest
514
665
 
515
- async function main() {
666
+ async function runIngest() {
516
667
  ensureDirs();
517
- cleanOldMessages();
518
- writeAgentFile();
519
- startHeartbeat();
520
668
 
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); });
669
+ // Read stdin
670
+ const chunks = [];
671
+ for await (const chunk of process.stdin) chunks.push(chunk);
672
+ const raw = Buffer.concat(chunks).toString('utf8');
673
+
674
+ let data;
675
+ try {
676
+ data = JSON.parse(raw);
677
+ } catch {
678
+ process.exit(0);
679
+ }
680
+
681
+ const sessionId = data.session_id || '';
682
+ const assistantMsg = data.last_assistant_message || '';
683
+ if (!sessionId || !assistantMsg) process.exit(0);
684
+
685
+ // Read user prompt saved by --capture-prompt hook
686
+ const tmp = tmpdir();
687
+ const promptPath = resolve(tmp, `agent-bridge-prompt-${sessionId}`);
688
+ let userMsg = '';
689
+ try {
690
+ userMsg = readFileSync(promptPath, 'utf8');
691
+ } catch {}
692
+
693
+ // Strip system tags
694
+ const tagPatterns = [
695
+ /<system-reminder>[\s\S]*?<\/system-reminder>/g,
696
+ /<task-notification>[\s\S]*?<\/task-notification>/g,
697
+ /<ide_opened_file>[\s\S]*?<\/ide_opened_file>/g,
698
+ /<ide_selection>[\s\S]*?<\/ide_selection>/g,
699
+ ];
700
+ let cleanAssistant = assistantMsg;
701
+ for (const pat of tagPatterns) cleanAssistant = cleanAssistant.replace(pat, '');
702
+ cleanAssistant = cleanAssistant.replace(/\n\s*\n/g, '\n').trim();
703
+
704
+ if (!cleanAssistant || cleanAssistant.length < 50) process.exit(0);
705
+
706
+ // Determine agent name from env or from active agent files
707
+ const name = process.env.AGENT_BRIDGE_NAME || findAgentNameForSession(sessionId);
708
+ if (!name) process.exit(0);
709
+
710
+ // Read existing context and append new exchange
711
+ const safeName = name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
712
+ const ctxPath = resolve(CONTEXT_DIR, `${safeName}.json`);
713
+ let existing = { exchanges: [] };
714
+ try {
715
+ const prev = JSON.parse(readFileSync(ctxPath, 'utf8'));
716
+ if (prev.exchanges) existing = prev;
717
+ else if (prev.content) existing = { exchanges: [{ role: 'context', content: prev.content }] };
718
+ } catch {}
719
+
720
+ // Keep last 20 exchanges to avoid unbounded growth
721
+ existing.exchanges.push({
722
+ timestamp: new Date().toISOString(),
723
+ user: userMsg.slice(0, 2000),
724
+ assistant: cleanAssistant.slice(0, 5000)
725
+ });
726
+ if (existing.exchanges.length > 20) {
727
+ existing.exchanges = existing.exchanges.slice(-20);
728
+ }
729
+
730
+ const ctxData = JSON.stringify({
731
+ agentName: name,
732
+ project: process.cwd().split(/[\\/]/).pop(),
733
+ updatedAt: new Date().toISOString(),
734
+ sessionId,
735
+ exchanges: existing.exchanges
736
+ }, null, 2);
529
737
 
530
- // Connect stdio transport
531
- const transport = new StdioServerTransport();
532
- await server.connect(transport);
738
+ const tmpPath = ctxPath + '.tmp';
739
+ try {
740
+ writeFileSync(tmpPath, ctxData);
741
+ renameSync(tmpPath, ctxPath);
742
+ } catch {
743
+ try { writeFileSync(ctxPath, ctxData); } catch {}
744
+ }
533
745
 
534
- console.error(`[AgentBridge] Server running`);
535
- console.error(`[AgentBridge] Agent ID: ${agentId}`);
536
- console.error(`[AgentBridge] Data dir: ${BRIDGE_DIR}`);
746
+ process.exit(0);
537
747
  }
538
748
 
539
- main().catch(err => {
540
- console.error(`[AgentBridge] Fatal: ${err.message}`);
541
- process.exit(1);
542
- });
749
+ function findAgentNameForSession(sessionId) {
750
+ // Look through agent files to find one from this process's cwd
751
+ try {
752
+ const files = readdirSync(AGENTS_DIR).filter(f => f.endsWith('.json'));
753
+ const cwd = process.cwd();
754
+ for (const file of files) {
755
+ const agent = readAgentFile(resolve(AGENTS_DIR, file));
756
+ if (agent && agent.name && agent.cwd === cwd) return agent.name;
757
+ }
758
+ } catch {}
759
+ return null;
760
+ }
761
+
762
+ // ============================================================
763
+ // Main
764
+ // ============================================================
765
+
766
+ if (process.argv.includes('--capture-prompt')) {
767
+ runCapturePrompt().catch(() => process.exit(0));
768
+ } else if (process.argv.includes('--ingest')) {
769
+ runIngest().catch(() => process.exit(0));
770
+ } else {
771
+ async function main() {
772
+ ensureDirs();
773
+ cleanOldMessages();
774
+ writeAgentFile();
775
+ startHeartbeat();
776
+
777
+ // Cleanup on exit
778
+ const cleanup = () => {
779
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
780
+ removeAgentFile();
781
+ };
782
+ process.on('exit', cleanup);
783
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
784
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
785
+
786
+ // Connect stdio transport
787
+ const transport = new StdioServerTransport();
788
+ await server.connect(transport);
789
+
790
+ console.error(`[AgentBridge] Server running`);
791
+ console.error(`[AgentBridge] Agent ID: ${agentId}`);
792
+ console.error(`[AgentBridge] Data dir: ${BRIDGE_DIR}`);
793
+ }
794
+
795
+ main().catch(err => {
796
+ console.error(`[AgentBridge] Fatal: ${err.message}`);
797
+ process.exit(1);
798
+ });
799
+ }