Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .claude/hooks/ts-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,4 @@ tsc_output=$(npx --prefix "$project_dir" tsc --noEmit 2>&1) || {

if [[ -n "$errors" ]]; then
echo -e "$errors" >&2
exit 2
fi
15 changes: 11 additions & 4 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { register as registerGetDesignOverview } from "./tools/get-design-overvi
import { register as registerQueryComponents } from "./tools/query-components.js";
import { register as registerQueryNet } from "./tools/query-net.js";
import { register as registerExportCadenceBoard } from "./tools/export-cadence-board.js";
import { register as registerExportCadenceConstraints } from "./tools/export-cadence-constraints.js";
import { register as registerQueryConstraints } from "./tools/query-constraints.js";

// =============================================================================
// Server Instructions
Expand All @@ -26,13 +28,15 @@ Supports IPC-2581 XML files (RevA, RevB, RevC) exported from any compliant EDA t
## Workflow Guidance

1. If starting from a Cadence Allegro .brd file, use \`export_cadence_board\` to generate the IPC-2581 XML first (Windows only)
2. Use \`get_design_overview\` first to understand the design structure, layer stackup, and size
3. Use \`query_components\` to find component placements by refdes pattern (regex)
4. Use \`query_net\` to trace a net's routing, trace widths, vias, and connected pins
2. To access design constraints (trace width rules, spacing rules, net classes, stackup), use \`export_cadence_constraints\` to generate a .tcfx file, then \`query_constraints\` to read it
3. Use \`get_design_overview\` first to understand the design structure, layer stackup, and size
4. Use \`query_components\` to find component placements by refdes pattern (regex)
5. Use \`query_net\` to trace a net's routing, trace widths, vias, and connected pins
6. Use \`render_net\` to visualize a net's routing geometry as SVG

## Tool Usage Tips

- All query tools accept an IPC-2581 XML file path as the first argument
- All query/render tools accept an IPC-2581 XML file path as the first argument
- Component refdes patterns use regex (e.g., "^U\\\\d+" for all ICs, "^C1$" for exact match)
- Net name patterns use regex (e.g., "DDR_D0", "^VCC", "CLK")
- All physical values (coordinates, trace widths) are normalized to microns regardless of the source file's native unit
Expand Down Expand Up @@ -71,6 +75,9 @@ export const createServer = (): McpServer => {
registerQueryComponents(server);
registerQueryNet(server);
registerExportCadenceBoard(server);
registerExportCadenceConstraints(server);
registerQueryConstraints(server);
// TODO: register render-net once PNG output via resvg-wasm is stable in compiled binaries

return server;
};
Expand Down
52 changes: 7 additions & 45 deletions src/tools/export-cadence-board.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,14 @@
import { stat, readdir, access } from "node:fs/promises";
import { stat } from "node:fs/promises";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { createMutex } from "./lib/async-mutex.js";
import type { CadenceInstall, ErrorResult, ExportCadenceBoardResult } from "./lib/types.js";
import { detectCadenceInstalls, requireWindows, serializeCadenceCall } from "./lib/cadence.js";
import type { ErrorResult, ExportCadenceBoardResult } from "./lib/types.js";
import { formatResult } from "./shared.js";

const execAsync = promisify(exec);
const serializeExport = createMutex();
const CADENCE_BASE = "C:/Cadence";

export const detectCadenceVersions = async (
cadenceBase = CADENCE_BASE
): Promise<CadenceInstall[]> => {
const installs: CadenceInstall[] = [];

try {
const entries = await readdir(cadenceBase);

for (const entry of entries) {
const match = entry.match(/^SPB_(\d+\.\d+)$/);
if (!match) continue;

const version = match[1];
const root = path.join(cadenceBase, entry);
const exePath = path.join(root, "tools", "bin", "ipc2581_out.exe");

try {
await access(exePath);
installs.push({ version, root, exePath });
} catch {
// ipc2581_out.exe not found in this install
}
}

installs.sort((a, b) => parseFloat(b.version) - parseFloat(a.version));
} catch {
// Cadence directory doesn't exist or isn't accessible
}

return installs;
};

const REV_B_FLAGS = "-f 1.03 -u MICRON -d -b -l -R -K -n -p -t -c -O -I -D -M -S -k -e";
const REV_C_FLAGS = "-f 1.04 -u MICRON -d -b -l -R -K -G -Y -p -t -c -O -I -D -M -A -B -C -U -k -e";
Expand All @@ -51,12 +17,8 @@ export const exportCadenceBoard = async (
brdPath: string,
options?: { output?: string; revision?: "B" | "C" }
): Promise<ExportCadenceBoardResult | ErrorResult> => {
if (process.platform !== "win32") {
return {
error:
"Cadence export is only available on Windows. The ipc2581_out utility requires a Windows environment with Cadence SPB installed.",
};
}
const windowsError = requireWindows("Cadence export");
if (windowsError) return windowsError;

const resolvedBrd = path.resolve(brdPath);
if (!resolvedBrd.toLowerCase().endsWith(".brd")) {
Expand All @@ -71,7 +33,7 @@ export const exportCadenceBoard = async (
return { error: `Board file not found: '${resolvedBrd}'` };
}

const installs = await detectCadenceVersions();
const installs = await detectCadenceInstalls("ipc2581_out.exe");
if (installs.length === 0) {
return {
error:
Expand All @@ -90,7 +52,7 @@ export const exportCadenceBoard = async (

const command = `"${cadence.exePath}" ${flags} -o "${outputBase}" "${resolvedBrd}"`;

return serializeExport(async () => {
return serializeCadenceCall(async () => {
try {
const { stdout, stderr } = await execAsync(command, {
cwd: brdDir,
Expand Down
13 changes: 13 additions & 0 deletions src/tools/export-cadence-constraints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { describe, it, expect } from "vitest";
import { exportCadenceConstraints } from "./export-cadence-constraints.js";
import { isErrorResult } from "./lib/types.js";

describe("exportCadenceConstraints", () => {
it("returns error on non-Windows platforms", async () => {
const result = await exportCadenceConstraints("C:/designs/test.brd");
expect(isErrorResult(result)).toBe(true);
if (isErrorResult(result)) {
expect(result.error).toContain("only available on Windows");
}
});
});
111 changes: 111 additions & 0 deletions src/tools/export-cadence-constraints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { stat } from "node:fs/promises";
import { exec } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { detectCadenceInstalls, requireWindows, serializeCadenceCall } from "./lib/cadence.js";
import type { ErrorResult, ExportCadenceConstraintsResult } from "./lib/types.js";
import { formatResult } from "./shared.js";

const execAsync = promisify(exec);

export const exportCadenceConstraints = async (
brdPath: string,
options?: { output?: string }
): Promise<ExportCadenceConstraintsResult | ErrorResult> => {
const windowsError = requireWindows("Cadence constraint export");
if (windowsError) return windowsError;

const resolvedBrd = path.resolve(brdPath);
if (!resolvedBrd.toLowerCase().endsWith(".brd")) {
return { error: `Expected a .brd file, got: '${path.basename(resolvedBrd)}'` };
}
try {
const s = await stat(resolvedBrd);
if (!s.isFile()) {
return { error: `'${resolvedBrd}' is not a file` };
}
} catch {
return { error: `Board file not found: '${resolvedBrd}'` };
}

const installs = await detectCadenceInstalls("techfile.exe");
if (installs.length === 0) {
return {
error:
"No Cadence SPB installation with techfile.exe found in C:/Cadence. Ensure Cadence Allegro/OrCAD PCB Editor is installed.",
};
}
const cadence = installs[0];

const brdDir = path.dirname(resolvedBrd);
const brdName = path.basename(resolvedBrd, ".brd");
const outputPath = options?.output ?? path.join(brdDir, `${brdName}_constraints.tcfx`);

const command = `"${cadence.exePath}" -w "${resolvedBrd}" "${outputPath}"`;

return serializeCadenceCall(async () => {
try {
const { stdout, stderr } = await execAsync(command, {
cwd: brdDir,
timeout: 120_000,
});

const log = (stdout + stderr).trim();

if (log.includes("License checking failed")) {
return {
error: `Cadence license check failed. Ensure a valid Allegro license is available. Log: ${log}`,
};
}

try {
const outStat = await stat(outputPath);
if (outStat.size < 100) {
return {
error: `Output file is suspiciously small (${outStat.size} bytes): '${outputPath}'`,
};
}
} catch {
return {
error: `Export completed but output file not found: '${outputPath}'`,
};
}

return {
success: true,
outputPath,
cadenceVersion: cadence.version,
log: log || undefined,
};
} catch (err: unknown) {
const execError = err as { message?: string; stdout?: string; stderr?: string };
const combinedLog = [execError.stdout, execError.stderr].filter(Boolean).join("\n").trim();
return {
error: `Cadence techfile failed: ${execError.message ?? "Unknown error"}${combinedLog ? `\nLog: ${combinedLog}` : ""}`,
};
}
});
};

export const register = (server: McpServer): void => {
server.registerTool(
"export_cadence_constraints",
{
description:
"Export a Cadence Allegro .brd file to .tcfx constraint XML. Windows only. Requires Cadence SPB installation (auto-detected). Calls are serialized internally to avoid license conflicts.",
inputSchema: {
board: z.string().describe("Path to Cadence Allegro .brd file"),
output: z
.string()
.optional()
.describe("Output .tcfx path. Defaults to <boardname>_constraints.tcfx next to the .brd"),
},
},
async ({ board, output }) => {
const result = await exportCadenceConstraints(board, { output });
return formatResult(result);
}
);
};
69 changes: 69 additions & 0 deletions src/tools/lib/cadence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Shared Cadence SPB utilities for export tools.
*
* Both export_cadence_board and export_cadence_constraints need Cadence
* installation detection and a shared mutex to avoid license conflicts.
* ES module caching guarantees the mutex singleton is shared across imports.
*/

import { readdir, access } from "node:fs/promises";
import path from "node:path";
import { createMutex } from "./async-mutex.js";
import type { CadenceInstall, ErrorResult } from "./types.js";

export const CADENCE_BASE = "C:/Cadence";

/**
* Scan for Cadence SPB installations that contain a specific executable.
* Returns installs sorted by version descending (newest first).
*/
export const detectCadenceInstalls = async (
exeName: string,
cadenceBase = CADENCE_BASE
): Promise<CadenceInstall[]> => {
const installs: CadenceInstall[] = [];

try {
const entries = await readdir(cadenceBase);

for (const entry of entries) {
const match = entry.match(/^SPB_(\d+\.\d+)$/);
if (!match) continue;

const version = match[1];
const root = path.join(cadenceBase, entry);
const exePath = path.join(root, "tools", "bin", exeName);

try {
await access(exePath);
installs.push({ version, root, exePath });
} catch {
// exe not found in this install
}
}

installs.sort((a, b) => parseFloat(b.version) - parseFloat(a.version));
} catch {
// Cadence directory doesn't exist or isn't accessible
}

return installs;
};

/**
* Shared mutex to serialize all Cadence tool invocations.
* Prevents license conflicts when multiple export calls happen concurrently.
*/
export const serializeCadenceCall = createMutex();

/**
* Platform guard. Returns an ErrorResult on non-Windows, null otherwise.
*/
export const requireWindows = (toolName: string): ErrorResult | null => {
if (process.platform !== "win32") {
return {
error: `${toolName} is only available on Windows. Requires a Windows environment with Cadence SPB installed.`,
};
}
return null;
};
58 changes: 58 additions & 0 deletions src/tools/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,61 @@ export interface ExportCadenceBoardResult {
cadenceVersion: string;
log?: string;
}

/**
* Result from export_cadence_constraints tool.
*/
export interface ExportCadenceConstraintsResult {
success: boolean;
outputPath: string;
cadenceVersion: string;
log?: string;
}

/**
* Generic TCFX constraint object. Attribute names (MIN_LINE_WIDTH, LINE_TO_LINE,
* etc.) carry domain semantics that the LLM interprets directly.
*/
export interface ConstraintObject {
name: string;
attributes: Record<string, { value: string; generic?: string }>;
references: Array<{ kind: string; name: string }>;
members: Array<{ kind: string; name: string }>;
crossSection?: CrossSection;
}

/**
* Stackup cross-section layer (nested inside Design section objects).
*/
export interface CrossSectionLayer {
type: string; // Conductor, Dielectric, Mask, Surface
attributes: Record<string, string>;
}

/**
* Cross-section data from a Design section object.
*/
export interface CrossSection {
primaryStackup?: string;
topIndex?: number;
bottomIndex?: number;
layers: CrossSectionLayer[];
}

/**
* Overview result from query_constraints (no section specified).
*/
export interface ConstraintsOverviewResult {
fileName: string;
fileSizeBytes: number;
sections: Array<{ name: string; objectCount: number }>;
}

/**
* Section query result from query_constraints.
*/
export interface ConstraintsSectionResult {
fileName: string;
section: string;
objects: ConstraintObject[];
}
Loading