@blocks-network/mcp-server 0.1.62 → 0.1.64

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.
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { getAgentStatus } from '../src/tools.js';
3
+ import {
4
+ fetchAgentStatus,
5
+ type AgentStatusResponse,
6
+ } from '../src/agent-status.js';
7
+ import {
8
+ PROTOCOL_VERSION_HEADER,
9
+ CURRENT_PROTOCOL_VERSION,
10
+ } from '../src/protocol-headers.js';
11
+ import { makeFakeDeps } from './helpers.js';
12
+
13
+ function mockResponse(body: unknown, ok = true, status = 200): Response {
14
+ return { ok, status, json: async () => body } as Response;
15
+ }
16
+
17
+ describe('get_agent_status (handler)', () => {
18
+ it('forwards baseUrl, apiKey and agentNames to fetchAgentStatus and emits JSON', async () => {
19
+ const result: AgentStatusResponse = {
20
+ agents: {
21
+ alice: {
22
+ agentName: 'alice',
23
+ instances: [],
24
+ onlineCount: 2,
25
+ totalActiveTasks: 1,
26
+ taskCount: 1,
27
+ },
28
+ },
29
+ };
30
+ const { deps, mocks } = makeFakeDeps({
31
+ apiKey: 'bk_test',
32
+ agentStatusResult: result,
33
+ });
34
+
35
+ const res = await getAgentStatus({ agentNames: ['alice'] }, deps);
36
+
37
+ expect(mocks.fetchAgentStatus).toHaveBeenCalledWith({
38
+ baseUrl: 'http://api.test',
39
+ apiKey: 'bk_test',
40
+ agentNames: ['alice'],
41
+ });
42
+ const parsed = JSON.parse(res.content[0].text);
43
+ expect(parsed.agents.alice.onlineCount).toBe(2);
44
+ });
45
+ });
46
+
47
+ describe('fetchAgentStatus (HTTP helper)', () => {
48
+ it('builds /api/v1/agent-status URL with agentNames csv and protocol header', async () => {
49
+ const fetchImpl = vi
50
+ .fn()
51
+ .mockResolvedValue(mockResponse({ agents: {} }));
52
+
53
+ await fetchAgentStatus({
54
+ baseUrl: 'http://api.test/',
55
+ agentNames: ['alice', 'bob'],
56
+ apiKey: 'bk_test',
57
+ fetchImpl: fetchImpl as unknown as typeof fetch,
58
+ });
59
+
60
+ const [url, init] = fetchImpl.mock.calls[0];
61
+ const parsed = new URL(String(url));
62
+ expect(parsed.pathname).toBe('/api/v1/agent-status');
63
+ expect(parsed.searchParams.get('agentNames')).toBe('alice,bob');
64
+ const headers = init?.headers as Record<string, string>;
65
+ expect(headers[PROTOCOL_VERSION_HEADER]).toBe(CURRENT_PROTOCOL_VERSION);
66
+ expect(headers['Authorization']).toBe('Bearer bk_test');
67
+ });
68
+
69
+ it('rejects empty input', async () => {
70
+ await expect(
71
+ fetchAgentStatus({
72
+ baseUrl: 'http://api.test',
73
+ agentNames: [],
74
+ }),
75
+ ).rejects.toThrow(/at least one/);
76
+ });
77
+
78
+ it('rejects more than 50 names', async () => {
79
+ const names = Array.from({ length: 51 }, (_, i) => `a${i}`);
80
+ await expect(
81
+ fetchAgentStatus({
82
+ baseUrl: 'http://api.test',
83
+ agentNames: names,
84
+ }),
85
+ ).rejects.toThrow(/at most 50/);
86
+ });
87
+
88
+ it('rejects names with invalid characters', async () => {
89
+ await expect(
90
+ fetchAgentStatus({
91
+ baseUrl: 'http://api.test',
92
+ agentNames: ['has-a-hyphen'],
93
+ }),
94
+ ).rejects.toThrow(/Invalid agent name/);
95
+ });
96
+
97
+ it('throws on non-OK responses', async () => {
98
+ const fetchImpl = vi.fn().mockResolvedValue(mockResponse({}, false, 500));
99
+
100
+ await expect(
101
+ fetchAgentStatus({
102
+ baseUrl: 'http://api.test',
103
+ agentNames: ['alice'],
104
+ fetchImpl: fetchImpl as unknown as typeof fetch,
105
+ }),
106
+ ).rejects.toThrow('HTTP 500');
107
+ });
108
+ });
package/tests/helpers.ts CHANGED
@@ -89,6 +89,9 @@ export interface FakeClientConfig {
89
89
  export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
90
90
  sendMessageMock: ReturnType<typeof vi.fn>;
91
91
  cancelTaskMock: ReturnType<typeof vi.fn>;
92
+ pauseTaskMock: ReturnType<typeof vi.fn>;
93
+ resumeTaskMock: ReturnType<typeof vi.fn>;
94
+ retryTaskMock: ReturnType<typeof vi.fn>;
92
95
  connectMock: ReturnType<typeof vi.fn>;
93
96
  listTasksMock: ReturnType<typeof vi.fn>;
94
97
  getTaskMock: ReturnType<typeof vi.fn>;
@@ -97,6 +100,9 @@ export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
97
100
  const sendMessageMock = vi.fn().mockResolvedValue(session);
98
101
  const connectMock = vi.fn().mockResolvedValue(session);
99
102
  const cancelTaskMock = vi.fn().mockResolvedValue(undefined);
103
+ const pauseTaskMock = vi.fn().mockResolvedValue(undefined);
104
+ const resumeTaskMock = vi.fn().mockResolvedValue(undefined);
105
+ const retryTaskMock = vi.fn().mockResolvedValue(undefined);
100
106
  const listTasksMock = vi.fn().mockResolvedValue(
101
107
  cfg.listResult ?? { tasks: [], totalCount: 0 },
102
108
  );
@@ -105,11 +111,17 @@ export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
105
111
  return {
106
112
  sendMessage: sendMessageMock,
107
113
  cancelTask: cancelTaskMock,
114
+ pauseTask: pauseTaskMock,
115
+ resumeTask: resumeTaskMock,
116
+ retryTask: retryTaskMock,
108
117
  connect: connectMock,
109
118
  listTasks: listTasksMock,
110
119
  getTask: getTaskMock,
111
120
  sendMessageMock,
112
121
  cancelTaskMock,
122
+ pauseTaskMock,
123
+ resumeTaskMock,
124
+ retryTaskMock,
113
125
  connectMock,
114
126
  listTasksMock,
115
127
  getTaskMock,
@@ -119,9 +131,13 @@ export function makeFakeClient(cfg: FakeClientConfig = {}): TaskClientLike & {
119
131
  export interface FakeDepsOverrides {
120
132
  baseUrl?: string;
121
133
  apiKey?: string;
134
+ orgId?: string;
122
135
  client?: TaskClientLike;
123
136
  agentEntry?: AgentEntryLike | null;
124
137
  listAgentsResult?: ListAgentsResult;
138
+ agentStatusResult?: { agents: Record<string, unknown> };
139
+ balance?: Record<string, unknown>;
140
+ topUpSession?: Record<string, unknown>;
125
141
  maxUploadBytes?: number;
126
142
  fileSize?: number;
127
143
  }
@@ -130,36 +146,76 @@ export function makeFakeDeps(overrides: FakeDepsOverrides = {}) {
130
146
  const client = overrides.client ?? makeFakeClient();
131
147
  const getBaseUrl = vi.fn().mockResolvedValue(overrides.baseUrl ?? 'http://api.test');
132
148
  const getApiKey = vi.fn().mockReturnValue(overrides.apiKey);
149
+ const getOrgId = vi.fn().mockReturnValue(overrides.orgId);
133
150
  const getTaskClient = vi.fn().mockResolvedValue(client);
134
151
  const getAgentByName = vi.fn().mockResolvedValue(overrides.agentEntry ?? null);
135
152
  const listAgents = vi
136
153
  .fn()
137
154
  .mockResolvedValue(overrides.listAgentsResult ?? { agents: [], totalCount: 0 });
155
+ const fetchAgentStatus = vi
156
+ .fn<ToolDeps['fetchAgentStatus']>()
157
+ .mockResolvedValue(
158
+ (overrides.agentStatusResult ?? { agents: {} }) as Awaited<
159
+ ReturnType<ToolDeps['fetchAgentStatus']>
160
+ >,
161
+ );
162
+ const getConsumerBalance = vi
163
+ .fn<ToolDeps['getConsumerBalance']>()
164
+ .mockResolvedValue(
165
+ (overrides.balance ?? {
166
+ balance: '0',
167
+ reservedBalance: '0',
168
+ availableBalance: '0',
169
+ updatedAt: '2026-05-28T00:00:00Z',
170
+ }) as Awaited<ReturnType<ToolDeps['getConsumerBalance']>>,
171
+ );
172
+ const createConsumerTopUp = vi
173
+ .fn<ToolDeps['createConsumerTopUp']>()
174
+ .mockResolvedValue(
175
+ (overrides.topUpSession ?? {
176
+ checkoutUrl: 'https://stripe.test/checkout/abc',
177
+ sessionId: 'cs_test_default',
178
+ }) as Awaited<ReturnType<ToolDeps['createConsumerTopUp']>>,
179
+ );
138
180
  const validateFilePath = vi.fn((p: string) => p);
181
+ const resolveSavePath = vi.fn((p: string) => p);
182
+ const writeFile = vi.fn();
139
183
  const fileSize = vi.fn().mockReturnValue(overrides.fileSize ?? 100);
140
- const filePartFromPath = vi.fn(async (path: string, opts: { partId?: string; contentType?: string }) => ({
141
- type: 'file' as const,
142
- partId: opts.partId ?? 'file',
143
- file: { id: 'file-id', mimeType: opts.contentType ?? 'application/octet-stream' },
144
- _path: path,
145
- }));
146
- const textPart = vi.fn((text: string, partId: string) => ({
147
- type: 'text' as const,
148
- partId,
149
- text,
150
- }));
184
+ const filePartFromPath = vi.fn<ToolDeps['filePartFromPath']>(
185
+ async (path, opts) =>
186
+ ({
187
+ type: 'file' as const,
188
+ partId: opts?.partId ?? 'file',
189
+ file: { id: 'file-id', mimeType: opts?.contentType ?? 'application/octet-stream' },
190
+ _path: path,
191
+ }) as Awaited<ReturnType<ToolDeps['filePartFromPath']>>,
192
+ );
193
+ const textPart = vi.fn<ToolDeps['textPart']>(
194
+ (text, partId) =>
195
+ ({
196
+ type: 'text' as const,
197
+ partId,
198
+ text,
199
+ }) as ReturnType<ToolDeps['textPart']>,
200
+ );
151
201
 
152
202
  const deps: ToolDeps = {
153
203
  getBaseUrl,
154
204
  getApiKey,
205
+ getOrgId,
155
206
  getTaskClient,
156
207
  getAgentByName,
157
208
  listAgents,
209
+ fetchAgentStatus,
210
+ getConsumerBalance,
211
+ createConsumerTopUp,
158
212
  validateFilePath,
213
+ resolveSavePath,
214
+ writeFile,
159
215
  fileSize,
160
216
  maxUploadBytes: overrides.maxUploadBytes ?? 25 * 1024 * 1024,
161
- filePartFromPath: filePartFromPath as unknown as ToolDeps['filePartFromPath'],
162
- textPart: textPart as unknown as ToolDeps['textPart'],
217
+ filePartFromPath,
218
+ textPart,
163
219
  };
164
220
 
165
221
  return {
@@ -168,10 +224,16 @@ export function makeFakeDeps(overrides: FakeDepsOverrides = {}) {
168
224
  mocks: {
169
225
  getBaseUrl,
170
226
  getApiKey,
227
+ getOrgId,
171
228
  getTaskClient,
172
229
  getAgentByName,
173
230
  listAgents,
231
+ fetchAgentStatus,
232
+ getConsumerBalance,
233
+ createConsumerTopUp,
174
234
  validateFilePath,
235
+ resolveSavePath,
236
+ writeFile,
175
237
  fileSize,
176
238
  filePartFromPath,
177
239
  textPart,
@@ -3,7 +3,7 @@ import { listAgents } from '../src/tools.js';
3
3
  import { makeFakeDeps } from './helpers.js';
4
4
 
5
5
  describe('list_agents', () => {
6
- it('renders one row per agent with skill names joined by commas', async () => {
6
+ it('renders one row per agent with tag names joined by commas', async () => {
7
7
  const { deps } = makeFakeDeps({
8
8
  listAgentsResult: {
9
9
  agents: [
@@ -11,7 +11,7 @@ describe('list_agents', () => {
11
11
  agentName: 'alice',
12
12
  name: 'Alice',
13
13
  listing: 'public',
14
- skills: [
14
+ tags: [
15
15
  { id: 'translate', name: 'Translate' },
16
16
  { id: 'summarize', name: 'Summarize' },
17
17
  ],
@@ -31,21 +31,21 @@ describe('list_agents', () => {
31
31
  ]);
32
32
  });
33
33
 
34
- it('forwards baseUrl, apiKey, skill, listing, limit to the registry helper', async () => {
34
+ it('forwards baseUrl, apiKey, tag, listing, limit to the registry helper', async () => {
35
35
  const { deps, mocks } = makeFakeDeps({
36
36
  apiKey: 'sk-test',
37
37
  baseUrl: 'http://api.test',
38
38
  });
39
39
 
40
40
  await listAgents(
41
- { skill: 'translate', listing: 'private', limit: 25 },
41
+ { tag: 'translate', listing: 'private', limit: 25 },
42
42
  deps,
43
43
  );
44
44
 
45
45
  expect(mocks.listAgents).toHaveBeenCalledWith({
46
46
  baseUrl: 'http://api.test',
47
47
  apiKey: 'sk-test',
48
- skill: 'translate',
48
+ tag: 'translate',
49
49
  listing: 'private',
50
50
  limit: 25,
51
51
  });
@@ -54,7 +54,7 @@ describe('list_agents', () => {
54
54
  it('defaults to "public" for missing listing and uses agentName when name is absent', async () => {
55
55
  const { deps } = makeFakeDeps({
56
56
  listAgentsResult: {
57
- agents: [{ agentName: 'naked', skills: [] }],
57
+ agents: [{ agentName: 'naked', tags: [] }],
58
58
  totalCount: 1,
59
59
  },
60
60
  });
@@ -1,9 +1,9 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
+ import { listAgentsAuthenticated } from '../src/registry-list.js';
2
3
  import {
3
- listAgentsAuthenticated,
4
4
  PROTOCOL_VERSION_HEADER,
5
5
  CURRENT_PROTOCOL_VERSION,
6
- } from '../src/registry-list.js';
6
+ } from '../src/protocol-headers.js';
7
7
 
8
8
  function mockResponse(body: unknown, ok = true, status = 200): Response {
9
9
  return {
@@ -57,7 +57,7 @@ describe('listAgentsAuthenticated', () => {
57
57
  await listAgentsAuthenticated({
58
58
  baseUrl: 'http://api.test/',
59
59
  listing: 'private',
60
- skill: 'translate',
60
+ tag: 'translate',
61
61
  limit: 50,
62
62
  fetchImpl: fetchImpl as unknown as typeof fetch,
63
63
  });
@@ -68,7 +68,7 @@ describe('listAgentsAuthenticated', () => {
68
68
  expect(parsed.searchParams.get('include')).toBe('full');
69
69
  expect(parsed.searchParams.get('listing')).toBe('private');
70
70
  expect(parsed.searchParams.get('scope')).toBe('owned');
71
- expect(parsed.searchParams.get('skill')).toBe('translate');
71
+ expect(parsed.searchParams.get('tag')).toBe('translate');
72
72
  expect(parsed.searchParams.get('limit')).toBe('50');
73
73
  });
74
74
 
@@ -104,7 +104,7 @@ describe('listAgentsAuthenticated', () => {
104
104
 
105
105
  it('returns parsed JSON body on success', async () => {
106
106
  const body = {
107
- agents: [{ agentName: 'alice', skills: [] }],
107
+ agents: [{ agentName: 'alice', tags: [] }],
108
108
  totalCount: 1,
109
109
  };
110
110
  const fetchImpl = vi
@@ -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
+