component-intelligence-tools.js•21.3 kB
/**
* Component Intelligence Tools for WebSee MCP Server
*
* Provides granular component inspection tools for React, Vue, and Angular applications.
* These tools integrate with the ComponentTracker to provide detailed component analysis.
*
* @module component-intelligence-tools
*/
import { z } from 'zod';
import { ComponentTracker } from '../component-tracker.js';
// ============================================================================
// Zod Schemas for Tool Parameters
// ============================================================================
export const ComponentTreeSchema = z.object({
url: z.string().url().describe('The URL of the page to analyze'),
includeDepth: z
.boolean()
.optional()
.default(true)
.describe('Include depth information for each component'),
filterFramework: z
.enum(['react', 'vue', 'angular', 'svelte', 'all'])
.optional()
.default('all')
.describe('Filter components by framework'),
});
export const ComponentGetPropsSchema = z.object({
url: z.string().url().describe('The page URL'),
selector: z.string().describe('CSS selector for the component'),
});
export const ComponentGetStateSchema = z.object({
url: z.string().url().describe('The page URL'),
selector: z.string().describe('CSS selector for the component'),
});
export const ComponentFindByNameSchema = z.object({
url: z.string().url().describe('The page URL'),
componentName: z.string().describe('Name of the component to find (case-sensitive)'),
includeProps: z.boolean().optional().default(true).describe('Include props in results'),
includeState: z.boolean().optional().default(true).describe('Include state in results'),
});
export const ComponentGetSourceSchema = z.object({
url: z.string().url().describe('The page URL'),
selector: z.string().describe('CSS selector for the component'),
});
export const ComponentTrackRendersSchema = z.object({
url: z.string().url().describe('The page URL'),
selector: z.string().describe('CSS selector for the component to track'),
duration: z
.number()
.min(1000)
.max(60000)
.default(5000)
.describe('Duration to track renders in milliseconds (1s-60s)'),
captureReasons: z
.boolean()
.optional()
.default(true)
.describe('Attempt to capture re-render reasons'),
});
export const ComponentGetContextSchema = z.object({
url: z.string().url().describe('The page URL'),
selector: z.string().describe('CSS selector for the component'),
});
export const ComponentGetHooksSchema = z.object({
url: z.string().url().describe('The page URL'),
selector: z.string().describe('CSS selector for the React component'),
});
// ============================================================================
// Tool Implementation Functions
// ============================================================================
/**
* Get full component hierarchy as a tree structure
*/
export async function componentTree(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
const allComponents = await tracker.getComponentTree();
// Filter by framework if specified
const filteredComponents = params.filterFramework === 'all'
? allComponents
: allComponents.filter((c) => c.type === params.filterFramework);
// Build tree structure with depth calculation
const componentMap = new Map();
filteredComponents.forEach((c) => componentMap.set(c.name, c));
const buildTree = (comp, depth = 0) => {
const node = {
name: comp.name,
type: comp.type,
depth,
children: [],
props: comp.props,
state: comp.state,
source: comp.source
? {
file: comp.source.file,
line: comp.source.line,
column: comp.source.column,
}
: undefined,
};
// Build children recursively
if (comp.children && comp.children.length > 0) {
node.children = comp.children
.map(childName => componentMap.get(childName))
.filter((child) => child !== undefined)
.map(child => buildTree(child, depth + 1));
}
return node;
};
// Find root components (those without parents)
const rootComponents = filteredComponents.filter((c) => !c.parent);
const tree = rootComponents.map(c => buildTree(c));
// Get unique frameworks
const frameworks = Array.from(new Set(allComponents.map((c) => c.type)));
return {
components: tree,
totalCount: filteredComponents.length,
frameworks,
};
}
finally {
// Cleanup is handled by page lifecycle
}
}
/**
* Get component props only
*/
export async function componentGetProps(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
const component = await tracker.getComponentAtElement(params.selector);
if (!component) {
throw new Error(`No component found at selector: ${params.selector}`);
}
return {
componentName: component.name,
props: component.props || {},
};
}
finally {
// Cleanup is handled by page lifecycle
}
}
/**
* Get component state only
*/
export async function componentGetState(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
const component = await tracker.getComponentAtElement(params.selector);
if (!component) {
throw new Error(`No component found at selector: ${params.selector}`);
}
return {
componentName: component.name,
state: component.state || null,
};
}
finally {
// Cleanup is handled by page lifecycle
}
}
/**
* Find all instances of a component by name
*/
export async function componentFindByName(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
const allComponents = await tracker.getComponentTree();
const matchingComponents = allComponents.filter((c) => c.name === params.componentName);
const instances = matchingComponents.map((comp) => {
const instance = {
selector: comp.domNodes[0] ? `#${comp.domNodes[0]}` : 'unknown',
domId: comp.domNodes[0],
props: params.includeProps ? comp.props || {} : {},
};
if (params.includeState && comp.state) {
instance.state = comp.state;
}
return instance;
});
return {
instances,
count: instances.length,
};
}
finally {
// Cleanup is handled by page lifecycle
}
}
/**
* Map component to source file
*/
export async function componentGetSource(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
const component = await tracker.getComponentAtElement(params.selector);
if (!component) {
throw new Error(`No component found at selector: ${params.selector}`);
}
if (!component.source) {
return {
file: 'unknown',
framework: component.type,
};
}
return {
file: component.source.file,
line: component.source.line,
column: component.source.column,
framework: component.type,
};
}
finally {
// Cleanup is handled by page lifecycle
}
}
/**
* Track component re-renders over time
*/
export async function componentTrackRenders(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
// Get the component to track
const component = await tracker.getComponentAtElement(params.selector);
if (!component) {
throw new Error(`No component found at selector: ${params.selector}`);
}
// Inject render tracking
await page.evaluate(({ captureReasons }) => {
const renderEvents = [];
// Setup React DevTools hook monitoring if available
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (hook) {
const originalOnCommit = hook.onCommitFiberRoot;
hook.onCommitFiberRoot = function (...args) {
const timestamp = performance.now();
renderEvents.push({
timestamp,
reason: captureReasons ? 'commit' : undefined,
});
if (originalOnCommit) {
return originalOnCommit.apply(this, args);
}
};
}
// Store events globally for retrieval
window.__websee_render_events = renderEvents;
}, { captureReasons: params.captureReasons });
// Wait for the specified duration
await page.waitForTimeout(params.duration);
// Retrieve render events
const events = await page.evaluate(() => {
return window.__websee_render_events || [];
});
// Process events
const processedRenders = events.map((event, index) => ({
timestamp: event.timestamp,
reason: event.reason,
duration: index > 0 ? event.timestamp - events[index - 1].timestamp : undefined,
}));
// Calculate average interval
const intervals = processedRenders
.map(r => r.duration)
.filter((d) => d !== undefined);
const averageInterval = intervals.length > 0 ? intervals.reduce((sum, d) => sum + d, 0) / intervals.length : 0;
return {
componentName: component.name,
renders: processedRenders,
totalRenders: processedRenders.length,
averageInterval,
};
}
finally {
// Cleanup tracking hooks
await page
.evaluate(() => {
delete window.__websee_render_events;
// Restore original hook if modified
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
if (hook && hook.__websee_original_onCommit) {
hook.onCommitFiberRoot = hook.__websee_original_onCommit;
delete hook.__websee_original_onCommit;
}
})
.catch(() => {
// Ignore errors during cleanup
});
}
}
/**
* Get React context values for a component
*/
export async function componentGetContext(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
const component = await tracker.getComponentAtElement(params.selector);
if (!component) {
throw new Error(`No component found at selector: ${params.selector}`);
}
// Extract context values from the component
const contexts = await page.evaluate(selector => {
const element = document.querySelector(selector);
if (!element)
return [];
const contexts = [];
// Try to access React fiber for context
const fiberKey = Object.keys(element).find(key => key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance'));
if (fiberKey) {
const fiber = element[fiberKey];
// Walk up the fiber tree to find contexts
let currentFiber = fiber;
while (currentFiber) {
if (currentFiber.dependencies?.firstContext) {
let contextItem = currentFiber.dependencies.firstContext;
let contextIndex = 0;
while (contextItem && contextIndex < 20) {
// Limit to prevent infinite loops
const contextValue = contextItem.memoizedValue;
contexts.push({
name: `Context_${contextIndex}`,
value: contextValue,
provider: currentFiber.type?.displayName || currentFiber.type?.name,
});
contextItem = contextItem.next;
contextIndex++;
}
}
currentFiber = currentFiber.return;
}
}
return contexts;
}, params.selector);
return { contexts };
}
finally {
// Cleanup is handled by page lifecycle
}
}
/**
* Get React hooks state for a component
*/
export async function componentGetHooks(page, params) {
const tracker = new ComponentTracker();
try {
await tracker.initialize(page);
await page.goto(params.url, { waitUntil: 'networkidle' });
const component = await tracker.getComponentAtElement(params.selector);
if (!component) {
throw new Error(`No component found at selector: ${params.selector}`);
}
if (component.type !== 'react') {
throw new Error(`Hooks are only available for React components. Found: ${component.type}`);
}
// Extract hooks information
const hooks = await page.evaluate(selector => {
const element = document.querySelector(selector);
if (!element)
return [];
const hooks = [];
// Try to access React fiber for hooks
const fiberKey = Object.keys(element).find(key => key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance'));
if (fiberKey) {
const fiber = element[fiberKey];
// Access memoizedState which contains hooks
let hookNode = fiber?.memoizedState;
let index = 0;
while (hookNode && index < 50) {
// Limit to prevent infinite loops
const hookInfo = {
type: determineHookType(hookNode, index),
value: hookNode.memoizedState,
index,
};
// Try to extract dependencies for effects
if (hookNode.deps !== null && hookNode.deps !== undefined) {
hookInfo.dependencies = hookNode.deps;
}
hooks.push(hookInfo);
hookNode = hookNode.next;
index++;
}
}
function determineHookType(hookNode, _index) {
// This is a heuristic approach since React doesn't expose hook types directly
if (hookNode.queue !== null && hookNode.queue !== undefined) {
return 'useState/useReducer';
}
if (hookNode.deps !== null && hookNode.deps !== undefined) {
return 'useEffect/useMemo/useCallback';
}
if (hookNode.memoizedState && typeof hookNode.memoizedState === 'object') {
if (hookNode.memoizedState.current !== undefined) {
return 'useRef';
}
}
return 'unknown';
}
return hooks;
}, params.selector);
return { hooks };
}
finally {
// Cleanup is handled by page lifecycle
}
}
// ============================================================================
// Tool Metadata for MCP Server Registration
// ============================================================================
export const COMPONENT_INTELLIGENCE_TOOLS = [
{
name: 'component_tree',
description: 'Get full React/Vue/Angular component hierarchy as a tree structure with depth information',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The URL of the page to analyze' },
includeDepth: {
type: 'boolean',
description: 'Include depth information for each component',
},
filterFramework: {
type: 'string',
enum: ['react', 'vue', 'angular', 'svelte', 'all'],
description: 'Filter components by framework',
},
},
required: ['url'],
},
},
{
name: 'component_get_props',
description: 'Get component props for a specific component selected by CSS selector',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL' },
selector: { type: 'string', description: 'CSS selector for the component' },
},
required: ['url', 'selector'],
},
},
{
name: 'component_get_state',
description: 'Get component state for a specific component selected by CSS selector',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL' },
selector: { type: 'string', description: 'CSS selector for the component' },
},
required: ['url', 'selector'],
},
},
{
name: 'component_find_by_name',
description: 'Find all instances of a component by name across the entire page',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL' },
componentName: {
type: 'string',
description: 'Name of the component to find (case-sensitive)',
},
includeProps: { type: 'boolean', description: 'Include props in results' },
includeState: { type: 'boolean', description: 'Include state in results' },
},
required: ['url', 'componentName'],
},
},
{
name: 'component_get_source',
description: 'Map a component to its source file, line, and column information',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL' },
selector: { type: 'string', description: 'CSS selector for the component' },
},
required: ['url', 'selector'],
},
},
{
name: 'component_track_renders',
description: 'Track component re-renders over a specified duration to identify performance issues',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL' },
selector: { type: 'string', description: 'CSS selector for the component to track' },
duration: {
type: 'number',
description: 'Duration to track renders in milliseconds (1000-60000)',
},
captureReasons: { type: 'boolean', description: 'Attempt to capture re-render reasons' },
},
required: ['url', 'selector'],
},
},
{
name: 'component_get_context',
description: 'Get React context values available to a component',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL' },
selector: { type: 'string', description: 'CSS selector for the component' },
},
required: ['url', 'selector'],
},
},
{
name: 'component_get_hooks',
description: 'Get React hooks state and information for a component',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'The page URL' },
selector: { type: 'string', description: 'CSS selector for the React component' },
},
required: ['url', 'selector'],
},
},
];
//# sourceMappingURL=component-intelligence-tools.js.map