@blocks-network/mcp-server 0.1.55 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -14
- package/dist/index.js +35 -305
- package/dist/registry-list.d.ts +33 -0
- package/dist/registry-list.js +33 -0
- package/dist/tools.d.ts +152 -0
- package/dist/tools.js +275 -0
- package/package.json +18 -6
- package/src/index.ts +55 -341
- package/src/registry-list.ts +58 -0
- package/src/tools.ts +496 -0
- package/tests/cancel-task.test.ts +24 -0
- package/tests/connect-task.test.ts +180 -0
- package/tests/get-agent-card.test.ts +53 -0
- package/tests/get-task.test.ts +76 -0
- package/tests/helpers.ts +188 -0
- package/tests/list-agents.test.ts +74 -0
- package/tests/list-tasks.test.ts +58 -0
- package/tests/registry-list.test.ts +120 -0
- package/tests/send-task.test.ts +221 -0
- package/vitest.config.ts +7 -0
- package/LICENSE +0 -26
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { resolve, relative } from 'node:path';
|
|
4
|
-
import { realpathSync
|
|
4
|
+
import { realpathSync } from 'node:fs';
|
|
5
5
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { z } from 'zod';
|
|
@@ -12,10 +12,23 @@ import {
|
|
|
12
12
|
fetchCdmConfig,
|
|
13
13
|
getAgent,
|
|
14
14
|
BLOCKS_MAX_UPLOAD_BYTES,
|
|
15
|
-
type TaskInfo,
|
|
16
|
-
type SendMessageRequestPart,
|
|
17
15
|
} from '@blocks-network/sdk';
|
|
18
16
|
|
|
17
|
+
import { listAgentsAuthenticated } from './registry-list.js';
|
|
18
|
+
import {
|
|
19
|
+
sendTask,
|
|
20
|
+
getTask,
|
|
21
|
+
listTasks,
|
|
22
|
+
cancelTask,
|
|
23
|
+
listAgents,
|
|
24
|
+
getAgentCard,
|
|
25
|
+
connectTask,
|
|
26
|
+
defaultFileSize,
|
|
27
|
+
type ToolDeps,
|
|
28
|
+
type TaskClientLike,
|
|
29
|
+
type AgentEntryLike,
|
|
30
|
+
} from './tools.js';
|
|
31
|
+
|
|
19
32
|
const ALLOWED_ROOT = process.env.BLOCKS_MCP_FILE_ROOT ?? process.cwd();
|
|
20
33
|
|
|
21
34
|
if (!process.env.BLOCKS_API_KEY) {
|
|
@@ -38,12 +51,6 @@ function validateFilePath(filePath: string): string {
|
|
|
38
51
|
return real;
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
function resolveBillingMode(task: TaskInfo): 'free' | 'paid' {
|
|
42
|
-
const mode = task.billingMode as string | undefined;
|
|
43
|
-
if (mode === 'paid') return 'paid';
|
|
44
|
-
return 'free';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
54
|
const taskClients = new Map<string, TaskClient>();
|
|
48
55
|
let resolvedBaseUrl: string | undefined;
|
|
49
56
|
|
|
@@ -58,59 +65,35 @@ async function getBaseUrl(): Promise<string> {
|
|
|
58
65
|
return resolvedBaseUrl;
|
|
59
66
|
}
|
|
60
67
|
|
|
61
|
-
|
|
62
|
-
agentName: string;
|
|
63
|
-
name?: string;
|
|
64
|
-
description?: string;
|
|
65
|
-
listing?: string;
|
|
66
|
-
billingMode?: string;
|
|
67
|
-
skills?: Array<{ id: string; name: string }>;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function listAgentsAuthenticated(opts: {
|
|
71
|
-
baseUrl: string;
|
|
72
|
-
apiKey?: string;
|
|
73
|
-
skill?: string;
|
|
74
|
-
listing?: 'public' | 'private';
|
|
75
|
-
limit?: number;
|
|
76
|
-
}): Promise<{ agents: AgentListEntry[]; totalCount?: number }> {
|
|
77
|
-
const params = new URLSearchParams({ include: 'full' });
|
|
78
|
-
if (opts.skill) params.set('skill', opts.skill);
|
|
79
|
-
if (opts.listing) {
|
|
80
|
-
params.set('listing', opts.listing);
|
|
81
|
-
if (opts.listing === 'private') params.set('scope', 'owned');
|
|
82
|
-
}
|
|
83
|
-
if (opts.limit) params.set('limit', String(opts.limit));
|
|
84
|
-
const url = `${opts.baseUrl.replace(/\/+$/, '')}/api/v1/registry/agents?${params}`;
|
|
85
|
-
const headers: Record<string, string> = {};
|
|
86
|
-
if (opts.apiKey) headers['Authorization'] = `Bearer ${opts.apiKey}`;
|
|
87
|
-
const response = await fetch(url, { headers });
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
throw new Error(`Registry list failed: HTTP ${response.status}`);
|
|
90
|
-
}
|
|
91
|
-
const data = await response.json() as { agents: AgentListEntry[]; totalCount?: number };
|
|
92
|
-
return data;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function getTaskClient(billingMode: 'free' | 'paid' = 'free'): Promise<TaskClient> {
|
|
68
|
+
async function getTaskClient(billingMode: 'free' | 'paid' = 'free'): Promise<TaskClientLike> {
|
|
96
69
|
let client = taskClients.get(billingMode);
|
|
97
70
|
if (!client) {
|
|
98
71
|
const apiKey = process.env.BLOCKS_API_KEY;
|
|
99
72
|
client = await TaskClient.create({ apiKey, billingMode });
|
|
100
73
|
taskClients.set(billingMode, client);
|
|
101
74
|
}
|
|
102
|
-
return client;
|
|
75
|
+
return client as unknown as TaskClientLike;
|
|
103
76
|
}
|
|
104
77
|
|
|
78
|
+
const deps: ToolDeps = {
|
|
79
|
+
getBaseUrl,
|
|
80
|
+
getApiKey: () => process.env.BLOCKS_API_KEY,
|
|
81
|
+
getTaskClient,
|
|
82
|
+
getAgentByName: (agentName, options) =>
|
|
83
|
+
getAgent(agentName, options) as Promise<AgentEntryLike | null>,
|
|
84
|
+
listAgents: listAgentsAuthenticated,
|
|
85
|
+
validateFilePath,
|
|
86
|
+
fileSize: defaultFileSize,
|
|
87
|
+
maxUploadBytes: BLOCKS_MAX_UPLOAD_BYTES,
|
|
88
|
+
filePartFromPath,
|
|
89
|
+
textPart,
|
|
90
|
+
};
|
|
91
|
+
|
|
105
92
|
const server = new McpServer({
|
|
106
93
|
name: 'blocks-network',
|
|
107
|
-
version: '0.1.
|
|
94
|
+
version: '0.1.60',
|
|
108
95
|
});
|
|
109
96
|
|
|
110
|
-
// ============================================================================
|
|
111
|
-
// Tool: send_task
|
|
112
|
-
// ============================================================================
|
|
113
|
-
|
|
114
97
|
server.tool(
|
|
115
98
|
'send_task',
|
|
116
99
|
'Send a task to a Blocks Network agent and wait for the result',
|
|
@@ -118,149 +101,27 @@ server.tool(
|
|
|
118
101
|
agentName: z.string().describe('Name of the target agent'),
|
|
119
102
|
message: z.string().describe('Text message to send to the agent'),
|
|
120
103
|
filePath: z.string().optional().describe('Optional file path to attach'),
|
|
121
|
-
inputs: z
|
|
104
|
+
inputs: z
|
|
105
|
+
.record(z.string(), z.string())
|
|
106
|
+
.optional()
|
|
107
|
+
.describe('Additional named inputs as {partId: value} pairs'),
|
|
122
108
|
taskKind: z.enum(['request', 'pipe']).optional().describe('Task kind (default: request)'),
|
|
123
109
|
duration: z.number().optional().describe('Duration in minutes (required for pipe tasks)'),
|
|
124
|
-
timeoutMs: z
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const apiKey = process.env.BLOCKS_API_KEY;
|
|
129
|
-
const entry = await getAgent(agentName, { baseUrl, apiKey });
|
|
130
|
-
const billingMode = entry?.billingMode ?? 'free';
|
|
131
|
-
const client = await getTaskClient(billingMode);
|
|
132
|
-
const declaredInputs = entry?.card?.io?.inputs ?? [];
|
|
133
|
-
const isTextLike = (ct: string) =>
|
|
134
|
-
ct.startsWith('text/') || ct === 'application/json' || ct.endsWith('+json');
|
|
135
|
-
const textInput = declaredInputs.find((i) => isTextLike(i.contentType));
|
|
136
|
-
const fileInput = declaredInputs.find((i) => !isTextLike(i.contentType));
|
|
137
|
-
|
|
138
|
-
const requestParts: SendMessageRequestPart[] = [];
|
|
139
|
-
const textPartId = textInput?.id ?? 'text';
|
|
140
|
-
if ((textInput || declaredInputs.length === 0) && !inputs?.[textPartId]) {
|
|
141
|
-
requestParts.push(textPart(message, textPartId));
|
|
142
|
-
}
|
|
143
|
-
if (filePath) {
|
|
144
|
-
const safePath = validateFilePath(filePath);
|
|
145
|
-
const fileSize = statSync(safePath).size;
|
|
146
|
-
if (fileSize > BLOCKS_MAX_UPLOAD_BYTES) {
|
|
147
|
-
return {
|
|
148
|
-
content: [{ type: 'text', text: `File too large (${fileSize} bytes). Maximum upload size is ${BLOCKS_MAX_UPLOAD_BYTES} bytes (25 MB).` }],
|
|
149
|
-
isError: true,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
requestParts.push(await filePartFromPath(safePath, {
|
|
153
|
-
partId: fileInput?.id ?? 'file',
|
|
154
|
-
contentType: fileInput?.contentType,
|
|
155
|
-
}));
|
|
156
|
-
}
|
|
157
|
-
if (inputs) {
|
|
158
|
-
for (const [partId, value] of Object.entries(inputs)) {
|
|
159
|
-
requestParts.push(textPart(value, partId));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const session = await client.sendMessage({
|
|
164
|
-
agentName,
|
|
165
|
-
requestParts,
|
|
166
|
-
taskKind,
|
|
167
|
-
duration,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const timeout = timeoutMs ?? 60000;
|
|
171
|
-
const results: string[] = [];
|
|
172
|
-
|
|
173
|
-
session.onProgress((event) => {
|
|
174
|
-
if (event.message) {
|
|
175
|
-
results.push(`[progress] ${event.message}`);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const terminal = await session.waitForTerminal(timeout);
|
|
181
|
-
const allArtifacts = session.listArtifacts();
|
|
182
|
-
const output = [`Task ${session.taskId} ${terminal.state}`, ...results];
|
|
183
|
-
|
|
184
|
-
for (const ref of allArtifacts) {
|
|
185
|
-
try {
|
|
186
|
-
const downloaded = await session.downloadArtifact(ref);
|
|
187
|
-
const isText = downloaded.mimeType.startsWith('text/') ||
|
|
188
|
-
downloaded.mimeType === 'application/json';
|
|
189
|
-
if (isText) {
|
|
190
|
-
const text = new TextDecoder().decode(downloaded.data);
|
|
191
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}]\n${text}`);
|
|
192
|
-
} else {
|
|
193
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)`);
|
|
194
|
-
}
|
|
195
|
-
} catch {
|
|
196
|
-
output.push(`[artifact: ${ref.fileName ?? 'unnamed'}] (download failed)`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (terminal.state === 'failed') {
|
|
201
|
-
output.push(`Error: ${terminal.error ?? terminal.reason ?? 'unknown'}`);
|
|
202
|
-
}
|
|
203
|
-
session.close();
|
|
204
|
-
return { content: [{ type: 'text', text: output.join('\n') }] };
|
|
205
|
-
} catch (err) {
|
|
206
|
-
session.close();
|
|
207
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
-
return { content: [{ type: 'text', text: `Task ${session.taskId} error: ${msg}` }], isError: true };
|
|
209
|
-
}
|
|
110
|
+
timeoutMs: z
|
|
111
|
+
.number()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('Timeout in milliseconds to wait for result (default: 60000)'),
|
|
210
114
|
},
|
|
115
|
+
(params) => sendTask(params, deps),
|
|
211
116
|
);
|
|
212
117
|
|
|
213
|
-
// ============================================================================
|
|
214
|
-
// Tool: get_task
|
|
215
|
-
// ============================================================================
|
|
216
|
-
|
|
217
118
|
server.tool(
|
|
218
119
|
'get_task',
|
|
219
120
|
'Get the current status of a task by ID, including artifact content if completed',
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
},
|
|
223
|
-
async ({ taskId }) => {
|
|
224
|
-
const freeClient = await getTaskClient('free');
|
|
225
|
-
const task = await freeClient.getTask(taskId);
|
|
226
|
-
const output = [JSON.stringify(task, null, 2)];
|
|
227
|
-
|
|
228
|
-
const terminalStates = new Set(['completed', 'failed', 'canceled']);
|
|
229
|
-
if (task.state && terminalStates.has(task.state)) {
|
|
230
|
-
try {
|
|
231
|
-
const billingMode = resolveBillingMode(task);
|
|
232
|
-
const client = await getTaskClient(billingMode);
|
|
233
|
-
const session = await client.connect({ taskId });
|
|
234
|
-
const allArtifacts = session.listArtifacts();
|
|
235
|
-
for (const ref of allArtifacts) {
|
|
236
|
-
try {
|
|
237
|
-
const downloaded = await session.downloadArtifact(ref);
|
|
238
|
-
const isText = downloaded.mimeType.startsWith('text/') ||
|
|
239
|
-
downloaded.mimeType === 'application/json';
|
|
240
|
-
if (isText) {
|
|
241
|
-
const text = new TextDecoder().decode(downloaded.data);
|
|
242
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}]\n${text}`);
|
|
243
|
-
} else {
|
|
244
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)`);
|
|
245
|
-
}
|
|
246
|
-
} catch {
|
|
247
|
-
output.push(`[artifact: ${ref.fileName ?? 'unnamed'}] (download failed)`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
session.close();
|
|
251
|
-
} catch {
|
|
252
|
-
// connect failed — return task info without artifacts
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return { content: [{ type: 'text', text: output.join('\n') }] };
|
|
257
|
-
},
|
|
121
|
+
{ taskId: z.string().describe('The task ID to look up') },
|
|
122
|
+
(params) => getTask(params, deps),
|
|
258
123
|
);
|
|
259
124
|
|
|
260
|
-
// ============================================================================
|
|
261
|
-
// Tool: list_tasks
|
|
262
|
-
// ============================================================================
|
|
263
|
-
|
|
264
125
|
server.tool(
|
|
265
126
|
'list_tasks',
|
|
266
127
|
'List tasks, optionally filtered by agent or state',
|
|
@@ -269,85 +130,37 @@ server.tool(
|
|
|
269
130
|
state: z.string().optional().describe('Filter by state (e.g. completed, failed, running)'),
|
|
270
131
|
limit: z.number().optional().describe('Max results to return'),
|
|
271
132
|
},
|
|
272
|
-
|
|
273
|
-
const client = await getTaskClient();
|
|
274
|
-
const result = await client.listTasks({ agentName, state, limit });
|
|
275
|
-
const lines = result.tasks.map((t: TaskInfo) =>
|
|
276
|
-
`${t.taskId} | ${t.agentName ?? '?'} | ${t.state ?? '?'} | ${t.createdTime ?? ''}`,
|
|
277
|
-
);
|
|
278
|
-
const header = `Tasks (${result.totalCount ?? result.tasks.length} total):`;
|
|
279
|
-
return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
|
|
280
|
-
},
|
|
133
|
+
(params) => listTasks(params, deps),
|
|
281
134
|
);
|
|
282
135
|
|
|
283
|
-
// ============================================================================
|
|
284
|
-
// Tool: cancel_task
|
|
285
|
-
// ============================================================================
|
|
286
|
-
|
|
287
136
|
server.tool(
|
|
288
137
|
'cancel_task',
|
|
289
138
|
'Cancel a running task',
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
},
|
|
293
|
-
async ({ taskId }) => {
|
|
294
|
-
const client = await getTaskClient();
|
|
295
|
-
await client.cancelTask(taskId);
|
|
296
|
-
return { content: [{ type: 'text', text: `Task ${taskId} cancelled.` }] };
|
|
297
|
-
},
|
|
139
|
+
{ taskId: z.string().describe('The task ID to cancel') },
|
|
140
|
+
(params) => cancelTask(params, deps),
|
|
298
141
|
);
|
|
299
142
|
|
|
300
|
-
// ============================================================================
|
|
301
|
-
// Tool: list_agents
|
|
302
|
-
// ============================================================================
|
|
303
|
-
|
|
304
143
|
server.tool(
|
|
305
144
|
'list_agents',
|
|
306
145
|
'List available agents in the Blocks Network registry. Use listing="private" with an API key to discover your private agents.',
|
|
307
146
|
{
|
|
308
147
|
skill: z.string().optional().describe('Filter by skill name'),
|
|
309
|
-
listing: z
|
|
148
|
+
listing: z
|
|
149
|
+
.enum(['public', 'private'])
|
|
150
|
+
.optional()
|
|
151
|
+
.describe('Filter by listing visibility (default: public)'),
|
|
310
152
|
limit: z.number().optional().describe('Max results to return'),
|
|
311
153
|
},
|
|
312
|
-
|
|
313
|
-
const baseUrl = await getBaseUrl();
|
|
314
|
-
const apiKey = process.env.BLOCKS_API_KEY;
|
|
315
|
-
const result = await listAgentsAuthenticated({ baseUrl, apiKey, skill, listing, limit });
|
|
316
|
-
|
|
317
|
-
const lines = result.agents.map((a) => {
|
|
318
|
-
const skills = a.skills?.map((s) => s.name).join(', ') ?? '';
|
|
319
|
-
return `${a.agentName} | ${a.name ?? a.agentName} | ${a.listing ?? 'public'} | ${skills}`;
|
|
320
|
-
});
|
|
321
|
-
const header = `Agents (${result.totalCount ?? result.agents.length}):`;
|
|
322
|
-
return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
|
|
323
|
-
},
|
|
154
|
+
(params) => listAgents(params, deps),
|
|
324
155
|
);
|
|
325
156
|
|
|
326
|
-
// ============================================================================
|
|
327
|
-
// Tool: get_agent_card
|
|
328
|
-
// ============================================================================
|
|
329
|
-
|
|
330
157
|
server.tool(
|
|
331
158
|
'get_agent_card',
|
|
332
159
|
'Get the full agent card for a specific agent',
|
|
333
|
-
{
|
|
334
|
-
|
|
335
|
-
},
|
|
336
|
-
async ({ agentName }) => {
|
|
337
|
-
const baseUrl = await getBaseUrl();
|
|
338
|
-
const apiKey = process.env.BLOCKS_API_KEY;
|
|
339
|
-
const entry = await getAgent(agentName, { baseUrl, apiKey });
|
|
340
|
-
if (!entry) {
|
|
341
|
-
return { content: [{ type: 'text', text: `Agent "${agentName}" not found.` }], isError: true };
|
|
342
|
-
}
|
|
343
|
-
return { content: [{ type: 'text', text: JSON.stringify(entry.card ?? entry, null, 2) }] };
|
|
344
|
-
},
|
|
160
|
+
{ agentName: z.string().describe('Agent name to look up') },
|
|
161
|
+
(params) => getAgentCard(params, deps),
|
|
345
162
|
);
|
|
346
163
|
|
|
347
|
-
// ============================================================================
|
|
348
|
-
// Tool: connect_task
|
|
349
|
-
// ============================================================================
|
|
350
|
-
|
|
351
164
|
server.tool(
|
|
352
165
|
'connect_task',
|
|
353
166
|
'Connect to an existing task, stream events and data until completion',
|
|
@@ -355,108 +168,9 @@ server.tool(
|
|
|
355
168
|
taskId: z.string().describe('The task ID to connect to'),
|
|
356
169
|
timeoutMs: z.number().optional().describe('Timeout in milliseconds (default: 60000)'),
|
|
357
170
|
},
|
|
358
|
-
|
|
359
|
-
const freeClient = await getTaskClient('free');
|
|
360
|
-
const task = await freeClient.getTask(taskId);
|
|
361
|
-
const billingMode = resolveBillingMode(task);
|
|
362
|
-
const client = await getTaskClient(billingMode);
|
|
363
|
-
const session = await client.connect({ taskId });
|
|
364
|
-
|
|
365
|
-
const timeout = timeoutMs ?? 60000;
|
|
366
|
-
const results: string[] = [];
|
|
367
|
-
const streamOutputs: Array<{ label: string; chunks: string[] }> = [];
|
|
368
|
-
const streamDrains: Promise<void>[] = [];
|
|
369
|
-
|
|
370
|
-
session.onProgress((event) => {
|
|
371
|
-
if (event.message) {
|
|
372
|
-
results.push(`[progress] ${event.message}`);
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const drainStream = (streamRef: { descriptor: { localDirection: string; format: string; declaredStream?: string; streamId: string }; open: () => { events: () => AsyncIterable<unknown>; bytes: () => AsyncIterable<Uint8Array> } }) => {
|
|
377
|
-
const dir = streamRef.descriptor.localDirection;
|
|
378
|
-
if (dir !== 'inbound' && dir !== 'bidirectional') return;
|
|
379
|
-
const format = streamRef.descriptor.format;
|
|
380
|
-
const label = streamRef.descriptor.declaredStream ?? streamRef.descriptor.streamId;
|
|
381
|
-
const entry = { label, chunks: [] as string[] };
|
|
382
|
-
streamOutputs.push(entry);
|
|
383
|
-
try {
|
|
384
|
-
const streamClient = streamRef.open();
|
|
385
|
-
const drain = (async () => {
|
|
386
|
-
if (format === 'events') {
|
|
387
|
-
for await (const event of streamClient.events()) {
|
|
388
|
-
entry.chunks.push(JSON.stringify(event) + '\n');
|
|
389
|
-
}
|
|
390
|
-
} else {
|
|
391
|
-
const decoder = new TextDecoder();
|
|
392
|
-
for await (const chunk of streamClient.bytes()) {
|
|
393
|
-
entry.chunks.push(decoder.decode(chunk, { stream: true }));
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
})();
|
|
397
|
-
streamDrains.push(drain.catch(() => {}));
|
|
398
|
-
} catch {
|
|
399
|
-
// stream already ended or terminal
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
for (const ref of session.listStreams()) {
|
|
404
|
-
drainStream(ref);
|
|
405
|
-
}
|
|
406
|
-
session.onStream(drainStream);
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
const terminal = await session.waitForTerminal(timeout);
|
|
410
|
-
await Promise.allSettled(streamDrains);
|
|
411
|
-
const allArtifacts = session.listArtifacts();
|
|
412
|
-
results.unshift(`Task ${taskId} ${terminal.state}`);
|
|
413
|
-
|
|
414
|
-
for (const s of streamOutputs) {
|
|
415
|
-
if (s.chunks.length > 0) {
|
|
416
|
-
results.push(`[stream: ${s.label}]\n${s.chunks.join('')}`);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
for (const ref of allArtifacts) {
|
|
421
|
-
try {
|
|
422
|
-
const downloaded = await session.downloadArtifact(ref);
|
|
423
|
-
const isText = downloaded.mimeType.startsWith('text/') ||
|
|
424
|
-
downloaded.mimeType === 'application/json';
|
|
425
|
-
if (isText) {
|
|
426
|
-
const text = new TextDecoder().decode(downloaded.data);
|
|
427
|
-
results.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}]\n${text}`);
|
|
428
|
-
} else {
|
|
429
|
-
results.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)`);
|
|
430
|
-
}
|
|
431
|
-
} catch {
|
|
432
|
-
results.push(`[artifact: ${ref.fileName ?? 'unnamed'}] (download failed)`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (terminal.state === 'failed') {
|
|
437
|
-
results.push(`Error: ${terminal.error ?? terminal.reason ?? 'unknown'}`);
|
|
438
|
-
}
|
|
439
|
-
await session.asyncClose();
|
|
440
|
-
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
441
|
-
} catch (err) {
|
|
442
|
-
await session.asyncClose();
|
|
443
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
444
|
-
const partial: string[] = [`Task ${taskId} timed out (${msg})`];
|
|
445
|
-
for (const s of streamOutputs) {
|
|
446
|
-
if (s.chunks.length > 0) {
|
|
447
|
-
partial.push(`[stream: ${s.label}]\n${s.chunks.join('')}`);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
partial.push(...results);
|
|
451
|
-
return { content: [{ type: 'text', text: partial.join('\n') }], isError: true };
|
|
452
|
-
}
|
|
453
|
-
},
|
|
171
|
+
(params) => connectTask(params, deps),
|
|
454
172
|
);
|
|
455
173
|
|
|
456
|
-
// ============================================================================
|
|
457
|
-
// Start server
|
|
458
|
-
// ============================================================================
|
|
459
|
-
|
|
460
174
|
function shutdown() {
|
|
461
175
|
for (const client of taskClients.values()) {
|
|
462
176
|
client.destroy();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated registry list helper.
|
|
3
|
+
*
|
|
4
|
+
* Calls GET /api/v1/registry/agents with the Blocks-Protocol-Version
|
|
5
|
+
* header (required by the backend protocol-version middleware) and an
|
|
6
|
+
* optional Bearer API key for private/owned listings.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const PROTOCOL_VERSION_HEADER = 'Blocks-Protocol-Version';
|
|
10
|
+
export const CURRENT_PROTOCOL_VERSION = '2026-05-01';
|
|
11
|
+
|
|
12
|
+
export interface AgentListEntry {
|
|
13
|
+
agentName: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
listing?: string;
|
|
17
|
+
billingMode?: string;
|
|
18
|
+
skills?: Array<{ id: string; name: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ListAgentsOptions {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
skill?: string;
|
|
25
|
+
listing?: 'public' | 'private';
|
|
26
|
+
limit?: number;
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ListAgentsResult {
|
|
31
|
+
agents: AgentListEntry[];
|
|
32
|
+
totalCount?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function listAgentsAuthenticated(
|
|
36
|
+
opts: ListAgentsOptions,
|
|
37
|
+
): Promise<ListAgentsResult> {
|
|
38
|
+
const params = new URLSearchParams({ include: 'full' });
|
|
39
|
+
if (opts.skill) params.set('skill', opts.skill);
|
|
40
|
+
if (opts.listing) {
|
|
41
|
+
params.set('listing', opts.listing);
|
|
42
|
+
if (opts.listing === 'private') params.set('scope', 'owned');
|
|
43
|
+
}
|
|
44
|
+
if (opts.limit) params.set('limit', String(opts.limit));
|
|
45
|
+
|
|
46
|
+
const url = `${opts.baseUrl.replace(/\/+$/, '')}/api/v1/registry/agents?${params}`;
|
|
47
|
+
const headers: Record<string, string> = {
|
|
48
|
+
[PROTOCOL_VERSION_HEADER]: CURRENT_PROTOCOL_VERSION,
|
|
49
|
+
};
|
|
50
|
+
if (opts.apiKey) headers['Authorization'] = `Bearer ${opts.apiKey}`;
|
|
51
|
+
|
|
52
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
53
|
+
const response = await fetchFn(url, { headers });
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`Registry list failed: HTTP ${response.status}`);
|
|
56
|
+
}
|
|
57
|
+
return (await response.json()) as ListAgentsResult;
|
|
58
|
+
}
|