@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { sendTask } from '../src/tools.js';
|
|
3
|
+
import { makeFakeDeps, makeFakeClient, makeFakeSession } from './helpers.js';
|
|
4
|
+
|
|
5
|
+
describe('send_task', () => {
|
|
6
|
+
it('sends a text part with default partId="text" when agent has no declared inputs', async () => {
|
|
7
|
+
const session = makeFakeSession({ taskId: 't1', terminal: { state: 'completed' } });
|
|
8
|
+
const client = makeFakeClient({ session });
|
|
9
|
+
const { deps, mocks } = makeFakeDeps({ client, agentEntry: null });
|
|
10
|
+
|
|
11
|
+
const res = await sendTask(
|
|
12
|
+
{ agentName: 'alice', message: 'hello' },
|
|
13
|
+
deps,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
expect(client.sendMessageMock).toHaveBeenCalledOnce();
|
|
17
|
+
const call = client.sendMessageMock.mock.calls[0][0];
|
|
18
|
+
expect(call.agentName).toBe('alice');
|
|
19
|
+
expect(call.requestParts).toHaveLength(1);
|
|
20
|
+
expect(mocks.textPart).toHaveBeenCalledWith('hello', 'text');
|
|
21
|
+
expect(res.isError).toBeUndefined();
|
|
22
|
+
expect(res.content[0].text).toContain('Task t1 completed');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('uses the agent card declared text input id', async () => {
|
|
26
|
+
const client = makeFakeClient();
|
|
27
|
+
const { deps, mocks } = makeFakeDeps({
|
|
28
|
+
client,
|
|
29
|
+
agentEntry: {
|
|
30
|
+
agentName: 'bob',
|
|
31
|
+
card: { io: { inputs: [{ id: 'prompt', contentType: 'text/plain' }] } },
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await sendTask({ agentName: 'bob', message: 'hi' }, deps);
|
|
36
|
+
|
|
37
|
+
expect(mocks.textPart).toHaveBeenCalledWith('hi', 'prompt');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('skips the default text part when caller supplies inputs override for the same id', async () => {
|
|
41
|
+
const client = makeFakeClient();
|
|
42
|
+
const { deps, mocks } = makeFakeDeps({
|
|
43
|
+
client,
|
|
44
|
+
agentEntry: {
|
|
45
|
+
agentName: 'bob',
|
|
46
|
+
card: { io: { inputs: [{ id: 'prompt', contentType: 'text/plain' }] } },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await sendTask(
|
|
51
|
+
{ agentName: 'bob', message: 'ignored', inputs: { prompt: 'override' } },
|
|
52
|
+
deps,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(mocks.textPart).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(mocks.textPart).toHaveBeenCalledWith('override', 'prompt');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('rejects file uploads larger than maxUploadBytes', async () => {
|
|
60
|
+
const client = makeFakeClient();
|
|
61
|
+
const { deps } = makeFakeDeps({
|
|
62
|
+
client,
|
|
63
|
+
maxUploadBytes: 1000,
|
|
64
|
+
fileSize: 5000,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const res = await sendTask(
|
|
68
|
+
{ agentName: 'alice', message: 'hi', filePath: '/tmp/big.bin' },
|
|
69
|
+
deps,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(res.isError).toBe(true);
|
|
73
|
+
expect(res.content[0].text).toContain('File too large');
|
|
74
|
+
expect(client.sendMessageMock).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('attaches a file part with the agent-declared file partId', async () => {
|
|
78
|
+
const client = makeFakeClient();
|
|
79
|
+
const { deps, mocks } = makeFakeDeps({
|
|
80
|
+
client,
|
|
81
|
+
agentEntry: {
|
|
82
|
+
agentName: 'alice',
|
|
83
|
+
card: {
|
|
84
|
+
io: {
|
|
85
|
+
inputs: [
|
|
86
|
+
{ id: 'prompt', contentType: 'text/plain' },
|
|
87
|
+
{ id: 'attachment', contentType: 'image/png' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
fileSize: 50,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await sendTask(
|
|
96
|
+
{ agentName: 'alice', message: 'hi', filePath: '/safe/img.png' },
|
|
97
|
+
deps,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(mocks.validateFilePath).toHaveBeenCalledWith('/safe/img.png');
|
|
101
|
+
expect(mocks.filePartFromPath).toHaveBeenCalledWith('/safe/img.png', {
|
|
102
|
+
partId: 'attachment',
|
|
103
|
+
contentType: 'image/png',
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('forwards taskKind and duration to sendMessage', async () => {
|
|
108
|
+
const client = makeFakeClient();
|
|
109
|
+
const { deps } = makeFakeDeps({ client });
|
|
110
|
+
|
|
111
|
+
await sendTask(
|
|
112
|
+
{ agentName: 'alice', message: 'hi', taskKind: 'pipe', duration: 30 },
|
|
113
|
+
deps,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const call = client.sendMessageMock.mock.calls[0][0];
|
|
117
|
+
expect(call.taskKind).toBe('pipe');
|
|
118
|
+
expect(call.duration).toBe(30);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('uses the billingMode from the agent registry entry', async () => {
|
|
122
|
+
const { deps, mocks } = makeFakeDeps({
|
|
123
|
+
agentEntry: { agentName: 'alice', billingMode: 'paid' },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
await sendTask({ agentName: 'alice', message: 'hi' }, deps);
|
|
127
|
+
|
|
128
|
+
expect(mocks.getTaskClient).toHaveBeenCalledWith('paid');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('emits progress lines, includes text artifacts inline, and closes the session', async () => {
|
|
132
|
+
const session = makeFakeSession({
|
|
133
|
+
taskId: 't9',
|
|
134
|
+
terminal: { state: 'completed' },
|
|
135
|
+
progressEvents: [{ message: 'starting' }, { message: 'half-done' }],
|
|
136
|
+
artifacts: [
|
|
137
|
+
{
|
|
138
|
+
fileName: 'out.txt',
|
|
139
|
+
mimeType: 'text/plain',
|
|
140
|
+
data: new TextEncoder().encode('hello world'),
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
const client = makeFakeClient({ session });
|
|
145
|
+
const { deps } = makeFakeDeps({ client });
|
|
146
|
+
|
|
147
|
+
const res = await sendTask({ agentName: 'alice', message: 'hi' }, deps);
|
|
148
|
+
|
|
149
|
+
const text = res.content[0].text;
|
|
150
|
+
expect(text).toContain('Task t9 completed');
|
|
151
|
+
expect(text).toContain('[progress] starting');
|
|
152
|
+
expect(text).toContain('[progress] half-done');
|
|
153
|
+
expect(text).toContain('[artifact: out.txt]');
|
|
154
|
+
expect(text).toContain('hello world');
|
|
155
|
+
expect(session.closeMock).toHaveBeenCalledOnce();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('summarises binary artifacts by size rather than embedding their bytes', async () => {
|
|
159
|
+
const session = makeFakeSession({
|
|
160
|
+
terminal: { state: 'completed' },
|
|
161
|
+
artifacts: [
|
|
162
|
+
{
|
|
163
|
+
fileName: 'image.png',
|
|
164
|
+
mimeType: 'image/png',
|
|
165
|
+
data: new Uint8Array(64),
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
const client = makeFakeClient({ session });
|
|
170
|
+
const { deps } = makeFakeDeps({ client });
|
|
171
|
+
|
|
172
|
+
const res = await sendTask({ agentName: 'alice', message: 'hi' }, deps);
|
|
173
|
+
|
|
174
|
+
expect(res.content[0].text).toContain('[artifact: image.png] (image/png, 64 bytes)');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('reports artifact download failures without crashing', async () => {
|
|
178
|
+
const session = makeFakeSession({
|
|
179
|
+
terminal: { state: 'completed' },
|
|
180
|
+
artifacts: [
|
|
181
|
+
{
|
|
182
|
+
fileName: 'broken.bin',
|
|
183
|
+
mimeType: 'application/octet-stream',
|
|
184
|
+
data: new Uint8Array(),
|
|
185
|
+
downloadFails: true,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
const client = makeFakeClient({ session });
|
|
190
|
+
const { deps } = makeFakeDeps({ client });
|
|
191
|
+
|
|
192
|
+
const res = await sendTask({ agentName: 'alice', message: 'hi' }, deps);
|
|
193
|
+
expect(res.content[0].text).toContain('[artifact: broken.bin] (download failed)');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('returns isError=true when waitForTerminal rejects (timeout)', async () => {
|
|
197
|
+
const session = makeFakeSession({
|
|
198
|
+
taskId: 't_timeout',
|
|
199
|
+
terminalRejects: new Error('timed out after 60000ms'),
|
|
200
|
+
});
|
|
201
|
+
const client = makeFakeClient({ session });
|
|
202
|
+
const { deps } = makeFakeDeps({ client });
|
|
203
|
+
|
|
204
|
+
const res = await sendTask({ agentName: 'alice', message: 'hi' }, deps);
|
|
205
|
+
|
|
206
|
+
expect(res.isError).toBe(true);
|
|
207
|
+
expect(res.content[0].text).toContain('Task t_timeout error: timed out after 60000ms');
|
|
208
|
+
expect(session.closeMock).toHaveBeenCalledOnce();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('appends an "Error:" line for failed terminals', async () => {
|
|
212
|
+
const session = makeFakeSession({
|
|
213
|
+
terminal: { state: 'failed', error: 'boom' },
|
|
214
|
+
});
|
|
215
|
+
const client = makeFakeClient({ session });
|
|
216
|
+
const { deps } = makeFakeDeps({ client });
|
|
217
|
+
|
|
218
|
+
const res = await sendTask({ agentName: 'alice', message: 'hi' }, deps);
|
|
219
|
+
expect(res.content[0].text).toContain('Error: boom');
|
|
220
|
+
});
|
|
221
|
+
});
|
package/vitest.config.ts
ADDED
package/LICENSE
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
Blocks Network Software Development Kit License Agreement
|
|
2
|
-
Copyright © 2026 Blocks Network Inc. All rights reserved.
|
|
3
|
-
|
|
4
|
-
Subject to the terms and conditions of the license, you are hereby granted
|
|
5
|
-
a non-exclusive, worldwide, royalty-free license to (a) copy and modify
|
|
6
|
-
the software in source code or binary form for use with the software services
|
|
7
|
-
and interfaces provided by Blocks Network, and (b) redistribute unmodified copies
|
|
8
|
-
of the software to third parties. The software may not be incorporated in
|
|
9
|
-
or used to provide any product or service competitive with the products
|
|
10
|
-
and services of Blocks Network.
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this license shall be included
|
|
13
|
-
in or with all copies or substantial portions of the software.
|
|
14
|
-
|
|
15
|
-
This license does not grant you permission to use the trade names, trademarks,
|
|
16
|
-
service marks, or product names of Blocks Network, except as required for reasonable
|
|
17
|
-
and customary use in describing the origin of the software and reproducing
|
|
18
|
-
the content of this license.
|
|
19
|
-
|
|
20
|
-
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF
|
|
21
|
-
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
22
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
23
|
-
EVENT SHALL BLOCKS NETWORK OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE
|
|
24
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
25
|
-
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
26
|
-
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|