@agentwonderland/mcp 0.1.45 → 0.1.46

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 @@
1
+ export {};
@@ -0,0 +1,66 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ const mockApiGet = vi.fn();
3
+ const mockGetAcceptedPaymentMethods = vi.fn();
4
+ const mockIsFavorite = vi.fn();
5
+ vi.mock("../../core/api-client.js", () => ({
6
+ apiGet: mockApiGet,
7
+ }));
8
+ vi.mock("../../core/payments.js", () => ({
9
+ getAcceptedPaymentMethods: mockGetAcceptedPaymentMethods,
10
+ }));
11
+ vi.mock("../../core/config.js", () => ({
12
+ isFavorite: mockIsFavorite,
13
+ }));
14
+ function flattenToolText(result) {
15
+ const content = result?.content ?? [];
16
+ return content
17
+ .filter((item) => item?.type === "text")
18
+ .map((item) => item.text ?? "")
19
+ .join("\n\n");
20
+ }
21
+ function makeServerHarness() {
22
+ const handlers = new Map();
23
+ return {
24
+ handlers,
25
+ server: {
26
+ tool(name, _description, _schema, handler) {
27
+ handlers.set(name, handler);
28
+ },
29
+ },
30
+ };
31
+ }
32
+ describe("search_agents MCP tool", () => {
33
+ beforeEach(() => {
34
+ vi.resetModules();
35
+ vi.clearAllMocks();
36
+ mockGetAcceptedPaymentMethods.mockReturnValue([]);
37
+ mockIsFavorite.mockReturnValue(false);
38
+ });
39
+ it("sorts popularity by live completed job stats after API enrichment", async () => {
40
+ mockApiGet.mockResolvedValueOnce([
41
+ {
42
+ id: "agent-low",
43
+ name: "Low History",
44
+ slug: "low-history",
45
+ pricePerRunUsd: "0.100000",
46
+ stats: { completedJobs: 2, avgRating: null, ratingCount: 0 },
47
+ },
48
+ {
49
+ id: "agent-high",
50
+ name: "High History",
51
+ slug: "high-history",
52
+ pricePerRunUsd: "0.100000",
53
+ stats: { completedJobs: 6, avgRating: null, ratingCount: 0 },
54
+ },
55
+ ]);
56
+ const { registerSearchTools } = await import("../search.js");
57
+ const harness = makeServerHarness();
58
+ registerSearchTools(harness.server);
59
+ const search = harness.handlers.get("search_agents");
60
+ expect(search).toBeDefined();
61
+ const result = await search({ query: "stock research", limit: 2, sort: "popularity" });
62
+ const output = flattenToolText(result);
63
+ expect(output.indexOf("High History")).toBeLessThan(output.indexOf("Low History"));
64
+ expect(output).toContain("High History (high-history) ☆☆☆☆☆ 6 jobs");
65
+ });
66
+ });
@@ -6,6 +6,23 @@ import { agentLine } from "../core/formatters.js";
6
6
  function text(t) {
7
7
  return { content: [{ type: "text", text: t }] };
8
8
  }
