@blocks-network/mcp-server 0.1.55 → 0.1.64
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 +18 -4
- package/dist/agent-status.d.ts +49 -0
- package/dist/agent-status.js +40 -0
- package/dist/billing.d.ts +50 -0
- package/dist/billing.js +72 -0
- package/dist/index.js +82 -306
- package/dist/protocol-headers.d.ts +11 -0
- package/dist/protocol-headers.js +11 -0
- package/dist/registry-list.d.ts +31 -0
- package/dist/registry-list.js +32 -0
- package/dist/tools.d.ts +197 -0
- package/dist/tools.js +441 -0
- package/package.json +18 -6
- package/src/agent-status.ts +89 -0
- package/src/billing.ts +122 -0
- package/src/index.ts +148 -335
- package/src/protocol-headers.ts +12 -0
- package/src/registry-list.ts +60 -0
- package/src/tools.ts +739 -0
- package/tests/billing.test.ts +203 -0
- package/tests/cancel-task.test.ts +24 -0
- package/tests/connect-task.test.ts +180 -0
- package/tests/download-artifact.test.ts +142 -0
- package/tests/get-agent-card.test.ts +53 -0
- package/tests/get-agent-status.test.ts +108 -0
- package/tests/get-task.test.ts +76 -0
- package/tests/helpers.ts +250 -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/tests/task-lifecycle.test.ts +64 -0
- package/vitest.config.ts +7 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolve, relative } from 'node:path';
|
|
3
|
-
import { realpathSync,
|
|
2
|
+
import { resolve, relative, dirname } from 'node:path';
|
|
3
|
+
import { realpathSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { TaskClient, textPart, filePartFromPath, fetchCdmConfig, getAgent, BLOCKS_MAX_UPLOAD_BYTES, } from '@blocks-network/sdk';
|
|
8
|
+
import { listAgentsAuthenticated } from './registry-list.js';
|
|
9
|
+
import { fetchAgentStatus, MAX_AGENT_NAMES } from './agent-status.js';
|
|
10
|
+
import { getConsumerBalance, createConsumerTopUp } from './billing.js';
|
|
11
|
+
import { sendTask, getTask, listTasks, cancelTask, pauseTask, resumeTask, retryTask, listAgents, getAgentCard, getAgentStatus, connectTask, downloadArtifact, checkBalance, requestTopup, defaultFileSize, } from './tools.js';
|
|
8
12
|
const ALLOWED_ROOT = process.env.BLOCKS_MCP_FILE_ROOT ?? process.cwd();
|
|
9
13
|
if (!process.env.BLOCKS_API_KEY) {
|
|
10
14
|
console.error('Warning: BLOCKS_API_KEY is not set. Private agents and paid tasks will not work.');
|
|
@@ -25,11 +29,18 @@ function validateFilePath(filePath) {
|
|
|
25
29
|
}
|
|
26
30
|
return real;
|
|
27
31
|
}
|
|
28
|
-
function
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
function resolveSavePath(filePath) {
|
|
33
|
+
const root = realpathSync(resolve(ALLOWED_ROOT));
|
|
34
|
+
const resolved = resolve(root, filePath);
|
|
35
|
+
const rel = relative(root, resolved);
|
|
36
|
+
if (rel.startsWith('..') || resolve(root, rel) !== resolved) {
|
|
37
|
+
throw new Error(`Save path must be within ${ALLOWED_ROOT}`);
|
|
38
|
+
}
|
|
39
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
40
|
+
return resolved;
|
|
41
|
+
}
|
|
42
|
+
function writeFile(filePath, data) {
|
|
43
|
+
writeFileSync(filePath, data);
|
|
33
44
|
}
|
|
34
45
|
const taskClients = new Map();
|
|
35
46
|
let resolvedBaseUrl;
|
|
@@ -44,28 +55,6 @@ async function getBaseUrl() {
|
|
|
44
55
|
resolvedBaseUrl = cdm.api.baseUrl;
|
|
45
56
|
return resolvedBaseUrl;
|
|
46
57
|
}
|
|
47
|
-
async function listAgentsAuthenticated(opts) {
|
|
48
|
-
const params = new URLSearchParams({ include: 'full' });
|
|
49
|
-
if (opts.skill)
|
|
50
|
-
params.set('skill', opts.skill);
|
|
51
|
-
if (opts.listing) {
|
|
52
|
-
params.set('listing', opts.listing);
|
|
53
|
-
if (opts.listing === 'private')
|
|
54
|
-
params.set('scope', 'owned');
|
|
55
|
-
}
|
|
56
|
-
if (opts.limit)
|
|
57
|
-
params.set('limit', String(opts.limit));
|
|
58
|
-
const url = `${opts.baseUrl.replace(/\/+$/, '')}/api/v1/registry/agents?${params}`;
|
|
59
|
-
const headers = {};
|
|
60
|
-
if (opts.apiKey)
|
|
61
|
-
headers['Authorization'] = `Bearer ${opts.apiKey}`;
|
|
62
|
-
const response = await fetch(url, { headers });
|
|
63
|
-
if (!response.ok) {
|
|
64
|
-
throw new Error(`Registry list failed: HTTP ${response.status}`);
|
|
65
|
-
}
|
|
66
|
-
const data = await response.json();
|
|
67
|
-
return data;
|
|
68
|
-
}
|
|
69
58
|
async function getTaskClient(billingMode = 'free') {
|
|
70
59
|
let client = taskClients.get(billingMode);
|
|
71
60
|
if (!client) {
|
|
@@ -75,301 +64,88 @@ async function getTaskClient(billingMode = 'free') {
|
|
|
75
64
|
}
|
|
76
65
|
return client;
|
|
77
66
|
}
|
|
67
|
+
const deps = {
|
|
68
|
+
getBaseUrl,
|
|
69
|
+
getApiKey: () => process.env.BLOCKS_API_KEY,
|
|
70
|
+
getOrgId: () => process.env.BLOCKS_ORG_ID,
|
|
71
|
+
getTaskClient,
|
|
72
|
+
getAgentByName: (agentName, options) => getAgent(agentName, options),
|
|
73
|
+
listAgents: listAgentsAuthenticated,
|
|
74
|
+
fetchAgentStatus,
|
|
75
|
+
getConsumerBalance,
|
|
76
|
+
createConsumerTopUp,
|
|
77
|
+
validateFilePath,
|
|
78
|
+
resolveSavePath,
|
|
79
|
+
writeFile,
|
|
80
|
+
fileSize: defaultFileSize,
|
|
81
|
+
maxUploadBytes: BLOCKS_MAX_UPLOAD_BYTES,
|
|
82
|
+
filePartFromPath,
|
|
83
|
+
textPart,
|
|
84
|
+
};
|
|
78
85
|
const server = new McpServer({
|
|
79
86
|
name: 'blocks-network',
|
|
80
|
-
version: '0.1.
|
|
87
|
+
version: '0.1.60',
|
|
81
88
|
});
|
|
82
|
-
// ============================================================================
|
|
83
|
-
// Tool: send_task
|
|
84
|
-
// ============================================================================
|
|
85
89
|
server.tool('send_task', 'Send a task to a Blocks Network agent and wait for the result', {
|
|
86
90
|
agentName: z.string().describe('Name of the target agent'),
|
|
87
91
|
message: z.string().describe('Text message to send to the agent'),
|
|
88
92
|
filePath: z.string().optional().describe('Optional file path to attach'),
|
|
89
|
-
inputs: z
|
|
93
|
+
inputs: z
|
|
94
|
+
.record(z.string(), z.string())
|
|
95
|
+
.optional()
|
|
96
|
+
.describe('Additional named inputs as {partId: value} pairs'),
|
|
90
97
|
taskKind: z.enum(['request', 'pipe']).optional().describe('Task kind (default: request)'),
|
|
91
98
|
duration: z.number().optional().describe('Duration in minutes (required for pipe tasks)'),
|
|
92
|
-
timeoutMs: z
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const client = await getTaskClient(billingMode);
|
|
99
|
-
const declaredInputs = entry?.card?.io?.inputs ?? [];
|
|
100
|
-
const isTextLike = (ct) => ct.startsWith('text/') || ct === 'application/json' || ct.endsWith('+json');
|
|
101
|
-
const textInput = declaredInputs.find((i) => isTextLike(i.contentType));
|
|
102
|
-
const fileInput = declaredInputs.find((i) => !isTextLike(i.contentType));
|
|
103
|
-
const requestParts = [];
|
|
104
|
-
const textPartId = textInput?.id ?? 'text';
|
|
105
|
-
if ((textInput || declaredInputs.length === 0) && !inputs?.[textPartId]) {
|
|
106
|
-
requestParts.push(textPart(message, textPartId));
|
|
107
|
-
}
|
|
108
|
-
if (filePath) {
|
|
109
|
-
const safePath = validateFilePath(filePath);
|
|
110
|
-
const fileSize = statSync(safePath).size;
|
|
111
|
-
if (fileSize > BLOCKS_MAX_UPLOAD_BYTES) {
|
|
112
|
-
return {
|
|
113
|
-
content: [{ type: 'text', text: `File too large (${fileSize} bytes). Maximum upload size is ${BLOCKS_MAX_UPLOAD_BYTES} bytes (25 MB).` }],
|
|
114
|
-
isError: true,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
requestParts.push(await filePartFromPath(safePath, {
|
|
118
|
-
partId: fileInput?.id ?? 'file',
|
|
119
|
-
contentType: fileInput?.contentType,
|
|
120
|
-
}));
|
|
121
|
-
}
|
|
122
|
-
if (inputs) {
|
|
123
|
-
for (const [partId, value] of Object.entries(inputs)) {
|
|
124
|
-
requestParts.push(textPart(value, partId));
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
const session = await client.sendMessage({
|
|
128
|
-
agentName,
|
|
129
|
-
requestParts,
|
|
130
|
-
taskKind,
|
|
131
|
-
duration,
|
|
132
|
-
});
|
|
133
|
-
const timeout = timeoutMs ?? 60000;
|
|
134
|
-
const results = [];
|
|
135
|
-
session.onProgress((event) => {
|
|
136
|
-
if (event.message) {
|
|
137
|
-
results.push(`[progress] ${event.message}`);
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
try {
|
|
141
|
-
const terminal = await session.waitForTerminal(timeout);
|
|
142
|
-
const allArtifacts = session.listArtifacts();
|
|
143
|
-
const output = [`Task ${session.taskId} ${terminal.state}`, ...results];
|
|
144
|
-
for (const ref of allArtifacts) {
|
|
145
|
-
try {
|
|
146
|
-
const downloaded = await session.downloadArtifact(ref);
|
|
147
|
-
const isText = downloaded.mimeType.startsWith('text/') ||
|
|
148
|
-
downloaded.mimeType === 'application/json';
|
|
149
|
-
if (isText) {
|
|
150
|
-
const text = new TextDecoder().decode(downloaded.data);
|
|
151
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}]\n${text}`);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
catch {
|
|
158
|
-
output.push(`[artifact: ${ref.fileName ?? 'unnamed'}] (download failed)`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
if (terminal.state === 'failed') {
|
|
162
|
-
output.push(`Error: ${terminal.error ?? terminal.reason ?? 'unknown'}`);
|
|
163
|
-
}
|
|
164
|
-
session.close();
|
|
165
|
-
return { content: [{ type: 'text', text: output.join('\n') }] };
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
session.close();
|
|
169
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
170
|
-
return { content: [{ type: 'text', text: `Task ${session.taskId} error: ${msg}` }], isError: true };
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
// ============================================================================
|
|
174
|
-
// Tool: get_task
|
|
175
|
-
// ============================================================================
|
|
176
|
-
server.tool('get_task', 'Get the current status of a task by ID, including artifact content if completed', {
|
|
177
|
-
taskId: z.string().describe('The task ID to look up'),
|
|
178
|
-
}, async ({ taskId }) => {
|
|
179
|
-
const freeClient = await getTaskClient('free');
|
|
180
|
-
const task = await freeClient.getTask(taskId);
|
|
181
|
-
const output = [JSON.stringify(task, null, 2)];
|
|
182
|
-
const terminalStates = new Set(['completed', 'failed', 'canceled']);
|
|
183
|
-
if (task.state && terminalStates.has(task.state)) {
|
|
184
|
-
try {
|
|
185
|
-
const billingMode = resolveBillingMode(task);
|
|
186
|
-
const client = await getTaskClient(billingMode);
|
|
187
|
-
const session = await client.connect({ taskId });
|
|
188
|
-
const allArtifacts = session.listArtifacts();
|
|
189
|
-
for (const ref of allArtifacts) {
|
|
190
|
-
try {
|
|
191
|
-
const downloaded = await session.downloadArtifact(ref);
|
|
192
|
-
const isText = downloaded.mimeType.startsWith('text/') ||
|
|
193
|
-
downloaded.mimeType === 'application/json';
|
|
194
|
-
if (isText) {
|
|
195
|
-
const text = new TextDecoder().decode(downloaded.data);
|
|
196
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}]\n${text}`);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
output.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)`);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
output.push(`[artifact: ${ref.fileName ?? 'unnamed'}] (download failed)`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
session.close();
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
// connect failed — return task info without artifacts
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return { content: [{ type: 'text', text: output.join('\n') }] };
|
|
213
|
-
});
|
|
214
|
-
// ============================================================================
|
|
215
|
-
// Tool: list_tasks
|
|
216
|
-
// ============================================================================
|
|
99
|
+
timeoutMs: z
|
|
100
|
+
.number()
|
|
101
|
+
.optional()
|
|
102
|
+
.describe('Timeout in milliseconds to wait for result (default: 60000)'),
|
|
103
|
+
}, (params) => sendTask(params, deps));
|
|
104
|
+
server.tool('get_task', 'Get the current status of a task by ID, including artifact content if completed', { taskId: z.string().describe('The task ID to look up') }, (params) => getTask(params, deps));
|
|
217
105
|
server.tool('list_tasks', 'List tasks, optionally filtered by agent or state', {
|
|
218
106
|
agentName: z.string().optional().describe('Filter by agent name'),
|
|
219
107
|
state: z.string().optional().describe('Filter by state (e.g. completed, failed, running)'),
|
|
220
108
|
limit: z.number().optional().describe('Max results to return'),
|
|
221
|
-
},
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
|
|
227
|
-
});
|
|
228
|
-
// ============================================================================
|
|
229
|
-
// Tool: cancel_task
|
|
230
|
-
// ============================================================================
|
|
231
|
-
server.tool('cancel_task', 'Cancel a running task', {
|
|
232
|
-
taskId: z.string().describe('The task ID to cancel'),
|
|
233
|
-
}, async ({ taskId }) => {
|
|
234
|
-
const client = await getTaskClient();
|
|
235
|
-
await client.cancelTask(taskId);
|
|
236
|
-
return { content: [{ type: 'text', text: `Task ${taskId} cancelled.` }] };
|
|
237
|
-
});
|
|
238
|
-
// ============================================================================
|
|
239
|
-
// Tool: list_agents
|
|
240
|
-
// ============================================================================
|
|
109
|
+
}, (params) => listTasks(params, deps));
|
|
110
|
+
server.tool('cancel_task', 'Cancel a running task', { taskId: z.string().describe('The task ID to cancel') }, (params) => cancelTask(params, deps));
|
|
111
|
+
server.tool('pause_task', 'Pause a running pipe task. Resume later with resume_task.', { taskId: z.string().describe('The task ID to pause') }, (params) => pauseTask(params, deps));
|
|
112
|
+
server.tool('resume_task', 'Resume a paused pipe task', { taskId: z.string().describe('The task ID to resume') }, (params) => resumeTask(params, deps));
|
|
113
|
+
server.tool('retry_task', 'Retry a failed task', { taskId: z.string().describe('The task ID to retry') }, (params) => retryTask(params, deps));
|
|
241
114
|
server.tool('list_agents', 'List available agents in the Blocks Network registry. Use listing="private" with an API key to discover your private agents.', {
|
|
242
|
-
|
|
243
|
-
listing: z
|
|
115
|
+
tag: z.string().optional().describe('Filter by tag slug'),
|
|
116
|
+
listing: z
|
|
117
|
+
.enum(['public', 'private'])
|
|
118
|
+
.optional()
|
|
119
|
+
.describe('Filter by listing visibility (default: public)'),
|
|
244
120
|
limit: z.number().optional().describe('Max results to return'),
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}, async ({ agentName }) => {
|
|
262
|
-
const baseUrl = await getBaseUrl();
|
|
263
|
-
const apiKey = process.env.BLOCKS_API_KEY;
|
|
264
|
-
const entry = await getAgent(agentName, { baseUrl, apiKey });
|
|
265
|
-
if (!entry) {
|
|
266
|
-
return { content: [{ type: 'text', text: `Agent "${agentName}" not found.` }], isError: true };
|
|
267
|
-
}
|
|
268
|
-
return { content: [{ type: 'text', text: JSON.stringify(entry.card ?? entry, null, 2) }] };
|
|
269
|
-
});
|
|
270
|
-
// ============================================================================
|
|
271
|
-
// Tool: connect_task
|
|
272
|
-
// ============================================================================
|
|
121
|
+
}, (params) => listAgents(params, deps));
|
|
122
|
+
server.tool('get_agent_card', 'Get the full agent card for a specific agent', { agentName: z.string().describe('Agent name to look up') }, (params) => getAgentCard(params, deps));
|
|
123
|
+
server.tool('get_agent_status', 'Check live availability for one or more agents: how many instances are online, total task count for the agent, and SDK/CLI versions per instance. (Per-instance live activity counters such as `activeTasks`, `concurrentTasksPerInstance`, `startedAt`, and `totalActiveTasks` are reserved in the response shape but not yet populated by the backend — they currently return 0.)', {
|
|
124
|
+
agentNames: z
|
|
125
|
+
.array(z.string())
|
|
126
|
+
.min(1)
|
|
127
|
+
.max(MAX_AGENT_NAMES)
|
|
128
|
+
.describe(`Agent names to check (1-${MAX_AGENT_NAMES})`),
|
|
129
|
+
}, (params) => getAgentStatus(params, deps));
|
|
130
|
+
server.tool('check_balance', 'Get the consumer billing balance for the configured org (BLOCKS_ORG_ID): ledger balance, active reservations, and available balance.', {}, (_params) => checkBalance({}, deps));
|
|
131
|
+
server.tool('request_topup', 'Create a Stripe Checkout session to add USD to the consumer balance. Returns a URL the user opens in a browser to complete payment; the MCP server does not handle payment itself. Minimum top-up is $5 (platform `MIN_BILLING_AMOUNT`).', {
|
|
132
|
+
amountUsd: z
|
|
133
|
+
.number()
|
|
134
|
+
.positive()
|
|
135
|
+
.describe('Amount to add in USD (e.g. 25 or 19.99). Whole-cent precision. Must be at least $5 (platform minimum).'),
|
|
136
|
+
}, (params) => requestTopup(params, deps));
|
|
273
137
|
server.tool('connect_task', 'Connect to an existing task, stream events and data until completion', {
|
|
274
138
|
taskId: z.string().describe('The task ID to connect to'),
|
|
275
139
|
timeoutMs: z.number().optional().describe('Timeout in milliseconds (default: 60000)'),
|
|
276
|
-
},
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const streamDrains = [];
|
|
286
|
-
session.onProgress((event) => {
|
|
287
|
-
if (event.message) {
|
|
288
|
-
results.push(`[progress] ${event.message}`);
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
const drainStream = (streamRef) => {
|
|
292
|
-
const dir = streamRef.descriptor.localDirection;
|
|
293
|
-
if (dir !== 'inbound' && dir !== 'bidirectional')
|
|
294
|
-
return;
|
|
295
|
-
const format = streamRef.descriptor.format;
|
|
296
|
-
const label = streamRef.descriptor.declaredStream ?? streamRef.descriptor.streamId;
|
|
297
|
-
const entry = { label, chunks: [] };
|
|
298
|
-
streamOutputs.push(entry);
|
|
299
|
-
try {
|
|
300
|
-
const streamClient = streamRef.open();
|
|
301
|
-
const drain = (async () => {
|
|
302
|
-
if (format === 'events') {
|
|
303
|
-
for await (const event of streamClient.events()) {
|
|
304
|
-
entry.chunks.push(JSON.stringify(event) + '\n');
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
const decoder = new TextDecoder();
|
|
309
|
-
for await (const chunk of streamClient.bytes()) {
|
|
310
|
-
entry.chunks.push(decoder.decode(chunk, { stream: true }));
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
})();
|
|
314
|
-
streamDrains.push(drain.catch(() => { }));
|
|
315
|
-
}
|
|
316
|
-
catch {
|
|
317
|
-
// stream already ended or terminal
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
for (const ref of session.listStreams()) {
|
|
321
|
-
drainStream(ref);
|
|
322
|
-
}
|
|
323
|
-
session.onStream(drainStream);
|
|
324
|
-
try {
|
|
325
|
-
const terminal = await session.waitForTerminal(timeout);
|
|
326
|
-
await Promise.allSettled(streamDrains);
|
|
327
|
-
const allArtifacts = session.listArtifacts();
|
|
328
|
-
results.unshift(`Task ${taskId} ${terminal.state}`);
|
|
329
|
-
for (const s of streamOutputs) {
|
|
330
|
-
if (s.chunks.length > 0) {
|
|
331
|
-
results.push(`[stream: ${s.label}]\n${s.chunks.join('')}`);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
for (const ref of allArtifacts) {
|
|
335
|
-
try {
|
|
336
|
-
const downloaded = await session.downloadArtifact(ref);
|
|
337
|
-
const isText = downloaded.mimeType.startsWith('text/') ||
|
|
338
|
-
downloaded.mimeType === 'application/json';
|
|
339
|
-
if (isText) {
|
|
340
|
-
const text = new TextDecoder().decode(downloaded.data);
|
|
341
|
-
results.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}]\n${text}`);
|
|
342
|
-
}
|
|
343
|
-
else {
|
|
344
|
-
results.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)`);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
results.push(`[artifact: ${ref.fileName ?? 'unnamed'}] (download failed)`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
if (terminal.state === 'failed') {
|
|
352
|
-
results.push(`Error: ${terminal.error ?? terminal.reason ?? 'unknown'}`);
|
|
353
|
-
}
|
|
354
|
-
await session.asyncClose();
|
|
355
|
-
return { content: [{ type: 'text', text: results.join('\n') }] };
|
|
356
|
-
}
|
|
357
|
-
catch (err) {
|
|
358
|
-
await session.asyncClose();
|
|
359
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
360
|
-
const partial = [`Task ${taskId} timed out (${msg})`];
|
|
361
|
-
for (const s of streamOutputs) {
|
|
362
|
-
if (s.chunks.length > 0) {
|
|
363
|
-
partial.push(`[stream: ${s.label}]\n${s.chunks.join('')}`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
partial.push(...results);
|
|
367
|
-
return { content: [{ type: 'text', text: partial.join('\n') }], isError: true };
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
// ============================================================================
|
|
371
|
-
// Start server
|
|
372
|
-
// ============================================================================
|
|
140
|
+
}, (params) => connectTask(params, deps));
|
|
141
|
+
server.tool('download_artifact', 'Download a single artifact from a task by file name. If savePath is provided, writes the artifact to disk under BLOCKS_MCP_FILE_ROOT; otherwise returns the content inline (text decoded, binary base64-encoded).', {
|
|
142
|
+
taskId: z.string().describe('The task ID that produced the artifact'),
|
|
143
|
+
fileName: z.string().describe('Artifact file name as listed by get_task'),
|
|
144
|
+
savePath: z
|
|
145
|
+
.string()
|
|
146
|
+
.optional()
|
|
147
|
+
.describe('Optional path (relative to BLOCKS_MCP_FILE_ROOT) to write the artifact to'),
|
|
148
|
+
}, (params) => downloadArtifact(params, deps));
|
|
373
149
|
function shutdown() {
|
|
374
150
|
for (const client of taskClients.values()) {
|
|
375
151
|
client.destroy();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared protocol-version header constants for direct-HTTP backend
|
|
3
|
+
* callers (`registry-list`, `agent-status`, `billing`).
|
|
4
|
+
*
|
|
5
|
+
* MUST stay in sync with
|
|
6
|
+
* blocks-sdk/sdks/node/src/runtime/protocol-version.ts (the canonical
|
|
7
|
+
* source). Until the SDK exports these from its public surface, both
|
|
8
|
+
* files must update atomically when the protocol version bumps.
|
|
9
|
+
*/
|
|
10
|
+
export declare const PROTOCOL_VERSION_HEADER = "Blocks-Protocol-Version";
|
|
11
|
+
export declare const CURRENT_PROTOCOL_VERSION = "2026-05-01";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared protocol-version header constants for direct-HTTP backend
|
|
3
|
+
* callers (`registry-list`, `agent-status`, `billing`).
|
|
4
|
+
*
|
|
5
|
+
* MUST stay in sync with
|
|
6
|
+
* blocks-sdk/sdks/node/src/runtime/protocol-version.ts (the canonical
|
|
7
|
+
* source). Until the SDK exports these from its public surface, both
|
|
8
|
+
* files must update atomically when the protocol version bumps.
|
|
9
|
+
*/
|
|
10
|
+
export const PROTOCOL_VERSION_HEADER = 'Blocks-Protocol-Version';
|
|
11
|
+
export const CURRENT_PROTOCOL_VERSION = '2026-05-01';
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
export interface AgentListEntry {
|
|
9
|
+
agentName: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
listing?: string;
|
|
13
|
+
billingMode?: string;
|
|
14
|
+
tags?: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
export interface ListAgentsOptions {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
tag?: string;
|
|
23
|
+
listing?: 'public' | 'private';
|
|
24
|
+
limit?: number;
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
}
|
|
27
|
+
export interface ListAgentsResult {
|
|
28
|
+
agents: AgentListEntry[];
|
|
29
|
+
totalCount?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare function listAgentsAuthenticated(opts: ListAgentsOptions): Promise<ListAgentsResult>;
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import { PROTOCOL_VERSION_HEADER, CURRENT_PROTOCOL_VERSION, } from './protocol-headers.js';
|
|
9
|
+
export async function listAgentsAuthenticated(opts) {
|
|
10
|
+
const params = new URLSearchParams({ include: 'full' });
|
|
11
|
+
if (opts.tag)
|
|
12
|
+
params.set('tag', opts.tag);
|
|
13
|
+
if (opts.listing) {
|
|
14
|
+
params.set('listing', opts.listing);
|
|
15
|
+
if (opts.listing === 'private')
|
|
16
|
+
params.set('scope', 'owned');
|
|
17
|
+
}
|
|
18
|
+
if (opts.limit)
|
|
19
|
+
params.set('limit', String(opts.limit));
|
|
20
|
+
const url = `${opts.baseUrl.replace(/\/+$/, '')}/api/v1/registry/agents?${params}`;
|
|
21
|
+
const headers = {
|
|
22
|
+
[PROTOCOL_VERSION_HEADER]: CURRENT_PROTOCOL_VERSION,
|
|
23
|
+
};
|
|
24
|
+
if (opts.apiKey)
|
|
25
|
+
headers['Authorization'] = `Bearer ${opts.apiKey}`;
|
|
26
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
27
|
+
const response = await fetchFn(url, { headers });
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Registry list failed: HTTP ${response.status}`);
|
|
30
|
+
}
|
|
31
|
+
return (await response.json());
|
|
32
|
+
}
|