From b7cf9729e96bdbf5524b44bd50c033fa757562e7 Mon Sep 17 00:00:00 2001 From: Joan Leon Date: Mon, 19 Oct 2026 08:00:46 +0200 Subject: [PATCH 1/2] test(eval): grade the MCP evals from the transcript The grader of the MCP evals reads the report and the tool calls of a run: whether the report names the cause the fixture has, and how the agent measured, as published snippets run through the webperf MCP server, published scripts given to evaluate_script of Chrome DevTools MCP (as code or by path), code the agent wrote itself, or other tools such as a performance trace. The summary takes the medians of tokens, turns and time per case and setup. Its tests run in test:prompt, with those of the Copy prompt evals. --- package.json | 2 +- scripts/eval-mcp/grade.mjs | 65 +++++++++++++++++++++++++++++++ scripts/eval-mcp/grade.test.mjs | 65 +++++++++++++++++++++++++++++++ scripts/eval-mcp/summary.mjs | 48 +++++++++++++++++++++++ scripts/eval-mcp/summary.test.mjs | 39 +++++++++++++++++++ 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 scripts/eval-mcp/grade.mjs create mode 100644 scripts/eval-mcp/grade.test.mjs create mode 100644 scripts/eval-mcp/summary.mjs create mode 100644 scripts/eval-mcp/summary.test.mjs diff --git a/package.json b/package.json index 77729b8e..a6efd954 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "check:emoji": "node scripts/check-emoji.js", "test:icons": "node --test lib/rehype-icons.test.js lib/strip-emoji.test.js scripts/check-emoji.test.js", "test:support": "node --test lib/browser-support.test.js lib/remark-browser-support.test.js", - "test:prompt": "node --test lib/snippet-prompt.test.mjs lib/remark-snippet-prompt.test.mjs scripts/eval-prompts/grade.test.mjs scripts/eval-prompts/summary.test.mjs", + "test:prompt": "node --test lib/snippet-prompt.test.mjs lib/remark-snippet-prompt.test.mjs scripts/eval-prompts/grade.test.mjs scripts/eval-prompts/summary.test.mjs scripts/eval-mcp/grade.test.mjs scripts/eval-mcp/summary.test.mjs", "eval:prompts": "node scripts/eval-prompts/run.mjs", "extension:build": "node scripts/build-extension.mjs", "extension:zip": "node scripts/package-extension.mjs", diff --git a/scripts/eval-mcp/grade.mjs b/scripts/eval-mcp/grade.mjs new file mode 100644 index 00000000..e7d811f0 --- /dev/null +++ b/scripts/eval-mcp/grade.mjs @@ -0,0 +1,65 @@ +// Grades a run of the MCP evals from its transcript (parseTranscript of eval-prompts): whether the +// report gets the diagnosis right, and how the agent measured. A measurement is a published snippet run +// through the webperf MCP server, a published script given to evaluate_script of Chrome DevTools MCP +// (as code or by path), code the agent wrote itself, or another tool such as a performance trace. + +export { parseTranscript } from '../eval-prompts/grade.mjs' + +const WEBPERF = 'mcp__webperf__' +const DEVTOOLS = 'mcp__chrome-devtools__' +// The tools of the webperf server that run snippets, and the argument that names what they run +const WEBPERF_RUNS = { run_snippet: 'snippet', run_workflow: 'workflow', open_page: 'track' } +// Tools that only move around the page, which do not measure anything by themselves +const NAVIGATION = new Set(['navigate_page', 'new_page', 'list_pages', 'select_page', 'close_page', 'take_snapshot', 'take_screenshot', 'wait_for', 'resize_page']) + +const nameOf = (catalog, name) => catalog.find((path) => path.split('/')[1] === name)?.split('/')[1] + +// The published snippet an evaluate_script call runs, from the script:"Name" of its result object or +// the scripts/.js path of a skill script +function publishedScript(input, catalog) { + if (typeof input.function === 'string') { + const name = input.function.match(/\bscript:\s*["']([\w-]+)["']/)?.[1] + const found = name && nameOf(catalog, name) + return found ? { snippet: found, sent: 'code', bytes: input.function.length } : null + } + if (typeof input.sourcePath === 'string') { + const found = nameOf(catalog, input.sourcePath.match(/\/scripts\/([\w-]+)\.js$/)?.[1]) + return found ? { snippet: found, sent: 'path', bytes: 0 } : null + } + return null +} + +export function snippetRuns(transcript, catalog) { + const runs = [] + for (const { name, input } of transcript.calls) { + if (name.startsWith(WEBPERF)) { + const tool = name.slice(WEBPERF.length) + const arg = WEBPERF_RUNS[tool] + if (!arg) continue + for (const snippet of [input[arg]].flat()) runs.push({ via: tool, snippet }) + } else if (name === `${DEVTOOLS}evaluate_script`) { + const published = publishedScript(input, catalog) + if (published) runs.push({ via: 'evaluate_script', ...published }) + } + } + return runs +} + +export function gradeRun(evalCase, transcript, catalog) { + const checks = evalCase.expect.diagnosis.map(({ id, pattern }) => ({ id, pass: pattern.test(transcript.report) })) + + const published = snippetRuns(transcript, catalog).length + const evaluations = transcript.calls.filter((c) => c.name === `${DEVTOOLS}evaluate_script`).length + const devtoolsPublished = snippetRuns(transcript, catalog).filter((r) => r.via === 'evaluate_script').length + const ownCode = evaluations - devtoolsPublished + const otherTools = transcript.calls + .filter((c) => c.name.startsWith(DEVTOOLS) && c.name !== `${DEVTOOLS}evaluate_script`) + .map((c) => c.name.slice(DEVTOOLS.length)) + const measuring = otherTools.filter((tool) => !NAVIGATION.has(tool)) + checks.push({ + id: 'measured', + pass: published + ownCode + measuring.length > 0, + detail: { published, ownCode, otherTools: [...new Set(otherTools)] }, + }) + return checks +} diff --git a/scripts/eval-mcp/grade.test.mjs b/scripts/eval-mcp/grade.test.mjs new file mode 100644 index 00000000..5345475b --- /dev/null +++ b/scripts/eval-mcp/grade.test.mjs @@ -0,0 +1,65 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { snippetRuns, gradeRun } from './grade.mjs' + +const CATALOG = ['CoreWebVitals/LCP', 'CoreWebVitals/LCP-Subparts', 'CoreWebVitals/INP', 'Interaction/Interactions'] + +const transcript = (calls, report = '') => ({ calls, report, tokens: { input: 1000, output: 100 }, durationMs: 1000, turns: 3 }) +const call = (name, input = {}, output = '') => ({ name, input, output }) +const failedDiagnosis = (checks) => checks.filter((c) => c.id !== 'measured' && !c.pass).map((c) => c.id).sort() + +const LCP_CASE = { + id: 'lcp-slow-image', + expect: { diagnosis: [{ id: 'lcp-element', pattern: /hero\.png| { + const runs = snippetRuns(transcript([call('mcp__webperf__run_snippet', { snippet: 'LCP-Subparts' }), call('mcp__webperf__run_workflow', { workflow: 'core-web-vitals' }), call('mcp__webperf__list_snippets')]), CATALOG) + assert.deepEqual(runs, [ + { via: 'run_snippet', snippet: 'LCP-Subparts' }, + { via: 'run_workflow', snippet: 'core-web-vitals' }, + ]) +}) + +test('snippetRuns recognizes a published script sent as code to evaluate_script', () => { + const code = '// snippets/CoreWebVitals/LCP-Subparts.js | sha256:abc\n(async()=>{return{script:"LCP-Subparts",status:"ok"}})()' + const runs = snippetRuns(transcript([call('mcp__chrome-devtools__evaluate_script', { function: code })]), CATALOG) + assert.deepEqual(runs, [{ via: 'evaluate_script', snippet: 'LCP-Subparts', sent: 'code', bytes: code.length }]) +}) + +test('snippetRuns recognizes a published script passed by path to evaluate_script', () => { + const runs = snippetRuns(transcript([call('mcp__chrome-devtools__evaluate_script', { sourcePath: '/x/skills/webperf-core-web-vitals/scripts/LCP.js' })]), CATALOG) + assert.deepEqual(runs, [{ via: 'evaluate_script', snippet: 'LCP', sent: 'path', bytes: 0 }]) +}) + +test('snippetRuns leaves out code the agent wrote itself', () => { + const runs = snippetRuns(transcript([call('mcp__chrome-devtools__evaluate_script', { function: '() => performance.getEntriesByType("navigation")' })]), CATALOG) + assert.deepEqual(runs, []) +}) + +test('gradeRun passes the diagnosis when the report names the element and the slow phase', () => { + const checks = gradeRun(LCP_CASE, transcript([], 'The LCP element is the hero.png. Most of the time is resource load time (2.1 s).'), CATALOG) + assert.deepEqual(failedDiagnosis(checks), []) +}) + +test('gradeRun fails each part of the diagnosis the report misses', () => { + const checks = gradeRun(LCP_CASE, transcript([], 'The page has a slow server response.'), CATALOG) + assert.deepEqual(failedDiagnosis(checks), ['lcp-element', 'slow-phase']) +}) + +test('gradeRun reports how the agent measured: published snippets, its own code and the other tools', () => { + const t = transcript([ + call('mcp__chrome-devtools__navigate_page', { url: 'http://x' }), + call('mcp__chrome-devtools__evaluate_script', { function: '() => 1' }), + call('mcp__chrome-devtools__evaluate_script', { sourcePath: '/s/scripts/LCP.js' }), + call('mcp__chrome-devtools__performance_start_trace', {}), + ]) + const { measured } = Object.fromEntries(gradeRun(LCP_CASE, t, CATALOG).map((c) => [c.id, c])) + assert.deepEqual(measured.detail, { published: 1, ownCode: 1, otherTools: ['navigate_page', 'performance_start_trace'] }) + assert.equal(measured.pass, true) +}) + +test('gradeRun fails the measured check when the agent measured nothing', () => { + const { measured } = Object.fromEntries(gradeRun(LCP_CASE, transcript([], 'LCP is probably the image.'), CATALOG).map((c) => [c.id, c])) + assert.equal(measured.pass, false) +}) diff --git a/scripts/eval-mcp/summary.mjs b/scripts/eval-mcp/summary.mjs new file mode 100644 index 00000000..430cb6bb --- /dev/null +++ b/scripts/eval-mcp/summary.mjs @@ -0,0 +1,48 @@ +// The summary of a run of the MCP evals: per case and setup, how many runs got the diagnosis right, the +// medians of tokens, turns and time, and how the agent measured. + +const median = (values) => { + const sorted = [...values].sort((a, b) => a - b) + const mid = Math.floor(sorted.length / 2) + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2 +} + +export function summarize(runs) { + const groups = new Map() + for (const run of runs) { + const key = `${run.case}\u0000${run.arm}` + if (!groups.has(key)) groups.set(key, []) + groups.get(key).push(run) + } + return [...groups.values()].map((group) => { + const measured = (run) => run.checks.find((c) => c.id === 'measured') + return { + case: group[0].case, + arm: group[0].arm, + runs: group.length, + diagnosis: group.filter((run) => run.checks.filter((c) => c.id !== 'measured').every((c) => c.pass)).length, + medianInput: median(group.map((run) => run.tokens.input)), + medianOutput: median(group.map((run) => run.tokens.output)), + medianTurns: median(group.map((run) => run.turns)), + medianSeconds: Math.round(median(group.map((run) => run.durationMs)) / 1000), + published: group.filter((run) => run.snippetRuns.length > 0).length, + ownCode: group.reduce((sum, run) => sum + (measured(run)?.detail.ownCode ?? 0), 0), + sentCodeBytes: group.reduce((sum, run) => sum + run.snippetRuns.reduce((s, r) => s + (r.bytes ?? 0), 0), 0), + } + }) +} + +const k = (n) => `${(n / 1000).toFixed(1)}k` + +export function markdownSummary(rows, { model, repeat, devtools }) { + const lines = [ + `Model: ${model}, ${repeat} runs per case and setup, ${devtools}.`, + '', + '| Case | Setup | Diagnosis | Input tokens | Output tokens | Turns | Time | Runs with published snippets | Own code calls | Script bytes sent |', + '| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |', + ...rows.map((r) => `| ${r.case} | ${r.arm} | ${r.diagnosis}/${r.runs} | ${k(r.medianInput)} | ${k(r.medianOutput)} | ${r.medianTurns} | ${r.medianSeconds} s | ${r.published}/${r.runs} | ${r.ownCode} | ${r.sentCodeBytes} |`), + '', + 'Tokens, turns and time are medians. Input tokens include the cache reads and writes.', + ] + return `${lines.join('\n')}\n` +} diff --git a/scripts/eval-mcp/summary.test.mjs b/scripts/eval-mcp/summary.test.mjs new file mode 100644 index 00000000..12e983b0 --- /dev/null +++ b/scripts/eval-mcp/summary.test.mjs @@ -0,0 +1,39 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { summarize, markdownSummary } from './summary.mjs' + +const runOf = (arm, n, { pass = true, input = 1000, output = 100, turns = 5, durationMs = 10000, snippetRuns = [], ownCode = 0 } = {}) => ({ + arm, case: 'lcp', run: n, + checks: [{ id: 'lcp-element', pass }, { id: 'slow-phase', pass: true }, { id: 'measured', pass: true, detail: { published: snippetRuns.length, ownCode, otherTools: [] } }], + tokens: { input, output }, turns, durationMs, snippetRuns, +}) + +test('summarize groups by case and arm, counts correct diagnoses and takes the medians', () => { + const [row] = summarize([ + runOf('webperf', 1, { input: 1000 }), + runOf('webperf', 2, { input: 3000, pass: false }), + runOf('webperf', 3, { input: 2000 }), + ]) + assert.deepEqual(row, { + case: 'lcp', arm: 'webperf', runs: 3, diagnosis: 2, + medianInput: 2000, medianOutput: 100, medianTurns: 5, medianSeconds: 10, + published: 0, ownCode: 0, sentCodeBytes: 0, + }) +}) + +test('summarize counts the runs with published snippets, the own code and the script bytes sent', () => { + const [row] = summarize([ + runOf('devtools-skill', 1, { snippetRuns: [{ via: 'evaluate_script', sent: 'code', bytes: 9000 }], ownCode: 1 }), + runOf('devtools-skill', 2, { snippetRuns: [{ via: 'evaluate_script', sent: 'path', bytes: 0 }] }), + ]) + assert.equal(row.published, 2) + assert.equal(row.ownCode, 1) + assert.equal(row.sentCodeBytes, 9000) +}) + +test('markdownSummary writes one row per case and arm', () => { + const md = markdownSummary(summarize([runOf('webperf', 1), runOf('devtools', 1)]), { model: 'sonnet', repeat: 1, devtools: 'chrome-devtools-mcp@1.11.0' }) + assert.match(md, /\| lcp \| webperf \| 1\/1 \|/) + assert.match(md, /\| lcp \| devtools \| 1\/1 \|/) + assert.match(md, /chrome-devtools-mcp@1\.11\.0/) +}) From 9b3af951b81b29caa58fc160d36bc40015b5af80 Mon Sep 17 00:00:00 2001 From: Joan Leon Date: Mon, 19 Oct 2026 08:00:48 +0200 Subject: [PATCH 2/2] feat(eval): compare the MCP server with Chrome DevTools MCP on pages with a known answer npm run eval:mcp asks the same question about a local page to an agent with the webperf MCP server, with Chrome DevTools MCP 1.11.0 and the webperf skills (loaded as a temporary plugin), and with Chrome DevTools MCP alone, each isolated from the settings, skills and MCP servers of the machine. The prompts name no tool or snippet. One page loads its LCP image in 2 s, a noise image so Chrome accepts it as a candidate; on the other, the "Add to cart" button blocks the main thread for 250 ms. It runs by hand, not in CI: a full run takes about 10 minutes and a million input tokens. --- CHANGELOG.md | 3 + package.json | 1 + scripts/eval-mcp/arms.mjs | 33 ++++ scripts/eval-mcp/cases.mjs | 34 ++++ scripts/eval-mcp/fixtures/inp-slow-click.html | 30 ++++ scripts/eval-mcp/fixtures/lcp-slow-image.html | 17 ++ scripts/eval-mcp/fixtures/product.png | Bin 0 -> 48178 bytes scripts/eval-mcp/run.mjs | 158 ++++++++++++++++++ 8 files changed, 276 insertions(+) create mode 100644 scripts/eval-mcp/arms.mjs create mode 100644 scripts/eval-mcp/cases.mjs create mode 100644 scripts/eval-mcp/fixtures/inp-slow-click.html create mode 100644 scripts/eval-mcp/fixtures/lcp-slow-image.html create mode 100644 scripts/eval-mcp/fixtures/product.png create mode 100644 scripts/eval-mcp/run.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 927e6008..748ae4a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Added +- `npm run eval:mcp`: the same question about a local page with a known answer, asked to an agent with the webperf MCP server, with Chrome DevTools MCP and the webperf skills, and with Chrome DevTools MCP alone, graded on the diagnosis and measured in tokens, turns and time. It runs by hand, not in CI. + ### Changed - CI skips the build and the end-to-end tests on its own when a pull request only changes documentation, instead of waiting for the `skip-e2e` label. Opening a pull request with the label fired two runs and cancelled one, which showed as a failed check. - Examples, CLI commands, the `OWN_DOMAINS` hint of the snippets and the test data use the domains reserved for documentation (`example.com`, `example.net`, `example.org`) instead of real sites. `scripts/example-domains.test.js`, in `npm run test:site`, fails when a real site comes back as an example. diff --git a/package.json b/package.json index a6efd954..df224ceb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test:support": "node --test lib/browser-support.test.js lib/remark-browser-support.test.js", "test:prompt": "node --test lib/snippet-prompt.test.mjs lib/remark-snippet-prompt.test.mjs scripts/eval-prompts/grade.test.mjs scripts/eval-prompts/summary.test.mjs scripts/eval-mcp/grade.test.mjs scripts/eval-mcp/summary.test.mjs", "eval:prompts": "node scripts/eval-prompts/run.mjs", + "eval:mcp": "node scripts/eval-mcp/run.mjs", "extension:build": "node scripts/build-extension.mjs", "extension:zip": "node scripts/package-extension.mjs", "test:extension": "node --test extension/lib/*.test.js lib/snippet-expression.test.mjs", diff --git a/scripts/eval-mcp/arms.mjs b/scripts/eval-mcp/arms.mjs new file mode 100644 index 00000000..3af8b2ff --- /dev/null +++ b/scripts/eval-mcp/arms.mjs @@ -0,0 +1,33 @@ +// The setups the MCP evals compare. Each one is an agent with the same model and prompt and no settings, +// skills or MCP servers of the machine, plus: +// webperf the MCP server of this repository (node cli/src/bin.js mcp) +// devtools-skill Chrome DevTools MCP and the webperf skills, the path the README describes +// devtools Chrome DevTools MCP alone, where the agent writes its own measurement code + +export const DEVTOOLS_MCP = 'chrome-devtools-mcp@1.11.0' + +export function arms({ root, pluginDir }) { + const devtools = { 'chrome-devtools': { command: 'npx', args: ['-y', DEVTOOLS_MCP, '--headless', '--isolated'] } } + return [ + { + id: 'webperf', + mcpServers: { webperf: { command: 'node', args: [`${root}/cli/src/bin.js`, 'mcp'] } }, + tools: '', + allowedTools: ['mcp__webperf'], + }, + { + id: 'devtools-skill', + mcpServers: devtools, + // Read for the scripts of the skills, Skill to load them + tools: 'Read,Skill', + allowedTools: ['mcp__chrome-devtools', 'Read', 'Skill'], + pluginDir, + }, + { + id: 'devtools', + mcpServers: devtools, + tools: '', + allowedTools: ['mcp__chrome-devtools'], + }, + ] +} diff --git a/scripts/eval-mcp/cases.mjs b/scripts/eval-mcp/cases.mjs new file mode 100644 index 00000000..eb183d8d --- /dev/null +++ b/scripts/eval-mcp/cases.mjs @@ -0,0 +1,34 @@ +// The tasks of the MCP evals. The prompts read like a person asking about a page: they name no tool and +// no snippet, so the evals measure what the agent does with each setup, not whether it follows a recipe. +// Each fixture has a known answer that the report has to give. + +export const CASES = [ + { + id: 'lcp-slow-image', + fixture: 'lcp-slow-image.html', + prompt: (url) => + `The Largest Contentful Paint of this page is slow: ${url}\n\nFind out why. Tell me which element is the LCP, which part of its loading takes the most time, and what to fix first.`, + expect: { + diagnosis: [ + // The product photo is the LCP element + { id: 'lcp-element', pattern: /product\.png|product photo| + `Users say this page feels slow when they click on it: ${url}\n\nFind which interaction is slow, what makes it slow, and what to fix.`, + expect: { + diagnosis: [ + // The "Add to cart" button, not the wishlist + { id: 'slow-target', pattern: /#add\b|add to cart/i }, + // Its click handler blocks the main thread: processing time + { id: 'slow-phase', pattern: /processing|event handler|click handler|\bhandler\b|long task|main thread|blocking/i }, + ], + }, + }, +] diff --git a/scripts/eval-mcp/fixtures/inp-slow-click.html b/scripts/eval-mcp/fixtures/inp-slow-click.html new file mode 100644 index 00000000..c34e83ee --- /dev/null +++ b/scripts/eval-mcp/fixtures/inp-slow-click.html @@ -0,0 +1,30 @@ + + + + + + Product page + + + +
+

