@blocks-network/mcp-server 0.1.62 → 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/LICENSE +26 -0
- package/README.md +32 -20
- 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 +50 -4
- package/dist/protocol-headers.d.ts +11 -0
- package/dist/protocol-headers.js +11 -0
- package/dist/registry-list.d.ts +2 -4
- package/dist/registry-list.js +3 -4
- package/dist/tools.d.ts +47 -2
- package/dist/tools.js +169 -3
- package/package.json +1 -1
- package/src/agent-status.ts +89 -0
- package/src/billing.ts +122 -0
- package/src/index.ts +102 -3
- package/src/protocol-headers.ts +12 -0
- package/src/registry-list.ts +7 -5
- package/src/tools.ts +248 -5
- package/tests/billing.test.ts +203 -0
- package/tests/download-artifact.test.ts +142 -0
- package/tests/get-agent-status.test.ts +108 -0
- package/tests/helpers.ts +75 -13
- package/tests/list-agents.test.ts +6 -6
- package/tests/registry-list.test.ts +5 -5
- package/tests/task-lifecycle.test.ts +64 -0
package/dist/tools.js
CHANGED
|
@@ -161,13 +161,13 @@ export async function listAgents(params, deps) {
|
|
|
161
161
|
const result = await deps.listAgents({
|
|
162
162
|
baseUrl,
|
|
163
163
|
apiKey,
|
|
164
|
-
|
|
164
|
+
tag: params.tag,
|
|
165
165
|
listing: params.listing,
|
|
166
166
|
limit: params.limit,
|
|
167
167
|
});
|
|
168
168
|
const lines = result.agents.map((a) => {
|
|
169
|
-
const
|
|
170
|
-
return `${a.agentName} | ${a.name ?? a.agentName} | ${a.listing ?? 'public'} | ${
|
|
169
|
+
const tags = a.tags?.map((t) => t.name).join(', ') ?? '';
|
|
170
|
+
return `${a.agentName} | ${a.name ?? a.agentName} | ${a.listing ?? 'public'} | ${tags}`;
|
|
171
171
|
});
|
|
172
172
|
const header = `Agents (${result.totalCount ?? result.agents.length}):`;
|
|
173
173
|
return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
|
|
@@ -268,6 +268,172 @@ export async function connectTask(params, deps) {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
// ============================================================================
|
|
271
|
+
// download_artifact
|
|
272
|
+
// ============================================================================
|
|
273
|
+
function bytesToBase64(bytes) {
|
|
274
|
+
let binary = '';
|
|
275
|
+
for (let i = 0; i < bytes.length; i++)
|
|
276
|
+
binary += String.fromCharCode(bytes[i]);
|
|
277
|
+
return Buffer.from(binary, 'binary').toString('base64');
|
|
278
|
+
}
|
|
279
|
+
export async function downloadArtifact(params, deps) {
|
|
280
|
+
const freeClient = await deps.getTaskClient('free');
|
|
281
|
+
const task = await freeClient.getTask(params.taskId);
|
|
282
|
+
const billingMode = resolveBillingMode(task);
|
|
283
|
+
const client = await deps.getTaskClient(billingMode);
|
|
284
|
+
let session;
|
|
285
|
+
try {
|
|
286
|
+
session = await client.connect({ taskId: params.taskId });
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: 'text', text: `Failed to connect to task ${params.taskId}: ${msg}` }],
|
|
292
|
+
isError: true,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const refs = session.listArtifacts();
|
|
297
|
+
const ref = refs.find((r) => r.fileName === params.fileName);
|
|
298
|
+
if (!ref) {
|
|
299
|
+
const available = refs.map((r) => r.fileName ?? 'unnamed').join(', ') || '(none)';
|
|
300
|
+
return {
|
|
301
|
+
content: [
|
|
302
|
+
{
|
|
303
|
+
type: 'text',
|
|
304
|
+
text: `Artifact "${params.fileName}" not found on task ${params.taskId}. Available: ${available}`,
|
|
305
|
+
},
|
|
306
|
+
],
|
|
307
|
+
isError: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const downloaded = await session.downloadArtifact(ref);
|
|
311
|
+
if (params.savePath) {
|
|
312
|
+
const safePath = deps.resolveSavePath(params.savePath);
|
|
313
|
+
deps.writeFile(safePath, downloaded.data);
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: `Saved ${downloaded.data.length} bytes to ${safePath} (${downloaded.mimeType})`,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (isTextMimeType(downloaded.mimeType)) {
|
|
324
|
+
const text = new TextDecoder().decode(downloaded.data);
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: 'text',
|
|
329
|
+
text: `[artifact: ${downloaded.fileName ?? params.fileName}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)\n${text}`,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
{
|
|
337
|
+
type: 'text',
|
|
338
|
+
text: `[artifact: ${downloaded.fileName ?? params.fileName}] (${downloaded.mimeType}, ${downloaded.data.length} bytes, base64)\n${bytesToBase64(downloaded.data)}`,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
session.close();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// pause_task / resume_task / retry_task
|
|
349
|
+
// ============================================================================
|
|
350
|
+
export async function pauseTask(params, deps) {
|
|
351
|
+
const client = await deps.getTaskClient();
|
|
352
|
+
await client.pauseTask(params.taskId);
|
|
353
|
+
return { content: [{ type: 'text', text: `Task ${params.taskId} paused.` }] };
|
|
354
|
+
}
|
|
355
|
+
export async function resumeTask(params, deps) {
|
|
356
|
+
const client = await deps.getTaskClient();
|
|
357
|
+
await client.resumeTask(params.taskId);
|
|
358
|
+
return { content: [{ type: 'text', text: `Task ${params.taskId} resumed.` }] };
|
|
359
|
+
}
|
|
360
|
+
export async function retryTask(params, deps) {
|
|
361
|
+
const client = await deps.getTaskClient();
|
|
362
|
+
await client.retryTask(params.taskId);
|
|
363
|
+
return { content: [{ type: 'text', text: `Task ${params.taskId} retry requested.` }] };
|
|
364
|
+
}
|
|
365
|
+
// ============================================================================
|
|
366
|
+
// get_agent_status
|
|
367
|
+
// ============================================================================
|
|
368
|
+
export async function getAgentStatus(params, deps) {
|
|
369
|
+
const baseUrl = await deps.getBaseUrl();
|
|
370
|
+
const apiKey = deps.getApiKey();
|
|
371
|
+
const result = await deps.fetchAgentStatus({
|
|
372
|
+
baseUrl,
|
|
373
|
+
apiKey,
|
|
374
|
+
agentNames: params.agentNames,
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// ============================================================================
|
|
381
|
+
// check_balance / request_topup
|
|
382
|
+
// ============================================================================
|
|
383
|
+
function requireBillingContext(deps) {
|
|
384
|
+
const apiKey = deps.getApiKey();
|
|
385
|
+
if (!apiKey) {
|
|
386
|
+
return {
|
|
387
|
+
content: [{ type: 'text', text: 'BLOCKS_API_KEY is required for billing operations.' }],
|
|
388
|
+
isError: true,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const orgId = deps.getOrgId();
|
|
392
|
+
if (!orgId) {
|
|
393
|
+
return {
|
|
394
|
+
content: [
|
|
395
|
+
{
|
|
396
|
+
type: 'text',
|
|
397
|
+
text: 'BLOCKS_ORG_ID is required for billing operations. Set it from your dashboard URL or `blocks whoami --json`.',
|
|
398
|
+
},
|
|
399
|
+
],
|
|
400
|
+
isError: true,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return { apiKey, orgId };
|
|
404
|
+
}
|
|
405
|
+
export async function checkBalance(_params, deps) {
|
|
406
|
+
const ctx = requireBillingContext(deps);
|
|
407
|
+
if ('content' in ctx)
|
|
408
|
+
return ctx;
|
|
409
|
+
const baseUrl = await deps.getBaseUrl();
|
|
410
|
+
const balance = await deps.getConsumerBalance({
|
|
411
|
+
baseUrl,
|
|
412
|
+
apiKey: ctx.apiKey,
|
|
413
|
+
orgId: ctx.orgId,
|
|
414
|
+
});
|
|
415
|
+
return {
|
|
416
|
+
content: [{ type: 'text', text: JSON.stringify(balance, null, 2) }],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
export async function requestTopup(params, deps) {
|
|
420
|
+
const ctx = requireBillingContext(deps);
|
|
421
|
+
if ('content' in ctx)
|
|
422
|
+
return ctx;
|
|
423
|
+
const baseUrl = await deps.getBaseUrl();
|
|
424
|
+
const session = await deps.createConsumerTopUp({
|
|
425
|
+
baseUrl,
|
|
426
|
+
apiKey: ctx.apiKey,
|
|
427
|
+
orgId: ctx.orgId,
|
|
428
|
+
amountUsd: params.amountUsd,
|
|
429
|
+
});
|
|
430
|
+
const url = session.checkoutUrl;
|
|
431
|
+
const body = url
|
|
432
|
+
? `Open this URL in a browser to complete the $${params.amountUsd.toFixed(2)} top-up:\n${url}`
|
|
433
|
+
: JSON.stringify(session, null, 2);
|
|
434
|
+
return { content: [{ type: 'text', text: body }] };
|
|
435
|
+
}
|
|
436
|
+
// ============================================================================
|
|
271
437
|
// Default file helpers (re-exported so index.ts can build a default ToolDeps)
|
|
272
438
|
// ============================================================================
|
|
273
439
|
export function defaultFileSize(path) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent presence/availability helper.
|
|
3
|
+
*
|
|
4
|
+
* Calls GET /api/v1/agent-status with the Blocks-Protocol-Version
|
|
5
|
+
* header. Backend route is optionalAuth so the API key is forwarded
|
|
6
|
+
* when available but not required.
|
|
7
|
+
*
|
|
8
|
+
* Response shape mirrors the service's agent-status types.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
PROTOCOL_VERSION_HEADER,
|
|
13
|
+
CURRENT_PROTOCOL_VERSION,
|
|
14
|
+
} from './protocol-headers.js';
|
|
15
|
+
|
|
16
|
+
/** MUST stay in sync with the service's agent-status types. */
|
|
17
|
+
export const MAX_AGENT_NAMES = 50;
|
|
18
|
+
export const AGENT_NAME_PATTERN = /^[a-zA-Z0-9_]+$/;
|
|
19
|
+
|
|
20
|
+
export interface AgentInstanceStatus {
|
|
21
|
+
instanceId: string;
|
|
22
|
+
uuid: string;
|
|
23
|
+
online: true;
|
|
24
|
+
/**
|
|
25
|
+
* Reserved — backend currently returns 0 (live activity counters are not
|
|
26
|
+
* yet populated by the agent-status service). Do not use for routing or
|
|
27
|
+
* availability decisions.
|
|
28
|
+
*/
|
|
29
|
+
activeTasks: number;
|
|
30
|
+
/** Reserved — backend currently returns 0. See `activeTasks`. */
|
|
31
|
+
concurrentTasksPerInstance: number;
|
|
32
|
+
/** Reserved — backend currently returns 0. See `activeTasks`. */
|
|
33
|
+
startedAt: number;
|
|
34
|
+
sdkVersion: string | null;
|
|
35
|
+
cliVersion: string | null;
|
|
36
|
+
preferredProtocolVersion: string | null;
|
|
37
|
+
protocolVersions: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AgentStatus {
|
|
41
|
+
agentName: string;
|
|
42
|
+
instances: AgentInstanceStatus[];
|
|
43
|
+
onlineCount: number;
|
|
44
|
+
/** Reserved — backend currently returns 0. See `AgentInstanceStatus.activeTasks`. */
|
|
45
|
+
totalActiveTasks: number;
|
|
46
|
+
taskCount: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AgentStatusResponse {
|
|
50
|
+
agents: Record<string, AgentStatus>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface FetchAgentStatusOptions {
|
|
54
|
+
baseUrl: string;
|
|
55
|
+
agentNames: string[];
|
|
56
|
+
apiKey?: string;
|
|
57
|
+
fetchImpl?: typeof fetch;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function fetchAgentStatus(
|
|
61
|
+
opts: FetchAgentStatusOptions,
|
|
62
|
+
): Promise<AgentStatusResponse> {
|
|
63
|
+
const names = opts.agentNames.map((n) => n.trim()).filter(Boolean);
|
|
64
|
+
if (names.length === 0) {
|
|
65
|
+
throw new Error('agentNames must contain at least one value');
|
|
66
|
+
}
|
|
67
|
+
if (names.length > MAX_AGENT_NAMES) {
|
|
68
|
+
throw new Error(`agentNames must be at most ${MAX_AGENT_NAMES} values`);
|
|
69
|
+
}
|
|
70
|
+
for (const n of names) {
|
|
71
|
+
if (!AGENT_NAME_PATTERN.test(n)) {
|
|
72
|
+
throw new Error(`Invalid agent name: ${n}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const params = new URLSearchParams({ agentNames: names.join(',') });
|
|
77
|
+
const url = `${opts.baseUrl.replace(/\/+$/, '')}/api/v1/agent-status?${params}`;
|
|
78
|
+
const headers: Record<string, string> = {
|
|
79
|
+
[PROTOCOL_VERSION_HEADER]: CURRENT_PROTOCOL_VERSION,
|
|
80
|
+
};
|
|
81
|
+
if (opts.apiKey) headers['Authorization'] = `Bearer ${opts.apiKey}`;
|
|
82
|
+
|
|
83
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
84
|
+
const response = await fetchFn(url, { headers });
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
throw new Error(`Agent status failed: HTTP ${response.status}`);
|
|
87
|
+
}
|
|
88
|
+
return (await response.json()) as AgentStatusResponse;
|
|
89
|
+
}
|
package/src/billing.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Billing helpers — direct HTTP to the routes that accept Bearer
|
|
3
|
+
* API-key auth (`requireAuth` middleware path on the backend).
|
|
4
|
+
*
|
|
5
|
+
* GET /api/v1/billing/:orgId/consumer/balance
|
|
6
|
+
* POST /api/v1/billing/:orgId/consumer/topup
|
|
7
|
+
*
|
|
8
|
+
* Other billing routes (ledger, usage-summary, dashboard-summary,
|
|
9
|
+
* topup-from-earnings) are session-only on the backend and are not
|
|
10
|
+
* callable from an MCP server holding only an API key.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
PROTOCOL_VERSION_HEADER,
|
|
15
|
+
CURRENT_PROTOCOL_VERSION,
|
|
16
|
+
} from './protocol-headers.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Platform minimum top-up amount in USD. MUST stay in sync with
|
|
20
|
+
* the service's MIN_BILLING_AMOUNT
|
|
21
|
+
* (decimal-dollar string `'5'`). The backend rejects values below this floor
|
|
22
|
+
* via `stripeMoneyAtLeast(MIN_BILLING_AMOUNT, ...)`; we mirror it here so MCP
|
|
23
|
+
* callers get the real contract up front instead of a runtime HTTP 400.
|
|
24
|
+
*/
|
|
25
|
+
export const MIN_TOPUP_AMOUNT_USD = 5;
|
|
26
|
+
|
|
27
|
+
export interface ConsumerBalance {
|
|
28
|
+
/** Ledger balance as a decimal-dollar string (e.g. "12.34"). */
|
|
29
|
+
balance: string;
|
|
30
|
+
/** Currently reserved (held for in-flight tasks). */
|
|
31
|
+
reservedBalance: string;
|
|
32
|
+
/** balance - reservedBalance. */
|
|
33
|
+
availableBalance: string;
|
|
34
|
+
/** ISO timestamp of when the balance snapshot was taken. */
|
|
35
|
+
updatedAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface TopUpSession {
|
|
39
|
+
/** Stripe Checkout URL the user opens in a browser to complete payment. */
|
|
40
|
+
checkoutUrl: string;
|
|
41
|
+
/** Stripe Checkout session id. */
|
|
42
|
+
sessionId: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface BillingClientBase {
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
orgId: string;
|
|
48
|
+
apiKey: string;
|
|
49
|
+
fetchImpl?: typeof fetch;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildHeaders(apiKey: string): Record<string, string> {
|
|
53
|
+
return {
|
|
54
|
+
[PROTOCOL_VERSION_HEADER]: CURRENT_PROTOCOL_VERSION,
|
|
55
|
+
Authorization: `Bearer ${apiKey}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function billingUrl(baseUrl: string, orgId: string, suffix: string): string {
|
|
61
|
+
return `${baseUrl.replace(/\/+$/, '')}/api/v1/billing/${encodeURIComponent(orgId)}${suffix}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function getConsumerBalance(
|
|
65
|
+
opts: BillingClientBase,
|
|
66
|
+
): Promise<ConsumerBalance> {
|
|
67
|
+
const url = billingUrl(opts.baseUrl, opts.orgId, '/consumer/balance');
|
|
68
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
69
|
+
const response = await fetchFn(url, {
|
|
70
|
+
method: 'GET',
|
|
71
|
+
headers: buildHeaders(opts.apiKey),
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) {
|
|
74
|
+
throw new Error(`Balance lookup failed: HTTP ${response.status}`);
|
|
75
|
+
}
|
|
76
|
+
return (await response.json()) as ConsumerBalance;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CreateTopUpOptions extends BillingClientBase {
|
|
80
|
+
/**
|
|
81
|
+
* Whole-dollar (or whole-cent decimal) USD amount, e.g. 25 or 19.99.
|
|
82
|
+
* Must be at least `MIN_TOPUP_AMOUNT_USD` ($5) — backend rejects below this.
|
|
83
|
+
*/
|
|
84
|
+
amountUsd: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function createConsumerTopUp(
|
|
88
|
+
opts: CreateTopUpOptions,
|
|
89
|
+
): Promise<TopUpSession> {
|
|
90
|
+
if (!Number.isFinite(opts.amountUsd) || opts.amountUsd <= 0) {
|
|
91
|
+
throw new Error('amountUsd must be a positive finite number');
|
|
92
|
+
}
|
|
93
|
+
if (Math.round(opts.amountUsd * 100) / 100 !== opts.amountUsd) {
|
|
94
|
+
throw new Error('amountUsd must be a whole-cent value (no sub-cent fractions)');
|
|
95
|
+
}
|
|
96
|
+
if (opts.amountUsd < MIN_TOPUP_AMOUNT_USD) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`amountUsd must be at least $${MIN_TOPUP_AMOUNT_USD}.00 (platform minimum)`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const amount = opts.amountUsd.toFixed(2);
|
|
102
|
+
|
|
103
|
+
const url = billingUrl(opts.baseUrl, opts.orgId, '/consumer/topup');
|
|
104
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
105
|
+
const response = await fetchFn(url, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: buildHeaders(opts.apiKey),
|
|
108
|
+
body: JSON.stringify({ amount }),
|
|
109
|
+
});
|
|
110
|
+
if (!response.ok) {
|
|
111
|
+
let detail = '';
|
|
112
|
+
try {
|
|
113
|
+
detail = await response.text();
|
|
114
|
+
} catch {
|
|
115
|
+
// ignore body decode failures
|
|
116
|
+
}
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Top-up failed: HTTP ${response.status}${detail ? ` — ${detail}` : ''}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return (await response.json()) as TopUpSession;
|
|
122
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { resolve, relative } from 'node:path';
|
|
4
|
-
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { resolve, relative, dirname } from 'node:path';
|
|
4
|
+
import { realpathSync, mkdirSync, writeFileSync } 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';
|
|
@@ -15,14 +15,23 @@ import {
|
|
|
15
15
|
} from '@blocks-network/sdk';
|
|
16
16
|
|
|
17
17
|
import { listAgentsAuthenticated } from './registry-list.js';
|
|
18
|
+
import { fetchAgentStatus, MAX_AGENT_NAMES } from './agent-status.js';
|
|
19
|
+
import { getConsumerBalance, createConsumerTopUp } from './billing.js';
|
|
18
20
|
import {
|
|
19
21
|
sendTask,
|
|
20
22
|
getTask,
|
|
21
23
|
listTasks,
|
|
22
24
|
cancelTask,
|
|
25
|
+
pauseTask,
|
|
26
|
+
resumeTask,
|
|
27
|
+
retryTask,
|
|
23
28
|
listAgents,
|
|
24
29
|
getAgentCard,
|
|
30
|
+
getAgentStatus,
|
|
25
31
|
connectTask,
|
|
32
|
+
downloadArtifact,
|
|
33
|
+
checkBalance,
|
|
34
|
+
requestTopup,
|
|
26
35
|
defaultFileSize,
|
|
27
36
|
type ToolDeps,
|
|
28
37
|
type TaskClientLike,
|
|
@@ -51,6 +60,21 @@ function validateFilePath(filePath: string): string {
|
|
|
51
60
|
return real;
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
function resolveSavePath(filePath: string): string {
|
|
64
|
+
const root = realpathSync(resolve(ALLOWED_ROOT));
|
|
65
|
+
const resolved = resolve(root, filePath);
|
|
66
|
+
const rel = relative(root, resolved);
|
|
67
|
+
if (rel.startsWith('..') || resolve(root, rel) !== resolved) {
|
|
68
|
+
throw new Error(`Save path must be within ${ALLOWED_ROOT}`);
|
|
69
|
+
}
|
|
70
|
+
mkdirSync(dirname(resolved), { recursive: true });
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeFile(filePath: string, data: Uint8Array): void {
|
|
75
|
+
writeFileSync(filePath, data);
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
const taskClients = new Map<string, TaskClient>();
|
|
55
79
|
let resolvedBaseUrl: string | undefined;
|
|
56
80
|
|
|
@@ -78,11 +102,17 @@ async function getTaskClient(billingMode: 'free' | 'paid' = 'free'): Promise<Tas
|
|
|
78
102
|
const deps: ToolDeps = {
|
|
79
103
|
getBaseUrl,
|
|
80
104
|
getApiKey: () => process.env.BLOCKS_API_KEY,
|
|
105
|
+
getOrgId: () => process.env.BLOCKS_ORG_ID,
|
|
81
106
|
getTaskClient,
|
|
82
107
|
getAgentByName: (agentName, options) =>
|
|
83
108
|
getAgent(agentName, options) as Promise<AgentEntryLike | null>,
|
|
84
109
|
listAgents: listAgentsAuthenticated,
|
|
110
|
+
fetchAgentStatus,
|
|
111
|
+
getConsumerBalance,
|
|
112
|
+
createConsumerTopUp,
|
|
85
113
|
validateFilePath,
|
|
114
|
+
resolveSavePath,
|
|
115
|
+
writeFile,
|
|
86
116
|
fileSize: defaultFileSize,
|
|
87
117
|
maxUploadBytes: BLOCKS_MAX_UPLOAD_BYTES,
|
|
88
118
|
filePartFromPath,
|
|
@@ -140,11 +170,32 @@ server.tool(
|
|
|
140
170
|
(params) => cancelTask(params, deps),
|
|
141
171
|
);
|
|
142
172
|
|
|
173
|
+
server.tool(
|
|
174
|
+
'pause_task',
|
|
175
|
+
'Pause a running pipe task. Resume later with resume_task.',
|
|
176
|
+
{ taskId: z.string().describe('The task ID to pause') },
|
|
177
|
+
(params) => pauseTask(params, deps),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
server.tool(
|
|
181
|
+
'resume_task',
|
|
182
|
+
'Resume a paused pipe task',
|
|
183
|
+
{ taskId: z.string().describe('The task ID to resume') },
|
|
184
|
+
(params) => resumeTask(params, deps),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
server.tool(
|
|
188
|
+
'retry_task',
|
|
189
|
+
'Retry a failed task',
|
|
190
|
+
{ taskId: z.string().describe('The task ID to retry') },
|
|
191
|
+
(params) => retryTask(params, deps),
|
|
192
|
+
);
|
|
193
|
+
|
|
143
194
|
server.tool(
|
|
144
195
|
'list_agents',
|
|
145
196
|
'List available agents in the Blocks Network registry. Use listing="private" with an API key to discover your private agents.',
|
|
146
197
|
{
|
|
147
|
-
|
|
198
|
+
tag: z.string().optional().describe('Filter by tag slug'),
|
|
148
199
|
listing: z
|
|
149
200
|
.enum(['public', 'private'])
|
|
150
201
|
.optional()
|
|
@@ -161,6 +212,40 @@ server.tool(
|
|
|
161
212
|
(params) => getAgentCard(params, deps),
|
|
162
213
|
);
|
|
163
214
|
|
|
215
|
+
server.tool(
|
|
216
|
+
'get_agent_status',
|
|
217
|
+
'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.)',
|
|
218
|
+
{
|
|
219
|
+
agentNames: z
|
|
220
|
+
.array(z.string())
|
|
221
|
+
.min(1)
|
|
222
|
+
.max(MAX_AGENT_NAMES)
|
|
223
|
+
.describe(`Agent names to check (1-${MAX_AGENT_NAMES})`),
|
|
224
|
+
},
|
|
225
|
+
(params) => getAgentStatus(params, deps),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
server.tool(
|
|
229
|
+
'check_balance',
|
|
230
|
+
'Get the consumer billing balance for the configured org (BLOCKS_ORG_ID): ledger balance, active reservations, and available balance.',
|
|
231
|
+
{},
|
|
232
|
+
(_params) => checkBalance({}, deps),
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
server.tool(
|
|
236
|
+
'request_topup',
|
|
237
|
+
'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`).',
|
|
238
|
+
{
|
|
239
|
+
amountUsd: z
|
|
240
|
+
.number()
|
|
241
|
+
.positive()
|
|
242
|
+
.describe(
|
|
243
|
+
'Amount to add in USD (e.g. 25 or 19.99). Whole-cent precision. Must be at least $5 (platform minimum).',
|
|
244
|
+
),
|
|
245
|
+
},
|
|
246
|
+
(params) => requestTopup(params, deps),
|
|
247
|
+
);
|
|
248
|
+
|
|
164
249
|
server.tool(
|
|
165
250
|
'connect_task',
|
|
166
251
|
'Connect to an existing task, stream events and data until completion',
|
|
@@ -171,6 +256,20 @@ server.tool(
|
|
|
171
256
|
(params) => connectTask(params, deps),
|
|
172
257
|
);
|
|
173
258
|
|
|
259
|
+
server.tool(
|
|
260
|
+
'download_artifact',
|
|
261
|
+
'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).',
|
|
262
|
+
{
|
|
263
|
+
taskId: z.string().describe('The task ID that produced the artifact'),
|
|
264
|
+
fileName: z.string().describe('Artifact file name as listed by get_task'),
|
|
265
|
+
savePath: z
|
|
266
|
+
.string()
|
|
267
|
+
.optional()
|
|
268
|
+
.describe('Optional path (relative to BLOCKS_MCP_FILE_ROOT) to write the artifact to'),
|
|
269
|
+
},
|
|
270
|
+
(params) => downloadArtifact(params, deps),
|
|
271
|
+
);
|
|
272
|
+
|
|
174
273
|
function shutdown() {
|
|
175
274
|
for (const client of taskClients.values()) {
|
|
176
275
|
client.destroy();
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
|
|
11
|
+
export const PROTOCOL_VERSION_HEADER = 'Blocks-Protocol-Version';
|
|
12
|
+
export const CURRENT_PROTOCOL_VERSION = '2026-05-01';
|
package/src/registry-list.ts
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* optional Bearer API key for private/owned listings.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
PROTOCOL_VERSION_HEADER,
|
|
11
|
+
CURRENT_PROTOCOL_VERSION,
|
|
12
|
+
} from './protocol-headers.js';
|
|
11
13
|
|
|
12
14
|
export interface AgentListEntry {
|
|
13
15
|
agentName: string;
|
|
@@ -15,13 +17,13 @@ export interface AgentListEntry {
|
|
|
15
17
|
description?: string;
|
|
16
18
|
listing?: string;
|
|
17
19
|
billingMode?: string;
|
|
18
|
-
|
|
20
|
+
tags?: Array<{ id: string; name: string }>;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export interface ListAgentsOptions {
|
|
22
24
|
baseUrl: string;
|
|
23
25
|
apiKey?: string;
|
|
24
|
-
|
|
26
|
+
tag?: string;
|
|
25
27
|
listing?: 'public' | 'private';
|
|
26
28
|
limit?: number;
|
|
27
29
|
fetchImpl?: typeof fetch;
|
|
@@ -36,7 +38,7 @@ export async function listAgentsAuthenticated(
|
|
|
36
38
|
opts: ListAgentsOptions,
|
|
37
39
|
): Promise<ListAgentsResult> {
|
|
38
40
|
const params = new URLSearchParams({ include: 'full' });
|
|
39
|
-
if (opts.
|
|
41
|
+
if (opts.tag) params.set('tag', opts.tag);
|
|
40
42
|
if (opts.listing) {
|
|
41
43
|
params.set('listing', opts.listing);
|
|
42
44
|
if (opts.listing === 'private') params.set('scope', 'owned');
|