@hivemind-os/collective-mcp-server 0.2.0

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.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +14 -0
  2. package/dist/index.d.ts +493 -0
  3. package/dist/index.js +2129 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +31 -0
  6. package/src/context.ts +58 -0
  7. package/src/encryption.ts +25 -0
  8. package/src/index.ts +41 -0
  9. package/src/resources/agent.ts +77 -0
  10. package/src/resources/capabilities.ts +24 -0
  11. package/src/resources/index.ts +70 -0
  12. package/src/resources/task.ts +58 -0
  13. package/src/resources/wallet.ts +32 -0
  14. package/src/tools/analytics.ts +122 -0
  15. package/src/tools/balance.ts +36 -0
  16. package/src/tools/deactivate.ts +33 -0
  17. package/src/tools/discover.ts +256 -0
  18. package/src/tools/dispute.ts +135 -0
  19. package/src/tools/execute-async.ts +39 -0
  20. package/src/tools/execute.ts +418 -0
  21. package/src/tools/index.ts +152 -0
  22. package/src/tools/indexer-client.ts +163 -0
  23. package/src/tools/marketplace-accept-bid.ts +43 -0
  24. package/src/tools/marketplace-bid.ts +56 -0
  25. package/src/tools/marketplace-browse.ts +66 -0
  26. package/src/tools/marketplace-post.ts +96 -0
  27. package/src/tools/metering.ts +214 -0
  28. package/src/tools/multi-execute.ts +218 -0
  29. package/src/tools/policy-update.ts +94 -0
  30. package/src/tools/register.ts +78 -0
  31. package/src/tools/relay-registry.ts +95 -0
  32. package/src/tools/stake.ts +103 -0
  33. package/src/tools/task-history.ts +86 -0
  34. package/src/tools/task-status.ts +66 -0
  35. package/tests/analytics.test.ts +41 -0
  36. package/tests/auth-errors.test.ts +85 -0
  37. package/tests/balance.test.ts +32 -0
  38. package/tests/context.test.ts +112 -0
  39. package/tests/discover.test.ts +207 -0
  40. package/tests/dispute.test.ts +140 -0
  41. package/tests/execute.test.ts +150 -0
  42. package/tests/marketplace.test.ts +117 -0
  43. package/tests/metering.test.ts +173 -0
  44. package/tests/multi-execute.test.ts +123 -0
  45. package/tests/relay-registry.test.ts +71 -0
  46. package/tests/stake.test.ts +90 -0
  47. package/tsconfig.json +9 -0
  48. package/tsup.config.ts +10 -0
  49. package/vitest.config.ts +8 -0