Trail running shoes

+ + +

+
+ + + diff --git a/scripts/eval-mcp/fixtures/lcp-slow-image.html b/scripts/eval-mcp/fixtures/lcp-slow-image.html new file mode 100644 index 00000000..9b33cb15 --- /dev/null +++ b/scripts/eval-mcp/fixtures/lcp-slow-image.html @@ -0,0 +1,17 @@ + + + + + + Product page + + + +
+

Trail running shoes

+ + Product photo +

Light, breathable and made for long distances.

+
+ + diff --git a/scripts/eval-mcp/fixtures/product.png b/scripts/eval-mcp/fixtures/product.png new file mode 100644 index 0000000000000000000000000000000000000000..0ff2d6e70192de8aa1c7aa898c29b9d47799c419 GIT binary patch literal 48178 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

7{|34pzZrOX#D~UFCp^r0rGd@)0oB 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

DW*myls?y?YnW>vKSC|8@uVUqxk+NwrO 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 d.isDirectory() && !d.name.startsWith('_')) + .flatMap((d) => readdirSync(join(ROOT, 'snippets', d.name)).filter((f) => f.endsWith('.js')).map((f) => `${d.name}/${f.slice(0, -3)}`)) + +const { values: opts } = parseArgs({ + options: { + arm: { type: 'string', multiple: true }, + case: { type: 'string', multiple: true }, + repeat: { type: 'string', default: '3' }, + model: { type: 'string', default: 'sonnet' }, + concurrency: { type: 'string', default: '2' }, + 'max-cost': { type: 'string', default: '2' }, + }, +}) + +function run(command, args, { cwd, input } = {}) { + return new Promise((done, fail) => { + const child = spawn(command, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] }) + let stdout = '' + child.stdout.on('data', (d) => (stdout += d)) + child.stderr.on('data', () => {}) + child.on('error', fail) + child.on('close', (code) => done({ code, stdout })) + child.stdin.end(input ?? '') + }) +} + +// Serves the fixtures; `/slow//` serves a file after a delay +function startServer() { + const server = createServer((req, res) => { + const path = decodeURIComponent(new URL(req.url, 'http://x').pathname).replace(/^\/+/, '') + const [, delay = 0, name = path] = path.match(/^slow\/(\d+)\/(.+)$/) ?? [] + const file = FIXTURE_DIRS.map((dir) => join(dir, name)).find((f) => !name.includes('..') && existsSync(f)) + if (!file || !name) return res.writeHead(404).end() + setTimeout(() => res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' }).end(readFileSync(file)), Number(delay)) + }) + return new Promise((done) => server.listen(0, '127.0.0.1', () => done(server))) +} + +// The generated skills, as a plugin that loads only for the devtools-skill setup +function buildPlugin(work) { + const dir = join(work, 'webperf-plugin') + mkdirSync(join(dir, '.claude-plugin'), { recursive: true }) + writeFileSync(join(dir, '.claude-plugin/plugin.json'), JSON.stringify({ name: 'webperf-skills', version: '0.0.0', description: 'WebPerf Snippets skills for the MCP evals' })) + cpSync(join(ROOT, '.claude/skills'), join(dir, 'skills'), { recursive: true }) + return dir +} + +// No settings, skills or MCP servers of the machine +const ISOLATED = ['--setting-sources', '', '--strict-mcp-config', '--no-session-persistence'] + +async function runAgent(arm, prompt, workdir) { + const config = join(workdir, '.mcp-eval.json') + writeFileSync(config, JSON.stringify({ mcpServers: arm.mcpServers })) + const args = [ + '-p', '--output-format', 'stream-json', '--verbose', + '--model', opts.model, + '--tools', arm.tools, '--allowedTools', arm.allowedTools.join(','), + '--mcp-config', config, + ...(arm.pluginDir ? ['--plugin-dir', arm.pluginDir] : []), + ...ISOLATED, + '--max-budget-usd', opts['max-cost'], + ] + const { stdout } = await run('claude', args, { cwd: workdir, input: prompt }) + return stdout +} + +async function pool(tasks, size) { + const results = [] + let next = 0 + const worker = async () => { + while (next < tasks.length) { + const i = next++ + results[i] = await tasks[i]() + } + } + await Promise.all(Array.from({ length: size }, worker)) + return results +} + +const thousands = (n) => `${(n / 1000).toFixed(1)}k` + +async function main() { + const work = mkdtempSync(join(tmpdir(), 'eval-mcp-')) + const arms = allArms({ root: ROOT, pluginDir: buildPlugin(work) }).filter((a) => !opts.arm || opts.arm.includes(a.id)) + const cases = CASES.filter((c) => !opts.case || opts.case.includes(c.id)) + if (!arms.length || !cases.length) throw new Error('No arm or case matches the filters') + const repeat = Number(opts.repeat) + const out = join(ROOT, 'workspace/evals-mcp', new Date().toISOString().replace(/[:.]/g, '-')) + mkdirSync(out, { recursive: true }) + console.log(`Arms: ${arms.map((a) => a.id).join(', ')}\nModel: ${opts.model}, ${repeat} runs each, ${DEVTOOLS_MCP}\nOutput: ${out}\n`) + + const server = await startServer() + const base = `http://127.0.0.1:${server.address().port}` + try { + const tasks = [] + for (const evalCase of cases) { + const prompt = evalCase.prompt(`${base}/${evalCase.fixture}`) + writeFileSync(join(out, `${evalCase.id}.prompt.md`), prompt) + for (const arm of arms) { + for (let i = 1; i <= repeat; i++) { + tasks.push(async () => { + const workdir = mkdtempSync(join(work, `${arm.id}-${evalCase.id}-${i}-`)) + const raw = await runAgent(arm, prompt, workdir) + writeFileSync(join(out, `${arm.id}.${evalCase.id}.${i}.jsonl`), raw) + const transcript = parseTranscript(raw) + const checks = gradeRun(evalCase, transcript, CATALOG) + const runs = snippetRuns(transcript, CATALOG) + const failed = checks.filter((c) => !c.pass).map((c) => c.id) + const { input, output } = transcript.tokens + console.log(`${arm.id} ${evalCase.id} #${i}: ${failed.length ? `FAIL ${failed.join(', ')}` : 'pass'} (${thousands(input)} in, ${thousands(output)} out, ${transcript.turns} turns, ${Math.round(transcript.durationMs / 1000)} s)`) + return { + arm: arm.id, case: evalCase.id, run: i, checks, tokens: transcript.tokens, durationMs: transcript.durationMs, turns: transcript.turns, + tools: transcript.calls.map((c) => c.name), snippetRuns: runs, report: transcript.report, + } + }) + } + } + } + const results = await pool(tasks, Number(opts.concurrency)) + writeFileSync(join(out, 'summary.json'), JSON.stringify({ model: opts.model, repeat, devtools: DEVTOOLS_MCP, runs: results }, null, 2)) + const markdown = markdownSummary(summarize(results), { model: opts.model, repeat, devtools: DEVTOOLS_MCP }) + writeFileSync(join(out, 'summary.md'), markdown) + console.log(`\n${markdown}`) + } finally { + server.close() + rmSync(work, { recursive: true, force: true }) + } +} + +main().catch((error) => { + console.error(error.message) + process.exit(1) +})