@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/dist/tools.js ADDED
@@ -0,0 +1,275 @@
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
+ skill: params.skill,
165
+ listing: params.listing,
166
+ limit: params.limit,
167
+ });
168
+ const lines = result.agents.map((a) => {
169
+ const skills = a.skills?.map((s) => s.name).join(', ') ?? '';
170
+ return `${a.agentName} | ${a.name ?? a.agentName} | ${a.listing ?? 'public'} | ${skills}`;
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
+ // Default file helpers (re-exported so index.ts can build a default ToolDeps)
272
+ // ============================================================================
273
+ export function defaultFileSize(path) {
274
+ return statSync(path).size;
275
+ }
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.62",
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
  }