Skip to content

Commit a7b5a10

Browse files
iamzifeiclaude
andcommitted
feat: Phase 2 — eval.agenthunter.io website
Static-data MVP website that reads evaluation results from JSON files: Pages: - Homepage: hero + sortable ranking table (12 agents) - Agent Profile (/agent/[slug]): SVG radar chart, score bars, tool cards, task scores with LLM reasoning — 12 static pages generated - Methodology: transparent explanation of the 5-dimension scoring - Blog: links to Dev.to article Tech: Next.js 16.2.3 + Tailwind CSS 4 + dark theme Data: reads from results/summary.json and results/*/report.json at build time No database — static site generation for speed and simplicity 17 pages generated successfully. Ready for Vercel deployment. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2c98cce commit a7b5a10

22 files changed

Lines changed: 1255 additions & 6 deletions

bun.lock

Lines changed: 172 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/web/next-env.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/// <reference types="next" />
2+
/// <reference types="next/image-types/global" />
3+
import "./.next/types/routes.d.ts";
4+
5+
// NOTE: This file should not be edited
6+
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

packages/web/next.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { NextConfig } from "next";
2+
3+
const nextConfig: NextConfig = {};
4+
5+
export default nextConfig;

packages/web/package.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,22 @@
33
"version": "0.1.0",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
6+
"dev": "next dev --turbopack",
77
"build": "next build",
88
"start": "next start",
99
"lint": "biome check src/"
10+
},
11+
"dependencies": {
12+
"next": "^16.2.3",
13+
"react": "^19.1.0",
14+
"react-dom": "^19.1.0"
15+
},
16+
"devDependencies": {
17+
"@tailwindcss/postcss": "^4",
18+
"@types/node": "^25",
19+
"@types/react": "^19",
20+
"@types/react-dom": "^19",
21+
"tailwindcss": "^4",
22+
"typescript": "^6"
1023
}
1124
}

packages/web/postcss.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const config = {
2+
plugins: {
3+
"@tailwindcss/postcss": {},
4+
},
5+
};
6+
7+
export default config;
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { notFound } from "next/navigation";
2+
import Link from "next/link";
3+
import type { Metadata } from "next";
4+
import { getAgent, getAllSlugs } from "@/lib/data";
5+
import { ScoreBadge } from "@/components/score-badge";
6+
import { ScoreBar } from "@/components/score-bar";
7+
import { RadarChart } from "@/components/radar-chart";
8+
import { ToolCard } from "@/components/tool-card";
9+
10+
// Generate static pages for all evaluated agents
11+
export function generateStaticParams() {
12+
return getAllSlugs().map((slug) => ({ slug }));
13+
}
14+
15+
// Dynamic metadata per agent
16+
export async function generateMetadata({
17+
params,
18+
}: {
19+
params: Promise<{ slug: string }>;
20+
}): Promise<Metadata> {
21+
const { slug } = await params;
22+
const report = getAgent(slug);
23+
if (!report) return { title: "Agent Not Found" };
24+
25+
return {
26+
title: `${report.meta.agentName}${report.scores.overall}/100`,
27+
description: `Evaluation report for ${report.meta.agentName}: ${report.scores.overall}/100 overall score across ${report.tools.length} tools.`,
28+
};
29+
}
30+
31+
export default async function AgentPage({
32+
params,
33+
}: {
34+
params: Promise<{ slug: string }>;
35+
}) {
36+
const { slug } = await params;
37+
const report = getAgent(slug);
38+
if (!report) notFound();
39+
40+
const { meta, scores, tools, execution } = report;
41+
42+
return (
43+
<div>
44+
{/* Back link */}
45+
<Link
46+
href="/"
47+
className="text-sm text-[var(--color-text-dim)] hover:text-white mb-6 inline-block"
48+
>
49+
&larr; Back to rankings
50+
</Link>
51+
52+
{/* Header */}
53+
<div className="flex flex-col md:flex-row md:items-start gap-6 mb-8">
54+
<div className="flex-1">
55+
<h1 className="text-3xl font-bold mb-2">{meta.agentName}</h1>
56+
<div className="flex flex-wrap gap-3 text-sm text-[var(--color-text-dim)]">
57+
<span className="px-2 py-0.5 rounded bg-[var(--color-surface)] border border-[var(--color-border)]">
58+
{meta.protocol.toUpperCase()}
59+
</span>
60+
{meta.capabilities.map((cap) => (
61+
<span
62+
key={cap}
63+
className="px-2 py-0.5 rounded bg-[var(--color-surface)] border border-[var(--color-border)]"
64+
>
65+
{cap}
66+
</span>
67+
))}
68+
</div>
69+
<p className="mt-3 text-sm text-[var(--color-text-dim)] font-mono">
70+
{meta.endpoint}
71+
</p>
72+
<p className="mt-1 text-xs text-[var(--color-text-dim)]">
73+
Evaluated {new Date(meta.evaluatedAt).toLocaleDateString()} with
74+
agent-eval v{meta.evalFrameworkVersion}
75+
{meta.judgeModel && ` (judge: ${meta.judgeModel})`}
76+
</p>
77+
</div>
78+
<div className="text-center">
79+
<ScoreBadge score={scores.overall} size="lg" />
80+
<p className="text-xs text-[var(--color-text-dim)] mt-1">
81+
Overall Score
82+
</p>
83+
</div>
84+
</div>
85+
86+
{/* Scores section */}
87+
<div className="grid md:grid-cols-2 gap-8 mb-8">
88+
{/* Radar chart */}
89+
<div className="flex justify-center items-center p-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)]">
90+
<RadarChart
91+
scores={{
92+
capability: scores.capability,
93+
reliability: scores.reliability,
94+
efficiency: scores.efficiency,
95+
safety: scores.safety,
96+
dx: scores.developerExperience,
97+
}}
98+
/>
99+
</div>
100+
101+
{/* Score bars */}
102+
<div className="space-y-4 p-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)]">
103+
<ScoreBar
104+
label="Capability"
105+
score={scores.capability}
106+
weight="30%"
107+
/>
108+
<ScoreBar
109+
label="Reliability"
110+
score={scores.reliability}
111+
weight="25%"
112+
/>
113+
<ScoreBar
114+
label="Efficiency"
115+
score={scores.efficiency}
116+
weight="20%"
117+
/>
118+
<ScoreBar label="Safety" score={scores.safety} weight="15%" />
119+
<ScoreBar
120+
label="Dev Experience"
121+
score={scores.developerExperience}
122+
weight="10%"
123+
/>
124+
</div>
125+
</div>
126+
127+
{/* Execution stats */}
128+
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
129+
{[
130+
{ label: "Tools", value: tools.length },
131+
{ label: "Tasks", value: execution.totalTasks },
132+
{ label: "Success Rate", value: `${Math.round(execution.overallSuccessRate * 100)}%` },
133+
{ label: "Avg Latency", value: `${execution.latency.avgMs}ms` },
134+
].map((stat) => (
135+
<div
136+
key={stat.label}
137+
className="p-4 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] text-center"
138+
>
139+
<div className="text-2xl font-bold">{stat.value}</div>
140+
<div className="text-xs text-[var(--color-text-dim)]">
141+
{stat.label}
142+
</div>
143+
</div>
144+
))}
145+
</div>
146+
147+
{/* Tools */}
148+
<section className="mb-8">
149+
<h2 className="text-xl font-semibold mb-4">
150+
Tools ({tools.length})
151+
</h2>
152+
<div className="grid md:grid-cols-2 gap-3">
153+
{tools.map((tool) => (
154+
<ToolCard key={tool.name} tool={tool} />
155+
))}
156+
</div>
157+
</section>
158+
159+
{/* Task Scores */}
160+
{scores.taskScores.length > 0 && (
161+
<section>
162+
<h2 className="text-xl font-semibold mb-4">
163+
Task Scores ({scores.taskScores.length})
164+
</h2>
165+
<div className="rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] overflow-x-auto">
166+
<table className="w-full text-sm">
167+
<thead>
168+
<tr className="border-b border-[var(--color-border)] text-[var(--color-text-dim)]">
169+
<th className="py-3 px-4 text-left">Tool</th>
170+
<th className="py-3 px-4 text-left">Difficulty</th>
171+
<th className="py-3 px-4 text-left">Capability</th>
172+
<th className="py-3 px-4 text-left">Safety</th>
173+
<th className="py-3 px-4 text-left hidden md:table-cell">
174+
Reasoning
175+
</th>
176+
</tr>
177+
</thead>
178+
<tbody>
179+
{scores.taskScores.map((ts, i) => (
180+
<tr
181+
key={i}
182+
className="border-b border-[var(--color-border)]/50"
183+
>
184+
<td className="py-2 px-4 font-mono text-xs">
185+
{ts.toolName}
186+
</td>
187+
<td className="py-2 px-4">
188+
<span
189+
className={`text-xs px-1.5 py-0.5 rounded ${
190+
ts.difficulty === "adversarial"
191+
? "bg-red-500/20 text-red-400"
192+
: ts.difficulty === "advanced"
193+
? "bg-amber-500/20 text-amber-400"
194+
: "bg-[var(--color-border)] text-[var(--color-text-dim)]"
195+
}`}
196+
>
197+
{ts.difficulty}
198+
</span>
199+
</td>
200+
<td className="py-2 px-4">
201+
{(ts.capabilityScore * 100).toFixed(0)}%
202+
</td>
203+
<td className="py-2 px-4">
204+
{(ts.safetyScore * 100).toFixed(0)}%
205+
</td>
206+
<td className="py-2 px-4 text-xs text-[var(--color-text-dim)] max-w-xs truncate hidden md:table-cell">
207+
{ts.reasoning}
208+
</td>
209+
</tr>
210+
))}
211+
</tbody>
212+
</table>
213+
</div>
214+
</section>
215+
)}
216+
</div>
217+
);
218+
}

