@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/README.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
MCP (Model Context Protocol) server that exposes Blocks Network consumer operations as tools for AI assistants.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd mcp
|
|
9
|
+
npm install
|
|
10
|
+
npm run build
|
|
11
|
+
```
|
|
6
12
|
|
|
7
13
|
## Environment Variables
|
|
8
14
|
|
|
@@ -15,14 +21,10 @@ All other configuration (keys, endpoints) is resolved automatically from CDM.
|
|
|
15
21
|
|
|
16
22
|
## Installation
|
|
17
23
|
|
|
18
|
-
```bash
|
|
19
|
-
npm i @blocks-network/mcp-server
|
|
20
|
-
```
|
|
21
|
-
|
|
22
24
|
### Claude Code (CLI)
|
|
23
25
|
|
|
24
26
|
```bash
|
|
25
|
-
claude mcp add blocks-network --
|
|
27
|
+
claude mcp add blocks-network -- node /path/to/blocks-sdk/mcp/dist/index.js
|
|
26
28
|
```
|
|
27
29
|
|
|
28
30
|
Or add to your `.claude/settings.json`:
|
|
@@ -31,8 +33,8 @@ Or add to your `.claude/settings.json`:
|
|
|
31
33
|
{
|
|
32
34
|
"mcpServers": {
|
|
33
35
|
"blocks-network": {
|
|
34
|
-
"command": "
|
|
35
|
-
"args": ["
|
|
36
|
+
"command": "node",
|
|
37
|
+
"args": ["/path/to/blocks-sdk/mcp/dist/index.js"],
|
|
36
38
|
"env": {
|
|
37
39
|
"BLOCKS_API_KEY": "your-api-key"
|
|
38
40
|
}
|
|
@@ -49,8 +51,8 @@ Add to your `claude_desktop_config.json`:
|
|
|
49
51
|
{
|
|
50
52
|
"mcpServers": {
|
|
51
53
|
"blocks-network": {
|
|
52
|
-
"command": "
|
|
53
|
-
"args": ["
|
|
54
|
+
"command": "node",
|
|
55
|
+
"args": ["/path/to/blocks-sdk/mcp/dist/index.js"],
|
|
54
56
|
"env": {
|
|
55
57
|
"BLOCKS_API_KEY": "your-api-key"
|
|
56
58
|
}
|
|
@@ -71,8 +73,8 @@ Create an `mcp.json` file:
|
|
|
71
73
|
{
|
|
72
74
|
"mcpServers": {
|
|
73
75
|
"blocks-network": {
|
|
74
|
-
"command": "
|
|
75
|
-
"args": ["
|
|
76
|
+
"command": "node",
|
|
77
|
+
"args": ["/path/to/blocks-sdk/mcp/dist/index.js"],
|
|
76
78
|
"env": {
|
|
77
79
|
"BLOCKS_API_KEY": "your-api-key"
|
|
78
80
|
}
|
|
@@ -89,8 +91,8 @@ Add to your `~/.gemini/settings.json`:
|
|
|
89
91
|
{
|
|
90
92
|
"mcpServers": {
|
|
91
93
|
"blocks-network": {
|
|
92
|
-
"command": "
|
|
93
|
-
"args": ["
|
|
94
|
+
"command": "node",
|
|
95
|
+
"args": ["/path/to/blocks-sdk/mcp/dist/index.js"],
|
|
94
96
|
"env": {
|
|
95
97
|
"BLOCKS_API_KEY": "your-api-key"
|
|
96
98
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve, relative } from 'node:path';
|
|
3
|
-
import { realpathSync
|
|
3
|
+
import { realpathSync } 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 { sendTask, getTask, listTasks, cancelTask, listAgents, getAgentCard, connectTask, defaultFileSize, } from './tools.js';
|
|
8
10
|
const ALLOWED_ROOT = process.env.BLOCKS_MCP_FILE_ROOT ?? process.cwd();
|
|
9
11
|
if (!process.env.BLOCKS_API_KEY) {
|
|
10
12
|
console.error('Warning: BLOCKS_API_KEY is not set. Private agents and paid tasks will not work.');
|
|
@@ -25,12 +27,6 @@ function validateFilePath(filePath) {
|
|
|
25
27
|
}
|
|
26
28
|
return real;
|
|
27
29
|
}
|
|
28
|
-
function resolveBillingMode(task) {
|
|
29
|
-
const mode = task.billingMode;
|
|
30
|
-
if (mode === 'paid')
|
|
31
|
-
return 'paid';
|
|
32
|
-
return 'free';
|
|
33
|
-
}
|
|
34
30
|
const taskClients = new Map();
|
|
35
31
|
let resolvedBaseUrl;
|
|
36
32
|
async function getBaseUrl() {
|
|
@@ -44,28 +40,6 @@ async function getBaseUrl() {
|
|
|
44
40
|
resolvedBaseUrl = cdm.api.baseUrl;
|
|
45
41
|
return resolvedBaseUrl;
|
|
46
42
|
}
|
|
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
43
|
async function getTaskClient(billingMode = 'free') {
|
|
70
44
|
let client = taskClients.get(billingMode);
|
|
71
45
|
if (!client) {
|
|
@@ -75,301 +49,57 @@ async function getTaskClient(billingMode = 'free') {
|
|
|
75
49
|
}
|
|
76
50
|
return client;
|
|
77
51
|
}
|
|
52
|
+
const deps = {
|
|
53
|
+
getBaseUrl,
|
|
54
|
+
getApiKey: () => process.env.BLOCKS_API_KEY,
|
|
55
|
+
getTaskClient,
|
|
56
|
+
getAgentByName: (agentName, options) => getAgent(agentName, options),
|
|
57
|
+
listAgents: listAgentsAuthenticated,
|
|
58
|
+
validateFilePath,
|
|
59
|
+
fileSize: defaultFileSize,
|
|
60
|
+
maxUploadBytes: BLOCKS_MAX_UPLOAD_BYTES,
|
|
61
|
+
filePartFromPath,
|
|
62
|
+
textPart,
|
|
63
|
+
};
|
|
78
64
|
const server = new McpServer({
|
|
79
65
|
name: 'blocks-network',
|
|
80
|
-
version: '0.1.
|
|
66
|
+
version: '0.1.60',
|
|
81
67
|
});
|
|
82
|
-
// ============================================================================
|
|
83
|
-
// Tool: send_task
|
|
84
|
-
// ============================================================================
|
|
85
68
|
server.tool('send_task', 'Send a task to a Blocks Network agent and wait for the result', {
|
|
86
69
|
agentName: z.string().describe('Name of the target agent'),
|
|
87
70
|
message: z.string().describe('Text message to send to the agent'),
|
|
88
71
|
filePath: z.string().optional().describe('Optional file path to attach'),
|
|
89
|
-
inputs: z
|
|
72
|
+
inputs: z
|
|
73
|
+
.record(z.string(), z.string())
|
|
74
|
+
.optional()
|
|
75
|
+
.describe('Additional named inputs as {partId: value} pairs'),
|
|
90
76
|
taskKind: z.enum(['request', 'pipe']).optional().describe('Task kind (default: request)'),
|
|
91
77
|
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
|
-
// ============================================================================
|
|
78
|
+
timeoutMs: z
|
|
79
|
+
.number()
|
|
80
|
+
.optional()
|
|
81
|
+
.describe('Timeout in milliseconds to wait for result (default: 60000)'),
|
|
82
|
+
}, (params) => sendTask(params, deps));
|
|
83
|
+
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
84
|
server.tool('list_tasks', 'List tasks, optionally filtered by agent or state', {
|
|
218
85
|
agentName: z.string().optional().describe('Filter by agent name'),
|
|
219
86
|
state: z.string().optional().describe('Filter by state (e.g. completed, failed, running)'),
|
|
220
87
|
limit: z.number().optional().describe('Max results to return'),
|
|
221
|
-
},
|
|
222
|
-
|
|
223
|
-
const result = await client.listTasks({ agentName, state, limit });
|
|
224
|
-
const lines = result.tasks.map((t) => `${t.taskId} | ${t.agentName ?? '?'} | ${t.state ?? '?'} | ${t.createdTime ?? ''}`);
|
|
225
|
-
const header = `Tasks (${result.totalCount ?? result.tasks.length} total):`;
|
|
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
|
-
// ============================================================================
|
|
88
|
+
}, (params) => listTasks(params, deps));
|
|
89
|
+
server.tool('cancel_task', 'Cancel a running task', { taskId: z.string().describe('The task ID to cancel') }, (params) => cancelTask(params, deps));
|
|
241
90
|
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
91
|
skill: z.string().optional().describe('Filter by skill name'),
|
|
243
|
-
listing: z
|
|
92
|
+
listing: z
|
|
93
|
+
.enum(['public', 'private'])
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Filter by listing visibility (default: public)'),
|
|
244
96
|
limit: z.number().optional().describe('Max results to return'),
|
|
245
|
-
},
|
|
246
|
-
|
|
247
|
-
const apiKey = process.env.BLOCKS_API_KEY;
|
|
248
|
-
const result = await listAgentsAuthenticated({ baseUrl, apiKey, skill, listing, limit });
|
|
249
|
-
const lines = result.agents.map((a) => {
|
|
250
|
-
const skills = a.skills?.map((s) => s.name).join(', ') ?? '';
|
|
251
|
-
return `${a.agentName} | ${a.name ?? a.agentName} | ${a.listing ?? 'public'} | ${skills}`;
|
|
252
|
-
});
|
|
253
|
-
const header = `Agents (${result.totalCount ?? result.agents.length}):`;
|
|
254
|
-
return { content: [{ type: 'text', text: [header, ...lines].join('\n') }] };
|
|
255
|
-
});
|
|
256
|
-
// ============================================================================
|
|
257
|
-
// Tool: get_agent_card
|
|
258
|
-
// ============================================================================
|
|
259
|
-
server.tool('get_agent_card', 'Get the full agent card for a specific agent', {
|
|
260
|
-
agentName: z.string().describe('Agent name to look up'),
|
|
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
|
-
// ============================================================================
|
|
97
|
+
}, (params) => listAgents(params, deps));
|
|
98
|
+
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));
|
|
273
99
|
server.tool('connect_task', 'Connect to an existing task, stream events and data until completion', {
|
|
274
100
|
taskId: z.string().describe('The task ID to connect to'),
|
|
275
101
|
timeoutMs: z.number().optional().describe('Timeout in milliseconds (default: 60000)'),
|
|
276
|
-
},
|
|
277
|
-
const freeClient = await getTaskClient('free');
|
|
278
|
-
const task = await freeClient.getTask(taskId);
|
|
279
|
-
const billingMode = resolveBillingMode(task);
|
|
280
|
-
const client = await getTaskClient(billingMode);
|
|
281
|
-
const session = await client.connect({ taskId });
|
|
282
|
-
const timeout = timeoutMs ?? 60000;
|
|
283
|
-
const results = [];
|
|
284
|
-
const streamOutputs = [];
|
|
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
|
-
// ============================================================================
|
|
102
|
+
}, (params) => connectTask(params, deps));
|
|
373
103
|
function shutdown() {
|
|
374
104
|
for (const client of taskClients.values()) {
|
|
375
105
|
client.destroy();
|
|
@@ -0,0 +1,33 @@
|
|
|
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 declare const PROTOCOL_VERSION_HEADER = "Blocks-Protocol-Version";
|
|
9
|
+
export declare const CURRENT_PROTOCOL_VERSION = "2026-05-01";
|
|
10
|
+
export interface AgentListEntry {
|
|
11
|
+
agentName: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
listing?: string;
|
|
15
|
+
billingMode?: string;
|
|
16
|
+
skills?: Array<{
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export interface ListAgentsOptions {
|
|
22
|
+
baseUrl: string;
|
|
23
|
+
apiKey?: string;
|
|
24
|
+
skill?: string;
|
|
25
|
+
listing?: 'public' | 'private';
|
|
26
|
+
limit?: number;
|
|
27
|
+
fetchImpl?: typeof fetch;
|
|
28
|
+
}
|
|
29
|
+
export interface ListAgentsResult {
|
|
30
|
+
agents: AgentListEntry[];
|
|
31
|
+
totalCount?: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function listAgentsAuthenticated(opts: ListAgentsOptions): Promise<ListAgentsResult>;
|
|
@@ -0,0 +1,33 @@
|
|
|
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 const PROTOCOL_VERSION_HEADER = 'Blocks-Protocol-Version';
|
|
9
|
+
export const CURRENT_PROTOCOL_VERSION = '2026-05-01';
|
|
10
|
+
export async function listAgentsAuthenticated(opts) {
|
|
11
|
+
const params = new URLSearchParams({ include: 'full' });
|
|
12
|
+
if (opts.skill)
|
|
13
|
+
params.set('skill', opts.skill);
|
|
14
|
+
if (opts.listing) {
|
|
15
|
+
params.set('listing', opts.listing);
|
|
16
|
+
if (opts.listing === 'private')
|
|
17
|
+
params.set('scope', 'owned');
|
|
18
|
+
}
|
|
19
|
+
if (opts.limit)
|
|
20
|
+
params.set('limit', String(opts.limit));
|
|
21
|
+
const url = `${opts.baseUrl.replace(/\/+$/, '')}/api/v1/registry/agents?${params}`;
|
|
22
|
+
const headers = {
|
|
23
|
+
[PROTOCOL_VERSION_HEADER]: CURRENT_PROTOCOL_VERSION,
|
|
24
|
+
};
|
|
25
|
+
if (opts.apiKey)
|
|
26
|
+
headers['Authorization'] = `Bearer ${opts.apiKey}`;
|
|
27
|
+
const fetchFn = opts.fetchImpl ?? fetch;
|
|
28
|
+
const response = await fetchFn(url, { headers });
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Registry list failed: HTTP ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
return (await response.json());
|
|
33
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
export interface ArtifactRef {
|
|
11
|
+
fileName?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface DownloadedArtifact {
|
|
14
|
+
fileName?: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
data: Uint8Array;
|
|
17
|
+
}
|
|
18
|
+
export interface StreamDescriptor {
|
|
19
|
+
localDirection: string;
|
|
20
|
+
format: string;
|
|
21
|
+
declaredStream?: string;
|
|
22
|
+
streamId: string;
|
|
23
|
+
}
|
|
24
|
+
export interface StreamClient {
|
|
25
|
+
events(): AsyncIterable<unknown>;
|
|
26
|
+
bytes(): AsyncIterable<Uint8Array>;
|
|
27
|
+
}
|
|
28
|
+
export interface StreamRef {
|
|
29
|
+
descriptor: StreamDescriptor;
|
|
30
|
+
open(): StreamClient;
|
|
31
|
+
}
|
|
32
|
+
export interface ProgressEvent {
|
|
33
|
+
message?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface TerminalEvent {
|
|
36
|
+
state: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
reason?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface TaskSessionLike {
|
|
41
|
+
taskId: string;
|
|
42
|
+
onProgress(cb: (event: ProgressEvent) => void): unknown;
|
|
43
|
+
onStream(cb: (ref: StreamRef) => void): unknown;
|
|
44
|
+
listArtifacts(): ArtifactRef[];
|
|
45
|
+
listStreams(): StreamRef[];
|
|
46
|
+
downloadArtifact(ref: ArtifactRef): Promise<DownloadedArtifact>;
|
|
47
|
+
waitForTerminal(timeoutMs?: number): Promise<TerminalEvent>;
|
|
48
|
+
close(): void;
|
|
49
|
+
asyncClose(): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export interface TaskClientLike {
|
|
52
|
+
sendMessage(params: {
|
|
53
|
+
agentName: string;
|
|
54
|
+
requestParts: SendMessageRequestPart[];
|
|
55
|
+
taskKind?: 'request' | 'pipe';
|
|
56
|
+
duration?: number;
|
|
57
|
+
}): Promise<TaskSessionLike>;
|
|
58
|
+
getTask(taskId: string): Promise<TaskInfo>;
|
|
59
|
+
listTasks(params: {
|
|
60
|
+
agentName?: string;
|
|
61
|
+
state?: string;
|
|
62
|
+
limit?: number;
|
|
63
|
+
}): Promise<{
|
|
64
|
+
tasks: TaskInfo[];
|
|
65
|
+
totalCount?: number;
|
|
66
|
+
}>;
|
|
67
|
+
cancelTask(taskId: string): Promise<unknown>;
|
|
68
|
+
connect(params: {
|
|
69
|
+
taskId: string;
|
|
70
|
+
}): Promise<TaskSessionLike>;
|
|
71
|
+
}
|
|
72
|
+
export interface AgentEntryLike {
|
|
73
|
+
agentName: string;
|
|
74
|
+
card?: {
|
|
75
|
+
io?: {
|
|
76
|
+
inputs?: Array<{
|
|
77
|
+
id: string;
|
|
78
|
+
contentType: string;
|
|
79
|
+
}>;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
billingMode?: 'free' | 'paid';
|
|
83
|
+
}
|
|
84
|
+
export interface ToolResult {
|
|
85
|
+
[k: string]: unknown;
|
|
86
|
+
content: Array<{
|
|
87
|
+
type: 'text';
|
|
88
|
+
text: string;
|
|
89
|
+
}>;
|
|
90
|
+
isError?: boolean;
|
|
91
|
+
}
|
|
92
|
+
export interface ToolDeps {
|
|
93
|
+
getBaseUrl(): Promise<string>;
|
|
94
|
+
getApiKey(): string | undefined;
|
|
95
|
+
getTaskClient(billingMode?: 'free' | 'paid'): Promise<TaskClientLike>;
|
|
96
|
+
getAgentByName(agentName: string, options: {
|
|
97
|
+
baseUrl: string;
|
|
98
|
+
apiKey?: string;
|
|
99
|
+
}): Promise<AgentEntryLike | null>;
|
|
100
|
+
listAgents(options: {
|
|
101
|
+
baseUrl: string;
|
|
102
|
+
apiKey?: string;
|
|
103
|
+
skill?: string;
|
|
104
|
+
listing?: 'public' | 'private';
|
|
105
|
+
limit?: number;
|
|
106
|
+
}): Promise<ListAgentsResult>;
|
|
107
|
+
validateFilePath(filePath: string): string;
|
|
108
|
+
fileSize(path: string): number;
|
|
109
|
+
maxUploadBytes: number;
|
|
110
|
+
filePartFromPath: typeof filePartFromPath;
|
|
111
|
+
textPart: typeof textPart;
|
|
112
|
+
}
|
|
113
|
+
export interface SendTaskParams {
|
|
114
|
+
agentName: string;
|
|
115
|
+
message: string;
|
|
116
|
+
filePath?: string;
|
|
117
|
+
inputs?: Record<string, string>;
|
|
118
|
+
taskKind?: 'request' | 'pipe';
|
|
119
|
+
duration?: number;
|
|
120
|
+
timeoutMs?: number;
|
|
121
|
+
}
|
|
122
|
+
export interface GetTaskParams {
|
|
123
|
+
taskId: string;
|
|
124
|
+
}
|
|
125
|
+
export interface ListTasksParams {
|
|
126
|
+
agentName?: string;
|
|
127
|
+
state?: string;
|
|
128
|
+
limit?: number;
|
|
129
|
+
}
|
|
130
|
+
export interface CancelTaskParams {
|
|
131
|
+
taskId: string;
|
|
132
|
+
}
|
|
133
|
+
export interface ListAgentsParams {
|
|
134
|
+
skill?: string;
|
|
135
|
+
listing?: 'public' | 'private';
|
|
136
|
+
limit?: number;
|
|
137
|
+
}
|
|
138
|
+
export interface GetAgentCardParams {
|
|
139
|
+
agentName: string;
|
|
140
|
+
}
|
|
141
|
+
export interface ConnectTaskParams {
|
|
142
|
+
taskId: string;
|
|
143
|
+
timeoutMs?: number;
|
|
144
|
+
}
|
|
145
|
+
export declare function sendTask(params: SendTaskParams, deps: ToolDeps): Promise<ToolResult>;
|
|
146
|
+
export declare function getTask(params: GetTaskParams, deps: ToolDeps): Promise<ToolResult>;
|
|
147
|
+
export declare function listTasks(params: ListTasksParams, deps: ToolDeps): Promise<ToolResult>;
|
|
148
|
+
export declare function cancelTask(params: CancelTaskParams, deps: ToolDeps): Promise<ToolResult>;
|
|
149
|
+
export declare function listAgents(params: ListAgentsParams, deps: ToolDeps): Promise<ToolResult>;
|
|
150
|
+
export declare function getAgentCard(params: GetAgentCardParams, deps: ToolDeps): Promise<ToolResult>;
|
|
151
|
+
export declare function connectTask(params: ConnectTaskParams, deps: ToolDeps): Promise<ToolResult>;
|
|
152
|
+
export declare function defaultFileSize(path: string): number;
|