@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,70 @@
1
+ import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import {
3
+ ListResourcesRequestSchema,
4
+ ListResourceTemplatesRequestSchema,
5
+ ReadResourceRequestSchema,
6
+ } from '@modelcontextprotocol/sdk/types.js';
7
+
8
+ import type { MeshToolContext } from '../context.js';
9
+ import { readAgentResource, meshAgentResourceTemplate } from './agent.js';
10
+ import { readCapabilitiesResource, meshCapabilitiesResource } from './capabilities.js';
11
+ import { readTaskResource, meshTaskResourceTemplate } from './task.js';
12
+ import { readWalletResource, meshWalletResource } from './wallet.js';
13
+
14
+ const staticResources = [meshCapabilitiesResource, meshWalletResource];
15
+ const resourceTemplates = [meshAgentResourceTemplate, meshTaskResourceTemplate];
16
+
17
+ export function registerResourceHandlers(server: Server, context: MeshToolContext): void {
18
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
19
+ resources: staticResources,
20
+ }));
21
+
22
+ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({
23
+ resourceTemplates,
24
+ }));
25
+
26
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
27
+ const uri = request.params.uri;
28
+ const data = await routeResourceRead(uri, context);
29
+
30
+ return {
31
+ contents: [
32
+ {
33
+ uri,
34
+ mimeType: 'application/json',
35
+ text: serialize(data),
36
+ },
37
+ ],
38
+ };
39
+ });
40
+ }
41
+
42
+ export const meshResourceDefinitions = {
43
+ resources: staticResources,
44
+ templates: resourceTemplates,
45
+ };
46
+
47
+ async function routeResourceRead(uri: string, context: MeshToolContext): Promise<unknown> {
48
+ const parsed = new URL(uri);
49
+
50
+ switch (parsed.host) {
51
+ case 'capabilities':
52
+ return readCapabilitiesResource(context);
53
+ case 'wallet':
54
+ return readWalletResource(context);
55
+ case 'agent':
56
+ return readAgentResource(decodeURIComponent(parsed.pathname.replace(/^\//, '')), context);
57
+ case 'task':
58
+ return readTaskResource(decodeURIComponent(parsed.pathname.replace(/^\//, '')), context);
59
+ default:
60
+ throw new Error(`Unknown resource: ${uri}`);
61
+ }
62
+ }
63
+
64
+ function serialize(payload: unknown): string {
65
+ return JSON.stringify(payload, bigintReplacer, 2);
66
+ }
67
+
68
+ function bigintReplacer(_key: string, value: unknown): unknown {
69
+ return typeof value === 'bigint' ? value.toString() : value;
70
+ }
@@ -0,0 +1,58 @@
1
+ import { TaskStatus } from '@hivemind-os/collective-types';
2
+
3
+ import type { MeshToolContext } from '../context.js';
4
+ import { fetchMeshBlob } from '../encryption.js';
5
+
6
+ const decoder = new TextDecoder();
7
+
8
+ export const meshTaskResourceTemplate = {
9
+ uriTemplate: 'mesh://task/{id}',
10
+ name: 'Task Details',
11
+ description: 'Resolve task details and result payload by task id',
12
+ mimeType: 'application/json',
13
+ };
14
+
15
+ export async function readTaskResource(taskId: string, context: MeshToolContext): Promise<{
16
+ id: string;
17
+ requester: string;
18
+ provider?: string;
19
+ capability: string;
20
+ input_blob_id: string;
21
+ result_blob_id?: string;
22
+ price_mist: string;
23
+ status: string;
24
+ created_at: number;
25
+ accepted_at?: number;
26
+ completed_at?: number;
27
+ expires_at: number;
28
+ agreement_hash?: string;
29
+ result?: string;
30
+ }> {
31
+ const task = await context.taskClient.getTask(taskId);
32
+ if (!task) {
33
+ throw new Error(`Task ${taskId} was not found.`);
34
+ }
35
+
36
+ let result: string | undefined;
37
+ if ((task.status === TaskStatus.COMPLETED || task.status === TaskStatus.RELEASED) && task.resultBlobId) {
38
+ const bytes = await fetchMeshBlob(context.blobStore, task.resultBlobId);
39
+ result = bytes ? decoder.decode(bytes) : undefined;
40
+ }
41
+
42
+ return {
43
+ id: task.id,
44
+ requester: task.requester,
45
+ provider: task.provider,
46
+ capability: task.capability,
47
+ input_blob_id: task.inputBlobId,
48
+ result_blob_id: task.resultBlobId,
49
+ price_mist: task.price.toString(),
50
+ status: TaskStatus[task.status] ?? 'UNKNOWN',
51
+ created_at: task.createdAt,
52
+ accepted_at: task.acceptedAt,
53
+ completed_at: task.completedAt,
54
+ expires_at: task.expiresAt,
55
+ agreement_hash: task.agreementHash,
56
+ result,
57
+ };
58
+ }
@@ -0,0 +1,32 @@
1
+ import { PaymentRail } from '@hivemind-os/collective-types';
2
+
3
+ import type { MeshToolContext } from '../context.js';
4
+ import { formatMistToSui } from '../tools/balance.js';
5
+
6
+ export const meshWalletResource = {
7
+ uri: 'mesh://wallet',
8
+ name: 'Wallet Overview',
9
+ description: 'Current wallet balance and local spending totals',
10
+ mimeType: 'application/json',
11
+ };
12
+
13
+ export async function readWalletResource(context: MeshToolContext): Promise<{
14
+ address: string;
15
+ balance_mist: string;
16
+ balance_sui: string;
17
+ spent_today_mist: string;
18
+ spent_month_mist: string;
19
+ rail: PaymentRail;
20
+ }> {
21
+ const address = context.keypair.getPublicKey().toSuiAddress();
22
+ const balance = await context.suiClient.getBalance(address);
23
+
24
+ return {
25
+ address,
26
+ balance_mist: balance.toString(),
27
+ balance_sui: formatMistToSui(balance),
28
+ spent_today_mist: context.spendingPolicy.getSpent('day', PaymentRail.SUI_ESCROW).toString(),
29
+ spent_month_mist: context.spendingPolicy.getSpent('month', PaymentRail.SUI_ESCROW).toString(),
30
+ rail: PaymentRail.SUI_ESCROW,
31
+ };
32
+ }
@@ -0,0 +1,122 @@
1
+ import type { MeshToolContext } from '../context.js';
2
+ import { executeIndexerQuery } from './indexer-client.js';
3
+
4
+ export interface MeshAnalyticsParams {
5
+ view?: 'summary' | 'task-volume' | 'top-providers' | 'marketplace';
6
+ period?: 'HOUR' | 'DAY' | 'WEEK';
7
+ buckets?: number;
8
+ limit?: number;
9
+ sort_by?: 'COMPLETED_TASKS' | 'EARNINGS' | 'REPUTATION';
10
+ }
11
+
12
+ export const meshAnalyticsTool = {
13
+ name: 'collective_analytics',
14
+ description: 'Query task volume, top providers, and marketplace analytics from the Agentic Mesh indexer',
15
+ inputSchema: {
16
+ type: 'object' as const,
17
+ properties: {
18
+ view: { type: 'string', enum: ['summary', 'task-volume', 'top-providers', 'marketplace'] },
19
+ period: { type: 'string', enum: ['HOUR', 'DAY', 'WEEK'] },
20
+ buckets: { type: 'number' },
21
+ limit: { type: 'number' },
22
+ sort_by: { type: 'string', enum: ['COMPLETED_TASKS', 'EARNINGS', 'REPUTATION'] },
23
+ },
24
+ },
25
+ };
26
+
27
+ export async function runMeshAnalytics(
28
+ params: MeshAnalyticsParams,
29
+ context: MeshToolContext,
30
+ ): Promise<Record<string, unknown>> {
31
+ const view = params.view ?? 'summary';
32
+
33
+ switch (view) {
34
+ case 'task-volume': {
35
+ const data = await executeIndexerQuery<{ taskVolume: unknown[] }>(
36
+ context,
37
+ `query TaskVolume($period: TimePeriod!, $buckets: Int) {
38
+ taskVolume(period: $period, buckets: $buckets) {
39
+ label
40
+ count
41
+ volumeMist
42
+ }
43
+ }`,
44
+ {
45
+ period: params.period ?? 'DAY',
46
+ buckets: params.buckets ?? 14,
47
+ },
48
+ );
49
+ return { view, period: params.period ?? 'DAY', buckets: params.buckets ?? 14, data: data.taskVolume };
50
+ }
51
+ case 'top-providers': {
52
+ const data = await executeIndexerQuery<{ topProviders: unknown[] }>(
53
+ context,
54
+ `query TopProviders($limit: Int, $sortBy: ProviderSortField) {
55
+ topProviders(limit: $limit, sortBy: $sortBy) {
56
+ did
57
+ owner
58
+ name
59
+ completedTasks
60
+ earningsMist
61
+ disputeCount
62
+ successRate
63
+ reputation
64
+ }
65
+ }`,
66
+ {
67
+ limit: params.limit ?? 10,
68
+ sortBy: params.sort_by ?? 'COMPLETED_TASKS',
69
+ },
70
+ );
71
+ return { view, limit: params.limit ?? 10, sort_by: params.sort_by ?? 'COMPLETED_TASKS', data: data.topProviders };
72
+ }
73
+ case 'marketplace': {
74
+ const data = await executeIndexerQuery<{ analytics: { marketplace: unknown } }>(
75
+ context,
76
+ `query MarketplaceAnalytics {
77
+ analytics {
78
+ marketplace {
79
+ averageBidCount
80
+ acceptanceRate
81
+ categoryPopularity {
82
+ category
83
+ taskCount
84
+ }
85
+ }
86
+ }
87
+ }`,
88
+ );
89
+ return { view, data: data.analytics.marketplace };
90
+ }
91
+ case 'summary':
92
+ default: {
93
+ const data = await executeIndexerQuery<{ analytics: unknown }>(
94
+ context,
95
+ `query AnalyticsSummary {
96
+ analytics {
97
+ totalAgents
98
+ activeAgents
99
+ totalTasks
100
+ completedTasks
101
+ disputedTasks
102
+ totalVolumeMist
103
+ averageGasCosts {
104
+ capability
105
+ averageGasMist
106
+ taskCount
107
+ }
108
+ marketplace {
109
+ averageBidCount
110
+ acceptanceRate
111
+ categoryPopularity {
112
+ category
113
+ taskCount
114
+ }
115
+ }
116
+ }
117
+ }`,
118
+ );
119
+ return { view: 'summary', data: data.analytics };
120
+ }
121
+ }
122
+ }
@@ -0,0 +1,36 @@
1
+ import type { MeshToolContext } from '../context.js';
2
+
3
+ export const meshBalanceTool = {
4
+ name: 'collective_balance',
5
+ description: 'Get the current wallet balance in MIST and SUI',
6
+ inputSchema: {
7
+ type: 'object' as const,
8
+ properties: {},
9
+ required: [],
10
+ },
11
+ };
12
+
13
+ export async function runMeshBalance(
14
+ _params: Record<string, never>,
15
+ context: MeshToolContext,
16
+ ): Promise<{ address: string; balance_mist: string; balance_sui: string }> {
17
+ const address = context.keypair.getPublicKey().toSuiAddress();
18
+ const balanceMist = await context.suiClient.getBalance(address);
19
+
20
+ return {
21
+ address,
22
+ balance_mist: balanceMist.toString(),
23
+ balance_sui: formatMistToSui(balanceMist),
24
+ };
25
+ }
26
+
27
+ export function formatMistToSui(balanceMist: bigint): string {
28
+ const whole = balanceMist / 1_000_000_000n;
29
+ const fraction = balanceMist % 1_000_000_000n;
30
+ if (fraction === 0n) {
31
+ return whole.toString();
32
+ }
33
+
34
+ const fractionText = fraction.toString().padStart(9, '0').replace(/0+$/, '');
35
+ return `${whole.toString()}.${fractionText}`;
36
+ }
@@ -0,0 +1,33 @@
1
+ import type { MeshToolContext } from '../context.js';
2
+
3
+ export interface MeshDeactivateParams {
4
+ agent_card_id: string;
5
+ }
6
+
7
+ export const meshDeactivateTool = {
8
+ name: 'collective_deactivate',
9
+ description: 'Deactivate an existing agent card',
10
+ inputSchema: {
11
+ type: 'object' as const,
12
+ properties: {
13
+ agent_card_id: { type: 'string', description: 'Agent card object id' },
14
+ },
15
+ required: ['agent_card_id'],
16
+ },
17
+ };
18
+
19
+ export async function runMeshDeactivate(
20
+ params: MeshDeactivateParams,
21
+ context: MeshToolContext,
22
+ ): Promise<{ tx_digest: string; status: 'deactivated' }> {
23
+ const result = await context.registryClient.deactivateAgent({
24
+ cardId: params.agent_card_id,
25
+ keypair: context.keypair,
26
+ });
27
+ context.agentCache.removeAgent(params.agent_card_id);
28
+
29
+ return {
30
+ tx_digest: result.txDigest,
31
+ status: 'deactivated',
32
+ };
33
+ }
@@ -0,0 +1,256 @@
1
+ import type { AgentCard, Capability } from '@hivemind-os/collective-types';
2
+ import { ReputationScoreCalculator } from '@hivemind-os/collective-core';
3
+
4
+ import type { MeshToolContext } from '../context.js';
5
+ import { queryIndexerAgents, resolveIndexerUrl } from './indexer-client.js';
6
+
7
+ const scoreCalculator = new ReputationScoreCalculator();
8
+
9
+ export interface MeshDiscoverParams {
10
+ capability: string;
11
+ limit?: number;
12
+ sort_by?: 'price' | 'reputation';
13
+ useIndexer?: boolean;
14
+ }
15
+
16
+ export const meshDiscoverTool = {
17
+ name: 'collective_discover',
18
+ description: 'Find agents by capability on the Agentic Mesh network',
19
+ inputSchema: {
20
+ type: 'object' as const,
21
+ properties: {
22
+ capability: { type: 'string', description: 'Capability name to search for' },
23
+ limit: { type: 'number', description: 'Max results (default 10)' },
24
+ sort_by: { type: 'string', enum: ['price', 'reputation'], description: 'Sort results by price or reputation' },
25
+ useIndexer: { type: 'boolean', description: 'Prefer the indexer GraphQL API when available' },
26
+ },
27
+ required: ['capability'],
28
+ },
29
+ };
30
+
31
+ export async function runMeshDiscover(
32
+ params: MeshDiscoverParams,
33
+ context: MeshToolContext,
34
+ ): Promise<{ capability: string; source: 'indexer' | 'cache' | 'registry'; agents: ReturnType<typeof summarizeAgent>[] }> {
35
+ const capability = params.capability.trim();
36
+ const limit = normalizeLimit(params.limit);
37
+ const sortBy = params.sort_by ?? 'price';
38
+ const { agents, source } = await discoverAgentsByCapability(capability, context, limit, sortBy, params.useIndexer);
39
+
40
+ return {
41
+ capability,
42
+ source,
43
+ agents: agents.map((agent) => summarizeAgent(agent, capability)),
44
+ };
45
+ }
46
+
47
+ export async function discoverAgentsByCapability(
48
+ capability: string,
49
+ context: MeshToolContext,
50
+ limit = 10,
51
+ sortBy: 'price' | 'reputation' = 'price',
52
+ useIndexer?: boolean,
53
+ ): Promise<{ agents: AgentCard[]; source: 'indexer' | 'cache' | 'registry' }> {
54
+ const normalizedCapability = capability.trim();
55
+ if (!normalizedCapability) {
56
+ return { agents: [], source: 'cache' };
57
+ }
58
+
59
+ if (shouldUseIndexer(context, useIndexer)) {
60
+ try {
61
+ const indexed = await queryIndexerAgents(context, {
62
+ capability: normalizedCapability,
63
+ limit,
64
+ });
65
+ if (indexed.length > 0) {
66
+ return {
67
+ agents: sortBy === 'reputation' ? indexed.slice(0, limit) : sortByPrice(indexed, normalizedCapability).slice(0, limit),
68
+ source: 'indexer',
69
+ };
70
+ }
71
+ } catch (error) {
72
+ context.logger?.warn?.({ err: error }, 'Indexer discovery failed; falling back to local cache.');
73
+ }
74
+ }
75
+
76
+ const cached = (await queryLocalCache(context, normalizedCapability, Math.max(limit * 2, limit), sortBy))
77
+ .filter((agent) => hasCapability(agent, normalizedCapability))
78
+ .slice(0, limit);
79
+
80
+ if (cached.length > 0) {
81
+ return {
82
+ agents: sortBy === 'reputation' ? cached : sortByPrice(cached, normalizedCapability).slice(0, limit),
83
+ source: 'cache',
84
+ };
85
+ }
86
+
87
+ const discovered = await context.registryClient.discoverByCapability(normalizedCapability, limit, {
88
+ sortByReputation: sortBy === 'reputation',
89
+ });
90
+ for (const agent of discovered) {
91
+ context.agentCache.upsertAgent(agent);
92
+ }
93
+
94
+ const ranked = sortBy === 'reputation' ? discovered : sortByPrice(discovered, normalizedCapability);
95
+ return { agents: ranked.slice(0, limit), source: 'registry' };
96
+ }
97
+
98
+ export async function resolveProviderCapability(
99
+ capability: string,
100
+ context: MeshToolContext,
101
+ providerDid?: string,
102
+ ): Promise<{ agent: AgentCard; capability: Capability }> {
103
+ const normalizedCapability = capability.trim();
104
+ if (!normalizedCapability) {
105
+ throw new Error('Capability is required.');
106
+ }
107
+
108
+ if (providerDid) {
109
+ const cachedAgent = context.agentCache.getAgentByDID(providerDid);
110
+ const cachedCapability = cachedAgent ? findCapability(cachedAgent, normalizedCapability) : undefined;
111
+ if (cachedAgent?.active && cachedCapability) {
112
+ return { agent: cachedAgent, capability: cachedCapability };
113
+ }
114
+
115
+ const { agents } = await discoverAgentsByCapability(normalizedCapability, context, 50);
116
+ const matched = agents.find((entry) => entry.did === providerDid);
117
+ const matchedCapability = matched ? findCapability(matched, normalizedCapability) : undefined;
118
+ if (!matched || !matchedCapability) {
119
+ throw new Error(`Provider ${providerDid} was not found for capability ${normalizedCapability}.`);
120
+ }
121
+
122
+ return { agent: matched, capability: matchedCapability };
123
+ }
124
+
125
+ const { agents } = await discoverAgentsByCapability(normalizedCapability, context, 20);
126
+ const ranked = agents
127
+ .map((agent) => ({ agent, capability: findCapability(agent, normalizedCapability) }))
128
+ .filter((entry): entry is { agent: AgentCard; capability: Capability } => Boolean(entry.capability))
129
+ .sort((left, right) => compareBigInt(left.capability.pricing.amount, right.capability.pricing.amount));
130
+
131
+ if (ranked.length === 0) {
132
+ throw new Error(`No providers found for capability ${normalizedCapability}.`);
133
+ }
134
+
135
+ return ranked[0];
136
+ }
137
+
138
+ export function summarizeAgent(agent: AgentCard, capability?: string): {
139
+ name: string;
140
+ did: AgentCard['did'];
141
+ capabilities: string[];
142
+ pricing: Array<{
143
+ capability: string;
144
+ price_mist: string;
145
+ rail: string;
146
+ currency: string;
147
+ }>;
148
+ reputation: {
149
+ success_rate: number;
150
+ total_tasks: number;
151
+ total_disputes: number;
152
+ total_earnings_mist: string;
153
+ };
154
+ endpoint?: string;
155
+ } {
156
+ const scopedCapabilities = capability
157
+ ? agent.capabilities.filter((entry) => capabilityNameEquals(entry.name, capability))
158
+ : agent.capabilities;
159
+ const reputation = scoreCalculator.computeScore(agent, []);
160
+
161
+ return {
162
+ name: agent.name,
163
+ did: agent.did,
164
+ capabilities: scopedCapabilities.map((entry) => entry.name),
165
+ pricing: scopedCapabilities.map((entry) => ({
166
+ capability: entry.name,
167
+ price_mist: entry.pricing.amount.toString(),
168
+ rail: entry.pricing.rail,
169
+ currency: entry.pricing.currency,
170
+ })),
171
+ reputation: {
172
+ success_rate: reputation.successRate,
173
+ total_tasks: reputation.totalTasks,
174
+ total_disputes: reputation.totalDisputes,
175
+ total_earnings_mist: reputation.totalEarningsMist.toString(),
176
+ },
177
+ endpoint: agent.endpoint,
178
+ };
179
+ }
180
+
181
+ async function queryLocalCache(
182
+ context: MeshToolContext,
183
+ capability: string,
184
+ limit: number,
185
+ sortBy: 'price' | 'reputation',
186
+ ): Promise<AgentCard[]> {
187
+ const cache = context.agentCache as MeshToolContext['agentCache'] & {
188
+ queryAgentsAdvanced?: (filters: {
189
+ capability?: string;
190
+ limit?: number;
191
+ sortBy?: 'stake' | 'reputation';
192
+ }) => Promise<AgentCard[]>;
193
+ };
194
+
195
+ if (typeof cache.queryAgentsAdvanced === 'function') {
196
+ return await cache.queryAgentsAdvanced({
197
+ capability,
198
+ limit,
199
+ sortBy: sortBy === 'reputation' ? 'reputation' : 'stake',
200
+ });
201
+ }
202
+
203
+ return context.agentCache.searchByCapability(capability, limit, { sortByReputation: sortBy === 'reputation' });
204
+ }
205
+
206
+ function shouldUseIndexer(context: MeshToolContext, useIndexer?: boolean): boolean {
207
+ if (useIndexer === false) {
208
+ return false;
209
+ }
210
+ return Boolean(resolveIndexerUrl(context));
211
+ }
212
+
213
+ function findCapability(agent: AgentCard, capability: string): Capability | undefined {
214
+ return agent.capabilities.find((entry) => capabilityNameEquals(entry.name, capability));
215
+ }
216
+
217
+ function hasCapability(agent: AgentCard, capability: string): boolean {
218
+ return findCapability(agent, capability) !== undefined;
219
+ }
220
+
221
+ function capabilityNameEquals(left: string, right: string): boolean {
222
+ return left.toLowerCase() === right.toLowerCase();
223
+ }
224
+
225
+ function compareBigInt(left: bigint, right: bigint): number {
226
+ if (left === right) {
227
+ return 0;
228
+ }
229
+
230
+ return left < right ? -1 : 1;
231
+ }
232
+
233
+ function normalizeLimit(limit?: number): number {
234
+ if (typeof limit !== 'number' || Number.isNaN(limit)) {
235
+ return 10;
236
+ }
237
+
238
+ return Math.max(1, Math.floor(limit));
239
+ }
240
+
241
+ function sortByPrice(agents: AgentCard[], capability: string): AgentCard[] {
242
+ return [...agents].sort((left, right) => {
243
+ const leftCapability = findCapability(left, capability);
244
+ const rightCapability = findCapability(right, capability);
245
+ if (!leftCapability && !rightCapability) {
246
+ return 0;
247
+ }
248
+ if (!leftCapability) {
249
+ return 1;
250
+ }
251
+ if (!rightCapability) {
252
+ return -1;
253
+ }
254
+ return compareBigInt(leftCapability.pricing.amount, rightCapability.pricing.amount);
255
+ });
256
+ }