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' ;
1212import {
1313 CompilerPipelineValue ,
1414 printReactiveFunctionWithOutlined ,
@@ -17,10 +17,14 @@ import {
1717 SourceLocation ,
1818} from 'babel-plugin-react-compiler/src' ;
1919import * as cheerio from 'cheerio' ;
20- import { queryAlgolia } from './utils/algolia' ;
20+ import { queryAlgolia } from './utils/algolia' ;
2121import 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
2529const 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+
434438async function main ( ) {
435439 const transport = new StdioServerTransport ( ) ;
436440 await server . connect ( transport ) ;
0 commit comments