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.
Files changed (133) hide show
  1. package/dist/index.cjs +199 -3
  2. package/package.json +64 -21
  3. package/packages/acp-bridge/package.json +2 -2
  4. package/packages/api-types/package.json +1 -1
  5. package/packages/benchmark/package.json +5 -5
  6. package/packages/bridge/package.json +7 -7
  7. package/packages/cli-tester/package.json +1 -1
  8. package/packages/config/package.json +2 -2
  9. package/packages/continuity/package.json +2 -2
  10. package/packages/daemon/dist/cloud-sync.d.ts +13 -1
  11. package/packages/daemon/dist/cloud-sync.d.ts.map +1 -1
  12. package/packages/daemon/dist/cloud-sync.js +169 -5
  13. package/packages/daemon/dist/cloud-sync.js.map +1 -1
  14. package/packages/daemon/dist/server.d.ts +5 -0
  15. package/packages/daemon/dist/server.d.ts.map +1 -1
  16. package/packages/daemon/dist/server.js +50 -0
  17. package/packages/daemon/dist/server.js.map +1 -1
  18. package/packages/daemon/package.json +12 -12
  19. package/packages/daemon/src/cloud-sync.ts +201 -5
  20. package/packages/daemon/src/server.ts +61 -0
  21. package/packages/hooks/package.json +4 -4
  22. package/packages/mcp/package.json +5 -5
  23. package/packages/memory/package.json +2 -2
  24. package/packages/policy/package.json +2 -2
  25. package/packages/protocol/package.json +1 -1
  26. package/packages/resiliency/package.json +1 -1
  27. package/packages/sdk/dist/client.d.ts +1 -1
  28. package/packages/sdk/dist/client.js +2 -2
  29. package/packages/sdk/package.json +3 -3
  30. package/packages/sdk/src/client.ts +2 -2
  31. package/packages/sdk-ts/README.md +65 -0
  32. package/packages/sdk-ts/dist/__tests__/integration.test.d.ts +2 -0
  33. package/packages/sdk-ts/dist/__tests__/integration.test.d.ts.map +1 -0
  34. package/packages/sdk-ts/dist/__tests__/integration.test.js +139 -0
  35. package/packages/sdk-ts/dist/__tests__/integration.test.js.map +1 -0
  36. package/packages/sdk-ts/dist/__tests__/quickstart.test.d.ts +2 -0
  37. package/packages/sdk-ts/dist/__tests__/quickstart.test.d.ts.map +1 -0
  38. package/packages/sdk-ts/dist/__tests__/quickstart.test.js +176 -0
  39. package/packages/sdk-ts/dist/__tests__/quickstart.test.js.map +1 -0
  40. package/packages/sdk-ts/dist/browser.d.ts +16 -0
  41. package/packages/sdk-ts/dist/browser.d.ts.map +1 -0
  42. package/packages/sdk-ts/dist/browser.js +19 -0
  43. package/packages/sdk-ts/dist/browser.js.map +1 -0
  44. package/packages/sdk-ts/dist/client.d.ts +91 -0
  45. package/packages/sdk-ts/dist/client.d.ts.map +1 -0
  46. package/packages/sdk-ts/dist/client.js +360 -0
  47. package/packages/sdk-ts/dist/client.js.map +1 -0
  48. package/packages/sdk-ts/dist/consensus-helpers.d.ts +103 -0
  49. package/packages/sdk-ts/dist/consensus-helpers.d.ts.map +1 -0
  50. package/packages/sdk-ts/dist/consensus-helpers.js +147 -0
  51. package/packages/sdk-ts/dist/consensus-helpers.js.map +1 -0
  52. package/packages/sdk-ts/dist/consensus.d.ts +72 -0
  53. package/packages/sdk-ts/dist/consensus.d.ts.map +1 -0
  54. package/packages/sdk-ts/dist/consensus.js +378 -0
  55. package/packages/sdk-ts/dist/consensus.js.map +1 -0
  56. package/packages/sdk-ts/dist/examples/demo.d.ts +2 -0
  57. package/packages/sdk-ts/dist/examples/demo.d.ts.map +1 -0
  58. package/packages/sdk-ts/dist/examples/demo.js +63 -0
  59. package/packages/sdk-ts/dist/examples/demo.js.map +1 -0
  60. package/packages/sdk-ts/dist/examples/example.d.ts +2 -0
  61. package/packages/sdk-ts/dist/examples/example.d.ts.map +1 -0
  62. package/packages/sdk-ts/dist/examples/example.js +80 -0
  63. package/packages/sdk-ts/dist/examples/example.js.map +1 -0
  64. package/packages/sdk-ts/dist/examples/quickstart.d.ts +2 -0
  65. package/packages/sdk-ts/dist/examples/quickstart.d.ts.map +1 -0
  66. package/packages/sdk-ts/dist/examples/quickstart.js +56 -0
  67. package/packages/sdk-ts/dist/examples/quickstart.js.map +1 -0
  68. package/packages/sdk-ts/dist/examples/ralph-loop.d.ts +2 -0
  69. package/packages/sdk-ts/dist/examples/ralph-loop.d.ts.map +1 -0
  70. package/packages/sdk-ts/dist/examples/ralph-loop.js +281 -0
  71. package/packages/sdk-ts/dist/examples/ralph-loop.js.map +1 -0
  72. package/packages/sdk-ts/dist/index.d.ts +9 -0
  73. package/packages/sdk-ts/dist/index.d.ts.map +1 -0
  74. package/packages/sdk-ts/dist/index.js +9 -0
  75. package/packages/sdk-ts/dist/index.js.map +1 -0
  76. package/packages/sdk-ts/dist/logs.d.ts +47 -0
  77. package/packages/sdk-ts/dist/logs.d.ts.map +1 -0
  78. package/packages/sdk-ts/dist/logs.js +137 -0
  79. package/packages/sdk-ts/dist/logs.js.map +1 -0
  80. package/packages/sdk-ts/dist/protocol.d.ts +249 -0
  81. package/packages/sdk-ts/dist/protocol.d.ts.map +1 -0
  82. package/packages/sdk-ts/dist/protocol.js +2 -0
  83. package/packages/sdk-ts/dist/protocol.js.map +1 -0
  84. package/packages/sdk-ts/dist/pty.d.ts +8 -0
  85. package/packages/sdk-ts/dist/pty.d.ts.map +1 -0
  86. package/packages/sdk-ts/dist/pty.js +14 -0
  87. package/packages/sdk-ts/dist/pty.js.map +1 -0
  88. package/packages/sdk-ts/dist/relay.d.ts +118 -0
  89. package/packages/sdk-ts/dist/relay.d.ts.map +1 -0
  90. package/packages/sdk-ts/dist/relay.js +355 -0
  91. package/packages/sdk-ts/dist/relay.js.map +1 -0
  92. package/packages/sdk-ts/dist/relaycast.d.ts +57 -0
  93. package/packages/sdk-ts/dist/relaycast.d.ts.map +1 -0
  94. package/packages/sdk-ts/dist/relaycast.js +110 -0
  95. package/packages/sdk-ts/dist/relaycast.js.map +1 -0
  96. package/packages/sdk-ts/dist/shadow.d.ts +100 -0
  97. package/packages/sdk-ts/dist/shadow.d.ts.map +1 -0
  98. package/packages/sdk-ts/dist/shadow.js +174 -0
  99. package/packages/sdk-ts/dist/shadow.js.map +1 -0
  100. package/packages/sdk-ts/package.json +75 -0
  101. package/packages/sdk-ts/scripts/bundle-agent-relay.mjs +53 -0
  102. package/packages/sdk-ts/src/__tests__/integration.test.ts +170 -0
  103. package/packages/sdk-ts/src/__tests__/quickstart.test.ts +198 -0
  104. package/packages/sdk-ts/src/browser.ts +57 -0
  105. package/packages/sdk-ts/src/client.ts +491 -0
  106. package/packages/sdk-ts/src/consensus-helpers.ts +253 -0
  107. package/packages/sdk-ts/src/consensus.ts +506 -0
  108. package/packages/sdk-ts/src/examples/demo.ts +88 -0
  109. package/packages/sdk-ts/src/examples/example.ts +91 -0
  110. package/packages/sdk-ts/src/examples/quickstart.ts +72 -0
  111. package/packages/sdk-ts/src/examples/ralph-loop.ts +352 -0
  112. package/packages/sdk-ts/src/examples/sample-prd.json +37 -0
  113. package/packages/sdk-ts/src/index.ts +8 -0
  114. package/packages/sdk-ts/src/logs.ts +163 -0
  115. package/packages/sdk-ts/src/protocol.ts +266 -0
  116. package/packages/sdk-ts/src/pty.ts +16 -0
  117. package/packages/sdk-ts/src/relay.ts +454 -0
  118. package/packages/sdk-ts/src/relaycast.ts +143 -0
  119. package/packages/sdk-ts/src/shadow.ts +230 -0
  120. package/packages/sdk-ts/tsconfig.json +16 -0
  121. package/packages/spawner/package.json +1 -1
  122. package/packages/state/package.json +1 -1
  123. package/packages/storage/package.json +2 -2
  124. package/packages/telemetry/package.json +1 -1
  125. package/packages/trajectory/package.json +2 -2
  126. package/packages/user-directory/package.json +2 -2
  127. package/packages/utils/package.json +3 -3
  128. package/packages/wrapper/dist/client.js +1 -1
  129. package/packages/wrapper/package.json +6 -6
  130. package/packages/wrapper/src/client.test.ts +1 -1
  131. package/packages/wrapper/src/client.ts +1 -1
  132. package/packages/mcp/SPEC.md +0 -1922
  133. package/packages/mcp/STAFFING_PLAN.md +0 -294
@@ -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.