🎉 Major v2.0 rebuild kickoff - Week 1 accomplished! ## Highlights ### Cross-Platform Support 🌍 - ✅ Linux primary platform (Ubuntu/Debian tested) - ✅ Windows fully supported - ✅ macOS experimental support - ✅ Platform-agnostic path handling (XDG spec) - ✅ Auto-detection of KiCAD installation ### Infrastructure 🏗️ - ✅ GitHub Actions CI/CD pipeline - ✅ Pytest framework with 20+ tests - ✅ Pre-commit hooks (Black, MyPy, ESLint) - ✅ Automated Linux installation script - ✅ Enhanced npm scripts ### IPC API Migration Prep 🚀 - ✅ Comprehensive migration plan (30 pages) - ✅ Backend abstraction layer (800+ lines) - ✅ Factory pattern with auto-detection - ✅ SWIG backward compatibility wrapper - ✅ IPC backend skeleton ready ### Documentation 📚 - ✅ Updated README (Linux installation) - ✅ CONTRIBUTING.md guide - ✅ Linux compatibility audit - ✅ IPC API migration plan - ✅ Session summaries - ✅ Platform-specific config templates ## Files Changed - 27 files created - ~3,000 lines of code/docs - 8 comprehensive documentation pages - 20+ unit tests - 5 abstraction layer modules ## Next Steps - Week 2: IPC API migration (project.py → component.py → routing.py) - Migrate from deprecated SWIG to official IPC API - JLCPCB/Digikey integration prep 🤖 Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
501 lines
18 KiB
TypeScript
501 lines
18 KiB
TypeScript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
import { spawn, ChildProcess } from 'child_process';
|
|
import { existsSync } from 'fs';
|
|
import path from 'path';
|
|
|
|
// Import all tool definitions for reference
|
|
// import { registerBoardTools } from './tools/board.js';
|
|
// import { registerComponentTools } from './tools/component.js';
|
|
// import { registerRoutingTools } from './tools/routing.js';
|
|
// import { registerDesignRuleTools } from './tools/design-rules.js';
|
|
// import { registerExportTools } from './tools/export.js';
|
|
// import { registerProjectTools } from './tools/project.js';
|
|
// import { registerSchematicTools } from './tools/schematic.js';
|
|
|
|
class KiCADServer {
|
|
private server: Server;
|
|
private pythonProcess: ChildProcess | null = null;
|
|
private kicadScriptPath: string;
|
|
private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
|
|
private processingRequest = false;
|
|
|
|
constructor() {
|
|
// Set absolute path to the Python KiCAD interface script
|
|
// Using a hardcoded path to avoid cwd() issues when running from Cline
|
|
this.kicadScriptPath = 'c:/repo/KiCAD-MCP/python/kicad_interface.py';
|
|
|
|
// Check if script exists
|
|
if (!existsSync(this.kicadScriptPath)) {
|
|
throw new Error(`KiCAD interface script not found: ${this.kicadScriptPath}`);
|
|
}
|
|
|
|
// Initialize the server
|
|
this.server = new Server(
|
|
{
|
|
name: 'kicad-mcp-server',
|
|
version: '1.0.0'
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {
|
|
// Empty object here, tools will be registered dynamically
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
// Initialize handler with direct pass-through to Python KiCAD interface
|
|
// We don't register TypeScript tools since we'll handle everything in Python
|
|
|
|
// Register tool list handler
|
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
tools: [
|
|
// Project tools
|
|
{
|
|
name: 'create_project',
|
|
description: 'Create a new KiCAD project',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
projectName: { type: 'string', description: 'Name of the new project' },
|
|
path: { type: 'string', description: 'Path where to create the project' },
|
|
template: { type: 'string', description: 'Optional template to use' }
|
|
},
|
|
required: ['projectName']
|
|
}
|
|
},
|
|
{
|
|
name: 'open_project',
|
|
description: 'Open an existing KiCAD project',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
filename: { type: 'string', description: 'Path to the project file' }
|
|
},
|
|
required: ['filename']
|
|
}
|
|
},
|
|
{
|
|
name: 'save_project',
|
|
description: 'Save the current KiCAD project',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
filename: { type: 'string', description: 'Optional path to save to' }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'get_project_info',
|
|
description: 'Get information about the current project',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {}
|
|
}
|
|
},
|
|
|
|
// Board tools
|
|
{
|
|
name: 'set_board_size',
|
|
description: 'Set the size of the PCB board',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
width: { type: 'number', description: 'Board width' },
|
|
height: { type: 'number', description: 'Board height' },
|
|
unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
|
|
},
|
|
required: ['width', 'height']
|
|
}
|
|
},
|
|
{
|
|
name: 'add_board_outline',
|
|
description: 'Add a board outline to the PCB',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
shape: { type: 'string', description: 'Shape of outline (rectangle, circle, polygon, rounded_rectangle)' },
|
|
width: { type: 'number', description: 'Width for rectangle shapes' },
|
|
height: { type: 'number', description: 'Height for rectangle shapes' },
|
|
radius: { type: 'number', description: 'Radius for circle shapes' },
|
|
cornerRadius: { type: 'number', description: 'Corner radius for rounded rectangles' },
|
|
points: { type: 'array', description: 'Array of points for polygon shapes' },
|
|
centerX: { type: 'number', description: 'X coordinate of center' },
|
|
centerY: { type: 'number', description: 'Y coordinate of center' },
|
|
unit: { type: 'string', description: 'Unit of measurement (mm or inch)' }
|
|
}
|
|
}
|
|
},
|
|
|
|
// Component tools
|
|
{
|
|
name: 'place_component',
|
|
description: 'Place a component on the PCB',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
componentId: { type: 'string', description: 'Component ID/footprint to place' },
|
|
position: { type: 'object', description: 'Position coordinates' },
|
|
reference: { type: 'string', description: 'Component reference designator' },
|
|
value: { type: 'string', description: 'Component value' },
|
|
rotation: { type: 'number', description: 'Rotation angle in degrees' },
|
|
layer: { type: 'string', description: 'Layer to place component on' }
|
|
},
|
|
required: ['componentId', 'position']
|
|
}
|
|
},
|
|
|
|
// Routing tools
|
|
{
|
|
name: 'add_net',
|
|
description: 'Add a new net to the PCB',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string', description: 'Net name' },
|
|
class: { type: 'string', description: 'Net class' }
|
|
},
|
|
required: ['name']
|
|
}
|
|
},
|
|
{
|
|
name: 'route_trace',
|
|
description: 'Route a trace between two points or pads',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
start: { type: 'object', description: 'Start point or pad' },
|
|
end: { type: 'object', description: 'End point or pad' },
|
|
layer: { type: 'string', description: 'Layer to route on' },
|
|
width: { type: 'number', description: 'Track width' },
|
|
net: { type: 'string', description: 'Net name' }
|
|
},
|
|
required: ['start', 'end']
|
|
}
|
|
},
|
|
|
|
// Schematic tools
|
|
{
|
|
name: 'create_schematic',
|
|
description: 'Create a new KiCAD schematic',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
projectName: { type: 'string', description: 'Name of the schematic project' },
|
|
path: { type: 'string', description: 'Path where to create the schematic file' },
|
|
metadata: { type: 'object', description: 'Optional metadata for the schematic' }
|
|
},
|
|
required: ['projectName']
|
|
}
|
|
},
|
|
{
|
|
name: 'load_schematic',
|
|
description: 'Load an existing KiCAD schematic',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
filename: { type: 'string', description: 'Path to the schematic file to load' }
|
|
},
|
|
required: ['filename']
|
|
}
|
|
},
|
|
{
|
|
name: 'add_schematic_component',
|
|
description: 'Add a component to a KiCAD schematic',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
schematicPath: { type: 'string', description: 'Path to the schematic file' },
|
|
component: {
|
|
type: 'object',
|
|
description: 'Component definition',
|
|
properties: {
|
|
type: { type: 'string', description: 'Component type (e.g., R, C, LED)' },
|
|
reference: { type: 'string', description: 'Reference designator (e.g., R1, C2)' },
|
|
value: { type: 'string', description: 'Component value (e.g., 10k, 0.1uF)' },
|
|
library: { type: 'string', description: 'Symbol library name' },
|
|
x: { type: 'number', description: 'X position in schematic' },
|
|
y: { type: 'number', description: 'Y position in schematic' },
|
|
rotation: { type: 'number', description: 'Rotation angle in degrees' },
|
|
properties: { type: 'object', description: 'Additional properties' }
|
|
},
|
|
required: ['type', 'reference']
|
|
}
|
|
},
|
|
required: ['schematicPath', 'component']
|
|
}
|
|
},
|
|
{
|
|
name: 'add_schematic_wire',
|
|
description: 'Add a wire connection to a KiCAD schematic',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
schematicPath: { type: 'string', description: 'Path to the schematic file' },
|
|
startPoint: {
|
|
type: 'array',
|
|
description: 'Starting point coordinates [x, y]',
|
|
items: { type: 'number' },
|
|
minItems: 2,
|
|
maxItems: 2
|
|
},
|
|
endPoint: {
|
|
type: 'array',
|
|
description: 'Ending point coordinates [x, y]',
|
|
items: { type: 'number' },
|
|
minItems: 2,
|
|
maxItems: 2
|
|
}
|
|
},
|
|
required: ['schematicPath', 'startPoint', 'endPoint']
|
|
}
|
|
},
|
|
{
|
|
name: 'list_schematic_libraries',
|
|
description: 'List available KiCAD symbol libraries',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
searchPaths: {
|
|
type: 'array',
|
|
description: 'Optional search paths for libraries',
|
|
items: { type: 'string' }
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'export_schematic_pdf',
|
|
description: 'Export a KiCAD schematic to PDF',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
schematicPath: { type: 'string', description: 'Path to the schematic file' },
|
|
outputPath: { type: 'string', description: 'Path for the output PDF file' }
|
|
},
|
|
required: ['schematicPath', 'outputPath']
|
|
}
|
|
}
|
|
]
|
|
}));
|
|
|
|
// Register tool call handler
|
|
this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
|
|
const toolName = request.params.name;
|
|
const args = request.params.arguments || {};
|
|
|
|
// Pass all commands directly to KiCAD Python interface
|
|
try {
|
|
return await this.callKicadScript(toolName, args);
|
|
} catch (error) {
|
|
console.error(`Error executing tool ${toolName}:`, error);
|
|
throw new Error(`Unknown tool: ${toolName}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
async start() {
|
|
try {
|
|
console.error('Starting KiCAD MCP server...');
|
|
|
|
// Start the Python process for KiCAD scripting
|
|
console.error(`Starting Python process with script: ${this.kicadScriptPath}`);
|
|
const pythonExe = 'C:\\Program Files\\KiCad\\9.0\\bin\\python.exe';
|
|
|
|
console.error(`Using Python executable: ${pythonExe}`);
|
|
this.pythonProcess = spawn(pythonExe, [this.kicadScriptPath], {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: {
|
|
...process.env,
|
|
PYTHONPATH: 'C:/Program Files/KiCad/9.0/lib/python3/dist-packages'
|
|
}
|
|
});
|
|
|
|
// Listen for process exit
|
|
this.pythonProcess.on('exit', (code, signal) => {
|
|
console.error(`Python process exited with code ${code} and signal ${signal}`);
|
|
this.pythonProcess = null;
|
|
});
|
|
|
|
// Listen for process errors
|
|
this.pythonProcess.on('error', (err) => {
|
|
console.error(`Python process error: ${err.message}`);
|
|
});
|
|
|
|
// Set up error logging for stderr
|
|
if (this.pythonProcess.stderr) {
|
|
this.pythonProcess.stderr.on('data', (data: Buffer) => {
|
|
console.error(`Python stderr: ${data.toString()}`);
|
|
});
|
|
}
|
|
|
|
// Connect to transport
|
|
const transport = new StdioServerTransport();
|
|
await this.server.connect(transport);
|
|
console.error('KiCAD MCP server running');
|
|
|
|
// Keep the process running
|
|
process.on('SIGINT', () => {
|
|
if (this.pythonProcess) {
|
|
this.pythonProcess.kill();
|
|
}
|
|
this.server.close().catch(console.error);
|
|
process.exit(0);
|
|
});
|
|
|
|
} catch (error: unknown) {
|
|
if (error instanceof Error) {
|
|
console.error('Failed to start MCP server:', error.message);
|
|
} else {
|
|
console.error('Failed to start MCP server: Unknown error');
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
private async callKicadScript(command: string, params: any): Promise<any> {
|
|
return new Promise((resolve, reject) => {
|
|
// Check if Python process is running
|
|
if (!this.pythonProcess) {
|
|
console.error('Python process is not running');
|
|
reject(new Error("Python process for KiCAD scripting is not running"));
|
|
return;
|
|
}
|
|
|
|
// Add request to queue
|
|
this.requestQueue.push({
|
|
request: { command, params },
|
|
resolve,
|
|
reject
|
|
});
|
|
|
|
// Process the queue if not already processing
|
|
if (!this.processingRequest) {
|
|
this.processNextRequest();
|
|
}
|
|
});
|
|
}
|
|
|
|
private processNextRequest(): void {
|
|
// If no more requests or already processing, return
|
|
if (this.requestQueue.length === 0 || this.processingRequest) {
|
|
return;
|
|
}
|
|
|
|
// Set processing flag
|
|
this.processingRequest = true;
|
|
|
|
// Get the next request
|
|
const { request, resolve, reject } = this.requestQueue.shift()!;
|
|
|
|
try {
|
|
console.error(`Processing KiCAD command: ${request.command}`);
|
|
|
|
// Format the command and parameters as JSON
|
|
const requestStr = JSON.stringify(request);
|
|
|
|
// Set up response handling
|
|
let responseData = '';
|
|
|
|
// Clear any previous listeners
|
|
if (this.pythonProcess?.stdout) {
|
|
this.pythonProcess.stdout.removeAllListeners('data');
|
|
}
|
|
|
|
// Set up new listeners
|
|
if (this.pythonProcess?.stdout) {
|
|
this.pythonProcess.stdout.on('data', (data: Buffer) => {
|
|
const chunk = data.toString();
|
|
console.error(`Received data chunk: ${chunk.length} bytes`);
|
|
responseData += chunk;
|
|
|
|
// Check if we have a complete response
|
|
try {
|
|
// Try to parse the response as JSON
|
|
const result = JSON.parse(responseData);
|
|
|
|
// If we get here, we have a valid JSON response
|
|
console.error(`Completed KiCAD command: ${request.command} with result: ${JSON.stringify(result)}`);
|
|
|
|
// Reset processing flag
|
|
this.processingRequest = false;
|
|
|
|
// Process next request if any
|
|
setTimeout(() => this.processNextRequest(), 0);
|
|
|
|
// Clear listeners
|
|
if (this.pythonProcess?.stdout) {
|
|
this.pythonProcess.stdout.removeAllListeners('data');
|
|
}
|
|
|
|
// Resolve with the expected MCP tool response format
|
|
if (result.success) {
|
|
resolve({
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: JSON.stringify(result, null, 2)
|
|
}
|
|
]
|
|
});
|
|
} else {
|
|
resolve({
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: result.errorDetails || result.message || 'Unknown error'
|
|
}
|
|
],
|
|
isError: true
|
|
});
|
|
}
|
|
} catch (e) {
|
|
// Not a complete JSON yet, keep collecting data
|
|
}
|
|
});
|
|
}
|
|
|
|
// Set a timeout
|
|
const timeout = setTimeout(() => {
|
|
console.error(`Command timeout: ${request.command}`);
|
|
|
|
// Clear listeners
|
|
if (this.pythonProcess?.stdout) {
|
|
this.pythonProcess.stdout.removeAllListeners('data');
|
|
}
|
|
|
|
// Reset processing flag
|
|
this.processingRequest = false;
|
|
|
|
// Process next request
|
|
setTimeout(() => this.processNextRequest(), 0);
|
|
|
|
// Reject the promise
|
|
reject(new Error(`Command timeout: ${request.command}`));
|
|
}, 30000); // 30 seconds timeout
|
|
|
|
// Write the request to the Python process
|
|
console.error(`Sending request: ${requestStr}`);
|
|
this.pythonProcess?.stdin?.write(requestStr + '\n');
|
|
} catch (error) {
|
|
console.error(`Error processing request: ${error}`);
|
|
|
|
// Reset processing flag
|
|
this.processingRequest = false;
|
|
|
|
// Process next request
|
|
setTimeout(() => this.processNextRequest(), 0);
|
|
|
|
// Reject the promise
|
|
reject(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start the server
|
|
const server = new KiCADServer();
|
|
server.start().catch(console.error);
|