packages/web/src/app/blog/page.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Metadata } from "next";
2+
3+
export const metadata: Metadata = {
4+
title: "Blog",
5+
};
6+
7+
export default function BlogPage() {
8+
return (
9+
<div className="max-w-2xl mx-auto">
10+
<h1 className="text-3xl font-bold mb-8">Blog</h1>
11+
12+
<a
13+
href="https://dev.to/jamesai/i-benchmarked-12-mcp-servers-heres-what-i-found-1124"
14+
target="_blank"
15+
rel="noopener noreferrer"
16+
className="block p-6 rounded-lg border border-[var(--color-border)] bg-[var(--color-surface)] hover:border-[var(--color-accent)]/50 transition group"
17+
>
18+
<p className="text-xs text-[var(--color-text-dim)] mb-2">
19+
April 14, 2026
20+
</p>
21+
<h2 className="text-xl font-semibold group-hover:text-[var(--color-accent)] transition">
22+
I Benchmarked 12 MCP Servers — Here&apos;s What I Found
23+
</h2>
24+
<p className="mt-2 text-sm text-[var(--color-text-dim)]">
25+
I built an open-source eval framework and scored 12 popular MCP
26+
servers across capability, reliability, efficiency, and safety. The
27+
results were surprising.
28+
</p>
29+
<span className="mt-3 inline-block text-sm text-[var(--color-accent)]">
30+
Read on Dev.to &rarr;
31+
</span>
32+
</a>
33+
</div>
34+
);
35+
}

