@agentwonderland/mcp 0.1.44 → 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.
- package/dist/core/__tests__/formatters.test.js +17 -1
- package/dist/core/formatters.d.ts +1 -0
- package/dist/core/formatters.js +5 -1
- package/dist/tools/__tests__/search.test.d.ts +1 -0
- package/dist/tools/__tests__/search.test.js +66 -0
- package/dist/tools/search.js +33 -0
- package/package.json +1 -1
- package/src/core/__tests__/formatters.test.ts +19 -1
- package/src/core/formatters.ts +6 -2
- package/src/tools/__tests__/search.test.ts +78 -0
- package/src/tools/search.ts +40 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { formatRunResult } from "../formatters.js";
|
|
2
|
+
import { agentLine, formatRunResult } from "../formatters.js";
|
|
3
3
|
describe("formatRunResult", () => {
|
|
4
4
|
it("does not show a paid line for credit-pack-backed runs with zero cost", () => {
|
|
5
5
|
const output = formatRunResult({
|
|
@@ -23,3 +23,19 @@ describe("formatRunResult", () => {
|
|
|
23
23
|
expect(output).toContain("Job ID: job-123");
|
|
24
24
|
});
|
|
25
25
|
});
|
|
26
|
+
describe("agentLine", () => {
|
|
27
|
+
it("surfaces live rating count and completed job history from stats", () => {
|
|
28
|
+
const output = agentLine({
|
|
29
|
+
name: "People Enrichment",
|
|
30
|
+
slug: "people-enrichment",
|
|
31
|
+
avgRating: 5,
|
|
32
|
+
ratingCount: 1,
|
|
33
|
+
totalExecutions: 0,
|
|
34
|
+
stats: { completedJobs: 3, avgRating: 5, ratingCount: 1 },
|
|
35
|
+
pricePerRunUsd: "0.100000",
|
|
36
|
+
});
|
|
37
|
+
expect(output).toContain("★★★★★ 5.0 (1 review)");
|
|
38
|
+
expect(output).toContain("3 jobs");
|
|
39
|
+
expect(output).toContain("$0.10/req");
|
|
40
|
+
});
|
|
41
|
+
});
|
package/dist/core/formatters.js
CHANGED
|
@@ -42,13 +42,17 @@ export function agentLine(agent) {
|
|
|
42
42
|
const name = agent.name ?? "Unknown";
|
|
43
43
|
const slug = agent.slug ? ` (${agent.slug})` : "";
|
|
44
44
|
const rating = agent.avgRating ?? agent.stats?.avgRating ?? null;
|
|
45
|
+
const ratingCount = agent.ratingCount ?? agent.stats?.ratingCount ?? 0;
|
|
46
|
+
const ratingText = rating != null && ratingCount > 0
|
|
47
|
+
? `${stars(rating)} ${rating.toFixed(1)} (${compactNumber(ratingCount)} review${ratingCount === 1 ? "" : "s"})`
|
|
48
|
+
: stars(rating);
|
|
45
49
|
const jobs = agent.stats?.completedJobs ?? agent.totalExecutions ?? 0;
|
|
46
50
|
const price = formatPrice(agent.pricePerRunUsd);
|
|
47
51
|
const provider = agent.provider?.name ? ` by ${agent.provider.name}` : "";
|
|
48
52
|
const reliability = agent.successRate != null && Number(agent.successRate) < 1
|
|
49
53
|
? ` • ${(Number(agent.successRate) * 100).toFixed(0)}% reliable`
|
|
50
54
|
: "";
|
|
51
|
-
return `${name}${slug}${provider} ${
|
|
55
|
+
return `${name}${slug}${provider} ${ratingText} ${compactNumber(jobs)} jobs • ${price}${reliability}`;
|
|
52
56
|
}
|
|
53
57
|
export function formatLastActive(lastActiveAt) {
|
|
54
58
|
if (!lastActiveAt)
|
|
@@ -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
|
+
});
|
package/dist/tools/search.js
CHANGED
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { formatRunResult } from "../formatters.js";
|
|
2
|
+
import { agentLine, formatRunResult } from "../formatters.js";
|
|
3
3
|
|
|
4
4
|
describe("formatRunResult", () => {
|
|
5
5
|
it("does not show a paid line for credit-pack-backed runs with zero cost", () => {
|
|
@@ -27,3 +27,21 @@ describe("formatRunResult", () => {
|
|
|
27
27
|
expect(output).toContain("Job ID: job-123");
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
describe("agentLine", () => {
|
|
32
|
+
it("surfaces live rating count and completed job history from stats", () => {
|
|
33
|
+
const output = agentLine({
|
|
34
|
+
name: "People Enrichment",
|
|
35
|
+
slug: "people-enrichment",
|
|
36
|
+
avgRating: 5,
|
|
37
|
+
ratingCount: 1,
|
|
38
|
+
totalExecutions: 0,
|
|
39
|
+
stats: { completedJobs: 3, avgRating: 5, ratingCount: 1 },
|
|
40
|
+
pricePerRunUsd: "0.100000",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(output).toContain("★★★★★ 5.0 (1 review)");
|
|
44
|
+
expect(output).toContain("3 jobs");
|
|
45
|
+
expect(output).toContain("$0.10/req");
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/core/formatters.ts
CHANGED
|
@@ -52,7 +52,7 @@ interface AgentLike {
|
|
|
52
52
|
ratingCount?: number;
|
|
53
53
|
totalExecutions?: number;
|
|
54
54
|
pricePerRunUsd?: string;
|
|
55
|
-
stats?: { completedJobs?: number; avgRating?: number | null };
|
|
55
|
+
stats?: { completedJobs?: number; avgRating?: number | null; ratingCount?: number };
|
|
56
56
|
provider?: { name?: string; slug?: string } | null;
|
|
57
57
|
[key: string]: unknown;
|
|
58
58
|
}
|
|
@@ -61,13 +61,17 @@ export function agentLine(agent: AgentLike): string {
|
|
|
61
61
|
const name = agent.name ?? "Unknown";
|
|
62
62
|
const slug = agent.slug ? ` (${agent.slug})` : "";
|
|
63
63
|
const rating = agent.avgRating ?? agent.stats?.avgRating ?? null;
|
|
64
|
+
const ratingCount = agent.ratingCount ?? agent.stats?.ratingCount ?? 0;
|
|
65
|
+
const ratingText = rating != null && ratingCount > 0
|
|
66
|
+
? `${stars(rating)} ${rating.toFixed(1)} (${compactNumber(ratingCount)} review${ratingCount === 1 ? "" : "s"})`
|
|
67
|
+
: stars(rating);
|
|
64
68
|
const jobs = agent.stats?.completedJobs ?? agent.totalExecutions ?? 0;
|
|
65
69
|
const price = formatPrice(agent.pricePerRunUsd);
|
|
66
70
|
const provider = agent.provider?.name ? ` by ${agent.provider.name}` : "";
|
|
67
71
|
const reliability = agent.successRate != null && Number(agent.successRate) < 1
|
|
68
72
|
? ` • ${(Number(agent.successRate) * 100).toFixed(0)}% reliable`
|
|
69
73
|
: "";
|
|
70
|
-
return `${name}${slug}${provider} ${
|
|
74
|
+
return `${name}${slug}${provider} ${ratingText} ${compactNumber(jobs)} jobs • ${price}${reliability}`;
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
export function formatLastActive(lastActiveAt: string | null | undefined): string | null {
|
|
@@ -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
|
+
});
|
package/src/tools/search.ts
CHANGED
|
@@ -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
|
|