stream0-channel 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/stream0-channel.mjs +390 -0
- package/package.json +17 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Stream0 Channel - MCP server for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Bridges Stream0 inbox <-> Claude Code session.
|
|
6
|
+
* Install: npx stream0-channel
|
|
7
|
+
*
|
|
8
|
+
* Environment variables:
|
|
9
|
+
* STREAM0_URL - Stream0 server URL (default: http://localhost:8080)
|
|
10
|
+
* STREAM0_API_KEY - API key for authentication
|
|
11
|
+
* STREAM0_AGENT_ID - This agent's ID on Stream0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import {
|
|
17
|
+
ListToolsRequestSchema,
|
|
18
|
+
CallToolRequestSchema,
|
|
19
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
+
|
|
21
|
+
const STREAM0_URL = process.env.STREAM0_URL || "http://localhost:8080";
|
|
22
|
+
const STREAM0_API_KEY = process.env.STREAM0_API_KEY || "";
|
|
23
|
+
const AGENT_ID = process.env.STREAM0_AGENT_ID || "";
|
|
24
|
+
|
|
25
|
+
if (!AGENT_ID) {
|
|
26
|
+
console.error("[stream0-channel] STREAM0_AGENT_ID not set");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const headers = { "Content-Type": "application/json" };
|
|
31
|
+
if (STREAM0_API_KEY) headers["X-API-Key"] = STREAM0_API_KEY;
|
|
32
|
+
|
|
33
|
+
// --- Stream0 HTTP helpers ---
|
|
34
|
+
|
|
35
|
+
async function stream0Get(path, params) {
|
|
36
|
+
const url = new URL(`${STREAM0_URL}${path}`);
|
|
37
|
+
if (params) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
38
|
+
const resp = await fetch(url.toString(), { headers, signal: AbortSignal.timeout(35000) });
|
|
39
|
+
return resp.json();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function stream0Post(path, body) {
|
|
43
|
+
const resp = await fetch(`${STREAM0_URL}${path}`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers,
|
|
46
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
47
|
+
signal: AbortSignal.timeout(10000),
|
|
48
|
+
});
|
|
49
|
+
return resp.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sleep(ms) {
|
|
53
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- MCP Server ---
|
|
57
|
+
|
|
58
|
+
const mcp = new Server(
|
|
59
|
+
{ name: "stream0-channel", version: "0.2.0" },
|
|
60
|
+
{
|
|
61
|
+
capabilities: {
|
|
62
|
+
experimental: { "claude/channel": {} },
|
|
63
|
+
tools: {},
|
|
64
|
+
},
|
|
65
|
+
instructions: `You are connected to Stream0, an agent collaboration network.
|
|
66
|
+
|
|
67
|
+
## Proactive collaboration
|
|
68
|
+
|
|
69
|
+
When the user asks you to collaborate with, delegate to, or consult other agents:
|
|
70
|
+
1. Use the **discover** tool to see which agents are available and what they do
|
|
71
|
+
2. Use the **delegate** tool to send a task and wait for the result
|
|
72
|
+
3. Present the result to the user
|
|
73
|
+
|
|
74
|
+
Examples of user requests that should trigger collaboration:
|
|
75
|
+
- "find someone to review my code"
|
|
76
|
+
- "ask the reviewer to look at this"
|
|
77
|
+
- "get feedback from other agents"
|
|
78
|
+
- "discuss this with the team"
|
|
79
|
+
|
|
80
|
+
## Responding to incoming messages
|
|
81
|
+
|
|
82
|
+
Messages from other agents arrive as <channel source="stream0-channel" thread_id="..." from="..." type="..."> tags.
|
|
83
|
+
|
|
84
|
+
When you receive a message:
|
|
85
|
+
1. Read it and understand what's being asked
|
|
86
|
+
2. Do the work
|
|
87
|
+
3. Reply using the reply tool with the thread_id and the sender's agent ID
|
|
88
|
+
4. Acknowledge the message using the ack tool with the message_id
|
|
89
|
+
|
|
90
|
+
Message types: request (do work), question (clarification needed), answer (response to your question), done (task complete), failed (task failed), message (general).
|
|
91
|
+
|
|
92
|
+
Always reply to requests with either done or failed. Never leave a request unanswered.`,
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// --- Tools ---
|
|
97
|
+
|
|
98
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
99
|
+
tools: [
|
|
100
|
+
{
|
|
101
|
+
name: "discover",
|
|
102
|
+
description:
|
|
103
|
+
"List all available agents on Stream0 with their descriptions. Use this to find agents that can help with a task.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "delegate",
|
|
111
|
+
description:
|
|
112
|
+
"Send a task to another agent and wait for their response. Handles the full lifecycle: sends the request, waits for the result, and returns the response. Use this when the user asks you to collaborate with or get help from another agent.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
to: {
|
|
117
|
+
type: "string",
|
|
118
|
+
description: "The agent ID to send the task to",
|
|
119
|
+
},
|
|
120
|
+
task: {
|
|
121
|
+
type: "string",
|
|
122
|
+
description: "Description of what you need the agent to do",
|
|
123
|
+
},
|
|
124
|
+
context: {
|
|
125
|
+
type: "string",
|
|
126
|
+
description:
|
|
127
|
+
"Additional context like code diffs, file contents, or other details the agent needs",
|
|
128
|
+
},
|
|
129
|
+
timeout: {
|
|
130
|
+
type: "number",
|
|
131
|
+
description: "Max seconds to wait for a response (default: 120, max: 300)",
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
required: ["to", "task"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "reply",
|
|
139
|
+
description:
|
|
140
|
+
"Send a reply back through Stream0 to another agent. Use this after processing an incoming message.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
type: "object",
|
|
143
|
+
properties: {
|
|
144
|
+
to: { type: "string", description: "The agent ID to reply to (from the channel message)" },
|
|
145
|
+
thread_id: { type: "string", description: "The thread_id from the incoming message" },
|
|
146
|
+
type: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description: "Message type: done, failed, answer, question, or message",
|
|
149
|
+
},
|
|
150
|
+
content: { type: "string", description: "Reply content as JSON string" },
|
|
151
|
+
},
|
|
152
|
+
required: ["to", "thread_id", "type", "content"],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "ack",
|
|
157
|
+
description: "Acknowledge a message after processing it so it won't appear again.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {
|
|
161
|
+
message_id: { type: "string", description: "The message ID to acknowledge" },
|
|
162
|
+
},
|
|
163
|
+
required: ["message_id"],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
170
|
+
const { name, arguments: args } = req.params;
|
|
171
|
+
|
|
172
|
+
// --- discover ---
|
|
173
|
+
if (name === "discover") {
|
|
174
|
+
const result = await stream0Get("/agents");
|
|
175
|
+
const agents = (result?.agents || [])
|
|
176
|
+
.filter((a) => a.id !== AGENT_ID)
|
|
177
|
+
.map((a) => ({
|
|
178
|
+
id: a.id,
|
|
179
|
+
description: a.description || "(no description)",
|
|
180
|
+
aliases: a.aliases || [],
|
|
181
|
+
online: a.last_seen
|
|
182
|
+
? Date.now() - new Date(a.last_seen).getTime() < 5 * 60 * 1000
|
|
183
|
+
: false,
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
if (agents.length === 0) {
|
|
187
|
+
return {
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: "text",
|
|
191
|
+
text: "No other agents are registered on Stream0. Start a worker agent first.",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const lines = agents.map(
|
|
198
|
+
(a) =>
|
|
199
|
+
`- **${a.id}**${a.online ? " (online)" : " (offline)"}: ${a.description}${a.aliases.length ? ` [aliases: ${a.aliases.join(", ")}]` : ""}`
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
content: [
|
|
204
|
+
{ type: "text", text: `Available agents:\n\n${lines.join("\n")}` },
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- delegate ---
|
|
210
|
+
if (name === "delegate") {
|
|
211
|
+
const { to, task, context, timeout: userTimeout } = args;
|
|
212
|
+
|
|
213
|
+
const timeoutSec = Math.min(Math.max(userTimeout || 120, 10), 300);
|
|
214
|
+
const threadId = `delegate-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
215
|
+
|
|
216
|
+
const content = { task };
|
|
217
|
+
if (context) content.context = context;
|
|
218
|
+
|
|
219
|
+
await stream0Post(`/agents/${to}/inbox`, {
|
|
220
|
+
thread_id: threadId,
|
|
221
|
+
from: AGENT_ID,
|
|
222
|
+
type: "request",
|
|
223
|
+
content,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
console.error(
|
|
227
|
+
`[stream0-channel] Delegated to ${to} (thread: ${threadId}), waiting up to ${timeoutSec}s...`
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
231
|
+
|
|
232
|
+
while (Date.now() < deadline) {
|
|
233
|
+
const pollTimeout = Math.min(25, Math.ceil((deadline - Date.now()) / 1000));
|
|
234
|
+
if (pollTimeout <= 0) break;
|
|
235
|
+
|
|
236
|
+
const result = await stream0Get(`/agents/${AGENT_ID}/inbox`, {
|
|
237
|
+
status: "unread",
|
|
238
|
+
thread_id: threadId,
|
|
239
|
+
timeout: String(pollTimeout),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const messages = result?.messages || [];
|
|
243
|
+
for (const msg of messages) {
|
|
244
|
+
await stream0Post(`/inbox/messages/${msg.id}/ack`);
|
|
245
|
+
|
|
246
|
+
if (msg.type === "done") {
|
|
247
|
+
const responseText =
|
|
248
|
+
typeof msg.content === "string"
|
|
249
|
+
? msg.content
|
|
250
|
+
: JSON.stringify(msg.content, null, 2);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `**${to}** completed the task (thread: ${threadId}):\n\n${responseText}`,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (msg.type === "failed") {
|
|
263
|
+
const errorText =
|
|
264
|
+
typeof msg.content === "string"
|
|
265
|
+
? msg.content
|
|
266
|
+
: JSON.stringify(msg.content, null, 2);
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
content: [
|
|
270
|
+
{
|
|
271
|
+
type: "text",
|
|
272
|
+
text: `**${to}** failed (thread: ${threadId}):\n\n${errorText}`,
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (msg.type === "question") {
|
|
279
|
+
const questionText =
|
|
280
|
+
typeof msg.content === "string"
|
|
281
|
+
? msg.content
|
|
282
|
+
: JSON.stringify(msg.content, null, 2);
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `**${to}** has a question (thread: ${threadId}):\n\n${questionText}\n\nUse the reply tool to answer: reply to="${to}" thread_id="${threadId}" type="answer"`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.error(
|
|
295
|
+
`[stream0-channel] Received [${msg.type}] from ${msg.from} on delegate thread, continuing to wait...`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: "text",
|
|
304
|
+
text: `Timed out waiting for **${to}** to respond after ${timeoutSec}s (thread: ${threadId}). The agent may still be working.`,
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --- reply ---
|
|
311
|
+
if (name === "reply") {
|
|
312
|
+
const { to, thread_id, type, content } = args;
|
|
313
|
+
|
|
314
|
+
let contentObj;
|
|
315
|
+
try {
|
|
316
|
+
contentObj = JSON.parse(content);
|
|
317
|
+
} catch {
|
|
318
|
+
contentObj = { text: content };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
await stream0Post(`/agents/${to}/inbox`, {
|
|
322
|
+
thread_id,
|
|
323
|
+
from: AGENT_ID,
|
|
324
|
+
type,
|
|
325
|
+
content: contentObj,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return { content: [{ type: "text", text: `Replied to ${to} (thread: ${thread_id})` }] };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// --- ack ---
|
|
332
|
+
if (name === "ack") {
|
|
333
|
+
const { message_id } = args;
|
|
334
|
+
await stream0Post(`/inbox/messages/${message_id}/ack`);
|
|
335
|
+
return { content: [{ type: "text", text: `Acknowledged ${message_id}` }] };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// --- Connect and start polling ---
|
|
342
|
+
|
|
343
|
+
await mcp.connect(new StdioServerTransport());
|
|
344
|
+
|
|
345
|
+
// Register agent on Stream0
|
|
346
|
+
await stream0Post("/agents", { id: AGENT_ID });
|
|
347
|
+
console.error(`[stream0-channel] Registered as ${AGENT_ID}, polling inbox...`);
|
|
348
|
+
|
|
349
|
+
const pushed = new Set();
|
|
350
|
+
|
|
351
|
+
async function pollLoop() {
|
|
352
|
+
while (true) {
|
|
353
|
+
try {
|
|
354
|
+
const result = await stream0Get(`/agents/${AGENT_ID}/inbox`, {
|
|
355
|
+
status: "unread",
|
|
356
|
+
timeout: "25",
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const messages = result?.messages || [];
|
|
360
|
+
for (const msg of messages) {
|
|
361
|
+
if (pushed.has(msg.id)) continue;
|
|
362
|
+
pushed.add(msg.id);
|
|
363
|
+
|
|
364
|
+
console.error(
|
|
365
|
+
`[stream0-channel] Pushing [${msg.type}] from ${msg.from} (thread: ${msg.thread_id})`
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
await mcp.notification({
|
|
369
|
+
method: "notifications/claude/channel",
|
|
370
|
+
params: {
|
|
371
|
+
content: JSON.stringify(msg.content || {}),
|
|
372
|
+
meta: {
|
|
373
|
+
message_id: msg.id,
|
|
374
|
+
thread_id: msg.thread_id,
|
|
375
|
+
from: msg.from,
|
|
376
|
+
type: msg.type,
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
} catch (e) {
|
|
382
|
+
if (e?.name !== "TimeoutError") {
|
|
383
|
+
console.error(`[stream0-channel] Error: ${e?.message || e}`);
|
|
384
|
+
await sleep(3000);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
pollLoop();
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stream0-channel",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Stream0 MCP channel for Claude Code",
|
|
5
|
+
"bin": {
|
|
6
|
+
"stream0-channel": "./bin/stream0-channel.mjs"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/risingwavelabs/stream0"
|
|
16
|
+
}
|
|
17
|
+
}
|