packages/web/src/app/globals.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@import "tailwindcss";
2+
3+
:root {
4+
--color-bg: #0a1628;
5+
--color-surface: #111d33;
6+
--color-border: #1e3050;
7+
--color-accent: #00d4ff;
8+
--color-accent-dim: #0891b2;
9+
--color-text: #e2e8f0;
10+
--color-text-dim: #94a3b8;
11+
}
12+
13+
body {
14+
background-color: var(--color-bg);
15+
color: var(--color-text);
16+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
17+
}

packages/web/src/app/layout.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import type { Metadata } from "next";
2+
import { Nav } from "@/components/nav";
3+
import { Footer } from "@/components/footer";
4+
import "./globals.css";
5+
6+
export const metadata: Metadata = {
7+
title: {
8+
default: "AgentHunter Eval — MCP Server Benchmarks",
9+
template: "%s | AgentHunter Eval",
10+
},
11+
description:
12+
"Independent benchmarks for MCP servers. Scored across capability, reliability, efficiency, safety, and developer experience.",
13+
openGraph: {
14+
title: "AgentHunter Eval — MCP Server Benchmarks",
15+
description:
16+
"Independent benchmarks for MCP servers. Scored across capability, reliability, efficiency, safety, and developer experience.",
17+
type: "website",
18+
},
19+
};
20+
21+
export default function RootLayout({
22+
children,
23+
}: {
24+
children: React.ReactNode;
25+
}) {
26+
return (
27+
<html lang="en" className="dark">
28+
<body className="min-h-screen flex flex-col">
29+
<Nav />
30+
<main className="flex-1 mx-auto w-full max-w-6xl px-6 py-8">
31+
{children}
32+
</main>
33+
<Footer />
34+
</body>
35+
</html>
36+
);
37+
}

0 commit comments

Comments
 (0)