Skip to content

Commit 6a1792d

Browse files
committed
[mcp] Add proper web-vitals metric collection
1 parent 5cc1639 commit 6a1792d

File tree

3 files changed

+202
-166
lines changed

3 files changed

+202
-166
lines changed

compiler/packages/react-mcp-server/src/index.ts

Lines changed: 97 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
9-
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
10-
import {z} from 'zod';
11-
import {compile, type PrintedCompilerPipelineValue} from './compiler';
8+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10+
import { z } from 'zod';
11+
import { compile, type PrintedCompilerPipelineValue } from './compiler';
1212
import {
1313
CompilerPipelineValue,
1414
printReactiveFunctionWithOutlined,
@@ -17,10 +17,14 @@ import {
1717
SourceLocation,
1818
} from 'babel-plugin-react-compiler/src';
1919
import * as cheerio from 'cheerio';
20-
import {queryAlgolia} from './utils/algolia';
20+
import { queryAlgolia } from './utils/algolia';
2121
import assertExhaustive from './utils/assertExhaustive';
22-
import {convert} from 'html-to-text';
23-
import {measurePerformance} from './tools/runtimePerf';
22+
import { convert } from 'html-to-text';
23+
import { measurePerformance } from './tools/runtimePerf';
24+
25+
function calculateMean(values: number[]): string {
26+
return values.length > 0 ? (values.reduce((acc, curr) => acc + curr, 0) / values.length) + 'ms' : 'could not collect';
27+
}
2428

2529
const server = new McpServer({
2630
name: 'React',
@@ -33,12 +37,12 @@ server.tool(
3337
{
3438
query: z.string(),
3539
},
36-
async ({query}) => {
40+
async ({ query }) => {
3741
try {
3842
const pages = await queryAlgolia(query);
3943
if (pages.length === 0) {
4044
return {
41-
content: [{type: 'text' as const, text: `No results`}],
45+
content: [{ type: 'text' as const, text: `No results` }],
4246
};
4347
}
4448
const content = pages.map(html => {
@@ -64,7 +68,7 @@ server.tool(
6468
} catch (err) {
6569
return {
6670
isError: true,
67-
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
71+
content: [{ type: 'text' as const, text: `Error: ${err.stack}` }],
6872
};
6973
}
7074
},
@@ -77,7 +81,7 @@ server.tool(
7781
text: z.string(),
7882
passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(),
7983
},
80-
async ({text, passName}) => {
84+
async ({ text, passName }) => {
8185
const pipelinePasses = new Map<
8286
string,
8387
Array<PrintedCompilerPipelineValue>
@@ -129,7 +133,7 @@ server.tool(
129133
}
130134
}
131135
};
132-
const errors: Array<{message: string; loc: SourceLocation | null}> = [];
136+
const errors: Array<{ message: string; loc: SourceLocation | null }> = [];
133137
const compilerOptions: Partial<PluginOptions> = {
134138
panicThreshold: 'none',
135139
logger: {
@@ -158,10 +162,10 @@ server.tool(
158162
if (result.code == null) {
159163
return {
160164
isError: true,
161-
content: [{type: 'text' as const, text: 'Error: Could not compile'}],
165+
content: [{ type: 'text' as const, text: 'Error: Could not compile' }],
162166
};
163167
}
164-
const requestedPasses: Array<{type: 'text'; text: string}> = [];
168+
const requestedPasses: Array<{ type: 'text'; text: string }> = [];
165169
if (passName != null) {
166170
switch (passName) {
167171
case 'All': {
@@ -262,91 +266,14 @@ server.tool(
262266
}
263267
return {
264268
content: [
265-
{type: 'text' as const, text: result.code},
269+
{ type: 'text' as const, text: result.code },
266270
...requestedPasses,
267271
],
268272
};
269273
} catch (err) {
270274
return {
271275
isError: true,
272-
content: [{type: 'text' as const, text: `Error: ${err.stack}`}],
273-
};
274-
}
275-
},
276-
);
277-
278-
server.tool(
279-
'review-react-runtime',
280-
`Run this tool every time you propose a performance related change to verify if your suggestion actually improves performance.
281-
<requirements>
282-
This tool has some requirements on the code input:
283-
- The react code that is passed into this tool MUST contain an App functional component without arrow function.
284-
- DO NOT export anything since we can't parse export syntax with this tool.
285-
- Only import React from 'react' and use all hooks and imports using the React. prefix like React.useState and React.useEffect
286-
</requirements>
287-
288-
<goals>
289-
- LCP - loading speed: good ≤ 2.5 s, needs-improvement 2.5-4 s, poor > 4 s
290-
- INP - input responsiveness: good ≤ 200 ms, needs-improvement 200-500 ms, poor > 500 ms
291-
- CLS - visual stability: good ≤ 0.10, needs-improvement 0.10-0.25, poor > 0.25
292-
- (Optional: FCP ≤ 1.8 s, TTFB ≤ 0.8 s)
293-
</goals>
294-
295-
<evaluation>
296-
Classify each metric with the thresholds above. Identify the worst category in the order poor > needs-improvement > good.
297-
</evaluation>
298-
299-
<iterate>
300-
(repeat until every metric is good or two consecutive cycles show no gain)
301-
- Apply one focused change based on the failing metric plus React-specific guidance:
302-
- LCP: lazy-load off-screen images, inline critical CSS, preconnect, use React.lazy + Suspense for below-the-fold modules. if the user requests for it, use React Server Components for static content (Server Components).
303-
- INP: wrap non-critical updates in useTransition, avoid calling setState inside useEffect.
304-
- CLS: reserve space via explicit width/height or aspect-ratio, keep stable list keys, use fixed-size skeleton loaders, animate only transform/opacity, avoid inserting ads or banners without placeholders.
305-
306-
Stop when every metric is classified as good. Return the final metric table and the list of applied changes.
307-
</iterate>
308-
`,
309-
{
310-
text: z.string(),
311-
iterations: z.number().optional().default(2),
312-
},
313-
async ({text, iterations}) => {
314-
try {
315-
const results = await measurePerformance(text, iterations);
316-
const formattedResults = `
317-
# React Component Performance Results
318-
319-
## Mean Render Time
320-
${results.renderTime / iterations}ms
321-
322-
## Mean Web Vitals
323-
- Cumulative Layout Shift (CLS): ${results.webVitals.cls / iterations}ms
324-
- Largest Contentful Paint (LCP): ${results.webVitals.lcp / iterations}ms
325-
- Interaction to Next Paint (INP): ${results.webVitals.inp / iterations}ms
326-
- First Input Delay (FID): ${results.webVitals.fid / iterations}ms
327-
328-
## Mean React Profiler
329-
- Actual Duration: ${results.reactProfiler.actualDuration / iterations}ms
330-
- Base Duration: ${results.reactProfiler.baseDuration / iterations}ms
331-
`;
332-
333-
return {
334-
content: [
335-
{
336-
type: 'text' as const,
337-
text: formattedResults,
338-
},
339-
],
340-
};
341-
} catch (error) {
342-
return {
343-
isError: true,
344-
content: [
345-
{
346-
type: 'text' as const,
347-
text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
348-
},
349-
],
276+
content: [{ type: 'text' as const, text: `Error: ${err.stack}` }],
350277
};
351278
}
352279
},
@@ -431,6 +358,83 @@ Server Components - Shift data-heavy logic to the server whenever possible. Brea
431358
],
432359
}));
433360

361+
server.tool(
362+
'review-react-runtime',
363+
`Run this tool every time you propose a performance related change to verify if your suggestion actually improves performance.
364+
<requirements>
365+
This tool has some requirements on the code input:
366+
- The react code that is passed into this tool MUST contain an App functional component without arrow function.
367+
- DO NOT export anything since we can't parse export syntax with this tool.
368+
- Only import React from 'react' and use all hooks and imports using the React. prefix like React.useState and React.useEffect
369+
</requirements>
370+
371+
<goals>
372+
- LCP - loading speed: good ≤ 2.5 s, needs-improvement 2.5-4 s, poor > 4 s
373+
- INP - input responsiveness: good ≤ 200 ms, needs-improvement 200-500 ms, poor > 500 ms
374+
- CLS - visual stability: good ≤ 0.10, needs-improvement 0.10-0.25, poor > 0.25
375+
- (Optional: FCP ≤ 1.8 s, TTFB ≤ 0.8 s)
376+
</goals>
377+
378+
<evaluation>
379+
Classify each metric with the thresholds above. Identify the worst category in the order poor > needs-improvement > good.
380+
</evaluation>
381+
382+
<iterate>
383+
(repeat until every metric is good or two consecutive cycles show no gain)
384+
- Apply one focused change based on the failing metric plus React-specific guidance:
385+
- LCP: lazy-load off-screen images, inline critical CSS, preconnect, use React.lazy + Suspense for below-the-fold modules. if the user requests for it, use React Server Components for static content (Server Components).
386+
- INP: wrap non-critical updates in useTransition, avoid calling setState inside useEffect.
387+
- CLS: reserve space via explicit width/height or aspect-ratio, keep stable list keys, use fixed-size skeleton loaders, animate only transform/opacity, avoid inserting ads or banners without placeholders.
388+
389+
Stop when every metric is classified as good. Return the final metric table and the list of applied changes.
390+
</iterate>
391+
`,
392+
{
393+
text: z.string(),
394+
iterations: z.number().optional().default(2),
395+
},
396+
async ({ text, iterations }) => {
397+
try {
398+
const results = await measurePerformance(text, iterations);
399+
const formattedResults = `
400+
# React Component Performance Results
401+
402+
## Mean Render Time
403+
${calculateMean(results.renderTime)}
404+
405+
TEST: ${results.webVitals.inp}
406+
## Mean Web Vitals
407+
- Cumulative Layout Shift (CLS): ${calculateMean(results.webVitals.cls)}
408+
- Largest Contentful Paint (LCP): ${calculateMean(results.webVitals.lcp)}
409+
- Interaction to Next Paint (INP): ${calculateMean(results.webVitals.inp)}
410+
411+
## Mean React Profiler
412+
- Actual Duration: ${calculateMean(results.reactProfiler.actualDuration)}
413+
- Base Duration: ${calculateMean(results.reactProfiler.baseDuration)}
414+
`;
415+
416+
return {
417+
content: [
418+
{
419+
type: 'text' as const,
420+
text: formattedResults,
421+
},
422+
],
423+
};
424+
} catch (error) {
425+
return {
426+
isError: true,
427+
content: [
428+
{
429+
type: 'text' as const,
430+
text: `Error measuring performance: ${error.message}\n\n${error.stack}`,
431+
},
432+
],
433+
};
434+
}
435+
},
436+
);
437+
434438
async function main() {
435439
const transport = new StdioServerTransport();
436440
await server.connect(transport);

0 commit comments

Comments
 (0)