9
+ function completedJobs(agent) {
10
+ const stats = agent.stats;
11
+ return stats?.completedJobs ?? agent.totalExecutions ?? 0;
12
+ }
13
+ function averageRating(agent) {
14
+ const stats = agent.stats;
15
+ return agent.avgRating ?? stats?.avgRating ?? 0;
16
+ }
17
+ function ratingCount(agent) {
18
+ const stats = agent.stats;
19
+ const topLevel = typeof agent.ratingCount === "number" ? agent.ratingCount : undefined;
20
+ return topLevel ?? stats?.ratingCount ?? 0;
21
+ }
22
+ function price(agent) {
23
+ const parsed = Number.parseFloat(agent.pricePerRunUsd ?? "0");
24
+ return Number.isFinite(parsed) ? parsed : 0;
25
+ }
9
26
  export function registerSearchTools(server) {
10
27
  server.tool("search_agents", "Search the Agent Wonderland marketplace for AI agents. Returns a ranked list of matching agents with ratings, pricing, and job counts.", {
11
28
  query: z.string().optional().describe("Search query (natural language or keywords)"),
@@ -61,6 +78,22 @@ export function registerSearchTools(server) {
61
78
  return typeof avg === "number" && avg >= min_rating;
62
79
  });
63
80
  }
81
+ // The API enriches list rows with live stats after registry search. Do a
82
+ // final local sort for stat-based MCP views so displayed jobs/ratings and
83
+ // result order cannot disagree while registry aggregates catch up.
84
+ if (sort === "popularity") {
85
+ agents = [...agents].sort((a, b) => completedJobs(b) - completedJobs(a) ||
86
+ ratingCount(b) - ratingCount(a) ||
87
+ averageRating(b) - averageRating(a));
88
+ }
89
+ else if (sort === "rating") {
90
+ agents = [...agents].sort((a, b) => averageRating(b) - averageRating(a) ||
91
+ ratingCount(b) - ratingCount(a) ||
92
+ completedJobs(b) - completedJobs(a));
93
+ }
94
+ else if (sort === "price") {
95
+ agents = [...agents].sort((a, b) => price(a) - price(b));
96
+ }
64
97
  // Trim to requested limit after filtering
65
98
  agents = agents.slice(0, requestedLimit);
66
99
  if (agents.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwonderland/mcp",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "type": "module",
5
5
  "description": "MCP server for the Agent Wonderland AI agent marketplace",
6
6
  "bin": {
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mockApiGet = vi.fn();
4
+ const mockGetAcceptedPaymentMethods = vi.fn();
5
+ const mockIsFavorite = vi.fn();
6
+
7
+ vi.mock("../../core/api-client.js", () => ({
8
+ apiGet: mockApiGet,
9
+ }));
10
+
11
+ vi.mock("../../core/payments.js", () => ({
12
+ getAcceptedPaymentMethods: mockGetAcceptedPaymentMethods,
13
+ }));
14
+
15
+ vi.mock("../../core/config.js", () => ({
16
+ isFavorite: mockIsFavorite,
17
+ }));
18
+
19
+ function flattenToolText(result: unknown): string {
20
+ const content = (result as { content?: Array<{ type?: string; text?: string }> })?.content ?? [];
21
+ return content
22
+ .filter((item) => item?.type === "text")
23
+ .map((item) => item.text ?? "")
24
+ .join("\n\n");
25
+ }
26
+
27
+ function makeServerHarness() {
28
+ const handlers = new Map<string, (args: Record<string, unknown>) => Promise<unknown>>();
29
+ return {
30
+ handlers,
31
+ server: {
32
+ tool(name: string, _description: string, _schema: unknown, handler: (args: Record<string, unknown>) => Promise<unknown>) {
33
+ handlers.set(name, handler);
34
+ },
35
+ },
36
+ };
37
+ }
38
+
39
+ describe("search_agents MCP tool", () => {
40
+ beforeEach(() => {
41
+ vi.resetModules();
42
+ vi.clearAllMocks();
43
+ mockGetAcceptedPaymentMethods.mockReturnValue([]);
44
+ mockIsFavorite.mockReturnValue(false);
45
+ });
46
+
47
+ it("sorts popularity by live completed job stats after API enrichment", async () => {
48
+ mockApiGet.mockResolvedValueOnce([
49
+ {
50
+ id: "agent-low",
51
+ name: "Low History",
52
+ slug: "low-history",
53
+ pricePerRunUsd: "0.100000",
54
+ stats: { completedJobs: 2, avgRating: null, ratingCount: 0 },
55
+ },
56
+ {
57
+ id: "agent-high",
58
+ name: "High History",
59
+ slug: "high-history",
60
+ pricePerRunUsd: "0.100000",
61
+ stats: { completedJobs: 6, avgRating: null, ratingCount: 0 },
62
+ },
63
+ ]);
64
+
65
+ const { registerSearchTools } = await import("../search.js");
66
+ const harness = makeServerHarness();
67
+ registerSearchTools(harness.server as never);
68
+
69
+ const search = harness.handlers.get("search_agents");
70
+ expect(search).toBeDefined();
71
+
72
+ const result = await search!({ query: "stock research", limit: 2, sort: "popularity" });
73
+ const output = flattenToolText(result);
74
+
75
+ expect(output.indexOf("High History")).toBeLessThan(output.indexOf("Low History"));
76
+ expect(output).toContain("High History (high-history) ☆☆☆☆☆ 6 jobs");
77
+ });
78
+ });
@@ -10,6 +10,27 @@ function text(t: string) {
10
10
  return { content: [{ type: "text" as const, text: t }] };
11
11
  }
12
12
 
13
+ function completedJobs(agent: AgentRecord): number {
14
+ const stats = agent.stats as { completedJobs?: number } | undefined;
15
+ return stats?.completedJobs ?? agent.totalExecutions ?? 0;
16
+ }
17
+
18
+ function averageRating(agent: AgentRecord): number {
19
+ const stats = agent.stats as { avgRating?: number | null } | undefined;
20
+ return agent.avgRating ?? stats?.avgRating ?? 0;
21
+ }
22
+
23
+ function ratingCount(agent: AgentRecord): number {
24
+ const stats = agent.stats as { ratingCount?: number } | undefined;
25
+ const topLevel = typeof agent.ratingCount === "number" ? agent.ratingCount : undefined;
26
+ return topLevel ?? stats?.ratingCount ?? 0;
27
+ }
28
+
29
+ function price(agent: AgentRecord): number {
30
+ const parsed = Number.parseFloat(agent.pricePerRunUsd ?? "0");
31
+ return Number.isFinite(parsed) ? parsed : 0;
32
+ }
33
+
13
34
  export function registerSearchTools(server: McpServer): void {
14
35
  server.tool(
15
36
  "search_agents",
@@ -71,6 +92,25 @@ export function registerSearchTools(server: McpServer): void {
71
92
  });
72
93
  }
73
94
 
95
+ // The API enriches list rows with live stats after registry search. Do a
96
+ // final local sort for stat-based MCP views so displayed jobs/ratings and
97
+ // result order cannot disagree while registry aggregates catch up.
98
+ if (sort === "popularity") {
99
+ agents = [...agents].sort((a, b) =>
100
+ completedJobs(b) - completedJobs(a) ||
101
+ ratingCount(b) - ratingCount(a) ||
102
+ averageRating(b) - averageRating(a),
103
+ );
104
+ } else if (sort === "rating") {
105
+ agents = [...agents].sort((a, b) =>
106
+ averageRating(b) - averageRating(a) ||
107
+ ratingCount(b) - ratingCount(a) ||
108
+ completedJobs(b) - completedJobs(a),
109
+ );
110
+ } else if (sort === "price") {
111
+ agents = [...agents].sort((a, b) => price(a) - price(b));
112
+ }
113
+
74
114
  // Trim to requested limit after filtering
75
115
  agents = agents.slice(0, requestedLimit);
76
116