next
Version:
The React Framework
166 lines (165 loc) • 6.25 kB
JavaScript
import { HMR_MESSAGE_SENT_TO_BROWSER } from '../../dev/hot-reloader-types';
import { createBrowserRequest, handleBrowserPageResponse, DEFAULT_BROWSER_REQUEST_TIMEOUT_MS } from './utils/browser-communication';
import { mcpTelemetryTracker } from '../mcp-telemetry-tracker';
export function registerGetPageMetadataTool(server, sendHmrMessage, getActiveConnectionCount) {
server.registerTool('get_page_metadata', {
description: 'Get runtime metadata about what contributes to the current page render from active browser sessions.',
inputSchema: {}
}, async (_request)=>{
// Track telemetry
mcpTelemetryTracker.recordToolCall('mcp/get_page_metadata');
try {
const connectionCount = getActiveConnectionCount();
if (connectionCount === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'No browser sessions connected. Please open your application in a browser to retrieve page metadata.'
})
}
]
};
}
const responses = await createBrowserRequest(HMR_MESSAGE_SENT_TO_BROWSER.REQUEST_PAGE_METADATA, sendHmrMessage, getActiveConnectionCount, DEFAULT_BROWSER_REQUEST_TIMEOUT_MS);
if (responses.length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
sessions: []
})
}
]
};
}
const sessionMetadata = [];
for (const response of responses){
if (response.data) {
// TODO: Add other metadata for the current page render here. Currently, we only have segment trie data.
const pageMetadata = convertSegmentTrieToPageMetadata(response.data);
sessionMetadata.push({
url: response.url,
metadata: pageMetadata
});
}
}
if (sessionMetadata.length === 0) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
sessions: []
})
}
]
};
}
const output = formatPageMetadata(sessionMetadata);
return {
content: [
{
type: 'text',
text: JSON.stringify(output)
}
]
};
} catch (error) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: error instanceof Error ? error.message : String(error)
})
}
]
};
}
});
}
export function handlePageMetadataResponse(requestId, segmentTrieData, url) {
handleBrowserPageResponse(requestId, segmentTrieData, url || '');
}
function convertSegmentTrieToPageMetadata(data) {
const segments = [];
if (data.segmentTrie) {
// Traverse the trie and collect all segments
function traverseTrie(node) {
if (node.value) {
segments.push({
type: node.value.type,
pagePath: node.value.pagePath,
boundaryType: node.value.boundaryType
});
}
for (const childNode of Object.values(node.children)){
if (childNode) {
traverseTrie(childNode);
}
}
}
traverseTrie(data.segmentTrie);
}
return {
segments,
routerType: data.routerType
};
}
function formatPageMetadata(sessionMetadata) {
const sessions = [];
for (const { url, metadata } of sessionMetadata){
let displayUrl = url;
try {
const urlObj = new URL(url);
displayUrl = urlObj.pathname + urlObj.search + urlObj.hash;
} catch {
// If URL parsing fails, use the original URL
}
// Ensure consistent output to avoid flaky tests
const sortedSegments = [
...metadata.segments
].sort((a, b)=>{
const typeOrder = (segment)=>{
const type = segment.boundaryType || segment.type;
if (type === 'layout') return 0;
if (type.startsWith('boundary:')) return 1;
if (type === 'page') return 2;
return 3;
};
const aOrder = typeOrder(a);
const bOrder = typeOrder(b);
if (aOrder !== bOrder) return aOrder - bOrder;
return a.pagePath.localeCompare(b.pagePath);
});
const formattedSegments = [];
for (const segment of sortedSegments){
const path = segment.pagePath;
const isBuiltin = path.startsWith('__next_builtin__');
const type = segment.boundaryType || segment.type;
const isBoundary = type.startsWith('boundary:');
let displayPath = path.replace(/@boundary$/, '').replace(/^__next_builtin__/, '');
if (!isBuiltin && !displayPath.startsWith('app/')) {
displayPath = `app/${displayPath}`;
}
formattedSegments.push({
path: displayPath,
type,
isBoundary,
isBuiltin
});
}
sessions.push({
url: displayUrl,
routerType: metadata.routerType,
segments: formattedSegments
});
}
return {
sessions
};
}
//# sourceMappingURL=get-page-metadata.js.map