agent-relay 2.2.23 → 2.3.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/dist/index.cjs +199 -3
- package/package.json +64 -21
- package/packages/acp-bridge/package.json +2 -2
- package/packages/api-types/package.json +1 -1
- package/packages/benchmark/package.json +5 -5
- package/packages/bridge/package.json +7 -7
- package/packages/cli-tester/package.json +1 -1
- package/packages/config/package.json +2 -2
- package/packages/continuity/package.json +2 -2
- package/packages/daemon/dist/cloud-sync.d.ts +13 -1
- package/packages/daemon/dist/cloud-sync.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-sync.js +169 -5
- package/packages/daemon/dist/cloud-sync.js.map +1 -1
- package/packages/daemon/dist/server.d.ts +5 -0
- package/packages/daemon/dist/server.d.ts.map +1 -1
- package/packages/daemon/dist/server.js +50 -0
- package/packages/daemon/dist/server.js.map +1 -1
- package/packages/daemon/package.json +12 -12
- package/packages/daemon/src/cloud-sync.ts +201 -5
- package/packages/daemon/src/server.ts +61 -0
- package/packages/hooks/package.json +4 -4
- package/packages/mcp/package.json +5 -5
- package/packages/memory/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/protocol/package.json +1 -1
- package/packages/resiliency/package.json +1 -1
- package/packages/sdk/dist/client.d.ts +1 -1
- package/packages/sdk/dist/client.js +2 -2
- package/packages/sdk/package.json +3 -3
- package/packages/sdk/src/client.ts +2 -2
- package/packages/sdk-ts/README.md +65 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.d.ts +2 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.d.ts.map +1 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.js +139 -0
- package/packages/sdk-ts/dist/__tests__/integration.test.js.map +1 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.d.ts +2 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.d.ts.map +1 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.js +176 -0
- package/packages/sdk-ts/dist/__tests__/quickstart.test.js.map +1 -0
- package/packages/sdk-ts/dist/browser.d.ts +16 -0
- package/packages/sdk-ts/dist/browser.d.ts.map +1 -0
- package/packages/sdk-ts/dist/browser.js +19 -0
- package/packages/sdk-ts/dist/browser.js.map +1 -0
- package/packages/sdk-ts/dist/client.d.ts +91 -0
- package/packages/sdk-ts/dist/client.d.ts.map +1 -0
- package/packages/sdk-ts/dist/client.js +360 -0
- package/packages/sdk-ts/dist/client.js.map +1 -0
- package/packages/sdk-ts/dist/consensus-helpers.d.ts +103 -0
- package/packages/sdk-ts/dist/consensus-helpers.d.ts.map +1 -0
- package/packages/sdk-ts/dist/consensus-helpers.js +147 -0
- package/packages/sdk-ts/dist/consensus-helpers.js.map +1 -0
- package/packages/sdk-ts/dist/consensus.d.ts +72 -0
- package/packages/sdk-ts/dist/consensus.d.ts.map +1 -0
- package/packages/sdk-ts/dist/consensus.js +378 -0
- package/packages/sdk-ts/dist/consensus.js.map +1 -0
- package/packages/sdk-ts/dist/examples/demo.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/demo.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/demo.js +63 -0
- package/packages/sdk-ts/dist/examples/demo.js.map +1 -0
- package/packages/sdk-ts/dist/examples/example.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/example.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/example.js +80 -0
- package/packages/sdk-ts/dist/examples/example.js.map +1 -0
- package/packages/sdk-ts/dist/examples/quickstart.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/quickstart.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/quickstart.js +56 -0
- package/packages/sdk-ts/dist/examples/quickstart.js.map +1 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.d.ts +2 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.d.ts.map +1 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.js +281 -0
- package/packages/sdk-ts/dist/examples/ralph-loop.js.map +1 -0
- package/packages/sdk-ts/dist/index.d.ts +9 -0
- package/packages/sdk-ts/dist/index.d.ts.map +1 -0
- package/packages/sdk-ts/dist/index.js +9 -0
- package/packages/sdk-ts/dist/index.js.map +1 -0
- package/packages/sdk-ts/dist/logs.d.ts +47 -0
- package/packages/sdk-ts/dist/logs.d.ts.map +1 -0
- package/packages/sdk-ts/dist/logs.js +137 -0
- package/packages/sdk-ts/dist/logs.js.map +1 -0
- package/packages/sdk-ts/dist/protocol.d.ts +249 -0
- package/packages/sdk-ts/dist/protocol.d.ts.map +1 -0
- package/packages/sdk-ts/dist/protocol.js +2 -0
- package/packages/sdk-ts/dist/protocol.js.map +1 -0
- package/packages/sdk-ts/dist/pty.d.ts +8 -0
- package/packages/sdk-ts/dist/pty.d.ts.map +1 -0
- package/packages/sdk-ts/dist/pty.js +14 -0
- package/packages/sdk-ts/dist/pty.js.map +1 -0
- package/packages/sdk-ts/dist/relay.d.ts +118 -0
- package/packages/sdk-ts/dist/relay.d.ts.map +1 -0
- package/packages/sdk-ts/dist/relay.js +355 -0
- package/packages/sdk-ts/dist/relay.js.map +1 -0
- package/packages/sdk-ts/dist/relaycast.d.ts +57 -0
- package/packages/sdk-ts/dist/relaycast.d.ts.map +1 -0
- package/packages/sdk-ts/dist/relaycast.js +110 -0
- package/packages/sdk-ts/dist/relaycast.js.map +1 -0
- package/packages/sdk-ts/dist/shadow.d.ts +100 -0
- package/packages/sdk-ts/dist/shadow.d.ts.map +1 -0
- package/packages/sdk-ts/dist/shadow.js +174 -0
- package/packages/sdk-ts/dist/shadow.js.map +1 -0
- package/packages/sdk-ts/package.json +75 -0
- package/packages/sdk-ts/scripts/bundle-agent-relay.mjs +53 -0
- package/packages/sdk-ts/src/__tests__/integration.test.ts +170 -0
- package/packages/sdk-ts/src/__tests__/quickstart.test.ts +198 -0
- package/packages/sdk-ts/src/browser.ts +57 -0
- package/packages/sdk-ts/src/client.ts +491 -0
- package/packages/sdk-ts/src/consensus-helpers.ts +253 -0
- package/packages/sdk-ts/src/consensus.ts +506 -0
- package/packages/sdk-ts/src/examples/demo.ts +88 -0
- package/packages/sdk-ts/src/examples/example.ts +91 -0
- package/packages/sdk-ts/src/examples/quickstart.ts +72 -0
- package/packages/sdk-ts/src/examples/ralph-loop.ts +352 -0
- package/packages/sdk-ts/src/examples/sample-prd.json +37 -0
- package/packages/sdk-ts/src/index.ts +8 -0
- package/packages/sdk-ts/src/logs.ts +163 -0
- package/packages/sdk-ts/src/protocol.ts +266 -0
- package/packages/sdk-ts/src/pty.ts +16 -0
- package/packages/sdk-ts/src/relay.ts +454 -0
- package/packages/sdk-ts/src/relaycast.ts +143 -0
- package/packages/sdk-ts/src/shadow.ts +230 -0
- package/packages/sdk-ts/tsconfig.json +16 -0
- package/packages/spawner/package.json +1 -1
- package/packages/state/package.json +1 -1
- package/packages/storage/package.json +2 -2
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +3 -3
- package/packages/wrapper/dist/client.js +1 -1
- package/packages/wrapper/package.json +6 -6
- package/packages/wrapper/src/client.test.ts +1 -1
- package/packages/wrapper/src/client.ts +1 -1
- package/packages/mcp/SPEC.md +0 -1922
- package/packages/mcp/STAFFING_PLAN.md +0 -294
package/packages/mcp/SPEC.md
DELETED
|
@@ -1,1922 +0,0 @@
|
|
|
1
|
-
# @agent-relay/mcp - Implementation Specification
|
|
2
|
-
|
|
3
|
-
> Comprehensive specification for the Agent Relay MCP Server package.
|
|
4
|
-
> This enables AI agents (Claude, Codex, Gemini, Cursor, etc.) to use Relay
|
|
5
|
-
> as a native tool for inter-agent communication.
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
|
|
9
|
-
The MCP (Model Context Protocol) server provides AI agents with native tools to:
|
|
10
|
-
- Send messages to other agents, channels, or broadcast
|
|
11
|
-
- Spawn and release worker agents
|
|
12
|
-
- Check inbox for pending messages
|
|
13
|
-
- List online agents
|
|
14
|
-
- Query connection status
|
|
15
|
-
|
|
16
|
-
**Key Design Decisions:**
|
|
17
|
-
- Separate package: `@agent-relay/mcp` (not bundled with main agent-relay)
|
|
18
|
-
- Full protocol spec included in prompts (not abbreviated)
|
|
19
|
-
- Error if daemon not running (don't auto-start - user should know)
|
|
20
|
-
- Socket discovery with priority: env var → cwd → scan data dir
|
|
21
|
-
- Cloud: Pre-baked in Docker image for all CLI tools
|
|
22
|
-
- Local: Frictionless `npx` one-liner installation
|
|
23
|
-
|
|
24
|
-
---
|
|
25
|
-
|
|
26
|
-
## Package Structure
|
|
27
|
-
|
|
28
|
-
```
|
|
29
|
-
packages/mcp/
|
|
30
|
-
├── package.json
|
|
31
|
-
├── tsconfig.json
|
|
32
|
-
├── README.md
|
|
33
|
-
├── SPEC.md # This file
|
|
34
|
-
├── src/
|
|
35
|
-
│ ├── index.ts # MCP server entry point
|
|
36
|
-
│ ├── bin.ts # CLI binary entry (npx @agent-relay/mcp)
|
|
37
|
-
│ ├── install-cli.ts # Install command implementation
|
|
38
|
-
│ ├── install.ts # Editor installation logic
|
|
39
|
-
│ ├── tools/
|
|
40
|
-
│ │ ├── index.ts # Tool exports
|
|
41
|
-
│ │ ├── relay-send.ts # Send message tool
|
|
42
|
-
│ │ ├── relay-spawn.ts # Spawn agent tool
|
|
43
|
-
│ │ ├── relay-release.ts # Release agent tool
|
|
44
|
-
│ │ ├── relay-inbox.ts # Check inbox tool
|
|
45
|
-
│ │ ├── relay-who.ts # List agents tool
|
|
46
|
-
│ │ └── relay-status.ts # Connection status tool
|
|
47
|
-
│ ├── prompts/
|
|
48
|
-
│ │ ├── index.ts # Prompt exports
|
|
49
|
-
│ │ └── protocol.ts # Full protocol documentation
|
|
50
|
-
│ ├── resources/
|
|
51
|
-
│ │ ├── index.ts # Resource exports
|
|
52
|
-
│ │ ├── agents.ts # relay://agents resource
|
|
53
|
-
│ │ ├── inbox.ts # relay://inbox resource
|
|
54
|
-
│ │ └── project.ts # relay://project resource
|
|
55
|
-
│ ├── client.ts # Relay daemon connection client
|
|
56
|
-
│ ├── discover.ts # Socket/project discovery
|
|
57
|
-
│ └── errors.ts # Error types and messages
|
|
58
|
-
└── tests/
|
|
59
|
-
├── tools.test.ts
|
|
60
|
-
├── discover.test.ts
|
|
61
|
-
└── install.test.ts
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
---
|
|
65
|
-
|
|
66
|
-
## Package Configuration
|
|
67
|
-
|
|
68
|
-
### package.json
|
|
69
|
-
|
|
70
|
-
```json
|
|
71
|
-
{
|
|
72
|
-
"name": "@agent-relay/mcp",
|
|
73
|
-
"version": "0.1.0",
|
|
74
|
-
"description": "MCP server for Agent Relay - gives AI agents native relay tools",
|
|
75
|
-
"type": "module",
|
|
76
|
-
"main": "dist/index.js",
|
|
77
|
-
"types": "dist/index.d.ts",
|
|
78
|
-
"bin": {
|
|
79
|
-
"agent-relay-mcp": "./dist/bin.js"
|
|
80
|
-
},
|
|
81
|
-
"exports": {
|
|
82
|
-
".": {
|
|
83
|
-
"types": "./dist/index.d.ts",
|
|
84
|
-
"import": "./dist/index.js"
|
|
85
|
-
},
|
|
86
|
-
"./install": {
|
|
87
|
-
"types": "./dist/install.d.ts",
|
|
88
|
-
"import": "./dist/install.js"
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
"scripts": {
|
|
92
|
-
"build": "tsc",
|
|
93
|
-
"clean": "rm -rf dist",
|
|
94
|
-
"test": "vitest run",
|
|
95
|
-
"prepublishOnly": "npm run build"
|
|
96
|
-
},
|
|
97
|
-
"dependencies": {
|
|
98
|
-
"@anthropic-ai/sdk": "^0.52.0",
|
|
99
|
-
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
100
|
-
"@agent-relay/protocol": "0.1.0"
|
|
101
|
-
},
|
|
102
|
-
"devDependencies": {
|
|
103
|
-
"@types/node": "^22.19.3",
|
|
104
|
-
"typescript": "^5.9.3",
|
|
105
|
-
"vitest": "^3.2.4"
|
|
106
|
-
},
|
|
107
|
-
"peerDependencies": {
|
|
108
|
-
"agent-relay": ">=0.1.0"
|
|
109
|
-
},
|
|
110
|
-
"peerDependenciesMeta": {
|
|
111
|
-
"agent-relay": {
|
|
112
|
-
"optional": true
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
"keywords": ["mcp", "agent-relay", "ai-agents", "claude", "cursor"],
|
|
116
|
-
"publishConfig": {
|
|
117
|
-
"access": "public"
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## MCP Tools
|
|
125
|
-
|
|
126
|
-
### 1. relay_send
|
|
127
|
-
|
|
128
|
-
Send a message to another agent, channel, or broadcast.
|
|
129
|
-
|
|
130
|
-
```typescript
|
|
131
|
-
// src/tools/relay-send.ts
|
|
132
|
-
import { z } from 'zod';
|
|
133
|
-
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
134
|
-
|
|
135
|
-
export const relaySendSchema = z.object({
|
|
136
|
-
to: z.string().describe(
|
|
137
|
-
'Target: agent name, #channel, or * for broadcast'
|
|
138
|
-
),
|
|
139
|
-
message: z.string().describe('Message content'),
|
|
140
|
-
thread: z.string().optional().describe('Optional thread ID for threaded conversations'),
|
|
141
|
-
await_response: z.boolean().optional().default(false).describe(
|
|
142
|
-
'If true, wait for a response (blocks until reply or timeout)'
|
|
143
|
-
),
|
|
144
|
-
timeout_ms: z.number().optional().default(30000).describe(
|
|
145
|
-
'Timeout in milliseconds when await_response is true'
|
|
146
|
-
),
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
export type RelaySendInput = z.infer<typeof relaySendSchema>;
|
|
150
|
-
|
|
151
|
-
export const relaySendTool: Tool = {
|
|
152
|
-
name: 'relay_send',
|
|
153
|
-
description: `Send a message via Agent Relay.
|
|
154
|
-
|
|
155
|
-
Examples:
|
|
156
|
-
- Direct message: to="Alice", message="Hello"
|
|
157
|
-
- Channel: to="#general", message="Team update"
|
|
158
|
-
- Broadcast: to="*", message="System notice"
|
|
159
|
-
- Threaded: to="Bob", message="Follow up", thread="task-123"
|
|
160
|
-
- Await reply: to="Worker", message="Process this", await_response=true`,
|
|
161
|
-
inputSchema: {
|
|
162
|
-
type: 'object',
|
|
163
|
-
properties: {
|
|
164
|
-
to: {
|
|
165
|
-
type: 'string',
|
|
166
|
-
description: 'Target: agent name, #channel, or * for broadcast',
|
|
167
|
-
},
|
|
168
|
-
message: {
|
|
169
|
-
type: 'string',
|
|
170
|
-
description: 'Message content',
|
|
171
|
-
},
|
|
172
|
-
thread: {
|
|
173
|
-
type: 'string',
|
|
174
|
-
description: 'Optional thread ID for threaded conversations',
|
|
175
|
-
},
|
|
176
|
-
await_response: {
|
|
177
|
-
type: 'boolean',
|
|
178
|
-
description: 'If true, wait for a response',
|
|
179
|
-
default: false,
|
|
180
|
-
},
|
|
181
|
-
timeout_ms: {
|
|
182
|
-
type: 'number',
|
|
183
|
-
description: 'Timeout in ms when await_response is true',
|
|
184
|
-
default: 30000,
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
required: ['to', 'message'],
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
export async function handleRelaySend(
|
|
192
|
-
client: RelayClient,
|
|
193
|
-
input: RelaySendInput
|
|
194
|
-
): Promise<string> {
|
|
195
|
-
const { to, message, thread, await_response, timeout_ms } = input;
|
|
196
|
-
|
|
197
|
-
if (await_response) {
|
|
198
|
-
const response = await client.sendAndWait(to, message, {
|
|
199
|
-
thread,
|
|
200
|
-
timeoutMs: timeout_ms,
|
|
201
|
-
});
|
|
202
|
-
return `Response from ${response.from}: ${response.content}`;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
await client.send(to, message, { thread });
|
|
206
|
-
return `Message sent to ${to}`;
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### 2. relay_spawn
|
|
211
|
-
|
|
212
|
-
Spawn a worker agent to handle a subtask.
|
|
213
|
-
|
|
214
|
-
```typescript
|
|
215
|
-
// src/tools/relay-spawn.ts
|
|
216
|
-
import { z } from 'zod';
|
|
217
|
-
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
218
|
-
|
|
219
|
-
export const relaySpawnSchema = z.object({
|
|
220
|
-
name: z.string().describe('Unique name for the worker agent'),
|
|
221
|
-
cli: z.enum(['claude', 'codex', 'gemini', 'droid', 'opencode']).describe(
|
|
222
|
-
'CLI tool to use for the worker'
|
|
223
|
-
),
|
|
224
|
-
task: z.string().describe('Task description/prompt for the worker'),
|
|
225
|
-
model: z.string().optional().describe('Model override (e.g., "claude-3-5-sonnet")'),
|
|
226
|
-
cwd: z.string().optional().describe('Working directory for the worker'),
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
export type RelaySpawnInput = z.infer<typeof relaySpawnSchema>;
|
|
230
|
-
|
|
231
|
-
export const relaySpawnTool: Tool = {
|
|
232
|
-
name: 'relay_spawn',
|
|
233
|
-
description: `Spawn a worker agent to handle a subtask.
|
|
234
|
-
|
|
235
|
-
The worker runs in a separate process with its own CLI instance.
|
|
236
|
-
You'll receive a confirmation when the worker is ready.
|
|
237
|
-
|
|
238
|
-
Example:
|
|
239
|
-
name="TestRunner"
|
|
240
|
-
cli="claude"
|
|
241
|
-
task="Run the test suite and report failures"`,
|
|
242
|
-
inputSchema: {
|
|
243
|
-
type: 'object',
|
|
244
|
-
properties: {
|
|
245
|
-
name: {
|
|
246
|
-
type: 'string',
|
|
247
|
-
description: 'Unique name for the worker agent',
|
|
248
|
-
},
|
|
249
|
-
cli: {
|
|
250
|
-
type: 'string',
|
|
251
|
-
enum: ['claude', 'codex', 'gemini', 'droid', 'opencode'],
|
|
252
|
-
description: 'CLI tool to use',
|
|
253
|
-
},
|
|
254
|
-
task: {
|
|
255
|
-
type: 'string',
|
|
256
|
-
description: 'Task description for the worker',
|
|
257
|
-
},
|
|
258
|
-
model: {
|
|
259
|
-
type: 'string',
|
|
260
|
-
description: 'Optional model override',
|
|
261
|
-
},
|
|
262
|
-
cwd: {
|
|
263
|
-
type: 'string',
|
|
264
|
-
description: 'Working directory for the worker',
|
|
265
|
-
},
|
|
266
|
-
},
|
|
267
|
-
required: ['name', 'cli', 'task'],
|
|
268
|
-
},
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
export async function handleRelaySpawn(
|
|
272
|
-
client: RelayClient,
|
|
273
|
-
input: RelaySpawnInput
|
|
274
|
-
): Promise<string> {
|
|
275
|
-
const { name, cli, task, model, cwd } = input;
|
|
276
|
-
|
|
277
|
-
const result = await client.spawn({
|
|
278
|
-
name,
|
|
279
|
-
cli,
|
|
280
|
-
task,
|
|
281
|
-
model,
|
|
282
|
-
cwd,
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
if (result.success) {
|
|
286
|
-
return `Worker "${name}" spawned successfully. It will message you when ready.`;
|
|
287
|
-
} else {
|
|
288
|
-
return `Failed to spawn worker: ${result.error}`;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
```
|
|
292
|
-
|
|
293
|
-
### 3. relay_release
|
|
294
|
-
|
|
295
|
-
Release (terminate) a worker agent.
|
|
296
|
-
|
|
297
|
-
```typescript
|
|
298
|
-
// src/tools/relay-release.ts
|
|
299
|
-
import { z } from 'zod';
|
|
300
|
-
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
301
|
-
|
|
302
|
-
export const relayReleaseSchema = z.object({
|
|
303
|
-
name: z.string().describe('Name of the worker to release'),
|
|
304
|
-
reason: z.string().optional().describe('Optional reason for release'),
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
export type RelayReleaseInput = z.infer<typeof relayReleaseSchema>;
|
|
308
|
-
|
|
309
|
-
export const relayReleaseTool: Tool = {
|
|
310
|
-
name: 'relay_release',
|
|
311
|
-
description: `Release (terminate) a worker agent.
|
|
312
|
-
|
|
313
|
-
Use this when a worker has completed its task or is no longer needed.
|
|
314
|
-
The worker will be gracefully terminated.
|
|
315
|
-
|
|
316
|
-
Example:
|
|
317
|
-
name="TestRunner"
|
|
318
|
-
reason="Tests completed successfully"`,
|
|
319
|
-
inputSchema: {
|
|
320
|
-
type: 'object',
|
|
321
|
-
properties: {
|
|
322
|
-
name: {
|
|
323
|
-
type: 'string',
|
|
324
|
-
description: 'Name of the worker to release',
|
|
325
|
-
},
|
|
326
|
-
reason: {
|
|
327
|
-
type: 'string',
|
|
328
|
-
description: 'Optional reason for release',
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
required: ['name'],
|
|
332
|
-
},
|
|
333
|
-
};
|
|
334
|
-
|
|
335
|
-
export async function handleRelayRelease(
|
|
336
|
-
client: RelayClient,
|
|
337
|
-
input: RelayReleaseInput
|
|
338
|
-
): Promise<string> {
|
|
339
|
-
const { name, reason } = input;
|
|
340
|
-
|
|
341
|
-
const result = await client.release(name, reason);
|
|
342
|
-
|
|
343
|
-
if (result.success) {
|
|
344
|
-
return `Worker "${name}" released.`;
|
|
345
|
-
} else {
|
|
346
|
-
return `Failed to release worker: ${result.error}`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
### 4. relay_inbox
|
|
352
|
-
|
|
353
|
-
Check for pending messages in your inbox.
|
|
354
|
-
|
|
355
|
-
```typescript
|
|
356
|
-
// src/tools/relay-inbox.ts
|
|
357
|
-
import { z } from 'zod';
|
|
358
|
-
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
359
|
-
|
|
360
|
-
export const relayInboxSchema = z.object({
|
|
361
|
-
limit: z.number().optional().default(10).describe('Max messages to return'),
|
|
362
|
-
unread_only: z.boolean().optional().default(true).describe('Only return unread messages'),
|
|
363
|
-
from: z.string().optional().describe('Filter by sender'),
|
|
364
|
-
channel: z.string().optional().describe('Filter by channel'),
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
export type RelayInboxInput = z.infer<typeof relayInboxSchema>;
|
|
368
|
-
|
|
369
|
-
export const relayInboxTool: Tool = {
|
|
370
|
-
name: 'relay_inbox',
|
|
371
|
-
description: `Check your inbox for pending messages.
|
|
372
|
-
|
|
373
|
-
Returns messages sent to you by other agents or in channels you're subscribed to.
|
|
374
|
-
|
|
375
|
-
Examples:
|
|
376
|
-
- Get all unread: (no params)
|
|
377
|
-
- From specific agent: from="Alice"
|
|
378
|
-
- From channel: channel="#general"`,
|
|
379
|
-
inputSchema: {
|
|
380
|
-
type: 'object',
|
|
381
|
-
properties: {
|
|
382
|
-
limit: {
|
|
383
|
-
type: 'number',
|
|
384
|
-
description: 'Max messages to return',
|
|
385
|
-
default: 10,
|
|
386
|
-
},
|
|
387
|
-
unread_only: {
|
|
388
|
-
type: 'boolean',
|
|
389
|
-
description: 'Only return unread messages',
|
|
390
|
-
default: true,
|
|
391
|
-
},
|
|
392
|
-
from: {
|
|
393
|
-
type: 'string',
|
|
394
|
-
description: 'Filter by sender',
|
|
395
|
-
},
|
|
396
|
-
channel: {
|
|
397
|
-
type: 'string',
|
|
398
|
-
description: 'Filter by channel',
|
|
399
|
-
},
|
|
400
|
-
},
|
|
401
|
-
required: [],
|
|
402
|
-
},
|
|
403
|
-
};
|
|
404
|
-
|
|
405
|
-
export async function handleRelayInbox(
|
|
406
|
-
client: RelayClient,
|
|
407
|
-
input: RelayInboxInput
|
|
408
|
-
): Promise<string> {
|
|
409
|
-
const messages = await client.getInbox(input);
|
|
410
|
-
|
|
411
|
-
if (messages.length === 0) {
|
|
412
|
-
return 'No messages in inbox.';
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const formatted = messages.map((m) => {
|
|
416
|
-
const channel = m.channel ? ` [${m.channel}]` : '';
|
|
417
|
-
const thread = m.thread ? ` (thread: ${m.thread})` : '';
|
|
418
|
-
return `[${m.id}] From ${m.from}${channel}${thread}:\n${m.content}`;
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
return `${messages.length} message(s):\n\n${formatted.join('\n\n---\n\n')}`;
|
|
422
|
-
}
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### 5. relay_who
|
|
426
|
-
|
|
427
|
-
List online agents and their status.
|
|
428
|
-
|
|
429
|
-
```typescript
|
|
430
|
-
// src/tools/relay-who.ts
|
|
431
|
-
import { z } from 'zod';
|
|
432
|
-
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
433
|
-
|
|
434
|
-
export const relayWhoSchema = z.object({
|
|
435
|
-
include_idle: z.boolean().optional().default(true).describe('Include idle agents'),
|
|
436
|
-
project: z.string().optional().describe('Filter by project (for multi-project setups)'),
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
export type RelayWhoInput = z.infer<typeof relayWhoSchema>;
|
|
440
|
-
|
|
441
|
-
export const relayWhoTool: Tool = {
|
|
442
|
-
name: 'relay_who',
|
|
443
|
-
description: `List online agents in the relay network.
|
|
444
|
-
|
|
445
|
-
Shows agent names, their CLI type, and current status.
|
|
446
|
-
|
|
447
|
-
Example output:
|
|
448
|
-
- Alice (claude) - active
|
|
449
|
-
- Bob (codex) - idle
|
|
450
|
-
- TestRunner (claude) - active [worker of: Alice]`,
|
|
451
|
-
inputSchema: {
|
|
452
|
-
type: 'object',
|
|
453
|
-
properties: {
|
|
454
|
-
include_idle: {
|
|
455
|
-
type: 'boolean',
|
|
456
|
-
description: 'Include idle agents',
|
|
457
|
-
default: true,
|
|
458
|
-
},
|
|
459
|
-
project: {
|
|
460
|
-
type: 'string',
|
|
461
|
-
description: 'Filter by project',
|
|
462
|
-
},
|
|
463
|
-
},
|
|
464
|
-
required: [],
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
export async function handleRelayWho(
|
|
469
|
-
client: RelayClient,
|
|
470
|
-
input: RelayWhoInput
|
|
471
|
-
): Promise<string> {
|
|
472
|
-
const agents = await client.listAgents(input);
|
|
473
|
-
|
|
474
|
-
if (agents.length === 0) {
|
|
475
|
-
return 'No agents online.';
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const formatted = agents.map((a) => {
|
|
479
|
-
const status = a.idle ? 'idle' : 'active';
|
|
480
|
-
const worker = a.parent ? ` [worker of: ${a.parent}]` : '';
|
|
481
|
-
return `- ${a.name} (${a.cli}) - ${status}${worker}`;
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
return `${agents.length} agent(s) online:\n${formatted.join('\n')}`;
|
|
485
|
-
}
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
### 6. relay_status
|
|
489
|
-
|
|
490
|
-
Get connection status and diagnostics.
|
|
491
|
-
|
|
492
|
-
```typescript
|
|
493
|
-
// src/tools/relay-status.ts
|
|
494
|
-
import { z } from 'zod';
|
|
495
|
-
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
496
|
-
|
|
497
|
-
export const relayStatusSchema = z.object({});
|
|
498
|
-
|
|
499
|
-
export type RelayStatusInput = z.infer<typeof relayStatusSchema>;
|
|
500
|
-
|
|
501
|
-
export const relayStatusTool: Tool = {
|
|
502
|
-
name: 'relay_status',
|
|
503
|
-
description: `Get relay connection status and diagnostics.
|
|
504
|
-
|
|
505
|
-
Returns:
|
|
506
|
-
- Connection state (connected/disconnected)
|
|
507
|
-
- Your agent name
|
|
508
|
-
- Project/socket info
|
|
509
|
-
- Daemon version`,
|
|
510
|
-
inputSchema: {
|
|
511
|
-
type: 'object',
|
|
512
|
-
properties: {},
|
|
513
|
-
required: [],
|
|
514
|
-
},
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
export async function handleRelayStatus(
|
|
518
|
-
client: RelayClient,
|
|
519
|
-
_input: RelayStatusInput
|
|
520
|
-
): Promise<string> {
|
|
521
|
-
const status = await client.getStatus();
|
|
522
|
-
|
|
523
|
-
return `Relay Status:
|
|
524
|
-
- Connected: ${status.connected ? 'Yes' : 'No'}
|
|
525
|
-
- Agent Name: ${status.agentName || 'Not registered'}
|
|
526
|
-
- Project: ${status.project || 'Unknown'}
|
|
527
|
-
- Socket: ${status.socketPath}
|
|
528
|
-
- Daemon Version: ${status.daemonVersion || 'Unknown'}
|
|
529
|
-
- Uptime: ${status.uptime || 'N/A'}`;
|
|
530
|
-
}
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
---
|
|
534
|
-
|
|
535
|
-
## MCP Prompts
|
|
536
|
-
|
|
537
|
-
### Protocol Documentation Prompt
|
|
538
|
-
|
|
539
|
-
This prompt is included automatically when an agent connects. It provides the full protocol documentation.
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
// src/prompts/protocol.ts
|
|
543
|
-
import type { Prompt } from '@modelcontextprotocol/sdk/types.js';
|
|
544
|
-
|
|
545
|
-
export const protocolPrompt: Prompt = {
|
|
546
|
-
name: 'relay_protocol',
|
|
547
|
-
description: 'Full Agent Relay protocol documentation',
|
|
548
|
-
arguments: [],
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
export const PROTOCOL_DOCUMENTATION = `
|
|
552
|
-
# Agent Relay Protocol
|
|
553
|
-
|
|
554
|
-
You are connected to Agent Relay, a real-time messaging system for AI agent coordination.
|
|
555
|
-
|
|
556
|
-
## Communication Patterns
|
|
557
|
-
|
|
558
|
-
### Direct Messages
|
|
559
|
-
Send a message to a specific agent by name:
|
|
560
|
-
\`\`\`
|
|
561
|
-
relay_send(to="Alice", message="Can you review this PR?")
|
|
562
|
-
\`\`\`
|
|
563
|
-
|
|
564
|
-
### Channel Messages
|
|
565
|
-
Send to a channel (prefix with #):
|
|
566
|
-
\`\`\`
|
|
567
|
-
relay_send(to="#engineering", message="Build complete")
|
|
568
|
-
\`\`\`
|
|
569
|
-
Channel messages are visible to all agents subscribed to that channel.
|
|
570
|
-
|
|
571
|
-
### Broadcast
|
|
572
|
-
Send to all online agents:
|
|
573
|
-
\`\`\`
|
|
574
|
-
relay_send(to="*", message="System maintenance in 5 minutes")
|
|
575
|
-
\`\`\`
|
|
576
|
-
Use sparingly - broadcasts interrupt all agents.
|
|
577
|
-
|
|
578
|
-
### Threaded Conversations
|
|
579
|
-
For multi-turn conversations, use thread IDs:
|
|
580
|
-
\`\`\`
|
|
581
|
-
relay_send(to="Bob", message="Starting task", thread="task-123")
|
|
582
|
-
relay_send(to="Bob", message="Task update", thread="task-123")
|
|
583
|
-
\`\`\`
|
|
584
|
-
|
|
585
|
-
### Await Response
|
|
586
|
-
Block and wait for a reply:
|
|
587
|
-
\`\`\`
|
|
588
|
-
relay_send(to="Worker", message="Process this file", await_response=true, timeout_ms=60000)
|
|
589
|
-
\`\`\`
|
|
590
|
-
|
|
591
|
-
## Spawning Workers
|
|
592
|
-
|
|
593
|
-
Create worker agents to parallelize work:
|
|
594
|
-
|
|
595
|
-
\`\`\`
|
|
596
|
-
relay_spawn(
|
|
597
|
-
name="TestRunner",
|
|
598
|
-
cli="claude",
|
|
599
|
-
task="Run the test suite in src/tests/ and report any failures"
|
|
600
|
-
)
|
|
601
|
-
\`\`\`
|
|
602
|
-
|
|
603
|
-
Workers:
|
|
604
|
-
- Run in separate processes
|
|
605
|
-
- Have their own CLI instance
|
|
606
|
-
- Can use relay to communicate back
|
|
607
|
-
- Should be released when done
|
|
608
|
-
|
|
609
|
-
### Worker Lifecycle
|
|
610
|
-
1. Spawn worker with task
|
|
611
|
-
2. Worker sends ACK when ready
|
|
612
|
-
3. Worker sends progress updates
|
|
613
|
-
4. Worker sends DONE when complete
|
|
614
|
-
5. Lead releases worker
|
|
615
|
-
|
|
616
|
-
### Release Workers
|
|
617
|
-
\`\`\`
|
|
618
|
-
relay_release(name="TestRunner", reason="Tests completed")
|
|
619
|
-
\`\`\`
|
|
620
|
-
|
|
621
|
-
## Message Protocol
|
|
622
|
-
|
|
623
|
-
When you receive messages, they follow this format:
|
|
624
|
-
\`\`\`
|
|
625
|
-
Relay message from Alice [msg-id-123]: Content here
|
|
626
|
-
\`\`\`
|
|
627
|
-
|
|
628
|
-
Channel messages include the channel:
|
|
629
|
-
\`\`\`
|
|
630
|
-
Relay message from Alice [msg-id-456] [#general]: Hello team!
|
|
631
|
-
\`\`\`
|
|
632
|
-
|
|
633
|
-
### ACK/DONE Protocol
|
|
634
|
-
When assigned a task:
|
|
635
|
-
1. Send ACK immediately: "ACK: Starting work on X"
|
|
636
|
-
2. Send progress updates as needed
|
|
637
|
-
3. Send DONE when complete: "DONE: Completed X with result Y"
|
|
638
|
-
|
|
639
|
-
Example:
|
|
640
|
-
\`\`\`
|
|
641
|
-
# When receiving a task
|
|
642
|
-
relay_send(to="Lead", message="ACK: Starting test suite run")
|
|
643
|
-
|
|
644
|
-
# ... do work ...
|
|
645
|
-
|
|
646
|
-
relay_send(to="Lead", message="DONE: All 42 tests passed")
|
|
647
|
-
\`\`\`
|
|
648
|
-
|
|
649
|
-
## Best Practices
|
|
650
|
-
|
|
651
|
-
### For Lead Agents
|
|
652
|
-
- Spawn workers for parallelizable tasks
|
|
653
|
-
- Keep track of spawned workers
|
|
654
|
-
- Release workers when done
|
|
655
|
-
- Use channels for team announcements
|
|
656
|
-
|
|
657
|
-
### For Worker Agents
|
|
658
|
-
- ACK immediately when receiving tasks
|
|
659
|
-
- Send progress updates for long tasks
|
|
660
|
-
- Send DONE with results when complete
|
|
661
|
-
- Ask clarifying questions if needed
|
|
662
|
-
|
|
663
|
-
### Message Etiquette
|
|
664
|
-
- Keep messages concise
|
|
665
|
-
- Include relevant context
|
|
666
|
-
- Use threads for related messages
|
|
667
|
-
- Don't spam broadcasts
|
|
668
|
-
|
|
669
|
-
## Checking Messages
|
|
670
|
-
|
|
671
|
-
Proactively check your inbox:
|
|
672
|
-
\`\`\`
|
|
673
|
-
relay_inbox()
|
|
674
|
-
relay_inbox(from="Lead")
|
|
675
|
-
relay_inbox(channel="#urgent")
|
|
676
|
-
\`\`\`
|
|
677
|
-
|
|
678
|
-
## Seeing Who's Online
|
|
679
|
-
|
|
680
|
-
\`\`\`
|
|
681
|
-
relay_who()
|
|
682
|
-
\`\`\`
|
|
683
|
-
|
|
684
|
-
## Error Handling
|
|
685
|
-
|
|
686
|
-
If relay returns an error:
|
|
687
|
-
- "Daemon not running" - The relay daemon needs to be started
|
|
688
|
-
- "Agent not found" - Target agent is offline
|
|
689
|
-
- "Channel not found" - Channel doesn't exist
|
|
690
|
-
- "Timeout" - No response within timeout period
|
|
691
|
-
|
|
692
|
-
## Multi-Project Communication
|
|
693
|
-
|
|
694
|
-
In multi-project setups, specify project:
|
|
695
|
-
\`\`\`
|
|
696
|
-
relay_send(to="frontend:Designer", message="Need UI mockup")
|
|
697
|
-
\`\`\`
|
|
698
|
-
|
|
699
|
-
Special targets:
|
|
700
|
-
- \`project:lead\` - Lead agent of that project
|
|
701
|
-
- \`project:*\` - Broadcast to project
|
|
702
|
-
- \`*:*\` - Broadcast to all projects
|
|
703
|
-
`;
|
|
704
|
-
|
|
705
|
-
export function getProtocolPrompt(): string {
|
|
706
|
-
return PROTOCOL_DOCUMENTATION;
|
|
707
|
-
}
|
|
708
|
-
```
|
|
709
|
-
|
|
710
|
-
---
|
|
711
|
-
|
|
712
|
-
## MCP Resources
|
|
713
|
-
|
|
714
|
-
### relay://agents
|
|
715
|
-
|
|
716
|
-
Live list of online agents.
|
|
717
|
-
|
|
718
|
-
```typescript
|
|
719
|
-
// src/resources/agents.ts
|
|
720
|
-
import type { Resource } from '@modelcontextprotocol/sdk/types.js';
|
|
721
|
-
|
|
722
|
-
export const agentsResource: Resource = {
|
|
723
|
-
uri: 'relay://agents',
|
|
724
|
-
name: 'Online Agents',
|
|
725
|
-
description: 'Live list of agents currently connected to relay',
|
|
726
|
-
mimeType: 'application/json',
|
|
727
|
-
};
|
|
728
|
-
|
|
729
|
-
export async function getAgentsResource(client: RelayClient): Promise<string> {
|
|
730
|
-
const agents = await client.listAgents({ include_idle: true });
|
|
731
|
-
return JSON.stringify(agents, null, 2);
|
|
732
|
-
}
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
### relay://inbox
|
|
736
|
-
|
|
737
|
-
Current inbox contents.
|
|
738
|
-
|
|
739
|
-
```typescript
|
|
740
|
-
// src/resources/inbox.ts
|
|
741
|
-
import type { Resource } from '@modelcontextprotocol/sdk/types.js';
|
|
742
|
-
|
|
743
|
-
export const inboxResource: Resource = {
|
|
744
|
-
uri: 'relay://inbox',
|
|
745
|
-
name: 'Message Inbox',
|
|
746
|
-
description: 'Your pending messages',
|
|
747
|
-
mimeType: 'application/json',
|
|
748
|
-
};
|
|
749
|
-
|
|
750
|
-
export async function getInboxResource(client: RelayClient): Promise<string> {
|
|
751
|
-
const messages = await client.getInbox({ unread_only: true, limit: 50 });
|
|
752
|
-
return JSON.stringify(messages, null, 2);
|
|
753
|
-
}
|
|
754
|
-
```
|
|
755
|
-
|
|
756
|
-
### relay://project
|
|
757
|
-
|
|
758
|
-
Current project configuration.
|
|
759
|
-
|
|
760
|
-
```typescript
|
|
761
|
-
// src/resources/project.ts
|
|
762
|
-
import type { Resource } from '@modelcontextprotocol/sdk/types.js';
|
|
763
|
-
|
|
764
|
-
export const projectResource: Resource = {
|
|
765
|
-
uri: 'relay://project',
|
|
766
|
-
name: 'Project Info',
|
|
767
|
-
description: 'Current relay project configuration',
|
|
768
|
-
mimeType: 'application/json',
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
export async function getProjectResource(client: RelayClient): Promise<string> {
|
|
772
|
-
const status = await client.getStatus();
|
|
773
|
-
return JSON.stringify({
|
|
774
|
-
project: status.project,
|
|
775
|
-
socketPath: status.socketPath,
|
|
776
|
-
daemonVersion: status.daemonVersion,
|
|
777
|
-
}, null, 2);
|
|
778
|
-
}
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
---
|
|
782
|
-
|
|
783
|
-
## Socket Discovery
|
|
784
|
-
|
|
785
|
-
The MCP server must find the relay daemon socket. Priority order:
|
|
786
|
-
|
|
787
|
-
```typescript
|
|
788
|
-
// src/discover.ts
|
|
789
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
790
|
-
import { join } from 'node:path';
|
|
791
|
-
import { homedir } from 'node:os';
|
|
792
|
-
|
|
793
|
-
export interface DiscoveryResult {
|
|
794
|
-
socketPath: string;
|
|
795
|
-
project: string;
|
|
796
|
-
source: 'env' | 'cwd' | 'scan';
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Discover relay daemon socket.
|
|
801
|
-
*
|
|
802
|
-
* Priority:
|
|
803
|
-
* 1. RELAY_SOCKET environment variable (explicit path)
|
|
804
|
-
* 2. RELAY_PROJECT environment variable (project name → data dir)
|
|
805
|
-
* 3. Current working directory .relay/config.json
|
|
806
|
-
* 4. Scan data directory for active sockets
|
|
807
|
-
*/
|
|
808
|
-
export function discoverSocket(): DiscoveryResult | null {
|
|
809
|
-
// 1. Explicit socket path
|
|
810
|
-
const socketEnv = process.env.RELAY_SOCKET;
|
|
811
|
-
if (socketEnv && existsSync(socketEnv)) {
|
|
812
|
-
return {
|
|
813
|
-
socketPath: socketEnv,
|
|
814
|
-
project: process.env.RELAY_PROJECT || 'unknown',
|
|
815
|
-
source: 'env',
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
// 2. Project name → data dir lookup
|
|
820
|
-
const projectEnv = process.env.RELAY_PROJECT;
|
|
821
|
-
if (projectEnv) {
|
|
822
|
-
const dataDir = getDataDir();
|
|
823
|
-
const projectSocket = join(dataDir, 'projects', projectEnv, 'daemon.sock');
|
|
824
|
-
if (existsSync(projectSocket)) {
|
|
825
|
-
return {
|
|
826
|
-
socketPath: projectSocket,
|
|
827
|
-
project: projectEnv,
|
|
828
|
-
source: 'env',
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// 3. Current working directory config
|
|
834
|
-
const cwdConfig = join(process.cwd(), '.relay', 'config.json');
|
|
835
|
-
if (existsSync(cwdConfig)) {
|
|
836
|
-
try {
|
|
837
|
-
const config = JSON.parse(readFileSync(cwdConfig, 'utf-8'));
|
|
838
|
-
if (config.socketPath && existsSync(config.socketPath)) {
|
|
839
|
-
return {
|
|
840
|
-
socketPath: config.socketPath,
|
|
841
|
-
project: config.project || 'local',
|
|
842
|
-
source: 'cwd',
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
} catch {
|
|
846
|
-
// Invalid config, continue
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// 4. Scan data directory for active sockets
|
|
851
|
-
const dataDir = getDataDir();
|
|
852
|
-
const projectsDir = join(dataDir, 'projects');
|
|
853
|
-
|
|
854
|
-
if (existsSync(projectsDir)) {
|
|
855
|
-
const projects = readdirSync(projectsDir, { withFileTypes: true })
|
|
856
|
-
.filter(d => d.isDirectory())
|
|
857
|
-
.map(d => d.name);
|
|
858
|
-
|
|
859
|
-
for (const project of projects) {
|
|
860
|
-
const socketPath = join(projectsDir, project, 'daemon.sock');
|
|
861
|
-
if (existsSync(socketPath)) {
|
|
862
|
-
return {
|
|
863
|
-
socketPath,
|
|
864
|
-
project,
|
|
865
|
-
source: 'scan',
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
return null;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function getDataDir(): string {
|
|
875
|
-
// Platform-specific data directory
|
|
876
|
-
const platform = process.platform;
|
|
877
|
-
|
|
878
|
-
if (platform === 'darwin') {
|
|
879
|
-
return join(homedir(), 'Library', 'Application Support', 'agent-relay');
|
|
880
|
-
} else if (platform === 'win32') {
|
|
881
|
-
return join(process.env.APPDATA || homedir(), 'agent-relay');
|
|
882
|
-
} else {
|
|
883
|
-
return join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'agent-relay');
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
```
|
|
887
|
-
|
|
888
|
-
---
|
|
889
|
-
|
|
890
|
-
## Relay Client
|
|
891
|
-
|
|
892
|
-
Connection to the daemon:
|
|
893
|
-
|
|
894
|
-
```typescript
|
|
895
|
-
// src/client.ts
|
|
896
|
-
import { connect, type Socket } from 'node:net';
|
|
897
|
-
import { EventEmitter } from 'node:events';
|
|
898
|
-
import { FrameParser, encodeFrame } from '@agent-relay/protocol/framing';
|
|
899
|
-
import type { Envelope } from '@agent-relay/protocol/types';
|
|
900
|
-
import { discoverSocket, type DiscoveryResult } from './discover.js';
|
|
901
|
-
import { RelayError, DaemonNotRunningError } from './errors.js';
|
|
902
|
-
|
|
903
|
-
export interface RelayClientOptions {
|
|
904
|
-
agentName?: string;
|
|
905
|
-
autoConnect?: boolean;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
export class RelayClient extends EventEmitter {
|
|
909
|
-
private socket: Socket | null = null;
|
|
910
|
-
private parser: FrameParser;
|
|
911
|
-
private discovery: DiscoveryResult | null = null;
|
|
912
|
-
private agentName: string;
|
|
913
|
-
private connected = false;
|
|
914
|
-
private messageHandlers = new Map<string, (response: any) => void>();
|
|
915
|
-
|
|
916
|
-
constructor(options: RelayClientOptions = {}) {
|
|
917
|
-
super();
|
|
918
|
-
this.parser = new FrameParser();
|
|
919
|
-
this.agentName = options.agentName || `mcp-${process.pid}`;
|
|
920
|
-
|
|
921
|
-
if (options.autoConnect !== false) {
|
|
922
|
-
this.connect();
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
async connect(): Promise<void> {
|
|
927
|
-
this.discovery = discoverSocket();
|
|
928
|
-
|
|
929
|
-
if (!this.discovery) {
|
|
930
|
-
throw new DaemonNotRunningError(
|
|
931
|
-
'Relay daemon not running. Start with: agent-relay daemon start'
|
|
932
|
-
);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
return new Promise((resolve, reject) => {
|
|
936
|
-
this.socket = connect(this.discovery!.socketPath);
|
|
937
|
-
|
|
938
|
-
this.socket.on('connect', () => {
|
|
939
|
-
this.connected = true;
|
|
940
|
-
this.handshake();
|
|
941
|
-
resolve();
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
this.socket.on('data', (data) => {
|
|
945
|
-
this.parser.push(data);
|
|
946
|
-
let frame;
|
|
947
|
-
while ((frame = this.parser.read())) {
|
|
948
|
-
this.handleFrame(frame);
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
this.socket.on('error', (err) => {
|
|
953
|
-
if (!this.connected) {
|
|
954
|
-
reject(new DaemonNotRunningError(
|
|
955
|
-
`Cannot connect to relay daemon: ${err.message}`
|
|
956
|
-
));
|
|
957
|
-
} else {
|
|
958
|
-
this.emit('error', err);
|
|
959
|
-
}
|
|
960
|
-
});
|
|
961
|
-
|
|
962
|
-
this.socket.on('close', () => {
|
|
963
|
-
this.connected = false;
|
|
964
|
-
this.emit('disconnect');
|
|
965
|
-
});
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
private handshake(): void {
|
|
970
|
-
this.sendEnvelope({
|
|
971
|
-
v: 1,
|
|
972
|
-
type: 'HELLO',
|
|
973
|
-
id: crypto.randomUUID(),
|
|
974
|
-
ts: Date.now(),
|
|
975
|
-
payload: {
|
|
976
|
-
name: this.agentName,
|
|
977
|
-
capabilities: ['mcp'],
|
|
978
|
-
},
|
|
979
|
-
});
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
private handleFrame(envelope: Envelope): void {
|
|
983
|
-
switch (envelope.type) {
|
|
984
|
-
case 'WELCOME':
|
|
985
|
-
this.emit('ready');
|
|
986
|
-
break;
|
|
987
|
-
case 'DELIVER':
|
|
988
|
-
this.emit('message', envelope.payload);
|
|
989
|
-
break;
|
|
990
|
-
case 'ACK':
|
|
991
|
-
const handler = this.messageHandlers.get(envelope.payload.id);
|
|
992
|
-
if (handler) {
|
|
993
|
-
handler(envelope.payload);
|
|
994
|
-
this.messageHandlers.delete(envelope.payload.id);
|
|
995
|
-
}
|
|
996
|
-
break;
|
|
997
|
-
case 'ERROR':
|
|
998
|
-
this.emit('error', new RelayError(envelope.payload.message));
|
|
999
|
-
break;
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
private sendEnvelope(envelope: Envelope): void {
|
|
1004
|
-
if (!this.socket || !this.connected) {
|
|
1005
|
-
throw new RelayError('Not connected to relay daemon');
|
|
1006
|
-
}
|
|
1007
|
-
this.socket.write(encodeFrame(envelope));
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
async send(
|
|
1011
|
-
to: string,
|
|
1012
|
-
message: string,
|
|
1013
|
-
options: { thread?: string } = {}
|
|
1014
|
-
): Promise<void> {
|
|
1015
|
-
const id = crypto.randomUUID();
|
|
1016
|
-
|
|
1017
|
-
this.sendEnvelope({
|
|
1018
|
-
v: 1,
|
|
1019
|
-
type: 'SEND',
|
|
1020
|
-
id,
|
|
1021
|
-
ts: Date.now(),
|
|
1022
|
-
payload: {
|
|
1023
|
-
to,
|
|
1024
|
-
content: message,
|
|
1025
|
-
thread: options.thread,
|
|
1026
|
-
},
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
async sendAndWait(
|
|
1031
|
-
to: string,
|
|
1032
|
-
message: string,
|
|
1033
|
-
options: { thread?: string; timeoutMs?: number } = {}
|
|
1034
|
-
): Promise<{ from: string; content: string }> {
|
|
1035
|
-
const timeoutMs = options.timeoutMs || 30000;
|
|
1036
|
-
|
|
1037
|
-
return new Promise((resolve, reject) => {
|
|
1038
|
-
const timeout = setTimeout(() => {
|
|
1039
|
-
reject(new RelayError(`Timeout waiting for response from ${to}`));
|
|
1040
|
-
}, timeoutMs);
|
|
1041
|
-
|
|
1042
|
-
const handler = (msg: any) => {
|
|
1043
|
-
if (msg.from === to || msg.thread === options.thread) {
|
|
1044
|
-
clearTimeout(timeout);
|
|
1045
|
-
this.off('message', handler);
|
|
1046
|
-
resolve(msg);
|
|
1047
|
-
}
|
|
1048
|
-
};
|
|
1049
|
-
|
|
1050
|
-
this.on('message', handler);
|
|
1051
|
-
this.send(to, message, options);
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
async spawn(options: {
|
|
1056
|
-
name: string;
|
|
1057
|
-
cli: string;
|
|
1058
|
-
task: string;
|
|
1059
|
-
model?: string;
|
|
1060
|
-
cwd?: string;
|
|
1061
|
-
}): Promise<{ success: boolean; error?: string }> {
|
|
1062
|
-
const id = crypto.randomUUID();
|
|
1063
|
-
|
|
1064
|
-
this.sendEnvelope({
|
|
1065
|
-
v: 1,
|
|
1066
|
-
type: 'SPAWN',
|
|
1067
|
-
id,
|
|
1068
|
-
ts: Date.now(),
|
|
1069
|
-
payload: options,
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
// Wait for ACK
|
|
1073
|
-
return new Promise((resolve) => {
|
|
1074
|
-
this.messageHandlers.set(id, (response) => {
|
|
1075
|
-
resolve({ success: response.success, error: response.error });
|
|
1076
|
-
});
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
async release(
|
|
1081
|
-
name: string,
|
|
1082
|
-
reason?: string
|
|
1083
|
-
): Promise<{ success: boolean; error?: string }> {
|
|
1084
|
-
const id = crypto.randomUUID();
|
|
1085
|
-
|
|
1086
|
-
this.sendEnvelope({
|
|
1087
|
-
v: 1,
|
|
1088
|
-
type: 'RELEASE',
|
|
1089
|
-
id,
|
|
1090
|
-
ts: Date.now(),
|
|
1091
|
-
payload: { name, reason },
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
return new Promise((resolve) => {
|
|
1095
|
-
this.messageHandlers.set(id, (response) => {
|
|
1096
|
-
resolve({ success: response.success, error: response.error });
|
|
1097
|
-
});
|
|
1098
|
-
});
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
async getInbox(options: {
|
|
1102
|
-
limit?: number;
|
|
1103
|
-
unread_only?: boolean;
|
|
1104
|
-
from?: string;
|
|
1105
|
-
channel?: string;
|
|
1106
|
-
} = {}): Promise<any[]> {
|
|
1107
|
-
const id = crypto.randomUUID();
|
|
1108
|
-
|
|
1109
|
-
this.sendEnvelope({
|
|
1110
|
-
v: 1,
|
|
1111
|
-
type: 'INBOX',
|
|
1112
|
-
id,
|
|
1113
|
-
ts: Date.now(),
|
|
1114
|
-
payload: options,
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
return new Promise((resolve) => {
|
|
1118
|
-
this.messageHandlers.set(id, (response) => {
|
|
1119
|
-
resolve(response.messages || []);
|
|
1120
|
-
});
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
async listAgents(options: {
|
|
1125
|
-
include_idle?: boolean;
|
|
1126
|
-
project?: string;
|
|
1127
|
-
} = {}): Promise<any[]> {
|
|
1128
|
-
const id = crypto.randomUUID();
|
|
1129
|
-
|
|
1130
|
-
this.sendEnvelope({
|
|
1131
|
-
v: 1,
|
|
1132
|
-
type: 'WHO',
|
|
1133
|
-
id,
|
|
1134
|
-
ts: Date.now(),
|
|
1135
|
-
payload: options,
|
|
1136
|
-
});
|
|
1137
|
-
|
|
1138
|
-
return new Promise((resolve) => {
|
|
1139
|
-
this.messageHandlers.set(id, (response) => {
|
|
1140
|
-
resolve(response.agents || []);
|
|
1141
|
-
});
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
async getStatus(): Promise<{
|
|
1146
|
-
connected: boolean;
|
|
1147
|
-
agentName: string;
|
|
1148
|
-
project: string;
|
|
1149
|
-
socketPath: string;
|
|
1150
|
-
daemonVersion?: string;
|
|
1151
|
-
uptime?: string;
|
|
1152
|
-
}> {
|
|
1153
|
-
return {
|
|
1154
|
-
connected: this.connected,
|
|
1155
|
-
agentName: this.agentName,
|
|
1156
|
-
project: this.discovery?.project || 'unknown',
|
|
1157
|
-
socketPath: this.discovery?.socketPath || 'unknown',
|
|
1158
|
-
daemonVersion: '0.1.0', // TODO: Get from daemon
|
|
1159
|
-
};
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
disconnect(): void {
|
|
1163
|
-
if (this.socket) {
|
|
1164
|
-
this.socket.end();
|
|
1165
|
-
this.socket = null;
|
|
1166
|
-
}
|
|
1167
|
-
this.connected = false;
|
|
1168
|
-
}
|
|
1169
|
-
}
|
|
1170
|
-
```
|
|
1171
|
-
|
|
1172
|
-
---
|
|
1173
|
-
|
|
1174
|
-
## Error Handling
|
|
1175
|
-
|
|
1176
|
-
```typescript
|
|
1177
|
-
// src/errors.ts
|
|
1178
|
-
|
|
1179
|
-
export class RelayError extends Error {
|
|
1180
|
-
constructor(message: string) {
|
|
1181
|
-
super(message);
|
|
1182
|
-
this.name = 'RelayError';
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
export class DaemonNotRunningError extends RelayError {
|
|
1187
|
-
constructor(message?: string) {
|
|
1188
|
-
super(message || 'Relay daemon is not running. Start with: agent-relay daemon start');
|
|
1189
|
-
this.name = 'DaemonNotRunningError';
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
export class AgentNotFoundError extends RelayError {
|
|
1194
|
-
constructor(agentName: string) {
|
|
1195
|
-
super(`Agent not found: ${agentName}`);
|
|
1196
|
-
this.name = 'AgentNotFoundError';
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
export class TimeoutError extends RelayError {
|
|
1201
|
-
constructor(operation: string, timeoutMs: number) {
|
|
1202
|
-
super(`Timeout after ${timeoutMs}ms: ${operation}`);
|
|
1203
|
-
this.name = 'TimeoutError';
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
export class ConnectionError extends RelayError {
|
|
1208
|
-
constructor(message: string) {
|
|
1209
|
-
super(`Connection error: ${message}`);
|
|
1210
|
-
this.name = 'ConnectionError';
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
```
|
|
1214
|
-
|
|
1215
|
-
---
|
|
1216
|
-
|
|
1217
|
-
## MCP Server Entry Point
|
|
1218
|
-
|
|
1219
|
-
```typescript
|
|
1220
|
-
// src/index.ts
|
|
1221
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
1222
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
1223
|
-
import {
|
|
1224
|
-
CallToolRequestSchema,
|
|
1225
|
-
GetPromptRequestSchema,
|
|
1226
|
-
ListPromptsRequestSchema,
|
|
1227
|
-
ListResourcesRequestSchema,
|
|
1228
|
-
ListToolsRequestSchema,
|
|
1229
|
-
ReadResourceRequestSchema,
|
|
1230
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
1231
|
-
|
|
1232
|
-
import { RelayClient } from './client.js';
|
|
1233
|
-
import { DaemonNotRunningError } from './errors.js';
|
|
1234
|
-
|
|
1235
|
-
// Tools
|
|
1236
|
-
import {
|
|
1237
|
-
relaySendTool,
|
|
1238
|
-
handleRelaySend,
|
|
1239
|
-
relaySpawnTool,
|
|
1240
|
-
handleRelaySpawn,
|
|
1241
|
-
relayReleaseTool,
|
|
1242
|
-
handleRelayRelease,
|
|
1243
|
-
relayInboxTool,
|
|
1244
|
-
handleRelayInbox,
|
|
1245
|
-
relayWhoTool,
|
|
1246
|
-
handleRelayWho,
|
|
1247
|
-
relayStatusTool,
|
|
1248
|
-
handleRelayStatus,
|
|
1249
|
-
} from './tools/index.js';
|
|
1250
|
-
|
|
1251
|
-
// Prompts
|
|
1252
|
-
import { protocolPrompt, getProtocolPrompt } from './prompts/protocol.js';
|
|
1253
|
-
|
|
1254
|
-
// Resources
|
|
1255
|
-
import {
|
|
1256
|
-
agentsResource,
|
|
1257
|
-
getAgentsResource,
|
|
1258
|
-
inboxResource,
|
|
1259
|
-
getInboxResource,
|
|
1260
|
-
projectResource,
|
|
1261
|
-
getProjectResource,
|
|
1262
|
-
} from './resources/index.js';
|
|
1263
|
-
|
|
1264
|
-
const TOOLS = [
|
|
1265
|
-
relaySendTool,
|
|
1266
|
-
relaySpawnTool,
|
|
1267
|
-
relayReleaseTool,
|
|
1268
|
-
relayInboxTool,
|
|
1269
|
-
relayWhoTool,
|
|
1270
|
-
relayStatusTool,
|
|
1271
|
-
];
|
|
1272
|
-
|
|
1273
|
-
const PROMPTS = [protocolPrompt];
|
|
1274
|
-
|
|
1275
|
-
const RESOURCES = [agentsResource, inboxResource, projectResource];
|
|
1276
|
-
|
|
1277
|
-
export async function createServer(): Promise<Server> {
|
|
1278
|
-
// Connect to relay daemon
|
|
1279
|
-
let client: RelayClient;
|
|
1280
|
-
try {
|
|
1281
|
-
client = new RelayClient();
|
|
1282
|
-
await client.connect();
|
|
1283
|
-
} catch (err) {
|
|
1284
|
-
if (err instanceof DaemonNotRunningError) {
|
|
1285
|
-
console.error('ERROR: ' + err.message);
|
|
1286
|
-
console.error('');
|
|
1287
|
-
console.error('To start the daemon:');
|
|
1288
|
-
console.error(' agent-relay daemon start');
|
|
1289
|
-
console.error('');
|
|
1290
|
-
console.error('Or for cloud workspaces, ensure the daemon is running.');
|
|
1291
|
-
process.exit(1);
|
|
1292
|
-
}
|
|
1293
|
-
throw err;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
const server = new Server(
|
|
1297
|
-
{
|
|
1298
|
-
name: 'agent-relay-mcp',
|
|
1299
|
-
version: '0.1.0',
|
|
1300
|
-
},
|
|
1301
|
-
{
|
|
1302
|
-
capabilities: {
|
|
1303
|
-
tools: {},
|
|
1304
|
-
prompts: {},
|
|
1305
|
-
resources: {},
|
|
1306
|
-
},
|
|
1307
|
-
}
|
|
1308
|
-
);
|
|
1309
|
-
|
|
1310
|
-
// List tools
|
|
1311
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
1312
|
-
tools: TOOLS,
|
|
1313
|
-
}));
|
|
1314
|
-
|
|
1315
|
-
// Call tool
|
|
1316
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1317
|
-
const { name, arguments: args } = request.params;
|
|
1318
|
-
|
|
1319
|
-
try {
|
|
1320
|
-
switch (name) {
|
|
1321
|
-
case 'relay_send':
|
|
1322
|
-
return { content: [{ type: 'text', text: await handleRelaySend(client, args) }] };
|
|
1323
|
-
case 'relay_spawn':
|
|
1324
|
-
return { content: [{ type: 'text', text: await handleRelaySpawn(client, args) }] };
|
|
1325
|
-
case 'relay_release':
|
|
1326
|
-
return { content: [{ type: 'text', text: await handleRelayRelease(client, args) }] };
|
|
1327
|
-
case 'relay_inbox':
|
|
1328
|
-
return { content: [{ type: 'text', text: await handleRelayInbox(client, args) }] };
|
|
1329
|
-
case 'relay_who':
|
|
1330
|
-
return { content: [{ type: 'text', text: await handleRelayWho(client, args) }] };
|
|
1331
|
-
case 'relay_status':
|
|
1332
|
-
return { content: [{ type: 'text', text: await handleRelayStatus(client, args) }] };
|
|
1333
|
-
default:
|
|
1334
|
-
throw new Error(`Unknown tool: ${name}`);
|
|
1335
|
-
}
|
|
1336
|
-
} catch (err) {
|
|
1337
|
-
return {
|
|
1338
|
-
content: [{ type: 'text', text: `Error: ${err.message}` }],
|
|
1339
|
-
isError: true,
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
});
|
|
1343
|
-
|
|
1344
|
-
// List prompts
|
|
1345
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1346
|
-
prompts: PROMPTS,
|
|
1347
|
-
}));
|
|
1348
|
-
|
|
1349
|
-
// Get prompt
|
|
1350
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
1351
|
-
const { name } = request.params;
|
|
1352
|
-
|
|
1353
|
-
if (name === 'relay_protocol') {
|
|
1354
|
-
return {
|
|
1355
|
-
description: 'Agent Relay protocol documentation',
|
|
1356
|
-
messages: [
|
|
1357
|
-
{
|
|
1358
|
-
role: 'user',
|
|
1359
|
-
content: { type: 'text', text: getProtocolPrompt() },
|
|
1360
|
-
},
|
|
1361
|
-
],
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
throw new Error(`Unknown prompt: ${name}`);
|
|
1366
|
-
});
|
|
1367
|
-
|
|
1368
|
-
// List resources
|
|
1369
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
1370
|
-
resources: RESOURCES,
|
|
1371
|
-
}));
|
|
1372
|
-
|
|
1373
|
-
// Read resource
|
|
1374
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1375
|
-
const { uri } = request.params;
|
|
1376
|
-
|
|
1377
|
-
switch (uri) {
|
|
1378
|
-
case 'relay://agents':
|
|
1379
|
-
return {
|
|
1380
|
-
contents: [{ uri, mimeType: 'application/json', text: await getAgentsResource(client) }],
|
|
1381
|
-
};
|
|
1382
|
-
case 'relay://inbox':
|
|
1383
|
-
return {
|
|
1384
|
-
contents: [{ uri, mimeType: 'application/json', text: await getInboxResource(client) }],
|
|
1385
|
-
};
|
|
1386
|
-
case 'relay://project':
|
|
1387
|
-
return {
|
|
1388
|
-
contents: [{ uri, mimeType: 'application/json', text: await getProjectResource(client) }],
|
|
1389
|
-
};
|
|
1390
|
-
default:
|
|
1391
|
-
throw new Error(`Unknown resource: ${uri}`);
|
|
1392
|
-
}
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
return server;
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
// Main entry point
|
|
1399
|
-
async function main() {
|
|
1400
|
-
const server = await createServer();
|
|
1401
|
-
const transport = new StdioServerTransport();
|
|
1402
|
-
await server.connect(transport);
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
main().catch((err) => {
|
|
1406
|
-
console.error('Fatal error:', err);
|
|
1407
|
-
process.exit(1);
|
|
1408
|
-
});
|
|
1409
|
-
```
|
|
1410
|
-
|
|
1411
|
-
---
|
|
1412
|
-
|
|
1413
|
-
## CLI Binary
|
|
1414
|
-
|
|
1415
|
-
```typescript
|
|
1416
|
-
// src/bin.ts
|
|
1417
|
-
#!/usr/bin/env node
|
|
1418
|
-
import { parseArgs } from 'node:util';
|
|
1419
|
-
import { runInstall } from './install-cli.js';
|
|
1420
|
-
|
|
1421
|
-
const { values, positionals } = parseArgs({
|
|
1422
|
-
allowPositionals: true,
|
|
1423
|
-
options: {
|
|
1424
|
-
help: { type: 'boolean', short: 'h' },
|
|
1425
|
-
version: { type: 'boolean', short: 'v' },
|
|
1426
|
-
editor: { type: 'string', short: 'e' },
|
|
1427
|
-
global: { type: 'boolean', short: 'g' },
|
|
1428
|
-
},
|
|
1429
|
-
});
|
|
1430
|
-
|
|
1431
|
-
const command = positionals[0];
|
|
1432
|
-
|
|
1433
|
-
if (values.help || !command) {
|
|
1434
|
-
console.log(`
|
|
1435
|
-
@agent-relay/mcp - MCP Server for Agent Relay
|
|
1436
|
-
|
|
1437
|
-
Usage:
|
|
1438
|
-
npx @agent-relay/mcp <command> [options]
|
|
1439
|
-
|
|
1440
|
-
Commands:
|
|
1441
|
-
install Install MCP server for your editor
|
|
1442
|
-
serve Run the MCP server (used by editors)
|
|
1443
|
-
|
|
1444
|
-
Install Options:
|
|
1445
|
-
-e, --editor <name> Editor to configure (claude, cursor, vscode, auto)
|
|
1446
|
-
-g, --global Install globally (not project-specific)
|
|
1447
|
-
|
|
1448
|
-
Examples:
|
|
1449
|
-
npx @agent-relay/mcp install # Auto-detect editor
|
|
1450
|
-
npx @agent-relay/mcp install --editor claude # Claude Code only
|
|
1451
|
-
npx @agent-relay/mcp install --editor cursor # Cursor only
|
|
1452
|
-
npx @agent-relay/mcp serve # Run server (for editors)
|
|
1453
|
-
`);
|
|
1454
|
-
process.exit(0);
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
if (values.version) {
|
|
1458
|
-
console.log('0.1.0');
|
|
1459
|
-
process.exit(0);
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
switch (command) {
|
|
1463
|
-
case 'install':
|
|
1464
|
-
runInstall({
|
|
1465
|
-
editor: values.editor as string | undefined,
|
|
1466
|
-
global: values.global as boolean | undefined,
|
|
1467
|
-
});
|
|
1468
|
-
break;
|
|
1469
|
-
|
|
1470
|
-
case 'serve':
|
|
1471
|
-
// Import and run the server
|
|
1472
|
-
import('./index.js');
|
|
1473
|
-
break;
|
|
1474
|
-
|
|
1475
|
-
default:
|
|
1476
|
-
console.error(`Unknown command: ${command}`);
|
|
1477
|
-
console.error('Run with --help for usage');
|
|
1478
|
-
process.exit(1);
|
|
1479
|
-
}
|
|
1480
|
-
```
|
|
1481
|
-
|
|
1482
|
-
---
|
|
1483
|
-
|
|
1484
|
-
## Installation System
|
|
1485
|
-
|
|
1486
|
-
```typescript
|
|
1487
|
-
// src/install-cli.ts
|
|
1488
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1489
|
-
import { join } from 'node:path';
|
|
1490
|
-
import { homedir } from 'node:os';
|
|
1491
|
-
|
|
1492
|
-
interface InstallOptions {
|
|
1493
|
-
editor?: string;
|
|
1494
|
-
global?: boolean;
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
interface EditorConfig {
|
|
1498
|
-
name: string;
|
|
1499
|
-
configPath: string;
|
|
1500
|
-
configKey: string;
|
|
1501
|
-
format: 'json' | 'jsonc';
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
const EDITORS: Record<string, EditorConfig> = {
|
|
1505
|
-
claude: {
|
|
1506
|
-
name: 'Claude Code',
|
|
1507
|
-
configPath: join(homedir(), '.claude', 'settings.json'),
|
|
1508
|
-
configKey: 'mcpServers',
|
|
1509
|
-
format: 'json',
|
|
1510
|
-
},
|
|
1511
|
-
cursor: {
|
|
1512
|
-
name: 'Cursor',
|
|
1513
|
-
configPath: join(homedir(), '.cursor', 'mcp.json'),
|
|
1514
|
-
configKey: 'mcpServers',
|
|
1515
|
-
format: 'json',
|
|
1516
|
-
},
|
|
1517
|
-
vscode: {
|
|
1518
|
-
name: 'VS Code',
|
|
1519
|
-
configPath: join(homedir(), '.vscode', 'mcp.json'),
|
|
1520
|
-
configKey: 'mcpServers',
|
|
1521
|
-
format: 'jsonc',
|
|
1522
|
-
},
|
|
1523
|
-
};
|
|
1524
|
-
|
|
1525
|
-
const MCP_SERVER_CONFIG = {
|
|
1526
|
-
command: 'npx',
|
|
1527
|
-
args: ['@agent-relay/mcp', 'serve'],
|
|
1528
|
-
};
|
|
1529
|
-
|
|
1530
|
-
export function runInstall(options: InstallOptions): void {
|
|
1531
|
-
const editors = options.editor
|
|
1532
|
-
? [options.editor]
|
|
1533
|
-
: detectInstalledEditors();
|
|
1534
|
-
|
|
1535
|
-
if (editors.length === 0) {
|
|
1536
|
-
console.log('No supported editors detected.');
|
|
1537
|
-
console.log('Supported editors: claude, cursor, vscode');
|
|
1538
|
-
console.log('');
|
|
1539
|
-
console.log('Specify manually with: npx @agent-relay/mcp install --editor <name>');
|
|
1540
|
-
process.exit(1);
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
console.log('Installing Agent Relay MCP server...');
|
|
1544
|
-
console.log('');
|
|
1545
|
-
|
|
1546
|
-
for (const editorKey of editors) {
|
|
1547
|
-
const editor = EDITORS[editorKey];
|
|
1548
|
-
if (!editor) {
|
|
1549
|
-
console.log(`Unknown editor: ${editorKey}`);
|
|
1550
|
-
continue;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
try {
|
|
1554
|
-
installForEditor(editor, options.global);
|
|
1555
|
-
console.log(` ✓ ${editor.name} configured`);
|
|
1556
|
-
} catch (err) {
|
|
1557
|
-
console.log(` ✗ ${editor.name}: ${err.message}`);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
console.log('');
|
|
1562
|
-
console.log('Installation complete!');
|
|
1563
|
-
console.log('');
|
|
1564
|
-
console.log('The relay tools will be available when you start your editor.');
|
|
1565
|
-
console.log('Make sure the relay daemon is running: agent-relay daemon start');
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
function detectInstalledEditors(): string[] {
|
|
1569
|
-
const detected: string[] = [];
|
|
1570
|
-
|
|
1571
|
-
for (const [key, editor] of Object.entries(EDITORS)) {
|
|
1572
|
-
// Check if config directory exists
|
|
1573
|
-
const configDir = join(editor.configPath, '..');
|
|
1574
|
-
if (existsSync(configDir)) {
|
|
1575
|
-
detected.push(key);
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
return detected;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
function installForEditor(editor: EditorConfig, global?: boolean): void {
|
|
1583
|
-
// Ensure directory exists
|
|
1584
|
-
const configDir = join(editor.configPath, '..');
|
|
1585
|
-
if (!existsSync(configDir)) {
|
|
1586
|
-
mkdirSync(configDir, { recursive: true });
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
// Read existing config or create new
|
|
1590
|
-
let config: Record<string, any> = {};
|
|
1591
|
-
if (existsSync(editor.configPath)) {
|
|
1592
|
-
const content = readFileSync(editor.configPath, 'utf-8');
|
|
1593
|
-
// Handle JSONC (comments) by stripping them
|
|
1594
|
-
const jsonContent = editor.format === 'jsonc'
|
|
1595
|
-
? content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
|
|
1596
|
-
: content;
|
|
1597
|
-
try {
|
|
1598
|
-
config = JSON.parse(jsonContent);
|
|
1599
|
-
} catch {
|
|
1600
|
-
// Start fresh if invalid
|
|
1601
|
-
config = {};
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// Add MCP server config
|
|
1606
|
-
if (!config[editor.configKey]) {
|
|
1607
|
-
config[editor.configKey] = {};
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
config[editor.configKey]['agent-relay'] = MCP_SERVER_CONFIG;
|
|
1611
|
-
|
|
1612
|
-
// Write config
|
|
1613
|
-
writeFileSync(editor.configPath, JSON.stringify(config, null, 2));
|
|
1614
|
-
}
|
|
1615
|
-
```
|
|
1616
|
-
|
|
1617
|
-
---
|
|
1618
|
-
|
|
1619
|
-
## Cloud Dockerfile Integration
|
|
1620
|
-
|
|
1621
|
-
Add to `deploy/workspace/Dockerfile`:
|
|
1622
|
-
|
|
1623
|
-
```dockerfile
|
|
1624
|
-
# === MCP Server for Agent Relay ===
|
|
1625
|
-
# Pre-install the MCP server so all CLIs have relay tools available
|
|
1626
|
-
|
|
1627
|
-
# Install the MCP package globally
|
|
1628
|
-
RUN npm install -g @agent-relay/mcp
|
|
1629
|
-
|
|
1630
|
-
# Configure for Claude Code (workspace user)
|
|
1631
|
-
RUN mkdir -p /home/workspace/.claude && \
|
|
1632
|
-
echo '{"mcpServers":{"agent-relay":{"command":"npx","args":["@agent-relay/mcp","serve"]}}}' \
|
|
1633
|
-
> /home/workspace/.claude/settings.json && \
|
|
1634
|
-
chown -R workspace:workspace /home/workspace/.claude
|
|
1635
|
-
|
|
1636
|
-
# Configure for Cursor
|
|
1637
|
-
RUN mkdir -p /home/workspace/.cursor && \
|
|
1638
|
-
echo '{"mcpServers":{"agent-relay":{"command":"npx","args":["@agent-relay/mcp","serve"]}}}' \
|
|
1639
|
-
> /home/workspace/.cursor/mcp.json && \
|
|
1640
|
-
chown -R workspace:workspace /home/workspace/.cursor
|
|
1641
|
-
|
|
1642
|
-
# Set environment for socket discovery
|
|
1643
|
-
ENV RELAY_PROJECT=${WORKSPACE_NAME:-default}
|
|
1644
|
-
```
|
|
1645
|
-
|
|
1646
|
-
---
|
|
1647
|
-
|
|
1648
|
-
## CLI Integration
|
|
1649
|
-
|
|
1650
|
-
Add to main `agent-relay` CLI in `packages/cli/src/commands/mcp.ts`:
|
|
1651
|
-
|
|
1652
|
-
```typescript
|
|
1653
|
-
// packages/cli/src/commands/mcp.ts
|
|
1654
|
-
import { Command } from 'commander';
|
|
1655
|
-
|
|
1656
|
-
export const mcpCommand = new Command('mcp')
|
|
1657
|
-
.description('MCP server management')
|
|
1658
|
-
.addCommand(
|
|
1659
|
-
new Command('install')
|
|
1660
|
-
.description('Install MCP server for editors')
|
|
1661
|
-
.option('-e, --editor <name>', 'Editor to configure')
|
|
1662
|
-
.option('-g, --global', 'Install globally')
|
|
1663
|
-
.action(async (options) => {
|
|
1664
|
-
// Dynamic import to avoid bundling mcp package
|
|
1665
|
-
const { runInstall } = await import('@agent-relay/mcp/install');
|
|
1666
|
-
runInstall(options);
|
|
1667
|
-
})
|
|
1668
|
-
)
|
|
1669
|
-
.addCommand(
|
|
1670
|
-
new Command('serve')
|
|
1671
|
-
.description('Run MCP server')
|
|
1672
|
-
.action(async () => {
|
|
1673
|
-
await import('@agent-relay/mcp');
|
|
1674
|
-
})
|
|
1675
|
-
);
|
|
1676
|
-
```
|
|
1677
|
-
|
|
1678
|
-
Add to `agent-relay setup` command:
|
|
1679
|
-
|
|
1680
|
-
```typescript
|
|
1681
|
-
// In setup command
|
|
1682
|
-
async function setupWorkspace(): Promise<void> {
|
|
1683
|
-
// ... existing setup ...
|
|
1684
|
-
|
|
1685
|
-
// Offer MCP installation
|
|
1686
|
-
const { installMcp } = await prompt({
|
|
1687
|
-
type: 'confirm',
|
|
1688
|
-
name: 'installMcp',
|
|
1689
|
-
message: 'Install MCP server for AI editors? (Claude Code, Cursor)',
|
|
1690
|
-
default: true,
|
|
1691
|
-
});
|
|
1692
|
-
|
|
1693
|
-
if (installMcp) {
|
|
1694
|
-
const { runInstall } = await import('@agent-relay/mcp/install');
|
|
1695
|
-
runInstall({ editor: 'auto' });
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
```
|
|
1699
|
-
|
|
1700
|
-
---
|
|
1701
|
-
|
|
1702
|
-
## Testing
|
|
1703
|
-
|
|
1704
|
-
### Unit Tests
|
|
1705
|
-
|
|
1706
|
-
```typescript
|
|
1707
|
-
// tests/tools.test.ts
|
|
1708
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
1709
|
-
import { handleRelaySend, handleRelaySpawn, handleRelayWho } from '../src/tools/index.js';
|
|
1710
|
-
|
|
1711
|
-
describe('relay_send', () => {
|
|
1712
|
-
const mockClient = {
|
|
1713
|
-
send: vi.fn(),
|
|
1714
|
-
sendAndWait: vi.fn(),
|
|
1715
|
-
};
|
|
1716
|
-
|
|
1717
|
-
beforeEach(() => {
|
|
1718
|
-
vi.clearAllMocks();
|
|
1719
|
-
});
|
|
1720
|
-
|
|
1721
|
-
it('sends direct message', async () => {
|
|
1722
|
-
mockClient.send.mockResolvedValue(undefined);
|
|
1723
|
-
|
|
1724
|
-
const result = await handleRelaySend(mockClient, {
|
|
1725
|
-
to: 'Alice',
|
|
1726
|
-
message: 'Hello',
|
|
1727
|
-
});
|
|
1728
|
-
|
|
1729
|
-
expect(result).toBe('Message sent to Alice');
|
|
1730
|
-
expect(mockClient.send).toHaveBeenCalledWith('Alice', 'Hello', {});
|
|
1731
|
-
});
|
|
1732
|
-
|
|
1733
|
-
it('sends to channel', async () => {
|
|
1734
|
-
mockClient.send.mockResolvedValue(undefined);
|
|
1735
|
-
|
|
1736
|
-
const result = await handleRelaySend(mockClient, {
|
|
1737
|
-
to: '#general',
|
|
1738
|
-
message: 'Team update',
|
|
1739
|
-
});
|
|
1740
|
-
|
|
1741
|
-
expect(result).toBe('Message sent to #general');
|
|
1742
|
-
});
|
|
1743
|
-
|
|
1744
|
-
it('awaits response when requested', async () => {
|
|
1745
|
-
mockClient.sendAndWait.mockResolvedValue({
|
|
1746
|
-
from: 'Worker',
|
|
1747
|
-
content: 'Done!',
|
|
1748
|
-
});
|
|
1749
|
-
|
|
1750
|
-
const result = await handleRelaySend(mockClient, {
|
|
1751
|
-
to: 'Worker',
|
|
1752
|
-
message: 'Process this',
|
|
1753
|
-
await_response: true,
|
|
1754
|
-
});
|
|
1755
|
-
|
|
1756
|
-
expect(result).toBe('Response from Worker: Done!');
|
|
1757
|
-
});
|
|
1758
|
-
});
|
|
1759
|
-
|
|
1760
|
-
describe('relay_spawn', () => {
|
|
1761
|
-
const mockClient = {
|
|
1762
|
-
spawn: vi.fn(),
|
|
1763
|
-
};
|
|
1764
|
-
|
|
1765
|
-
it('spawns worker successfully', async () => {
|
|
1766
|
-
mockClient.spawn.mockResolvedValue({ success: true });
|
|
1767
|
-
|
|
1768
|
-
const result = await handleRelaySpawn(mockClient, {
|
|
1769
|
-
name: 'TestRunner',
|
|
1770
|
-
cli: 'claude',
|
|
1771
|
-
task: 'Run tests',
|
|
1772
|
-
});
|
|
1773
|
-
|
|
1774
|
-
expect(result).toContain('spawned successfully');
|
|
1775
|
-
});
|
|
1776
|
-
|
|
1777
|
-
it('handles spawn failure', async () => {
|
|
1778
|
-
mockClient.spawn.mockResolvedValue({ success: false, error: 'Out of resources' });
|
|
1779
|
-
|
|
1780
|
-
const result = await handleRelaySpawn(mockClient, {
|
|
1781
|
-
name: 'TestRunner',
|
|
1782
|
-
cli: 'claude',
|
|
1783
|
-
task: 'Run tests',
|
|
1784
|
-
});
|
|
1785
|
-
|
|
1786
|
-
expect(result).toContain('Failed to spawn');
|
|
1787
|
-
expect(result).toContain('Out of resources');
|
|
1788
|
-
});
|
|
1789
|
-
});
|
|
1790
|
-
```
|
|
1791
|
-
|
|
1792
|
-
### Discovery Tests
|
|
1793
|
-
|
|
1794
|
-
```typescript
|
|
1795
|
-
// tests/discover.test.ts
|
|
1796
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
1797
|
-
import { discoverSocket } from '../src/discover.js';
|
|
1798
|
-
import { existsSync } from 'node:fs';
|
|
1799
|
-
|
|
1800
|
-
vi.mock('node:fs');
|
|
1801
|
-
|
|
1802
|
-
describe('discoverSocket', () => {
|
|
1803
|
-
beforeEach(() => {
|
|
1804
|
-
vi.clearAllMocks();
|
|
1805
|
-
delete process.env.RELAY_SOCKET;
|
|
1806
|
-
delete process.env.RELAY_PROJECT;
|
|
1807
|
-
});
|
|
1808
|
-
|
|
1809
|
-
it('uses RELAY_SOCKET env var first', () => {
|
|
1810
|
-
process.env.RELAY_SOCKET = '/tmp/test.sock';
|
|
1811
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1812
|
-
|
|
1813
|
-
const result = discoverSocket();
|
|
1814
|
-
|
|
1815
|
-
expect(result?.socketPath).toBe('/tmp/test.sock');
|
|
1816
|
-
expect(result?.source).toBe('env');
|
|
1817
|
-
});
|
|
1818
|
-
|
|
1819
|
-
it('uses RELAY_PROJECT env var second', () => {
|
|
1820
|
-
process.env.RELAY_PROJECT = 'myproject';
|
|
1821
|
-
vi.mocked(existsSync).mockImplementation((path) => {
|
|
1822
|
-
return String(path).includes('myproject');
|
|
1823
|
-
});
|
|
1824
|
-
|
|
1825
|
-
const result = discoverSocket();
|
|
1826
|
-
|
|
1827
|
-
expect(result?.project).toBe('myproject');
|
|
1828
|
-
expect(result?.source).toBe('env');
|
|
1829
|
-
});
|
|
1830
|
-
|
|
1831
|
-
it('returns null when no socket found', () => {
|
|
1832
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
1833
|
-
|
|
1834
|
-
const result = discoverSocket();
|
|
1835
|
-
|
|
1836
|
-
expect(result).toBeNull();
|
|
1837
|
-
});
|
|
1838
|
-
});
|
|
1839
|
-
```
|
|
1840
|
-
|
|
1841
|
-
### Integration Tests
|
|
1842
|
-
|
|
1843
|
-
```typescript
|
|
1844
|
-
// tests/integration.test.ts
|
|
1845
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
1846
|
-
import { createServer } from '../src/index.js';
|
|
1847
|
-
import { spawn } from 'node:child_process';
|
|
1848
|
-
|
|
1849
|
-
describe('MCP Server Integration', () => {
|
|
1850
|
-
let daemonProcess: any;
|
|
1851
|
-
|
|
1852
|
-
beforeAll(async () => {
|
|
1853
|
-
// Start a test daemon
|
|
1854
|
-
daemonProcess = spawn('agent-relay', ['daemon', 'start', '--test']);
|
|
1855
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1856
|
-
});
|
|
1857
|
-
|
|
1858
|
-
afterAll(() => {
|
|
1859
|
-
daemonProcess?.kill();
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
it('connects to daemon and lists tools', async () => {
|
|
1863
|
-
const server = await createServer();
|
|
1864
|
-
// Test would interact with server here
|
|
1865
|
-
});
|
|
1866
|
-
});
|
|
1867
|
-
```
|
|
1868
|
-
|
|
1869
|
-
---
|
|
1870
|
-
|
|
1871
|
-
## Implementation Order
|
|
1872
|
-
|
|
1873
|
-
### Phase 1: Core Infrastructure
|
|
1874
|
-
1. Create package structure (`packages/mcp/`)
|
|
1875
|
-
2. Implement socket discovery (`discover.ts`)
|
|
1876
|
-
3. Implement relay client (`client.ts`)
|
|
1877
|
-
4. Implement error types (`errors.ts`)
|
|
1878
|
-
|
|
1879
|
-
### Phase 2: MCP Tools
|
|
1880
|
-
1. Implement `relay_send` tool
|
|
1881
|
-
2. Implement `relay_inbox` tool
|
|
1882
|
-
3. Implement `relay_who` tool
|
|
1883
|
-
4. Implement `relay_status` tool
|
|
1884
|
-
5. Implement `relay_spawn` tool
|
|
1885
|
-
6. Implement `relay_release` tool
|
|
1886
|
-
|
|
1887
|
-
### Phase 3: MCP Server
|
|
1888
|
-
1. Implement MCP server entry point (`index.ts`)
|
|
1889
|
-
2. Add protocol prompt (`prompts/protocol.ts`)
|
|
1890
|
-
3. Add resources (`resources/*.ts`)
|
|
1891
|
-
4. Implement CLI binary (`bin.ts`)
|
|
1892
|
-
|
|
1893
|
-
### Phase 4: Installation
|
|
1894
|
-
1. Implement editor installation (`install.ts`, `install-cli.ts`)
|
|
1895
|
-
2. Add to main CLI (`agent-relay mcp install`)
|
|
1896
|
-
3. Add to setup command
|
|
1897
|
-
|
|
1898
|
-
### Phase 5: Cloud Integration
|
|
1899
|
-
1. Update workspace Dockerfile
|
|
1900
|
-
2. Add environment variables
|
|
1901
|
-
3. Test with all CLI tools (Claude, Codex, Gemini, Droid, OpenCode)
|
|
1902
|
-
|
|
1903
|
-
---
|
|
1904
|
-
|
|
1905
|
-
## Success Criteria
|
|
1906
|
-
|
|
1907
|
-
1. **Local Install**: `npx @agent-relay/mcp install` works and configures Claude Code
|
|
1908
|
-
2. **Cloud Install**: Workspaces have MCP pre-configured for all CLI tools
|
|
1909
|
-
3. **All Tools Work**: All 6 tools function correctly
|
|
1910
|
-
4. **Protocol Doc**: Full protocol documentation available via prompts
|
|
1911
|
-
5. **Error Messages**: Clear errors when daemon not running
|
|
1912
|
-
6. **Multi-Project**: Socket discovery works across projects
|
|
1913
|
-
|
|
1914
|
-
---
|
|
1915
|
-
|
|
1916
|
-
## Dependencies
|
|
1917
|
-
|
|
1918
|
-
- `@modelcontextprotocol/sdk` - MCP SDK
|
|
1919
|
-
- `@agent-relay/protocol` - Protocol types and framing
|
|
1920
|
-
- `zod` - Schema validation (already in protocol)
|
|
1921
|
-
|
|
1922
|
-
No new external dependencies required beyond MCP SDK.
|