@blocks-network/mcp-server 0.1.55 → 0.1.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -14
- package/dist/index.js +35 -305
- package/dist/registry-list.d.ts +33 -0
- package/dist/registry-list.js +33 -0
- package/dist/tools.d.ts +152 -0
- package/dist/tools.js +275 -0
- package/package.json +18 -6
- package/src/index.ts +55 -341
- package/src/registry-list.ts +58 -0
- package/src/tools.ts +496 -0
- package/tests/cancel-task.test.ts +24 -0
- package/tests/connect-task.test.ts +180 -0
- package/tests/get-agent-card.test.ts +53 -0
- package/tests/get-task.test.ts +76 -0
- package/tests/helpers.ts +188 -0
- package/tests/list-agents.test.ts +74 -0
- package/tests/list-tasks.test.ts +58 -0
- package/tests/registry-list.test.ts +120 -0
- package/tests/send-task.test.ts +221 -0
- package/vitest.config.ts +7 -0
- package/LICENSE +0 -26
package/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.
|
|
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": "
|
|
19
|
-
"@modelcontextprotocol/sdk": "^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
|
}
|