@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.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Tool handlers for the Blocks Network MCP server.
3
+ *
4
+ * Each handler is a pure function that takes its dependencies (clients,
5
+ * registry helpers, fs/path utilities) so it can be unit-tested without
6
+ * touching the network or the filesystem.
7
+ */
8
+ import { textPart, filePartFromPath, type TaskInfo, type SendMessageRequestPart } from '@blocks-network/sdk';
9
+ import type { ListAgentsResult } from './registry-list.js';
10
+ import type { AgentStatusResponse } from './agent-status.js';
11
+ import type { ConsumerBalance, TopUpSession } from './billing.js';
12
+ export interface ArtifactRef {
13
+ fileName?: string;
14
+ }
15
+ export interface DownloadedArtifact {
16
+ fileName?: string;
17
+ mimeType: string;
18
+ data: Uint8Array;
19
+ }
20
+ export interface StreamDescriptor {
21
+ localDirection: string;
22
+ format: string;
23
+ declaredStream?: string;
24
+ streamId: string;
25
+ }
26
+ export interface StreamClient {
27
+ events(): AsyncIterable<unknown>;
28
+ bytes(): AsyncIterable<Uint8Array>;
29
+ }
30
+ export interface StreamRef {
31
+ descriptor: StreamDescriptor;
32
+ open(): StreamClient;
33
+ }
34
+ export interface ProgressEvent {
35
+ message?: string;
36
+ }
37
+ export interface TerminalEvent {
38
+ state: string;
39
+ error?: string;
40
+ reason?: string;
41
+ }
42
+ export interface TaskSessionLike {
43
+ taskId: string;
44
+ onProgress(cb: (event: ProgressEvent) => void): unknown;
45
+ onStream(cb: (ref: StreamRef) => void): unknown;
46
+ listArtifacts(): ArtifactRef[];
47
+ listStreams(): StreamRef[];
48
+ downloadArtifact(ref: ArtifactRef): Promise<DownloadedArtifact>;
49
+ waitForTerminal(timeoutMs?: number): Promise<TerminalEvent>;
50
+ close(): void;
51
+ asyncClose(): Promise<void>;
52
+ }
53
+ export interface TaskClientLike {
54
+ sendMessage(params: {
55
+ agentName: string;
56
+ requestParts: SendMessageRequestPart[];
57
+ taskKind?: 'request' | 'pipe';
58
+ duration?: number;
59
+ }): Promise<TaskSessionLike>;
60
+ getTask(taskId: string): Promise<TaskInfo>;
61
+ listTasks(params: {
62
+ agentName?: string;
63
+ state?: string;
64
+ limit?: number;
65
+ }): Promise<{
66
+ tasks: TaskInfo[];
67
+ totalCount?: number;
68
+ }>;
69
+ cancelTask(taskId: string): Promise<unknown>;
70
+ pauseTask(taskId: string): Promise<unknown>;
71
+ resumeTask(taskId: string): Promise<unknown>;
72
+ retryTask(taskId: string): Promise<unknown>;
73
+ connect(params: {
74
+ taskId: string;
75
+ }): Promise<TaskSessionLike>;
76
+ }
77
+ export interface AgentEntryLike {
78
+ agentName: string;
79
+ card?: {
80
+ io?: {
81
+ inputs?: Array<{
82
+ id: string;
83
+ contentType: string;
84
+ }>;
85
+ };
86
+ };
87
+ billingMode?: 'free' | 'paid';
88
+ }
89
+ export interface ToolResult {
90
+ [k: string]: unknown;
91
+ content: Array<{
92
+ type: 'text';
93
+ text: string;
94
+ }>;
95
+ isError?: boolean;
96
+ }
97
+ export interface ToolDeps {
98
+ getBaseUrl(): Promise<string>;
99
+ getApiKey(): string | undefined;
100
+ getTaskClient(billingMode?: 'free' | 'paid'): Promise<TaskClientLike>;
101
+ getAgentByName(agentName: string, options: {
102
+ baseUrl: string;
103
+ apiKey?: string;
104
+ }): Promise<AgentEntryLike | null>;
105
+ listAgents(options: {
106
+ baseUrl: string;
107
+ apiKey?: string;
108
+ tag?: string;
109
+ listing?: 'public' | 'private';
110
+ limit?: number;
111
+ }): Promise<ListAgentsResult>;
112
+ validateFilePath(filePath: string): string;
113
+ resolveSavePath(filePath: string): string;
114
+ writeFile(filePath: string, data: Uint8Array): void;
115
+ fileSize(path: string): number;
116
+ maxUploadBytes: number;
117
+ filePartFromPath: typeof filePartFromPath;
118
+ textPart: typeof textPart;
119
+ getOrgId(): string | undefined;
120
+ fetchAgentStatus(options: {
121
+ baseUrl: string;
122
+ apiKey?: string;
123
+ agentNames: string[];
124
+ }): Promise<AgentStatusResponse>;
125
+ getConsumerBalance(options: {
126
+ baseUrl: string;
127
+ apiKey: string;
128
+ orgId: string;
129
+ }): Promise<ConsumerBalance>;
130
+ createConsumerTopUp(options: {
131
+ baseUrl: string;
132
+ apiKey: string;
133
+ orgId: string;
134
+ amountUsd: number;
135
+ }): Promise<TopUpSession>;
136
+ }
137
+ export interface SendTaskParams {
138
+ agentName: string;
139
+ message: string;
140
+ filePath?: string;
141
+ inputs?: Record<string, string>;
142
+ taskKind?: 'request' | 'pipe';
143
+ duration?: number;
144
+ timeoutMs?: number;
145
+ }
146
+ export interface GetTaskParams {
147
+ taskId: string;
148
+ }
149
+ export interface ListTasksParams {
150
+ agentName?: string;
151
+ state?: string;
152
+ limit?: number;
153
+ }
154
+ export interface CancelTaskParams {
155
+ taskId: string;
156
+ }
157
+ export interface ListAgentsParams {
158
+ tag?: string;
159
+ listing?: 'public' | 'private';
160
+ limit?: number;
161
+ }
162
+ export interface GetAgentCardParams {
163
+ agentName: string;
164
+ }
165
+ export interface ConnectTaskParams {
166
+ taskId: string;
167
+ timeoutMs?: number;
168
+ }
169
+ export interface DownloadArtifactParams {
170
+ taskId: string;
171
+ fileName: string;
172
+ savePath?: string;
173
+ }
174
+ export interface TaskIdParams {
175
+ taskId: string;
176
+ }
177
+ export interface GetAgentStatusParams {
178
+ agentNames: string[];
179
+ }
180
+ export interface RequestTopUpParams {
181
+ amountUsd: number;
182
+ }
183
+ export declare function sendTask(params: SendTaskParams, deps: ToolDeps): Promise<ToolResult>;
184
+ export declare function getTask(params: GetTaskParams, deps: ToolDeps): Promise<ToolResult>;
185
+ export declare function listTasks(params: ListTasksParams, deps: ToolDeps): Promise<ToolResult>;
186
+ export declare function cancelTask(params: CancelTaskParams, deps: ToolDeps): Promise<ToolResult>;
187
+ export declare function listAgents(params: ListAgentsParams, deps: ToolDeps): Promise<ToolResult>;
188
+ export declare function getAgentCard(params: GetAgentCardParams, deps: ToolDeps): Promise<ToolResult>;
189
+ export declare function connectTask(params: ConnectTaskParams, deps: ToolDeps): Promise<ToolResult>;
190
+ export declare function downloadArtifact(params: DownloadArtifactParams, deps: ToolDeps): Promise<ToolResult>;
191
+ export declare function pauseTask(params: TaskIdParams, deps: ToolDeps): Promise<ToolResult>;
192
+ export declare function resumeTask(params: TaskIdParams, deps: ToolDeps): Promise<ToolResult>;
193
+ export declare function retryTask(params: TaskIdParams, deps: ToolDeps): Promise<ToolResult>;
194
+ export declare function getAgentStatus(params: GetAgentStatusParams, deps: ToolDeps): Promise<ToolResult>;
195
+ export declare function checkBalance(_params: Record<string, never>, deps: ToolDeps): Promise<ToolResult>;
196
+ export declare function requestTopup(params: RequestTopUpParams, deps: ToolDeps): Promise<ToolResult>;
197
+ export declare function defaultFileSize(path: string): number;
package/dist/tools.js ADDED
@@ -0,0 +1,441 @@
1
+ /**
2
+ * Tool handlers for the Blocks Network MCP server.
3
+ *
4
+ * Each handler is a pure function that takes its dependencies (clients,
5
+ * registry helpers, fs/path utilities) so it can be unit-tested without
6
+ * touching the network or the filesystem.
7
+ */
8
+ import { statSync } from 'node:fs';
9
+ // ============================================================================
10
+ // Helpers
11
+ // ============================================================================
12
+ const TERMINAL_STATES = new Set(['completed', 'failed', 'canceled']);
13
+ function resolveBillingMode(task) {
14
+ const mode = task.billingMode;
15
+ return mode === 'paid' ? 'paid' : 'free';
16
+ }
17
+ function isTextLikeContentType(ct) {
18
+ return ct.startsWith('text/') || ct === 'application/json' || ct.endsWith('+json');
19
+ }
20
+ function isTextMimeType(mimeType) {
21
+ return mimeType.startsWith('text/') || mimeType === 'application/json';
22
+ }
23
+ async function appendArtifacts(session, refs, out) {
24
+ for (const ref of refs) {
25
+ try {
26
+ const downloaded = await session.downloadArtifact(ref);
27
+ if (isTextMimeType(downloaded.mimeType)) {
28
+ const text = new TextDecoder().decode(downloaded.data);
29
+ out.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}]\n${text}`);
30
+ }
31
+ else {
32
+ out.push(`[artifact: ${downloaded.fileName ?? 'unnamed'}] (${downloaded.mimeType}, ${downloaded.data.length} bytes)`);
33
+ }
34
+ }
35
+ catch {
36
+ out.push(`[artifact: ${ref.fileName ?? 'unnamed'}] (download failed)`);
37
+ }
38
+ }
39
+ }
40
+ // ============================================================================
41
+ // send_task
42
+ // ============================================================================
43
+ export async function sendTask(params, deps) {
44
+ const baseUrl = await deps.getBaseUrl();
45
+ const apiKey = deps.getApiKey();
46
+ const entry = await deps.getAgentByName(params.agentName, { baseUrl, apiKey });
47
+ const billingMode = entry?.billingMode ?? 'free';
48
+ const client = await deps.getTaskClient(billingMode);
49
+ const declaredInputs = entry?.card?.io?.inputs ?? [];
50
+ const textInput = declaredInputs.find((i) => isTextLikeContentType(i.contentType));
51
+ const fileInput = declaredInputs.find((i) => !isTextLikeContentType(i.contentType));
52
+ const requestParts = [];
53
+ const textPartId = textInput?.id ?? 'text';
54
+ if ((textInput || declaredInputs.length === 0) && !params.inputs?.[textPartId]) {
55
+ requestParts.push(deps.textPart(params.message, textPartId));
56
+ }
57
+ if (params.filePath) {
58
+ const safePath = deps.validateFilePath(params.filePath);
59
+ const size = deps.fileSize(safePath);
60
+ if (size > deps.maxUploadBytes) {
61
+ return {
62
+ content: [
63
+ {
64
+ type: 'text',
65
+ text: `File too large (${size} bytes). Maximum upload size is ${deps.maxUploadBytes} bytes (25 MB).`,
66
+ },
67
+ ],
68
+ isError: true,
69
+ };
70
+ }
71
+ requestParts.push(await deps.filePartFromPath(safePath, {
72
+ partId: fileInput?.id ?? 'file',
73
+ contentType: fileInput?.contentType,
74
+ }));
75
+ }
76
+ if (params.inputs) {
77
+ for (const [partId, value] of Object.entries(params.inputs)) {
78
+ requestParts.push(deps.textPart(value, partId));
79
+ }
80
+ }
81
+ const session = await client.sendMessage({
82
+ agentName: params.agentName,
83
+ requestParts,
84
+ taskKind: params.taskKind,
85
+ duration: params.duration,
86
+ });
87
+ const timeout = params.timeoutMs ?? 60000;
88
+ const progressLines = [];
89
+ session.onProgress((event) => {
90
+ if (event.message)
91
+ progressLines.push(`[progress] ${event.message}`);
92
+ });
93
+ try {
94
+ const terminal = await session.waitForTerminal(timeout);
95
+ const output = [`Task ${session.taskId} ${terminal.state}`, ...progressLines];
96
+ await appendArtifacts(session, session.listArtifacts(), output);
97
+ if (terminal.state === 'failed') {
98
+ output.push(`Error: ${terminal.error ?? terminal.reason ?? 'unknown'}`);
99
+ }
100
+ session.close();
101
+ return { content: [{ type: 'text', text: output.join('\n') }] };
102
+ }
103
+ catch (err) {
104
+ session.close();
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ return {
107
+ content: [{ type: 'text', text: `Task ${session.taskId} error: ${msg}` }],
108
+ isError: true,
109
+ };
110
+ }
111
+ }
112
+ // ============================================================================
113
+ // get_task
114
+ // ============================================================================
115
+ export async function getTask(params, deps) {
116
+ const freeClient = await deps.getTaskClient('free');
117
+ const task = await freeClient.getTask(params.taskId);
118
+ const output = [JSON.stringify(task, null, 2)];
119
+ if (task.state && TERMINAL_STATES.has(task.state)) {
120
+ try {
121
+ const billingMode = resolveBillingMode(task);
122
+ const client = await deps.getTaskClient(billingMode);
123
+ const session = await client.connect({ taskId: params.taskId });
124
+ await appendArtifacts(session, session.listArtifacts(), output);
125
+ session.close();
126
+ }
127
+ catch {
128
+ // connect failed — return task info without artifacts
129
+ }
130
+ }
131
+ return { content: [{ type: 'text', text: output.join('\n') }] };
132
+ }
133
+ // ============================================================================
134
+ // list_tasks
135
+ // ============================================================================
136
+ export async function listTasks(params, deps) {
137
+ const client = await deps.getTaskClient();
138
+ const result = await client.listTasks({
139
+ agentName: params.agentName,
140
+ state: params.state,
141
+ limit: params.limit,
142
+ });
143
+ const lines = result.tasks.map((t) => `${t.taskId} | ${t.agentName ?? '?'} | ${t.state ?? '?'} | ${t.createdTime ?? ''}`);
144
+ const header = `Tasks (${result.totalCount ?? result.tasks.length} total):`;
145
+ return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
146
+ }
147
+ // ============================================================================
148
+ // cancel_task
149
+ // ============================================================================
150
+ export async function cancelTask(params, deps) {
151
+ const client = await deps.getTaskClient();
152
+ await client.cancelTask(params.taskId);
153
+ return { content: [{ type: 'text', text: `Task ${params.taskId} cancelled.` }] };
154
+ }
155
+ // ============================================================================
156
+ // list_agents
157
+ // ============================================================================
158
+ export async function listAgents(params, deps) {
159
+ const baseUrl = await deps.getBaseUrl();
160
+ const apiKey = deps.getApiKey();
161
+ const result = await deps.listAgents({
162
+ baseUrl,
163
+ apiKey,
164
+ tag: params.tag,
165
+ listing: params.listing,
166
+ limit: params.limit,
167
+ });
168
+ const lines = result.agents.map((a) => {
169
+ const tags = a.tags?.map((t) => t.name).join(', ') ?? '';
170
+ return `${a.agentName} | ${a.name ?? a.agentName} | ${a.listing ?? 'public'} | ${tags}`;
171
+ });
172
+ const header = `Agents (${result.totalCount ?? result.agents.length}):`;
173
+ return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
174
+ }
175
+ // ============================================================================
176
+ // get_agent_card
177
+ // ============================================================================
178
+ export async function getAgentCard(params, deps) {
179
+ const baseUrl = await deps.getBaseUrl();
180
+ const apiKey = deps.getApiKey();
181
+ const entry = await deps.getAgentByName(params.agentName, { baseUrl, apiKey });
182
+ if (!entry) {
183
+ return {
184
+ content: [{ type: 'text', text: `Agent "${params.agentName}" not found.` }],
185
+ isError: true,
186
+ };
187
+ }
188
+ return {
189
+ content: [{ type: 'text', text: JSON.stringify(entry.card ?? entry, null, 2) }],
190
+ };
191
+ }
192
+ // ============================================================================
193
+ // connect_task
194
+ // ============================================================================
195
+ export async function connectTask(params, deps) {
196
+ const freeClient = await deps.getTaskClient('free');
197
+ const task = await freeClient.getTask(params.taskId);
198
+ const billingMode = resolveBillingMode(task);
199
+ const client = await deps.getTaskClient(billingMode);
200
+ const session = await client.connect({ taskId: params.taskId });
201
+ const timeout = params.timeoutMs ?? 60000;
202
+ const progressLines = [];
203
+ const streamOutputs = [];
204
+ const streamDrains = [];
205
+ session.onProgress((event) => {
206
+ if (event.message)
207
+ progressLines.push(`[progress] ${event.message}`);
208
+ });
209
+ const drainStream = (streamRef) => {
210
+ const dir = streamRef.descriptor.localDirection;
211
+ if (dir !== 'inbound' && dir !== 'bidirectional')
212
+ return;
213
+ const format = streamRef.descriptor.format;
214
+ const label = streamRef.descriptor.declaredStream ?? streamRef.descriptor.streamId;
215
+ const entry = { label, chunks: [] };
216
+ streamOutputs.push(entry);
217
+ try {
218
+ const streamClient = streamRef.open();
219
+ const drain = (async () => {
220
+ if (format === 'events') {
221
+ for await (const event of streamClient.events()) {
222
+ entry.chunks.push(JSON.stringify(event) + '\n');
223
+ }
224
+ }
225
+ else {
226
+ const decoder = new TextDecoder();
227
+ for await (const chunk of streamClient.bytes()) {
228
+ entry.chunks.push(decoder.decode(chunk, { stream: true }));
229
+ }
230
+ }
231
+ })();
232
+ streamDrains.push(drain.catch(() => { }));
233
+ }
234
+ catch {
235
+ // stream already ended or terminal
236
+ }
237
+ };
238
+ for (const ref of session.listStreams())
239
+ drainStream(ref);
240
+ session.onStream(drainStream);
241
+ try {
242
+ const terminal = await session.waitForTerminal(timeout);
243
+ await Promise.allSettled(streamDrains);
244
+ const out = [`Task ${params.taskId} ${terminal.state}`, ...progressLines];
245
+ for (const s of streamOutputs) {
246
+ if (s.chunks.length > 0) {
247
+ out.push(`[stream: ${s.label}]\n${s.chunks.join('')}`);
248
+ }
249
+ }
250
+ await appendArtifacts(session, session.listArtifacts(), out);
251
+ if (terminal.state === 'failed') {
252
+ out.push(`Error: ${terminal.error ?? terminal.reason ?? 'unknown'}`);
253
+ }
254
+ await session.asyncClose();
255
+ return { content: [{ type: 'text', text: out.join('\n') }] };
256
+ }
257
+ catch (err) {
258
+ await session.asyncClose();
259
+ const msg = err instanceof Error ? err.message : String(err);
260
+ const partial = [`Task ${params.taskId} timed out (${msg})`];
261
+ for (const s of streamOutputs) {
262
+ if (s.chunks.length > 0) {
263
+ partial.push(`[stream: ${s.label}]\n${s.chunks.join('')}`);
264
+ }
265
+ }
266
+ partial.push(...progressLines);
267
+ return { content: [{ type: 'text', text: partial.join('\n') }], isError: true };
268
+ }
269
+ }
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
+ // ============================================================================
437
+ // Default file helpers (re-exported so index.ts can build a default ToolDeps)
438
+ // ============================================================================
439
+ export function defaultFileSize(path) {
440
+ return statSync(path).size;
441
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocks-network/mcp-server",
3
- "version": "0.1.55",
3
+ "version": "0.1.64",
4
4
  "description": "MCP server for Blocks Network consumer operations",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,19 +12,31 @@
12
12
  "prebuild": "npm run build:sdk",
13
13
  "build": "tsc",
14
14
  "start": "node dist/index.js",
15
- "dev": "tsx src/index.ts"
15
+ "dev": "tsx src/index.ts",
16
+ "test": "vitest run"
16
17
  },
17
18
  "dependencies": {
18
- "@blocks-network/sdk": "file:../sdks/node",
19
- "@modelcontextprotocol/sdk": "^1.12.1",
19
+ "@blocks-network/sdk": "^0.1.60",
20
+ "@modelcontextprotocol/sdk": "^1.29",
20
21
  "zod": "^3.25.1"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@types/node": "^22.0.0",
24
25
  "tsx": "^4.19.0",
25
- "typescript": "^5.7.0"
26
+ "typescript": "^5.7.0",
27
+ "vitest": "^3.2.4"
26
28
  },
27
29
  "engines": {
28
30
  "node": ">=20.0.0"
29
- }
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/blocksnetwork/blocks-sdk.git",
35
+ "directory": "mcp"
36
+ },
37
+ "homepage": "https://github.com/blocksnetwork/blocks-sdk/tree/main/mcp#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/blocksnetwork/blocks-sdk/issues"
40
+ },
41
+ "license": "SEE LICENSE IN LICENSE"
30
42
  }