Skip to main content
Glama
by NellyW8
index.js35.9 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import { exec, spawn } from "child_process"; import { promises as fs } from "fs"; import { tmpdir } from "os"; import { join, basename } from "path"; import { promisify } from "util"; import { homedir } from "os"; const execAsync = promisify(exec); // Helper functions function getStringProperty(obj, key, defaultValue = "") { if (obj && typeof obj === 'object' && key in obj) { const value = obj[key]; return typeof value === 'string' ? value : defaultValue; } return defaultValue; } function getNumberProperty(obj, key, defaultValue = 10.0) { if (obj && typeof obj === 'object' && key in obj) { const value = obj[key]; return typeof value === 'number' ? value : defaultValue; } return defaultValue; } function validateRequiredString(obj, key, toolName) { const value = getStringProperty(obj, key); if (!value) { throw new McpError(ErrorCode.InvalidParams, `Missing required parameter '${key}' for tool '${toolName}'`); } return value; } // Enhanced exec with better timeout and error handling async function execAsyncWithTimeout(command, options = {}, timeoutMs = 600000) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { childProcess.kill('SIGKILL'); reject(new Error(`Command timed out after ${timeoutMs}ms: ${command}`)); }, timeoutMs); // Ensure encoding is set to get string output and increase buffer size const execOptions = { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, // 10MB default buffer ...options }; const childProcess = exec(command, execOptions, (error, stdout, stderr) => { clearTimeout(timeout); if (error) { reject(error); } else { // Convert to string if needed (though with utf8 encoding it should already be string) const stdoutStr = typeof stdout === 'string' ? stdout : stdout.toString(); const stderrStr = typeof stderr === 'string' ? stderr : stderr.toString(); resolve({ stdout: stdoutStr, stderr: stderrStr }); } }); }); } // Alternative spawn-based execution for better TTY handling async function spawnAsyncWithTimeout(command, args, options = {}, timeoutMs = 600000) { return new Promise((resolve, reject) => { let stdout = ''; let stderr = ''; const timeout = setTimeout(() => { childProcess.kill('SIGKILL'); reject(new Error(`Command timed out after ${timeoutMs}ms: ${command} ${args.join(' ')}`)); }, timeoutMs); const childProcess = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], ...options }); childProcess.stdout?.on('data', (data) => { stdout += data.toString(); }); childProcess.stderr?.on('data', (data) => { stderr += data.toString(); }); childProcess.on('close', (code) => { clearTimeout(timeout); if (code === 0) { resolve({ stdout, stderr }); } else { reject(new Error(`Command failed with exit code ${code}: ${command} ${args.join(' ')}\n${stderr}`)); } }); childProcess.on('error', (error) => { clearTimeout(timeout); reject(error); }); }); } // Check if a command exists async function commandExists(command) { try { await execAsync(`which ${command}`); return true; } catch { return false; } } class EDAServer { tempDir; projects = new Map(); openlaneDir; constructor() { this.tempDir = join(tmpdir(), `eda_mcp_${Date.now()}`); this.openlaneDir = join(homedir(), "openlane-projects"); this.initTempDir().catch(() => { }); } async initTempDir() { try { await fs.mkdir(this.tempDir, { recursive: true }); await fs.mkdir(this.openlaneDir, { recursive: true }); } catch { // Silent fail } } async synthesizeVerilog(verilogCode, topModule, target = "generic") { try { const projectId = Math.random().toString(36).substring(2, 15); const projectDir = join(this.tempDir, `project_${projectId}`); await fs.mkdir(projectDir, { recursive: true }); // Store project info this.projects.set(projectId, { dir: projectDir, type: "synthesis" }); // Write Verilog file const verilogFile = join(projectDir, "design.v"); await fs.writeFile(verilogFile, verilogCode); // Create synthesis script let synthScript; switch (target.toLowerCase()) { case "ice40": synthScript = ` read_verilog design.v hierarchy -check -top ${topModule} synth_ice40 -top ${topModule} write_verilog synth_output.v stat `; break; case "xilinx": synthScript = ` read_verilog design.v hierarchy -check -top ${topModule} synth_xilinx -top ${topModule} write_verilog synth_output.v stat `; break; default: synthScript = ` read_verilog design.v hierarchy -check -top ${topModule} synth -top ${topModule} techmap opt write_verilog synth_output.v stat `; } const scriptFile = join(projectDir, "synth.ys"); await fs.writeFile(scriptFile, synthScript); // Run Yosys const { stdout, stderr } = await execAsync(`yosys -s ${scriptFile}`, { cwd: projectDir, timeout: 120000, }); let synthVerilog = ""; try { synthVerilog = await fs.readFile(join(projectDir, "synth_output.v"), 'utf8'); } catch { synthVerilog = "Synthesis output not generated"; } return JSON.stringify({ project_id: projectId, success: true, stdout, stderr, synthesized_verilog: synthVerilog, target, }, null, 2); } catch (error) { return JSON.stringify({ success: false, error: error.message || String(error), }, null, 2); } } async simulateVerilog(verilogCode, testbenchCode) { try { const projectId = Math.random().toString(36).substring(2, 15); const projectDir = join(this.tempDir, `sim_project_${projectId}`); await fs.mkdir(projectDir, { recursive: true }); // Store project info this.projects.set(projectId, { dir: projectDir, type: "simulation" }); // Write design and testbench files await fs.writeFile(join(projectDir, "design.v"), verilogCode); await fs.writeFile(join(projectDir, "testbench.v"), testbenchCode); // Compile and run simulation const compileCmd = `iverilog -o simulation design.v testbench.v`; const { stdout: compileOut, stderr: compileErr } = await execAsync(compileCmd, { cwd: projectDir, timeout: 60000, }); const { stdout: simOut, stderr: simErr } = await execAsync('./simulation', { cwd: projectDir, timeout: 60000, }); return JSON.stringify({ project_id: projectId, success: true, compile_stdout: compileOut, compile_stderr: compileErr, sim_stdout: simOut, sim_stderr: simErr, note: `Use view_waveform with project_id: ${projectId} to open GTKWave` }, null, 2); } catch (error) { return JSON.stringify({ success: false, error: error.message || String(error), }, null, 2); } } async viewWaveform(projectId, vcdFile = "output.vcd") { try { // Check if project exists const project = this.projects.get(projectId); if (!project) { return JSON.stringify({ success: false, error: `Project ${projectId} not found. Run a simulation first.`, }, null, 2); } const vcdPath = join(project.dir, vcdFile); // Check if VCD file exists try { await fs.access(vcdPath); } catch { // List available files to help user const files = await fs.readdir(project.dir); const vcdFiles = files.filter(f => f.endsWith('.vcd')); return JSON.stringify({ success: false, error: `VCD file '${vcdFile}' not found in project ${projectId}`, available_vcd_files: vcdFiles, note: "Make sure your testbench includes $dumpfile() and $dumpvars() commands" }, null, 2); } // Check if GTKWave is available if (!(await commandExists('gtkwave'))) { return JSON.stringify({ success: false, error: "GTKWave not found. Please install GTKWave to view waveforms.", install_instructions: { macos: "brew install gtkwave", linux: "sudo apt-get install gtkwave", windows: "Install GTKWave from http://gtkwave.sourceforge.net/" } }, null, 2); } // Launch GTKWave in background const gtkwaveCmd = `gtkwave "${vcdPath}" &`; await execAsync(gtkwaveCmd, { cwd: project.dir, timeout: 5000 }); return JSON.stringify({ success: true, message: `GTKWave launched for project ${projectId}`, vcd_file: vcdFile, vcd_path: vcdPath, project_type: project.type }, null, 2); } catch (error) { return JSON.stringify({ success: false, error: error.message || String(error), }, null, 2); } } // Enhanced OpenLane with better error handling and environment detection async runOpenlane(verilogCode, designName, clockPort = "clk", clockPeriod = 10.0, openInKlayout = true) { try { const projectId = Math.random().toString(36).substring(2, 15); const projectName = `${designName}_${projectId}`; const projectDir = join(this.openlaneDir, projectName); // Store project info this.projects.set(projectId, { dir: projectDir, type: "openlane" }); // Create project directory await fs.mkdir(projectDir, { recursive: true }); // Write Verilog file const verilogFile = join(projectDir, `${designName}.v`); await fs.writeFile(verilogFile, verilogCode); // Create OpenLane config const configContent = { DESIGN_NAME: designName, VERILOG_FILES: [`${designName}.v`], CLOCK_PORT: clockPort, CLOCK_PERIOD: clockPeriod, // Additional OpenLane settings for better results FP_SIZING: "absolute", DIE_AREA: "0 0 100 100", FP_PDN_MULTILAYER: false, QUIT_ON_TIMING_VIOLATIONS: false, QUIT_ON_MAGIC_DRC: false, QUIT_ON_LVS_ERROR: false, RUN_KLAYOUT_XOR: false, RUN_KLAYOUT_DRC: false }; const configFile = join(projectDir, "config.json"); await fs.writeFile(configFile, JSON.stringify(configContent, null, 2)); // Find Python command let pythonCmd = "python3"; const pythonCandidates = [ "python3", "python" ]; for (const candidate of pythonCandidates) { if (await commandExists(candidate)) { pythonCmd = candidate; break; } } // Run the OpenLane command using Docker MCP approach console.error(`Starting OpenLane flow for ${designName}...`); console.error(`Working directory: ${projectDir}`); console.error(`This may take up to 10 minutes...`); const openlaneCmd = `${pythonCmd} -m openlane --dockerized config.json`; console.error(`Executing: ${openlaneCmd}`); // Create a wrapper script to handle TTY issues const wrapperScript = `#!/bin/bash set -e # Disable TTY requirements export DEBIAN_FRONTEND=noninteractive export CI=true export TERM=dumb # Change to project directory cd "${projectDir}" # Run OpenLane with script command to simulate TTY script -q /dev/null ${pythonCmd} -m openlane --dockerized config.json `; const wrapperPath = join(projectDir, 'run_openlane.sh'); await fs.writeFile(wrapperPath, wrapperScript); await execAsyncWithTimeout(`chmod +x "${wrapperPath}"`, {}); // Run the OpenLane command using the wrapper script console.error(`Starting OpenLane flow for ${designName}...`); console.error(`Working directory: ${projectDir}`); console.error(`This may take up to 10 minutes...`); console.error(`Using wrapper script to handle TTY`); const { stdout, stderr } = await execAsyncWithTimeout(`"${wrapperPath}"`, { cwd: projectDir, env: { ...process.env, PATH: process.env.PATH + ":/usr/local/bin:/opt/homebrew/bin" }, maxBuffer: 10 * 1024 * 1024, // 10MB buffer instead of default 1MB timeout: 600000 }, 600000); // 10 minutes timeout // Find the latest run directory const runsDir = join(projectDir, "runs"); let latestRun = ""; let gdsFile = ""; try { const runs = await fs.readdir(runsDir); if (runs.length > 0) { // Sort runs by name (they include timestamps) and get the latest latestRun = runs.sort().reverse()[0]; const finalDir = join(runsDir, latestRun, "final", "gds"); // Find GDS file try { const gdsFiles = await fs.readdir(finalDir); const gdsFilesList = gdsFiles.filter(f => f.endsWith('.gds')); if (gdsFilesList.length > 0) { gdsFile = join(finalDir, gdsFilesList[0]); } } catch { // GDS directory might not exist } } } catch { // Runs directory might not exist } let klayoutResult = ""; // Open in KLayout if requested and GDS file exists if (openInKlayout && gdsFile) { try { // Check if KLayout is available if (await commandExists('klayout')) { // Launch KLayout with the GDS file const klayoutCmd = `open -a KLayout "${gdsFile}" &`; await execAsyncWithTimeout(klayoutCmd, {}, 10000); klayoutResult = `KLayout launched with GDS file: ${basename(gdsFile)}`; } else { klayoutResult = "KLayout not found. Install KLayout to view GDS files."; } } catch (error) { klayoutResult = `KLayout launch failed: ${error.message}`; } } return JSON.stringify({ project_id: projectId, success: true, design_name: designName, project_dir: projectDir, latest_run: latestRun, gds_file: gdsFile ? basename(gdsFile) : "Not generated", gds_path: gdsFile, klayout_status: klayoutResult, command_used: openlaneCmd, stdout: stdout.length > 2000 ? stdout.substring(0, 2000) + "...(truncated)" : stdout, stderr: stderr.length > 2000 ? stderr.substring(0, 2000) + "...(truncated)" : stderr, note: "OpenLane flow completed. Check the runs directory for detailed results." }, null, 2); } catch (error) { // Simple error reporting const errorMessage = error.message || String(error); console.error(`OpenLane error: ${errorMessage}`); return JSON.stringify({ success: false, error: errorMessage, note: "OpenLane flow failed. Make sure Docker is running and try: docker pull efabless/openlane:latest" }, null, 2); } } // Enhanced view GDS with better error handling async viewGds(projectId, gdsFile) { try { const project = this.projects.get(projectId); if (!project) { return JSON.stringify({ success: false, error: `Project ${projectId} not found.`, }, null, 2); } let gdsPath = ""; if (gdsFile) { // Specific GDS file provided gdsPath = join(project.dir, gdsFile); } else { // Auto-find GDS file in OpenLane project const runsDir = join(project.dir, "runs"); try { const runs = await fs.readdir(runsDir); if (runs.length > 0) { const latestRun = runs.sort().reverse()[0]; const finalDir = join(runsDir, latestRun, "final", "gds"); const gdsFiles = await fs.readdir(finalDir); const gdsFilesList = gdsFiles.filter(f => f.endsWith('.gds')); if (gdsFilesList.length > 0) { gdsPath = join(finalDir, gdsFilesList[0]); } } } catch { return JSON.stringify({ success: false, error: "No GDS files found in project. Run OpenLane flow first.", }, null, 2); } } if (!gdsPath) { return JSON.stringify({ success: false, error: "No GDS file found to open.", }, null, 2); } // Check if GDS file exists try { await fs.access(gdsPath); } catch { return JSON.stringify({ success: false, error: `GDS file not found: ${gdsPath}`, }, null, 2); } // Launch KLayout directly with the correct command format const klayoutCmd = `open -a KLayout "${gdsPath}"`; await execAsyncWithTimeout(klayoutCmd, { shell: true }, 10000); return JSON.stringify({ success: true, message: `KLayout launched with GDS file`, gds_file: basename(gdsPath), gds_path: gdsPath, project_id: projectId, command_executed: klayoutCmd }, null, 2); } catch (error) { return JSON.stringify({ success: false, error: error.message || String(error), }, null, 2); } } async readOpenlaneReports(projectId, reportType) { try { const project = this.projects.get(projectId); if (!project) { return JSON.stringify({ success: false, error: `Project ${projectId} not found.`, }, null, 2); } const runsDir = join(project.dir, "runs"); let latestRun = ""; try { const runs = await fs.readdir(runsDir); if (runs.length === 0) { return JSON.stringify({ success: false, error: "No OpenLane runs found. Run OpenLane flow first.", }, null, 2); } latestRun = runs.sort().reverse()[0]; } catch { return JSON.stringify({ success: false, error: "No runs directory found. Run OpenLane flow first.", }, null, 2); } const reportsDir = join(project.dir, "runs", latestRun, "reports"); const finalDir = join(project.dir, "runs", latestRun, "final"); // Simple results object const results = { project_id: projectId, run_id: latestRun, success: true, ppa_metrics: { power_mw: null, max_frequency_mhz: null, total_cells: null, logic_area_um2: null, timing_slack_ns: null }, design_status: { synthesis_complete: false, timing_clean: false, routing_complete: false }, reports: {} }; // Helper to safely read file const readFile = async (path) => { try { return await fs.readFile(path, 'utf8'); } catch { return null; } }; // Read synthesis report const synthReport = await readFile(join(reportsDir, "synthesis", "1-synthesis.stat.rpt")); if (synthReport) { results.design_status.synthesis_complete = true; results.reports.synthesis = synthReport.substring(0, 2000); const cellMatch = synthReport.match(/Number of cells:\s*(\d+)/); if (cellMatch) { results.ppa_metrics.total_cells = parseInt(cellMatch[1]); } } // Read timing report try { const routingDir = join(reportsDir, "routing"); const files = await fs.readdir(routingDir); for (const file of files) { if (file.includes('sta') || file.includes('timing')) { const timingReport = await readFile(join(routingDir, file)); if (timingReport) { results.reports.timing = timingReport.substring(0, 2000); const wnsMatch = timingReport.match(/WNS.*?(-?\d+\.?\d*)/i); if (wnsMatch) { const wns = parseFloat(wnsMatch[1]); results.ppa_metrics.timing_slack_ns = wns; results.design_status.timing_clean = wns >= 0; } break; } } } } catch { // Timing reports not available } // Read final summary if available const finalSummary = await readFile(join(finalDir, "final.summary.rpt")); if (finalSummary) { results.reports.final_summary = finalSummary.substring(0, 3000); results.design_status.routing_complete = true; } // Add analysis summary const issues = []; if (!results.design_status.synthesis_complete) issues.push("Synthesis incomplete"); if (!results.design_status.timing_clean) issues.push("Timing violations detected"); if (!results.design_status.routing_complete) issues.push("Routing incomplete"); results.summary = { status: issues.length === 0 ? "SUCCESS" : "ISSUES_FOUND", issues: issues, note: "PPA metrics and design status extracted from OpenLane reports" }; return JSON.stringify(results, null, 2); } catch (error) { return JSON.stringify({ success: false, error: error.message || String(error), }, null, 2); } } } // Initialize the server const server = new Server({ name: "yosys-server", version: "1.0.0" }, { capabilities: { tools: {} } }); const edaServer = new EDAServer(); // Register tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "synthesize_verilog", description: "Synthesize Verilog code using Yosys for various FPGA targets", inputSchema: { type: "object", properties: { verilog_code: { type: "string", description: "The Verilog source code to synthesize" }, top_module: { type: "string", description: "Name of the top-level module" }, target: { type: "string", description: "Target technology (generic, ice40, xilinx, intel)", default: "generic" }, }, required: ["verilog_code", "top_module"], }, }, { name: "simulate_verilog", description: "Simulate Verilog code using Icarus Verilog", inputSchema: { type: "object", properties: { verilog_code: { type: "string", description: "The Verilog design code" }, testbench_code: { type: "string", description: "The testbench code" }, }, required: ["verilog_code", "testbench_code"], }, }, { name: "view_waveform", description: "Open VCD waveform file in GTKWave viewer", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project ID from simulation (required)" }, vcd_file: { type: "string", description: "VCD filename (default: output.vcd)", default: "output.vcd" }, }, required: ["project_id"], }, }, { name: "run_openlane", description: "Run complete ASIC design flow using OpenLane (RTL to GDSII). This process can take up to 10 minutes.", inputSchema: { type: "object", properties: { verilog_code: { type: "string", description: "The Verilog RTL code for ASIC implementation" }, design_name: { type: "string", description: "Name of the design (will be used for module and files)" }, clock_port: { type: "string", description: "Name of the clock port", default: "clk" }, clock_period: { type: "number", description: "Clock period in nanoseconds", default: 10.0 }, open_in_klayout: { type: "boolean", description: "Automatically open result in KLayout", default: true }, }, required: ["verilog_code", "design_name"], }, }, { name: "view_gds", description: "Open GDSII file in KLayout viewer", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project ID from OpenLane run" }, gds_file: { type: "string", description: "Specific GDS filename (optional, auto-detected if not provided)" }, }, required: ["project_id"], }, }, // Add this object to the tools array, right after the view_gds tool { name: "read_openlane_reports", description: "Read OpenLane report files for LLM analysis. Returns all reports or specific category for detailed analysis of PPA metrics, timing, routing quality, and other design results.", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "Project ID from OpenLane run" }, report_type: { type: "string", description: "Specific report category to read (synthesis, placement, routing, final, etc.). Leave empty to read all reports.", default: "" }, }, required: ["project_id"], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "synthesize_verilog": { const verilogCode = validateRequiredString(args, "verilog_code", name); const topModule = validateRequiredString(args, "top_module", name); const target = getStringProperty(args, "target", "generic"); return { content: [{ type: "text", text: await edaServer.synthesizeVerilog(verilogCode, topModule, target), }], }; } case "simulate_verilog": { const verilogCode = validateRequiredString(args, "verilog_code", name); const testbenchCode = validateRequiredString(args, "testbench_code", name); return { content: [{ type: "text", text: await edaServer.simulateVerilog(verilogCode, testbenchCode), }], }; } case "view_waveform": { const projectId = validateRequiredString(args, "project_id", name); const vcdFile = getStringProperty(args, "vcd_file", "output.vcd"); return { content: [{ type: "text", text: await edaServer.viewWaveform(projectId, vcdFile), }], }; } case "run_openlane": { const verilogCode = validateRequiredString(args, "verilog_code", name); const designName = validateRequiredString(args, "design_name", name); const clockPort = getStringProperty(args, "clock_port", "clk"); const clockPeriod = getNumberProperty(args, "clock_period", 10.0); const openInKlayout = args && args.open_in_klayout !== false; // Default true return { content: [{ type: "text", text: await edaServer.runOpenlane(verilogCode, designName, clockPort, clockPeriod, openInKlayout), }], }; } case "view_gds": { const projectId = validateRequiredString(args, "project_id", name); const gdsFile = getStringProperty(args, "gds_file", ""); return { content: [{ type: "text", text: await edaServer.viewGds(projectId, gdsFile || undefined), }], }; } // Add this case right after the view_gds case case "read_openlane_reports": { const projectId = validateRequiredString(args, "project_id", name); const reportType = getStringProperty(args, "report_type", ""); return { content: [{ type: "text", text: await edaServer.readOpenlaneReports(projectId, reportType || undefined), }], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${name}`); } } catch (error) { if (error instanceof McpError) throw error; const errorMessage = error instanceof Error ? error.message : String(error); throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${errorMessage}`); } }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); // Only log to stderr, never stdout (stdout is for JSON-RPC) console.error("Enhanced Yosys MCP Server running on stdio"); console.error("Features: Synthesis, Simulation, OpenLane ASIC flow"); console.error("OpenLane timeout extended to 10 minutes"); } main().catch((error) => { console.error("Server failed to start:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/NellyW8/mcp-EDA'

If you have feedback or need assistance with the MCP directory API, please join our Discord server