From 2c1e4e45136dfe02fa9ece1ecdc9ec2e7bd75db9 Mon Sep 17 00:00:00 2001 From: Jorge Cabiedes Acosta Date: Tue, 24 Jun 2025 22:56:21 -0700 Subject: [PATCH] Reformat code to modularize tools --- .../packages/react-mcp-server/src/index.ts | 282 ++++-------------- .../react-mcp-server/src/tools/compileTool.ts | 198 ++++++++++++ .../react-mcp-server/src/tools/devDocsTool.ts | 47 +++ .../react-mcp-server/src/tools/runtimePerf.ts | 10 +- 4 files changed, 306 insertions(+), 231 deletions(-) create mode 100644 compiler/packages/react-mcp-server/src/tools/compileTool.ts create mode 100644 compiler/packages/react-mcp-server/src/tools/devDocsTool.ts diff --git a/compiler/packages/react-mcp-server/src/index.ts b/compiler/packages/react-mcp-server/src/index.ts index 2871027d64..22e4609d4e 100644 --- a/compiler/packages/react-mcp-server/src/index.ts +++ b/compiler/packages/react-mcp-server/src/index.ts @@ -8,20 +8,11 @@ import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'; import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; import {z} from 'zod'; -import {compile, type PrintedCompilerPipelineValue} from './compiler'; -import { - CompilerPipelineValue, - printReactiveFunctionWithOutlined, - printFunctionWithOutlined, - PluginOptions, - SourceLocation, -} from 'babel-plugin-react-compiler/src'; -import * as cheerio from 'cheerio'; -import {queryAlgolia} from './utils/algolia'; import assertExhaustive from './utils/assertExhaustive'; -import {convert} from 'html-to-text'; import {measurePerformance} from './tools/runtimePerf'; import {parseReactComponentTree} from './tools/componentTree'; +import compileTool from './tools/compileTool'; +import devDocsTool from './tools/devDocsTool'; function calculateMean(values: number[]): string { return values.length > 0 @@ -41,40 +32,29 @@ server.tool( query: z.string(), }, async ({query}) => { - try { - const pages = await queryAlgolia(query); - if (pages.length === 0) { + const result = await devDocsTool(query); + + switch (result.kind) { + case 'success': { return { - content: [{type: 'text' as const, text: `No results`}], + isError: false, + content: result.content.map(text => { + return { + type: 'text' as const, + text: text, + }; + }), }; } - const content = pages.map(html => { - const $ = cheerio.load(html); - // react.dev should always have at least one
with the main content - const article = $('article').html(); - if (article != null) { - return { - type: 'text' as const, - text: convert(article), - }; - } else { - return { - type: 'text' as const, - // Fallback to converting the whole page to text. - text: convert($.html()), - }; - } - }); - return { - content, - }; - } catch (err) { - return { - isError: true, - content: [{type: 'text' as const, text: `Error: ${err.stack}`}], - }; + case 'error': + return { + isError: true, + content: [{type: 'text' as const, text: result.text}], + }; + default: + assertExhaustive(result, `Unhandled result ${JSON.stringify(result)}`); } - }, + } ); server.tool( @@ -93,199 +73,47 @@ server.tool( passName: z.enum(['HIR', 'ReactiveFunction', 'All', '@DEBUG']).optional(), }, async ({text, passName}) => { - const pipelinePasses = new Map< - string, - Array - >(); - const recordPass: ( - result: PrintedCompilerPipelineValue, - ) => void = result => { - const entry = pipelinePasses.get(result.name); - if (Array.isArray(entry)) { - entry.push(result); - } else { - pipelinePasses.set(result.name, [result]); - } - }; - const logIR = (result: CompilerPipelineValue): void => { - switch (result.kind) { - case 'ast': { - break; - } - case 'hir': { - recordPass({ - kind: 'hir', - fnName: result.value.id, - name: result.name, - value: printFunctionWithOutlined(result.value), - }); - break; - } - case 'reactive': { - recordPass({ - kind: 'reactive', - fnName: result.value.id, - name: result.name, - value: printReactiveFunctionWithOutlined(result.value), - }); - break; - } - case 'debug': { - recordPass({ - kind: 'debug', - fnName: null, - name: result.name, - value: result.value, - }); - break; - } - default: { - assertExhaustive(result, `Unhandled result ${result}`); - } - } - }; - const errors: Array<{message: string; loc: SourceLocation | null}> = []; - const compilerOptions: Partial = { - panicThreshold: 'none', - logger: { - debugLogIRs: logIR, - logEvent: (_filename, event): void => { - if (event.kind === 'CompileError') { - const detail = event.detail; - const loc = - detail.loc == null || typeof detail.loc == 'symbol' - ? event.fnLoc - : detail.loc; - errors.push({ - message: detail.reason, - loc, - }); - } - }, - }, - }; - try { - const result = await compile({ - text, - file: 'anonymous.tsx', - options: compilerOptions, - }); - if (result.code == null) { + const results = await compileTool(text, passName); + + switch (results.kind) { + case 'success': { return { - isError: true, - content: [{type: 'text' as const, text: 'Error: Could not compile'}], - }; - } - const requestedPasses: Array<{type: 'text'; text: string}> = []; - if (passName != null) { - switch (passName) { - case 'All': { - const hir = pipelinePasses.get('PropagateScopeDependenciesHIR'); - if (hir !== undefined) { - for (const pipelineValue of hir) { - requestedPasses.push({ - type: 'text' as const, - text: pipelineValue.value, - }); - } - } - const reactiveFunc = pipelinePasses.get('PruneHoistedContexts'); - if (reactiveFunc !== undefined) { - for (const pipelineValue of reactiveFunc) { - requestedPasses.push({ - type: 'text' as const, - text: pipelineValue.value, - }); - } - } - break; - } - case 'HIR': { - // Last pass before HIR -> ReactiveFunction - const requestedPass = pipelinePasses.get( - 'PropagateScopeDependenciesHIR', - ); - if (requestedPass !== undefined) { - for (const pipelineValue of requestedPass) { - requestedPasses.push({ - type: 'text' as const, - text: pipelineValue.value, - }); - } - } else { - console.error(`Could not find requested pass ${passName}`); - } - break; - } - case 'ReactiveFunction': { - // Last pass - const requestedPass = pipelinePasses.get('PruneHoistedContexts'); - if (requestedPass !== undefined) { - for (const pipelineValue of requestedPass) { - requestedPasses.push({ - type: 'text' as const, - text: pipelineValue.value, - }); - } - } else { - console.error(`Could not find requested pass ${passName}`); - } - break; - } - case '@DEBUG': { - for (const [, pipelinePass] of pipelinePasses) { - for (const pass of pipelinePass) { - requestedPasses.push({ - type: 'text' as const, - text: `${pass.name}\n\n${pass.value}`, - }); - } - } - break; - } - default: { - assertExhaustive( - passName, - `Unhandled passName option: ${passName}`, - ); - } - } - const requestedPass = pipelinePasses.get(passName); - if (requestedPass !== undefined) { - for (const pipelineValue of requestedPass) { - if (pipelineValue.name === passName) { - requestedPasses.push({ - type: 'text' as const, - text: pipelineValue.value, - }); - } - } - } - } - if (errors.length > 0) { - return { - content: errors.map(err => { + isError: false, + content: results.content.map(text => { return { type: 'text' as const, - text: - err.loc === null || typeof err.loc === 'symbol' - ? `React Compiler bailed out:\n\n${err.message}` - : `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`, + text, }; }), }; } - return { - content: [ - {type: 'text' as const, text: result.code}, - ...requestedPasses, - ], - }; - } catch (err) { - return { - isError: true, - content: [{type: 'text' as const, text: `Error: ${err.stack}`}], - }; + case 'bailout': { + return { + isError: true, + content: results.content.map(text => { + return { + type: 'text' as const, + text, + }; + }), + }; + } + case 'error': + case 'compile-error': + return { + isError: true, + content: [ + { + type: 'text' as const, + text: results.text, + }, + ], + }; + default: + assertExhaustive( + results, + `Unhandled result ${JSON.stringify(results)}`, + ); } }, ); diff --git a/compiler/packages/react-mcp-server/src/tools/compileTool.ts b/compiler/packages/react-mcp-server/src/tools/compileTool.ts new file mode 100644 index 0000000000..0a83a746f3 --- /dev/null +++ b/compiler/packages/react-mcp-server/src/tools/compileTool.ts @@ -0,0 +1,198 @@ +import {compile, type PrintedCompilerPipelineValue} from '../compiler'; +import { + CompilerPipelineValue, + printReactiveFunctionWithOutlined, + printFunctionWithOutlined, + PluginOptions, + SourceLocation, +} from 'babel-plugin-react-compiler/src'; +import assertExhaustive from '../utils/assertExhaustive'; + +type PassNameType = 'HIR' | 'ReactiveFunction' | 'All' | '@DEBUG' | undefined; + +type CompilerToolOutput = + | { + kind: 'success'; + content: Array; + } + | { + kind: 'bailout'; + content: Array; + } + | { + kind: 'compile-error'; + text: string; + } + | { + kind: 'error'; + text: string; + }; + +export default async function compileTool( + text: string, + passName: PassNameType, +): Promise { + const pipelinePasses = new Map>(); + const recordPass: (result: PrintedCompilerPipelineValue) => void = result => { + const entry = pipelinePasses.get(result.name); + if (Array.isArray(entry)) { + entry.push(result); + } else { + pipelinePasses.set(result.name, [result]); + } + }; + const logIR = (result: CompilerPipelineValue): void => { + switch (result.kind) { + case 'ast': { + break; + } + case 'hir': { + recordPass({ + kind: 'hir', + fnName: result.value.id, + name: result.name, + value: printFunctionWithOutlined(result.value), + }); + break; + } + case 'reactive': { + recordPass({ + kind: 'reactive', + fnName: result.value.id, + name: result.name, + value: printReactiveFunctionWithOutlined(result.value), + }); + break; + } + case 'debug': { + recordPass({ + kind: 'debug', + fnName: null, + name: result.name, + value: result.value, + }); + break; + } + default: { + assertExhaustive(result, `Unhandled result ${result}`); + } + } + }; + const errors: Array<{message: string; loc: SourceLocation | null}> = []; + const compilerOptions: Partial = { + panicThreshold: 'none', + logger: { + debugLogIRs: logIR, + logEvent: (_filename, event): void => { + if (event.kind === 'CompileError') { + const detail = event.detail; + const loc = + detail.loc == null || typeof detail.loc == 'symbol' + ? event.fnLoc + : detail.loc; + errors.push({ + message: detail.reason, + loc, + }); + } + }, + }, + }; + try { + const result = await compile({ + text, + file: 'anonymous.tsx', + options: compilerOptions, + }); + if (result.code == null) { + return { + kind: 'compile-error', + text: 'Error: Could not compile', + }; + } + const requestedPasses: Array = []; + if (passName != null) { + switch (passName) { + case 'All': { + const hir = pipelinePasses.get('PropagateScopeDependenciesHIR'); + if (hir !== undefined) { + for (const pipelineValue of hir) { + requestedPasses.push(pipelineValue.value); + } + } + const reactiveFunc = pipelinePasses.get('PruneHoistedContexts'); + if (reactiveFunc !== undefined) { + for (const pipelineValue of reactiveFunc) { + requestedPasses.push(pipelineValue.value); + } + } + break; + } + case 'HIR': { + // Last pass before HIR -> ReactiveFunction + const requestedPass = pipelinePasses.get( + 'PropagateScopeDependenciesHIR', + ); + if (requestedPass !== undefined) { + for (const pipelineValue of requestedPass) { + requestedPasses.push(pipelineValue.value); + } + } else { + console.error(`Could not find requested pass ${passName}`); + } + break; + } + case 'ReactiveFunction': { + // Last pass + const requestedPass = pipelinePasses.get('PruneHoistedContexts'); + if (requestedPass !== undefined) { + for (const pipelineValue of requestedPass) { + requestedPasses.push(pipelineValue.value); + } + } else { + console.error(`Could not find requested pass ${passName}`); + } + break; + } + case '@DEBUG': { + for (const [, pipelinePass] of pipelinePasses) { + for (const pass of pipelinePass) { + requestedPasses.push(`${pass.name}\n\n${pass.value}`); + } + } + break; + } + default: { + assertExhaustive(passName, `Unhandled passName option: ${passName}`); + } + } + const requestedPass = pipelinePasses.get(passName); + if (requestedPass !== undefined) { + for (const pipelineValue of requestedPass) { + if (pipelineValue.name === passName) { + requestedPasses.push(pipelineValue.value); + } + } + } + } + if (errors.length > 0) { + return { + kind: 'bailout', + content: errors.map(err => { + return err.loc === null || typeof err.loc === 'symbol' + ? `React Compiler bailed out:\n\n${err.message}` + : `React Compiler bailed out:\n\n${err.message}@${err.loc.start.line}:${err.loc.end.line}`; + }), + }; + } + return { + kind: 'success', + content: [result.code, ...requestedPasses], + }; + } catch (err) { + return { + kind: 'error', + text: `Error: ${err.stack}`, + }; + } +} diff --git a/compiler/packages/react-mcp-server/src/tools/devDocsTool.ts b/compiler/packages/react-mcp-server/src/tools/devDocsTool.ts new file mode 100644 index 0000000000..774c1a1bdc --- /dev/null +++ b/compiler/packages/react-mcp-server/src/tools/devDocsTool.ts @@ -0,0 +1,47 @@ +import * as cheerio from 'cheerio'; +import {convert} from 'html-to-text'; +import {queryAlgolia} from '../utils/algolia'; + +type DevDocsToolOutput = { + kind: 'success'; + content: Array; +} | { + kind: 'error'; + text: string; +} + +/** + * Tool for querying React dev docs from react.dev + * @param query The search query to look up in the React documentation + * @returns A promise that resolves to the search results + */ +export default async function devDocsTool(query: string): Promise { + try { + const pages = await queryAlgolia(query); + if (pages.length === 0) { + return { + kind: 'error', + text: `No results`, + }; + } + const content = pages.map(html => { + const $ = cheerio.load(html); + // react.dev should always have at least one
with the main content + const article = $('article').html(); + if (article != null) { + return convert(article) + } else { + return convert($.html()) + } + }); + return { + kind: 'success', + content, + }; + } catch (err) { + return { + kind: 'error', + text: `Error: ${err.stack}`, + }; + } +} diff --git a/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts b/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts index 30badc833d..dd12047811 100644 --- a/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts +++ b/compiler/packages/react-mcp-server/src/tools/runtimePerf.ts @@ -125,14 +125,16 @@ export async function measurePerformance( // ui chaos monkey const selectors = await page.evaluate(() => { - window.__INTERACTABLE_SELECTORS__ = []; + (window as any).__INTERACTABLE_SELECTORS__ = []; const elements = Array.from(document.querySelectorAll('a')).concat( - Array.from(document.querySelectorAll('button')), + Array.from(document.querySelectorAll('button')) as any, ); for (const el of elements) { - window.__INTERACTABLE_SELECTORS__.push(el.tagName.toLowerCase()); + (window as any).__INTERACTABLE_SELECTORS__.push( + el.tagName.toLowerCase(), + ); } - return window.__INTERACTABLE_SELECTORS__; + return (window as any).__INTERACTABLE_SELECTORS__; }); await Promise.all(