/**
* Advanced analysis tools: field layout for C++ classes
*/
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { FieldLayoutOptions } from "../types.js";
import { SEARCHFOX_BASE_URL } from "../constants.js";
interface FieldInfo {
offset: number;
size: number;
type: string;
name: string;
}
interface BaseClassInfo {
offset: number;
size: number;
type: string;
}
interface FieldLayoutResult {
class_name: string;
size?: number;
alignment?: number;
base_classes?: BaseClassInfo[];
fields?: FieldInfo[];
}
/**
* Get field layout information for a C++ class or struct
*/
export async function getFieldLayout(
options: FieldLayoutOptions
): Promise<CallToolResult> {
try {
const repo = options.repo || "mozilla-central";
const queryString = `field-layout:'${options.class_name}'`;
// Build URL
const searchParams = new URLSearchParams();
searchParams.set("q", queryString);
const url = `${SEARCHFOX_BASE_URL}/${repo}/query/default?${searchParams.toString()}`;
// Execute query
const response = await fetch(url, {
headers: {
Accept: "application/json",
"User-Agent": `searchfox-mcp/1.0.0 (+https://github.com/canova/searchfox-mcp)`,
},
});
if (!response.ok) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `HTTP ${response.status}: ${response.statusText}`,
},
null,
2
),
},
],
isError: true,
};
}
const responseText = await response.text();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let jsonData: any;
try {
jsonData = JSON.parse(responseText);
} catch {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: "Failed to parse response as JSON",
raw_response: responseText.substring(0, 500),
},
null,
2
),
},
],
isError: true,
};
}
// Parse field layout from response
const result = parseFieldLayout(options.class_name, jsonData);
if (!result) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
class_name: options.class_name,
error: "No field layout information found",
note: "Field layout only works with C++ classes and structs",
},
null,
2
),
},
],
isError: true,
};
}
// Format markdown output
const markdown = formatFieldLayoutMarkdown(result);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
class_name: options.class_name,
repo,
size_bytes: result.size,
alignment_bytes: result.alignment,
base_classes_count: result.base_classes?.length || 0,
fields_count: result.fields?.length || 0,
markdown_output: markdown,
query_url: url,
raw_data: result,
},
null,
2
),
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: JSON.stringify(
{
error: `Field layout query failed: ${error instanceof Error ? error.message : String(error)}`,
},
null,
2
),
},
],
isError: true,
};
}
}
/**
* Parse field layout from Searchfox response
*/
function parseFieldLayout(
className: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any
): FieldLayoutResult | null {
const symbolKey = `T_${className}`;
// Navigate to SymbolTreeTableList.tables
const tables = data?.SymbolTreeTableList?.tables;
if (!Array.isArray(tables)) {
return null;
}
// Find the table with our symbol
for (const table of tables) {
const jumprefs = table?.jumprefs;
if (!jumprefs || typeof jumprefs !== "object") {
continue;
}
const symbolInfo = jumprefs[symbolKey];
if (!symbolInfo) {
continue;
}
// Extract metadata (may be in variants for template classes)
let meta = symbolInfo.meta;
if (
meta?.variants &&
Array.isArray(meta.variants) &&
meta.variants.length > 0
) {
meta = meta.variants[0];
}
if (!meta) {
continue;
}
const result: FieldLayoutResult = {
class_name: className,
size: meta.sizeBytes,
alignment: meta.alignmentBytes,
};
// Parse base classes (supers)
if (meta.supers && Array.isArray(meta.supers)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result.base_classes = meta.supers.map((base: any) => {
const baseSym = base.sym || "unknown";
const baseType = baseSym.startsWith("T_")
? baseSym.substring(2)
: baseSym;
return {
offset: base.offsetBytes || 0,
size: base.sizeBytes || 0,
type: baseType,
};
});
}
// Parse fields
if (meta.fields && Array.isArray(meta.fields)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result.fields = meta.fields.map((field: any) => {
// Extract field name from pretty name (last part after ::)
const prettyName = field.pretty || "unnamed";
const name = prettyName.includes("::")
? prettyName.split("::").pop() || "unnamed"
: prettyName;
return {
offset: field.offsetBytes || 0,
size: field.sizeBytes || 0,
type: field.type || "unknown",
name,
};
});
}
return result;
}
return null;
}
/**
* Format field layout as markdown
*/
function formatFieldLayoutMarkdown(result: FieldLayoutResult): string {
let output = `# Field Layout: ${result.class_name}\n\n`;
// Size and alignment
if (result.size !== undefined) {
output += `**Size**: ${result.size} bytes`;
if (result.alignment !== undefined) {
output += `, **Alignment**: ${result.alignment} bytes`;
}
output += "\n\n";
}
// Base classes
if (result.base_classes && result.base_classes.length > 0) {
output += "## Base Classes\n\n";
output += "| Offset | Size | Type |\n";
output += "|--------|------|------|\n";
for (const base of result.base_classes) {
output += `| ${base.offset} | ${base.size} | \`${base.type}\` |\n`;
}
output += "\n";
}
// Fields
if (result.fields && result.fields.length > 0) {
output += "## Fields\n\n";
output += "| Offset | Size | Type | Name |\n";
output += "|--------|------|------|------|\n";
for (const field of result.fields) {
output += `| ${field.offset} | ${field.size} | \`${field.type}\` | ${field.name} |\n`;
}
output += "\n";
}
return output;
}