diff --git a/package-lock.json b/package-lock.json index 46e69c1..e806c4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "devDependencies": { "@cfworker/json-schema": "^4.1.1", "@types/express": "^5.0.5", + "@types/glob": "^8.1.0", "@types/node": "^20.19.0", "nodemon": "^3.0.1", "typescript": "^5.9.3" @@ -107,6 +108,17 @@ "@types/send": "*" } }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -121,6 +133,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", diff --git a/package.json b/package.json index 1806aeb..64e5e62 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "devDependencies": { "@cfworker/json-schema": "^4.1.1", "@types/express": "^5.0.5", + "@types/glob": "^8.1.0", "@types/node": "^20.19.0", "nodemon": "^3.0.1", "typescript": "^5.9.3" diff --git a/python/commands/project.py b/python/commands/project.py index 52bf1b2..f284c38 100644 --- a/python/commands/project.py +++ b/python/commands/project.py @@ -19,7 +19,8 @@ class ProjectCommands: def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]: """Create a new KiCAD project""" try: - project_name = params.get("projectName", "New_Project") + # Accept both 'name' (from MCP tool) and 'projectName' (legacy) + project_name = params.get("name") or params.get("projectName", "New_Project") path = params.get("path", os.getcwd()) template = params.get("template") diff --git a/python/commands/schematic.py b/python/commands/schematic.py index edeb5ee..e481a60 100644 --- a/python/commands/schematic.py +++ b/python/commands/schematic.py @@ -1,5 +1,8 @@ from skip import Schematic import os +import logging + +logger = logging.getLogger('kicad_interface') class SchematicManager: """Core schematic operations using kicad-skip""" @@ -33,21 +36,21 @@ class SchematicManager: # For now, we'll just create the schematic. pass # Placeholder for potential metadata handling - print(f"Created new schematic: {name}") + logger.info(f"Created new schematic: {name}") return sch @staticmethod def load_schematic(file_path): """Load an existing schematic""" if not os.path.exists(file_path): - print(f"Error: Schematic file not found at {file_path}") + logger.error(f"Schematic file not found at {file_path}") return None try: sch = Schematic(file_path) - print(f"Loaded schematic from: {file_path}") + logger.info(f"Loaded schematic from: {file_path}") return sch except Exception as e: - print(f"Error loading schematic from {file_path}: {e}") + logger.error(f"Error loading schematic from {file_path}: {e}") return None @staticmethod @@ -56,10 +59,10 @@ class SchematicManager: try: # kicad-skip uses write method, not save schematic.write(file_path) - print(f"Saved schematic to: {file_path}") + logger.info(f"Saved schematic to: {file_path}") return True except Exception as e: - print(f"Error saving schematic to {file_path}: {e}") + logger.error(f"Error saving schematic to {file_path}: {e}") return False @staticmethod @@ -72,7 +75,7 @@ class SchematicManager: "generator": schematic.generator, # Add other relevant properties if needed } - print("Extracted schematic metadata") + logger.debug("Extracted schematic metadata") return metadata if __name__ == '__main__': diff --git a/python/kicad_interface.py b/python/kicad_interface.py index 9b5922b..cbef060 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -454,10 +454,11 @@ class KiCADInterface: """Create a new schematic""" logger.info("Creating schematic") try: - project_name = params.get("projectName") + # Accept both 'name' (from MCP tool) and 'projectName' (legacy) + project_name = params.get("name") or params.get("projectName") path = params.get("path", ".") metadata = params.get("metadata", {}) - + if not project_name: return {"success": False, "message": "Project name is required"} diff --git a/src/server.ts b/src/server.ts index d8e27a8..4a0a2c9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -82,6 +82,8 @@ export class KiCADMcpServer { private stdioTransport!: StdioServerTransport; private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = []; private processingRequest = false; + private responseBuffer: string = ''; + private currentRequestHandler: { resolve: Function, reject: Function, timeoutHandle: NodeJS.Timeout } | null = null; /** * Constructor for the KiCAD MCP Server @@ -290,7 +292,14 @@ export class KiCADMcpServer { logger.error(`Python stderr: ${data.toString()}`); }); } - + + // Set up persistent stdout handler (instead of adding/removing per request) + if (this.pythonProcess.stdout) { + this.pythonProcess.stdout.on('data', (data: Buffer) => { + this.handlePythonResponse(data); + }); + } + // Connect server to STDIO transport logger.info('Connecting MCP server to STDIO transport...'); try { @@ -365,6 +374,64 @@ export class KiCADMcpServer { }); } + /** + * Handle incoming data from Python process stdout + * This is a persistent handler that processes all responses + */ + private handlePythonResponse(data: Buffer): void { + const chunk = data.toString(); + logger.debug(`Received data chunk: ${chunk.length} bytes`); + this.responseBuffer += chunk; + + // Try to parse complete JSON responses (may have multiple or partial) + this.tryParseResponse(); + } + + /** + * Try to parse a complete JSON response from the buffer + */ + private tryParseResponse(): void { + if (!this.currentRequestHandler) { + // No pending request, clear buffer if it has data (shouldn't happen) + if (this.responseBuffer.trim()) { + logger.warn(`Received data with no pending request: ${this.responseBuffer.substring(0, 100)}...`); + this.responseBuffer = ''; + } + return; + } + + try { + // Try to parse the response as JSON + const result = JSON.parse(this.responseBuffer); + + // If we get here, we have a valid JSON response + logger.debug(`Completed KiCAD command with result: ${result.success ? 'success' : 'failure'}`); + + // Clear the timeout since we got a response + if (this.currentRequestHandler.timeoutHandle) { + clearTimeout(this.currentRequestHandler.timeoutHandle); + } + + // Get the handler before clearing + const handler = this.currentRequestHandler; + + // Clear state + this.responseBuffer = ''; + this.currentRequestHandler = null; + this.processingRequest = false; + + // Resolve the promise with the result + handler.resolve(result); + + // Process next request if any + setTimeout(() => this.processNextRequest(), 0); + + } catch (e) { + // Not a complete JSON yet, keep collecting data + // This is normal for large responses that come in chunks + } + } + /** * Process the next request in the queue */ @@ -373,102 +440,56 @@ export class KiCADMcpServer { 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 { logger.debug(`Processing KiCAD command: ${request.command}`); - + // Format the command and parameters as JSON const requestStr = JSON.stringify(request); - - // Set up response handling - let responseData = ''; - let timeoutHandle: NodeJS.Timeout | null = null; - // Clear any previous listeners - if (this.pythonProcess?.stdout) { - this.pythonProcess.stdout.removeAllListeners('data'); - this.pythonProcess.stdout.removeAllListeners('end'); - } - - // Set up new listeners - if (this.pythonProcess?.stdout) { - this.pythonProcess.stdout.on('data', (data: Buffer) => { - const chunk = data.toString(); - logger.debug(`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 - logger.debug(`Completed KiCAD command: ${request.command} with result: ${result.success ? 'success' : 'failure'}`); - - // Clear the timeout since we got a response - if (timeoutHandle) { - clearTimeout(timeoutHandle); - } - - // 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'); - this.pythonProcess.stdout.removeAllListeners('end'); - } - - // Resolve the promise with the result - resolve(result); - } catch (e) { - // Not a complete JSON yet, keep collecting data - } - }); - } + // Clear response buffer for new request + this.responseBuffer = ''; // Set a timeout (use command-specific timeout or default) const timeoutDuration = request.timeout || 30000; - timeoutHandle = setTimeout(() => { + const timeoutHandle = setTimeout(() => { logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`); + logger.error(`Buffer contents: ${this.responseBuffer.substring(0, 200)}...`); - // Clear listeners - if (this.pythonProcess?.stdout) { - this.pythonProcess.stdout.removeAllListeners('data'); - this.pythonProcess.stdout.removeAllListeners('end'); - } - - // Reset processing flag + // Clear state + this.responseBuffer = ''; + this.currentRequestHandler = null; this.processingRequest = false; - // Process next request - setTimeout(() => this.processNextRequest(), 0); - // Reject the promise reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`)); + + // Process next request + setTimeout(() => this.processNextRequest(), 0); }, timeoutDuration); - + + // Store the current request handler + this.currentRequestHandler = { resolve, reject, timeoutHandle }; + // Write the request to the Python process logger.debug(`Sending request: ${requestStr}`); this.pythonProcess?.stdin?.write(requestStr + '\n'); } catch (error) { logger.error(`Error processing request: ${error}`); - + // Reset processing flag this.processingRequest = false; - + this.currentRequestHandler = null; + // Process next request setTimeout(() => this.processNextRequest(), 0); - + // Reject the promise reject(error); }