@@ -0,0 +1,207 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { PaymentRail, type AgentCard } from '@hivemind-os/collective-types';
4
+
5
+ import type { MeshToolContext } from '../src/context.js';
6
+ import { runMeshDiscover } from '../src/tools/discover.js';
7
+
8
+ function createAgent(overrides: Partial<AgentCard> = {}): AgentCard {
9
+ return {
10
+ id: '0xagent-1',
11
+ owner: '0xowner-1',
12
+ did: 'did:mesh:agent-1' as AgentCard['did'],
13
+ name: 'Summarizer',
14
+ description: 'Summarizes text',
15
+ capabilities: [
16
+ {
17
+ name: 'summarize',
18
+ description: 'Summarize documents',
19
+ version: '1.0.0',
20
+ pricing: {
21
+ rail: PaymentRail.SUI_ESCROW,
22
+ amount: 100n,
23
+ currency: 'MIST',
24
+ },
25
+ },
26
+ ],
27
+ endpoint: 'mesh://agent/did:mesh:agent-1',
28
+ active: true,
29
+ version: 1,
30
+ registeredAt: 1_000,
31
+ updatedAt: 1_000,
32
+ ...overrides,
33
+ };
34
+ }
35
+
36
+ function createContext(overrides: Partial<MeshToolContext> = {}): MeshToolContext {
37
+ return {
38
+ did: 'did:mesh:test' as MeshToolContext['did'],
39
+ keypair: {} as MeshToolContext['keypair'],
40
+ suiClient: {
41
+ getBalance: vi.fn(),
42
+ queryEvents: vi.fn(),
43
+ } as unknown as MeshToolContext['suiClient'],
44
+ registryClient: {
45
+ discoverByCapability: vi.fn().mockResolvedValue([]),
46
+ getAgentCard: vi.fn(),
47
+ } as unknown as MeshToolContext['registryClient'],
48
+ taskClient: {} as MeshToolContext['taskClient'],
49
+ agentCache: {
50
+ searchByCapability: vi.fn().mockReturnValue([]),
51
+ getAgentByDID: vi.fn(),
52
+ getAllActive: vi.fn().mockReturnValue([]),
53
+ upsertAgent: vi.fn(),
54
+ removeAgent: vi.fn(),
55
+ } as unknown as MeshToolContext['agentCache'],
56
+ blobStore: {} as MeshToolContext['blobStore'],
57
+ spendingPolicy: {} as MeshToolContext['spendingPolicy'],
58
+ networkConfig: {
59
+ rpcUrl: 'http://127.0.0.1:9000',
60
+ faucetUrl: 'http://127.0.0.1:9123',
61
+ packageId: '0x1',
62
+ registryId: '0x2',
63
+ },
64
+ ...overrides,
65
+ };
66
+ }
67
+
68
+ describe('runMeshDiscover', () => {
69
+ it('returns matching agents from cache', async () => {
70
+ const agent = createAgent();
71
+ const context = createContext({
72
+ agentCache: {
73
+ searchByCapability: vi.fn().mockReturnValue([agent]),
74
+ getAgentByDID: vi.fn(),
75
+ getAllActive: vi.fn().mockReturnValue([agent]),
76
+ upsertAgent: vi.fn(),
77
+ removeAgent: vi.fn(),
78
+ } as unknown as MeshToolContext['agentCache'],
79
+ });
80
+
81
+ const result = await runMeshDiscover({ capability: 'summarize' }, context);
82
+
83
+ expect(result.source).toBe('cache');
84
+ expect(result.agents).toHaveLength(1);
85
+ expect(result.agents[0]).toMatchObject({ name: 'Summarizer', did: 'did:mesh:agent-1' });
86
+ expect(result.agents[0].reputation.total_tasks).toBe(0);
87
+ expect(context.registryClient.discoverByCapability).not.toHaveBeenCalled();
88
+ });
89
+
90
+ it('uses the indexer when configured', async () => {
91
+ const context = createContext({
92
+ indexer: {
93
+ graphqlUrl: 'http://localhost:4000/graphql',
94
+ fetch: vi.fn().mockResolvedValue({
95
+ ok: true,
96
+ json: async () => ({
97
+ data: {
98
+ agents: {
99
+ nodes: [
100
+ {
101
+ id: '0xagent-1',
102
+ owner: '0xowner-1',
103
+ did: 'did:mesh:agent-1',
104
+ name: 'Summarizer',
105
+ description: 'Summarizes text',
106
+ endpoint: 'mesh://agent/did:mesh:agent-1',
107
+ active: true,
108
+ version: 1,
109
+ registeredAt: '1000',
110
+ updatedAt: '1000',
111
+ totalTasksCompleted: 5,
112
+ totalTasksFailed: 1,
113
+ totalTasksDisputed: 0,
114
+ totalEarningsMist: '1000',
115
+ hasStake: true,
116
+ stakeMist: '100',
117
+ stakeType: 'agent',
118
+ capabilities: [
119
+ {
120
+ name: 'summarize',
121
+ description: 'Summarize documents',
122
+ version: '1.0.0',
123
+ executionMode: 'sync',
124
+ paymentRails: ['sui-escrow'],
125
+ pricing: { rail: 'sui-escrow', amount: '100', currency: 'MIST' },
126
+ },
127
+ ],
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ }),
133
+ }) as unknown as typeof fetch,
134
+ },
135
+ });
136
+
137
+ const result = await runMeshDiscover({ capability: 'summarize' }, context);
138
+
139
+ expect(result.source).toBe('indexer');
140
+ expect(result.agents[0]?.reputation.total_tasks).toBe(6);
141
+ expect(context.registryClient.discoverByCapability).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('falls back to sui discovery when cache is empty', async () => {
145
+ const agent = createAgent();
146
+ const context = createContext({
147
+ registryClient: {
148
+ discoverByCapability: vi.fn().mockResolvedValue([agent]),
149
+ getAgentCard: vi.fn(),
150
+ } as unknown as MeshToolContext['registryClient'],
151
+ });
152
+
153
+ const result = await runMeshDiscover({ capability: 'summarize' }, context);
154
+
155
+ expect(result.source).toBe('registry');
156
+ expect(result.agents).toHaveLength(1);
157
+ expect(context.registryClient.discoverByCapability).toHaveBeenCalledWith('summarize', 10, { sortByReputation: false });
158
+ expect(context.agentCache.upsertAgent).toHaveBeenCalledWith(agent);
159
+ });
160
+
161
+ it('sorts by reputation when requested', async () => {
162
+ const stronger = createAgent({ totalTasksCompleted: 10, totalTasksFailed: 1 });
163
+ const weaker = createAgent({ id: '0xagent-2', did: 'did:mesh:agent-2' as AgentCard['did'], totalTasksCompleted: 1, totalTasksFailed: 3 });
164
+ const context = createContext({
165
+ agentCache: {
166
+ searchByCapability: vi.fn().mockImplementation((_capability, _limit, options?: { sortByReputation?: boolean }) => (
167
+ options?.sortByReputation ? [stronger, weaker] : [weaker, stronger]
168
+ )),
169
+ getAgentByDID: vi.fn(),
170
+ getAllActive: vi.fn().mockReturnValue([weaker, stronger]),
171
+ upsertAgent: vi.fn(),
172
+ removeAgent: vi.fn(),
173
+ } as unknown as MeshToolContext['agentCache'],
174
+ });
175
+
176
+ const result = await runMeshDiscover({ capability: 'summarize', sort_by: 'reputation' }, context);
177
+
178
+ expect(result.agents.map((agent) => agent.did)).toEqual([stronger.did, weaker.did]);
179
+ });
180
+
181
+ it('respects the limit parameter', async () => {
182
+ const context = createContext({
183
+ agentCache: {
184
+ searchByCapability: vi.fn().mockReturnValue([
185
+ createAgent(),
186
+ createAgent({ id: '0xagent-2', did: 'did:mesh:agent-2' as AgentCard['did'] }),
187
+ ]),
188
+ getAgentByDID: vi.fn(),
189
+ getAllActive: vi.fn().mockReturnValue([]),
190
+ upsertAgent: vi.fn(),
191
+ removeAgent: vi.fn(),
192
+ } as unknown as MeshToolContext['agentCache'],
193
+ });
194
+
195
+ const result = await runMeshDiscover({ capability: 'summarize', limit: 1 }, context);
196
+
197
+ expect(result.agents).toHaveLength(1);
198
+ });
199
+
200
+ it('returns an empty array when no matches exist', async () => {
201
+ const context = createContext();
202
+
203
+ const result = await runMeshDiscover({ capability: 'summarize' }, context);
204
+
205
+ expect(result.agents).toEqual([]);
206
+ });
207
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { DisputeStatus } from '@hivemind-os/collective-types';
4
+
5
+ import type { MeshToolContext } from '../src/context.js';
6
+ import { runMeshDispute } from '../src/tools/dispute.js';
7
+
8
+ function createContext(overrides: Partial<MeshToolContext> = {}): MeshToolContext {
9
+ return {
10
+ did: 'did:mesh:test' as MeshToolContext['did'],
11
+ keypair: {
12
+ getPublicKey: () => ({
13
+ toSuiAddress: () => '0xabc',
14
+ }),
15
+ } as MeshToolContext['keypair'],
16
+ suiClient: {
17
+ getBalance: vi.fn(),
18
+ queryEvents: vi.fn(),
19
+ } as unknown as MeshToolContext['suiClient'],
20
+ registryClient: {} as MeshToolContext['registryClient'],
21
+ taskClient: {} as MeshToolContext['taskClient'],
22
+ agentCache: {} as MeshToolContext['agentCache'],
23
+ blobStore: {
24
+ store: vi.fn(async () => ({ blobId: 'walrus:evidence' })),
25
+ fetch: vi.fn(),
26
+ exists: vi.fn(),
27
+ delete: vi.fn(),
28
+ } as unknown as MeshToolContext['blobStore'],
29
+ spendingPolicy: {} as MeshToolContext['spendingPolicy'],
30
+ networkConfig: {
31
+ rpcUrl: 'http://127.0.0.1:9000',
32
+ faucetUrl: 'http://127.0.0.1:9123',
33
+ packageId: '0x1',
34
+ registryId: '0x2',
35
+ },
36
+ disputeClient: {
37
+ openDispute: vi.fn(),
38
+ respondToDispute: vi.fn(),
39
+ acceptResolution: vi.fn(),
40
+ getDispute: vi.fn(),
41
+ getDisputeByTask: vi.fn(),
42
+ } as unknown as MeshToolContext['disputeClient'],
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ describe('runMeshDispute', () => {
48
+ it('opens disputes and stores evidence first', async () => {
49
+ const context = createContext();
50
+ vi.mocked(context.disputeClient?.openDispute as never).mockResolvedValue({ disputeId: '0xdispute', txDigest: '0xtx' });
51
+
52
+ const result = await runMeshDispute({
53
+ action: 'open',
54
+ task_id: '0x3',
55
+ evidence: '{"reason":"bad output"}',
56
+ proposed_split_mist: '500',
57
+ }, context);
58
+
59
+ expect(context.blobStore.store).toHaveBeenCalledOnce();
60
+ expect(context.disputeClient?.openDispute).toHaveBeenCalledWith({
61
+ taskId: '0x3',
62
+ evidenceBlobId: 'walrus:evidence',
63
+ proposedSplitMist: 500n,
64
+ arbitratorAddress: undefined,
65
+ signer: context.keypair,
66
+ });
67
+ expect(result).toMatchObject({ action: 'open', dispute_id: '0xdispute', tx_digest: '0xtx' });
68
+ });
69
+
70
+ it('responds and accepts disputes', async () => {
71
+ const context = createContext();
72
+ vi.mocked(context.disputeClient?.respondToDispute as never).mockResolvedValue({ txDigest: '0xrespond' });
73
+ vi.mocked(context.disputeClient?.acceptResolution as never).mockResolvedValue({
74
+ requesterAmount: 250n,
75
+ providerAmount: 750n,
76
+ txDigest: '0xaccept',
77
+ });
78
+
79
+ await expect(runMeshDispute({
80
+ action: 'respond',
81
+ dispute_id: '0x4',
82
+ evidence_blob_id: 'walrus:reply',
83
+ proposed_split_mist: '250',
84
+ }, context)).resolves.toMatchObject({ action: 'respond', tx_digest: '0xrespond' });
85
+
86
+ await expect(runMeshDispute({
87
+ action: 'accept',
88
+ dispute_id: '0x4',
89
+ task_id: '0x3',
90
+ }, context)).resolves.toMatchObject({
91
+ action: 'accept',
92
+ requester_amount_mist: '250',
93
+ provider_amount_mist: '750',
94
+ tx_digest: '0xaccept',
95
+ });
96
+ });
97
+
98
+ it('rejects ambiguous status requests and conflicting evidence sources', async () => {
99
+ const context = createContext();
100
+
101
+ await expect(runMeshDispute({ action: 'status' }, context)).rejects.toThrow(
102
+ 'Provide exactly one of dispute_id or task_id when action=status',
103
+ );
104
+
105
+ await expect(runMeshDispute({
106
+ action: 'open',
107
+ task_id: '0x3',
108
+ evidence: 'inline',
109
+ evidence_blob_id: 'walrus:evidence',
110
+ proposed_split_mist: '500',
111
+ }, context)).rejects.toThrow('Provide exactly one of evidence or evidence_blob_id for dispute open/respond actions');
112
+ });
113
+
114
+ it('returns dispute status by task id', async () => {
115
+ const context = createContext();
116
+ vi.mocked(context.disputeClient?.getDisputeByTask as never).mockResolvedValue({
117
+ id: '0xdispute',
118
+ taskId: '0x3',
119
+ requester: '0xrequester',
120
+ provider: '0xprovider',
121
+ escrowAmount: 1_000n,
122
+ status: DisputeStatus.OPEN,
123
+ requesterEvidenceBlob: 'walrus:req',
124
+ providerEvidenceBlob: undefined,
125
+ requesterProposedSplit: 500n,
126
+ providerProposedSplit: 0n,
127
+ arbitrator: undefined,
128
+ rulingSplit: 0n,
129
+ openedAt: 100,
130
+ respondedAt: undefined,
131
+ resolvedAt: undefined,
132
+ resolutionDeadline: 200,
133
+ });
134
+
135
+ const result = await runMeshDispute({ action: 'status', task_id: '0x3' }, context);
136
+
137
+ expect(context.disputeClient?.getDisputeByTask).toHaveBeenCalledWith('0x3');
138
+ expect(result).toMatchObject({ action: 'status', dispute: { id: '0xdispute', taskId: '0x3' } });
139
+ });
140
+ });
@@ -0,0 +1,150 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { PaymentRail, TaskStatus, type AgentCard } from '@hivemind-os/collective-types';
4
+
5
+ const relayExecuteMock = vi.fn();
6
+
7
+ vi.mock('@hivemind-os/collective-core', async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import('@hivemind-os/collective-core')>();
9
+ return {
10
+ ...actual,
11
+ PaymentRailSelector: class {
12
+ selectRail() {
13
+ return 'x402-base';
14
+ }
15
+ },
16
+ RelayConsumerClient: class {
17
+ async executeSync(...args: unknown[]) {
18
+ return relayExecuteMock(...args);
19
+ }
20
+ },
21
+ };
22
+ });
23
+
24
+ import { runMeshExecute } from '../src/tools/execute.js';
25
+
26
+ function createAgent(): AgentCard {
27
+ return {
28
+ id: 'agent-1',
29
+ owner: 'owner',
30
+ did: 'did:mesh:provider' as AgentCard['did'],
31
+ name: 'Provider',
32
+ description: 'Test provider',
33
+ active: true,
34
+ version: 1,
35
+ registeredAt: Date.now(),
36
+ updatedAt: Date.now(),
37
+ endpoint: 'mesh://agent/provider',
38
+ relayEndpoints: [{ endpoint: 'https://relay.example', modes: ['sync', 'fallback'] }],
39
+ capabilities: [
40
+ {
41
+ name: 'echo',
42
+ description: 'Echo capability',
43
+ version: '1.0.0',
44
+ pricing: {
45
+ rail: PaymentRail.SUI_ESCROW,
46
+ amount: 5n,
47
+ currency: 'MIST',
48
+ },
49
+ executionMode: 'sync',
50
+ paymentRails: [PaymentRail.X402_BASE, PaymentRail.SUI_TRANSFER, PaymentRail.SUI_ESCROW],
51
+ },
52
+ ],
53
+ };
54
+ }
55
+
56
+ function createContext(agent: AgentCard) {
57
+ return {
58
+ keypair: {} as never,
59
+ blobStore: {
60
+ store: vi.fn(async () => ({ blobId: 'blob-in' })),
61
+ fetch: vi.fn(async () => new TextEncoder().encode('async-result')),
62
+ },
63
+ taskClient: {
64
+ postTask: vi.fn(async () => ({ taskId: 'task-1' })),
65
+ getTask: vi.fn(async () => ({ status: TaskStatus.COMPLETED, resultBlobId: 'blob-out' })),
66
+ releasePayment: vi.fn(async () => undefined),
67
+ },
68
+ agentCache: {
69
+ searchByCapability: vi.fn(() => [agent]),
70
+ getAgentByDID: vi.fn((did: string) => (did === agent.did ? agent : undefined)),
71
+ upsertAgent: vi.fn(),
72
+ },
73
+ registryClient: {
74
+ discoverByCapability: vi.fn(async () => []),
75
+ getAgentCardByOwner: vi.fn(async () => null),
76
+ },
77
+ meshClient: {} as never,
78
+ spendingPolicy: {
79
+ evaluate: vi.fn(() => ({ approved: true })),
80
+ record: vi.fn(),
81
+ },
82
+ networkConfig: {} as never,
83
+ encryption: undefined as { enabled: boolean; requireEncryption: boolean; publicKey?: string } | undefined,
84
+ relayAuthProvider: {} as never,
85
+ x402Client: {} as never,
86
+ logger: {
87
+ warn: vi.fn(),
88
+ },
89
+ };
90
+ }
91
+
92
+ describe('runMeshExecute', () => {
93
+ it('returns relay sync results when relay execution succeeds', async () => {
94
+ relayExecuteMock.mockResolvedValueOnce({
95
+ result: { ok: true },
96
+ paymentReceipt: 'receipt-1',
97
+ latencyMs: 12,
98
+ taskId: 'relay-task-1',
99
+ providerDid: 'did:mesh:provider',
100
+ });
101
+ const agent = createAgent();
102
+ const context = createContext(agent);
103
+
104
+ const result = await runMeshExecute({ capability: 'echo', input: 'hello', mode: 'sync' }, context as never);
105
+
106
+ expect(result.execution_mode).toBe('sync');
107
+ expect(result.task_id).toBe('relay-task-1');
108
+ expect(result.result).toBe(JSON.stringify({ ok: true }));
109
+ expect(result.payment_rail).toBe(PaymentRail.X402_BASE);
110
+ expect(context.taskClient.postTask).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('falls back to async execution when relay sync is unavailable', async () => {
114
+ relayExecuteMock.mockRejectedValueOnce(new Error('fetch failed'));
115
+ const agent = createAgent();
116
+ const context = createContext(agent);
117
+
118
+ const result = await runMeshExecute({ capability: 'echo', input: 'hello' }, context as never);
119
+
120
+ expect(result.execution_mode).toBe('async');
121
+ expect(result.task_id).toBe('task-1');
122
+ expect(result.result).toBe('async-result');
123
+ expect(result.payment_rail).toBe(PaymentRail.SUI_ESCROW);
124
+ expect(context.taskClient.postTask).toHaveBeenCalledOnce();
125
+ expect(context.taskClient.releasePayment).toHaveBeenCalledOnce();
126
+ expect(context.logger.warn).toHaveBeenCalledOnce();
127
+ });
128
+
129
+ it('stores encrypted input blobs when the provider publishes an encryption key', async () => {
130
+ relayExecuteMock.mockRejectedValueOnce(new Error('fetch failed'));
131
+ const agent = {
132
+ ...createAgent(),
133
+ encryptionPublicKey: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
134
+ };
135
+ const context = createContext(agent);
136
+ const storeEncrypted = vi.fn(async () => ({ blobId: 'encrypted-blob', hash: 'hash-1' }));
137
+ const fetchDecrypted = vi.fn(async () => new TextEncoder().encode('async-result'));
138
+ context.blobStore = {
139
+ ...context.blobStore,
140
+ storeEncrypted,
141
+ fetchDecrypted,
142
+ };
143
+ context.encryption = { enabled: true, requireEncryption: false };
144
+
145
+ await runMeshExecute({ capability: 'echo', input: 'hello' }, context as never);
146
+
147
+ expect(storeEncrypted).toHaveBeenCalledOnce();
148
+ expect(context.taskClient.postTask).toHaveBeenCalledWith(expect.objectContaining({ inputBlobId: 'encrypted-blob' }));
149
+ });
150
+ });
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { MeshToolContext } from '../src/context.js';
4
+ import { runMeshMarketplaceAcceptBid } from '../src/tools/marketplace-accept-bid.js';
5
+ import { runMeshMarketplaceBid } from '../src/tools/marketplace-bid.js';
6
+ import { runMeshMarketplaceBrowse } from '../src/tools/marketplace-browse.js';
7
+ import { runMeshMarketplacePost } from '../src/tools/marketplace-post.js';
8
+
9
+ function createContext(overrides: Partial<MeshToolContext> = {}): MeshToolContext {
10
+ return {
11
+ did: 'did:mesh:test' as MeshToolContext['did'],
12
+ keypair: {
13
+ getPublicKey: () => ({
14
+ toSuiAddress: () => '0xabc',
15
+ }),
16
+ } as MeshToolContext['keypair'],
17
+ suiClient: {
18
+ getBalance: vi.fn(),
19
+ queryEvents: vi.fn(),
20
+ } as unknown as MeshToolContext['suiClient'],
21
+ registryClient: {} as MeshToolContext['registryClient'],
22
+ taskClient: {} as MeshToolContext['taskClient'],
23
+ agentCache: {} as MeshToolContext['agentCache'],
24
+ blobStore: {
25
+ store: vi.fn(async () => ({ blobId: 'walrus:input' })),
26
+ fetch: vi.fn(),
27
+ exists: vi.fn(),
28
+ delete: vi.fn(),
29
+ } as unknown as MeshToolContext['blobStore'],
30
+ spendingPolicy: {} as MeshToolContext['spendingPolicy'],
31
+ networkConfig: {
32
+ rpcUrl: 'http://127.0.0.1:9000',
33
+ faucetUrl: 'http://127.0.0.1:9123',
34
+ packageId: '0x1',
35
+ registryId: '0x2',
36
+ },
37
+ marketplaceClient: {
38
+ postOpenTask: vi.fn(),
39
+ browseOpenTasks: vi.fn(),
40
+ placeBid: vi.fn(),
41
+ acceptBid: vi.fn(),
42
+ } as unknown as MeshToolContext['marketplaceClient'],
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ describe('marketplace MCP tools', () => {
48
+ it('posts marketplace tasks and uploads inline input', async () => {
49
+ const context = createContext();
50
+ vi.mocked(context.marketplaceClient?.postOpenTask as never).mockResolvedValue({ taskId: '0xtask', txDigest: '0xtx' });
51
+
52
+ const result = await runMeshMarketplacePost({
53
+ capability: 'summarize',
54
+ category: 'analysis',
55
+ price_mist: '500',
56
+ input: 'hello world',
57
+ }, context);
58
+
59
+ expect(context.blobStore.store).toHaveBeenCalledOnce();
60
+ expect(context.marketplaceClient?.postOpenTask).toHaveBeenCalledWith({
61
+ capability: 'summarize',
62
+ category: 'analysis',
63
+ inputBlobId: 'walrus:input',
64
+ agreementHash: undefined,
65
+ priceMist: 500n,
66
+ disputeWindowMs: 60_000,
67
+ expiryHours: 24,
68
+ signer: context.keypair,
69
+ });
70
+ expect(result).toMatchObject({ task_id: '0xtask', tx_digest: '0xtx' });
71
+ });
72
+
73
+ it('browses marketplace tasks with filters', async () => {
74
+ const context = createContext();
75
+ vi.mocked(context.marketplaceClient?.browseOpenTasks as never).mockResolvedValue([{ id: '0xtask', category: 'analysis' }]);
76
+
77
+ const result = await runMeshMarketplaceBrowse({ category: 'analysis', max_price_mist: '750', limit: 5 }, context);
78
+
79
+ expect(context.marketplaceClient?.browseOpenTasks).toHaveBeenCalledWith({
80
+ category: 'analysis',
81
+ minPriceMist: undefined,
82
+ maxPriceMist: 750n,
83
+ limit: 5,
84
+ });
85
+ expect(result).toMatchObject({ count: 1 });
86
+ });
87
+
88
+ it('rejects conflicting marketplace input sources', async () => {
89
+ const context = createContext();
90
+
91
+ await expect(runMeshMarketplacePost({
92
+ capability: 'summarize',
93
+ category: 'analysis',
94
+ price_mist: '500',
95
+ input: 'hello world',
96
+ input_blob_id: 'walrus:input',
97
+ }, context)).rejects.toThrow('Provide exactly one of input or input_blob_id when posting a marketplace task');
98
+ });
99
+
100
+ it('places and accepts bids', async () => {
101
+ const context = createContext();
102
+ vi.mocked(context.marketplaceClient?.placeBid as never).mockResolvedValue({ bidId: '0xbid', txDigest: '0xbidtx', reputationScore: 99n });
103
+ vi.mocked(context.marketplaceClient?.acceptBid as never).mockResolvedValue({ txDigest: '0xaccept', rejectedBidIds: ['0xbid-2'] });
104
+
105
+ await expect(runMeshMarketplaceBid({ task_id: '0xtask', bid_price_mist: '400', evidence: 'proposal' }, context)).resolves.toMatchObject({
106
+ bid_id: '0xbid',
107
+ tx_digest: '0xbidtx',
108
+ reputation_score: '99',
109
+ });
110
+
111
+ await expect(runMeshMarketplaceAcceptBid({ task_id: '0xtask', bid_id: '0xbid' }, context)).resolves.toMatchObject({
112
+ bid_id: '0xbid',
113
+ rejected_bid_ids: ['0xbid-2'],
114
+ tx_digest: '0xaccept',
115
+ });
116
+ });
117
+ });