@blocks-network/mcp-server 0.1.62 → 0.1.65

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.
@@ -1,9 +1,12 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import {
3
3
  listAgentsAuthenticated,
4
+ listAllAgentsAuthenticated,
5
+ } from '../src/registry-list.js';
6
+ import {
4
7
  PROTOCOL_VERSION_HEADER,
5
8
  CURRENT_PROTOCOL_VERSION,
6
- } from '../src/registry-list.js';
9
+ } from '../src/protocol-headers.js';
7
10
 
8
11
  function mockResponse(body: unknown, ok = true, status = 200): Response {
9
12
  return {
@@ -57,7 +60,7 @@ describe('listAgentsAuthenticated', () => {
57
60
  await listAgentsAuthenticated({
58
61
  baseUrl: 'http://api.test/',
59
62
  listing: 'private',
60
- skill: 'translate',
63
+ tag: 'translate',
61
64
  limit: 50,
62
65
  fetchImpl: fetchImpl as unknown as typeof fetch,
63
66
  });
@@ -68,7 +71,7 @@ describe('listAgentsAuthenticated', () => {
68
71
  expect(parsed.searchParams.get('include')).toBe('full');
69
72
  expect(parsed.searchParams.get('listing')).toBe('private');
70
73
  expect(parsed.searchParams.get('scope')).toBe('owned');
71
- expect(parsed.searchParams.get('skill')).toBe('translate');
74
+ expect(parsed.searchParams.get('tag')).toBe('translate');
72
75
  expect(parsed.searchParams.get('limit')).toBe('50');
73
76
  });
74
77
 
@@ -104,7 +107,7 @@ describe('listAgentsAuthenticated', () => {
104
107
 
105
108
  it('returns parsed JSON body on success', async () => {
106
109
  const body = {
107
- agents: [{ agentName: 'alice', skills: [] }],
110
+ agents: [{ agentName: 'alice', tags: [] }],
108
111
  totalCount: 1,
109
112
  };
110
113
  const fetchImpl = vi
@@ -117,4 +120,125 @@ describe('listAgentsAuthenticated', () => {
117
120
  });
118
121
  expect(result).toEqual(body);
119
122
  });
123
+
124
+ it('forwards the cursor query param when provided', async () => {
125
+ const fetchImpl = vi.fn().mockResolvedValue(
126
+ mockResponse({ agents: [], totalCount: 0 }),
127
+ );
128
+
129
+ await listAgentsAuthenticated({
130
+ baseUrl: 'http://api.test',
131
+ cursor: 'abc123',
132
+ fetchImpl: fetchImpl as unknown as typeof fetch,
133
+ });
134
+
135
+ const [url] = fetchImpl.mock.calls[0];
136
+ expect(new URL(String(url)).searchParams.get('cursor')).toBe('abc123');
137
+ });
138
+ });
139
+
140
+ describe('listAllAgentsAuthenticated', () => {
141
+ function agent(name: string) {
142
+ return { agentName: name, tags: [] };
143
+ }
144
+
145
+ it('follows the next cursor across pages and aggregates results', async () => {
146
+ const fetchImpl = vi
147
+ .fn()
148
+ .mockResolvedValueOnce(
149
+ mockResponse({
150
+ agents: [agent('a'), agent('b')],
151
+ next: 'c1',
152
+ totalCount: 5,
153
+ totalOnlineCount: 3,
154
+ }),
155
+ )
156
+ .mockResolvedValueOnce(
157
+ mockResponse({ agents: [agent('c'), agent('d')], next: 'c2' }),
158
+ )
159
+ .mockResolvedValueOnce(
160
+ mockResponse({ agents: [agent('e')], next: null }),
161
+ );
162
+
163
+ const result = await listAllAgentsAuthenticated({
164
+ baseUrl: 'http://api.test',
165
+ fetchImpl: fetchImpl as unknown as typeof fetch,
166
+ });
167
+
168
+ expect(result.agents.map((a) => a.agentName)).toEqual(['a', 'b', 'c', 'd', 'e']);
169
+ expect(result.totalCount).toBe(5);
170
+ expect(result.totalOnlineCount).toBe(3);
171
+ expect(fetchImpl).toHaveBeenCalledTimes(3);
172
+
173
+ // First request carries no cursor; follow-ups carry the prior page's next.
174
+ expect(new URL(String(fetchImpl.mock.calls[0][0])).searchParams.get('cursor')).toBeNull();
175
+ expect(new URL(String(fetchImpl.mock.calls[1][0])).searchParams.get('cursor')).toBe('c1');
176
+ expect(new URL(String(fetchImpl.mock.calls[2][0])).searchParams.get('cursor')).toBe('c2');
177
+ });
178
+
179
+ it('stops after a single page when next is absent', async () => {
180
+ const fetchImpl = vi.fn().mockResolvedValue(
181
+ mockResponse({ agents: [agent('a')], totalCount: 1 }),
182
+ );
183
+
184
+ const result = await listAllAgentsAuthenticated({
185
+ baseUrl: 'http://api.test',
186
+ fetchImpl: fetchImpl as unknown as typeof fetch,
187
+ });
188
+
189
+ expect(fetchImpl).toHaveBeenCalledOnce();
190
+ expect(result.agents.map((a) => a.agentName)).toEqual(['a']);
191
+ });
192
+
193
+ it('requests the max page size (limit=100) by default', async () => {
194
+ const fetchImpl = vi.fn().mockResolvedValue(
195
+ mockResponse({ agents: [], next: null }),
196
+ );
197
+
198
+ await listAllAgentsAuthenticated({
199
+ baseUrl: 'http://api.test',
200
+ fetchImpl: fetchImpl as unknown as typeof fetch,
201
+ });
202
+
203
+ expect(new URL(String(fetchImpl.mock.calls[0][0])).searchParams.get('limit')).toBe('100');
204
+ });
205
+
206
+ it('caps the total agents and shrinks the final page request', async () => {
207
+ const fetchImpl = vi
208
+ .fn()
209
+ .mockResolvedValueOnce(
210
+ mockResponse({ agents: [agent('a'), agent('b')], next: 'c1' }),
211
+ )
212
+ .mockResolvedValueOnce(
213
+ mockResponse({ agents: [agent('c'), agent('d')], next: 'c2' }),
214
+ );
215
+
216
+ const result = await listAllAgentsAuthenticated({
217
+ baseUrl: 'http://api.test',
218
+ maxAgents: 3,
219
+ fetchImpl: fetchImpl as unknown as typeof fetch,
220
+ });
221
+
222
+ expect(result.agents.map((a) => a.agentName)).toEqual(['a', 'b', 'c']);
223
+ // Two requests only. The cap bounds each page size: first page asks for
224
+ // min(100, 3)=3; after 2 returned, the second asks for the remaining 1.
225
+ expect(fetchImpl).toHaveBeenCalledTimes(2);
226
+ expect(new URL(String(fetchImpl.mock.calls[0][0])).searchParams.get('limit')).toBe('3');
227
+ expect(new URL(String(fetchImpl.mock.calls[1][0])).searchParams.get('limit')).toBe('1');
228
+ });
229
+
230
+ it('terminates when a backend returns a stuck non-null cursor', async () => {
231
+ const fetchImpl = vi.fn().mockResolvedValue(
232
+ mockResponse({ agents: [agent('a')], next: 'always' }),
233
+ );
234
+
235
+ const result = await listAllAgentsAuthenticated({
236
+ baseUrl: 'http://api.test',
237
+ fetchImpl: fetchImpl as unknown as typeof fetch,
238
+ });
239
+
240
+ // Bounded by the internal page guard rather than looping forever.
241
+ expect(fetchImpl).toHaveBeenCalledTimes(1000);
242
+ expect(result.agents).toHaveLength(1000);
243
+ });
120
244
  });
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { searchAgents } from '../src/tools.js';
3
+ import { makeFakeDeps } from './helpers.js';
4
+
5
+ // A status response where every named agent has one online instance, so the
6
+ // default online-only filter is a no-op for these agents.
7
+ function allOnline(...names: string[]) {
8
+ const agents: Record<string, unknown> = {};
9
+ for (const name of names) {
10
+ agents[name] = { agentName: name, instances: [], onlineCount: 1, taskCount: 0 };
11
+ }
12
+ return { agents };
13
+ }
14
+
15
+ describe('search_agent', () => {
16
+ it('forwards the query to the registry helper as `q`', async () => {
17
+ const { deps, mocks } = makeFakeDeps({
18
+ apiKey: 'sk-test',
19
+ baseUrl: 'http://api.test',
20
+ });
21
+
22
+ await searchAgents(
23
+ { query: 'translate', tag: 'language', listing: 'private', limit: 25 },
24
+ deps,
25
+ );
26
+
27
+ expect(mocks.listAgents).toHaveBeenCalledWith({
28
+ baseUrl: 'http://api.test',
29
+ apiKey: 'sk-test',
30
+ q: 'translate',
31
+ tag: 'language',
32
+ listing: 'private',
33
+ maxAgents: 25,
34
+ });
35
+ });
36
+
37
+ it('renders matching agents with the query in the header', async () => {
38
+ const { deps } = makeFakeDeps({
39
+ listAgentsResult: {
40
+ agents: [
41
+ {
42
+ agentName: 'alice',
43
+ name: 'Alice',
44
+ listing: 'public',
45
+ tags: [{ id: 'translate', name: 'Translate' }],
46
+ },
47
+ ],
48
+ totalCount: 1,
49
+ },
50
+ agentStatusResult: allOnline('alice'),
51
+ });
52
+
53
+ const res = await searchAgents({ query: 'trans' }, deps);
54
+ const lines = res.content[0].text.split('\n');
55
+ expect(lines).toEqual([
56
+ 'Agents matching "trans" (1 online of 1 total):',
57
+ 'alice | Alice | public | Translate',
58
+ ]);
59
+ });
60
+
61
+ it('trims whitespace from the query before sending it', async () => {
62
+ const { deps, mocks } = makeFakeDeps();
63
+
64
+ await searchAgents({ query: ' hello ' }, deps);
65
+
66
+ expect(mocks.listAgents).toHaveBeenCalledWith(
67
+ expect.objectContaining({ q: 'hello' }),
68
+ );
69
+ });
70
+
71
+ it('rejects an empty (or whitespace-only) query without calling the registry', async () => {
72
+ const { deps, mocks } = makeFakeDeps();
73
+
74
+ const res = await searchAgents({ query: ' ' }, deps);
75
+ expect(res.isError).toBe(true);
76
+ expect(res.content[0].text).toContain('must not be empty');
77
+ expect(mocks.listAgents).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it('drops offline matches by default, keeping only online ones', async () => {
81
+ const { deps } = makeFakeDeps({
82
+ listAgentsResult: {
83
+ agents: [
84
+ { agentName: 'online', name: 'Online' },
85
+ { agentName: 'offline', name: 'Offline' },
86
+ ],
87
+ totalCount: 2,
88
+ },
89
+ agentStatusResult: {
90
+ agents: {
91
+ online: { agentName: 'online', instances: [], onlineCount: 1, taskCount: 0 },
92
+ offline: { agentName: 'offline', instances: [], onlineCount: 0, taskCount: 0 },
93
+ },
94
+ },
95
+ });
96
+
97
+ const res = await searchAgents({ query: 'foo' }, deps);
98
+ const lines = res.content[0].text.split('\n');
99
+ expect(lines).toEqual([
100
+ 'Agents matching "foo" (1 online of 2 total):',
101
+ 'online | Online | public | ',
102
+ ]);
103
+ });
104
+
105
+ it('includes offline matches and skips the status check when includeOffline is true', async () => {
106
+ const { deps, mocks } = makeFakeDeps({
107
+ listAgentsResult: {
108
+ agents: [{ agentName: 'online' }, { agentName: 'offline' }],
109
+ totalCount: 2,
110
+ },
111
+ });
112
+
113
+ const res = await searchAgents({ query: 'foo', includeOffline: true }, deps);
114
+ expect(res.content[0].text.split('\n')[0]).toBe(
115
+ 'Agents matching "foo" (2 of 2 total):',
116
+ );
117
+ expect(mocks.fetchAgentStatus).not.toHaveBeenCalled();
118
+ });
119
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { pauseTask, resumeTask, retryTask } from '../src/tools.js';
3
+ import { makeFakeDeps, makeFakeClient } from './helpers.js';
4
+
5
+ describe('pause_task', () => {
6
+ it('forwards taskId to client.pauseTask and confirms', async () => {
7
+ const client = makeFakeClient();
8
+ const { deps } = makeFakeDeps({ client });
9
+
10
+ const res = await pauseTask({ taskId: 't_1' }, deps);
11
+
12
+ expect(client.pauseTaskMock).toHaveBeenCalledWith('t_1');
13
+ expect(res.content[0].text).toBe('Task t_1 paused.');
14
+ });
15
+
16
+ it('propagates errors from pauseTask', async () => {
17
+ const client = makeFakeClient();
18
+ client.pauseTaskMock.mockRejectedValueOnce(new Error('already terminal'));
19
+ const { deps } = makeFakeDeps({ client });
20
+
21
+ await expect(pauseTask({ taskId: 't_err' }, deps)).rejects.toThrow('already terminal');
22
+ });
23
+ });
24
+
25
+ describe('resume_task', () => {
26
+ it('forwards taskId to client.resumeTask and confirms', async () => {
27
+ const client = makeFakeClient();
28
+ const { deps } = makeFakeDeps({ client });
29
+
30
+ const res = await resumeTask({ taskId: 't_2' }, deps);
31
+
32
+ expect(client.resumeTaskMock).toHaveBeenCalledWith('t_2');
33
+ expect(res.content[0].text).toBe('Task t_2 resumed.');
34
+ });
35
+
36
+ it('propagates errors from resumeTask', async () => {
37
+ const client = makeFakeClient();
38
+ client.resumeTaskMock.mockRejectedValueOnce(new Error('not paused'));
39
+ const { deps } = makeFakeDeps({ client });
40
+
41
+ await expect(resumeTask({ taskId: 't_err' }, deps)).rejects.toThrow('not paused');
42
+ });
43
+ });
44
+
45
+ describe('retry_task', () => {
46
+ it('forwards taskId to client.retryTask and confirms', async () => {
47
+ const client = makeFakeClient();
48
+ const { deps } = makeFakeDeps({ client });
49
+
50
+ const res = await retryTask({ taskId: 't_3' }, deps);
51
+
52
+ expect(client.retryTaskMock).toHaveBeenCalledWith('t_3');
53
+ expect(res.content[0].text).toBe('Task t_3 retry requested.');
54
+ });
55
+
56
+ it('propagates errors from retryTask', async () => {
57
+ const client = makeFakeClient();
58
+ client.retryTaskMock.mockRejectedValueOnce(new Error('not failed'));
59
+ const { deps } = makeFakeDeps({ client });
60
+
61
+ await expect(retryTask({ taskId: 't_err' }, deps)).rejects.toThrow('not failed');
62
+ });
63
+ });
64
+