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.
- package/README.md +138 -0
- package/package.json +36 -0
- 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
|
+
});
|