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.
@@ -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
+ }