Merge pull request #17 from fariouche/main
Fixes create_schematic timeout
This commit is contained in:
19
package-lock.json
generated
19
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cfworker/json-schema": "^4.1.1",
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
"@types/glob": "^8.1.0",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
@@ -107,6 +108,17 @@
|
|||||||
"@types/send": "*"
|
"@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": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||||
@@ -121,6 +133,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.24",
|
"version": "20.19.24",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cfworker/json-schema": "^4.1.1",
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
"@types/express": "^5.0.5",
|
"@types/express": "^5.0.5",
|
||||||
|
"@types/glob": "^8.1.0",
|
||||||
"@types/node": "^20.19.0",
|
"@types/node": "^20.19.0",
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class ProjectCommands:
|
|||||||
def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
def create_project(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""Create a new KiCAD project"""
|
"""Create a new KiCAD project"""
|
||||||
try:
|
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())
|
path = params.get("path", os.getcwd())
|
||||||
template = params.get("template")
|
template = params.get("template")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from skip import Schematic
|
from skip import Schematic
|
||||||
import os
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger('kicad_interface')
|
||||||
|
|
||||||
class SchematicManager:
|
class SchematicManager:
|
||||||
"""Core schematic operations using kicad-skip"""
|
"""Core schematic operations using kicad-skip"""
|
||||||
@@ -33,21 +36,21 @@ class SchematicManager:
|
|||||||
# For now, we'll just create the schematic.
|
# For now, we'll just create the schematic.
|
||||||
pass # Placeholder for potential metadata handling
|
pass # Placeholder for potential metadata handling
|
||||||
|
|
||||||
print(f"Created new schematic: {name}")
|
logger.info(f"Created new schematic: {name}")
|
||||||
return sch
|
return sch
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_schematic(file_path):
|
def load_schematic(file_path):
|
||||||
"""Load an existing schematic"""
|
"""Load an existing schematic"""
|
||||||
if not os.path.exists(file_path):
|
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
|
return None
|
||||||
try:
|
try:
|
||||||
sch = Schematic(file_path)
|
sch = Schematic(file_path)
|
||||||
print(f"Loaded schematic from: {file_path}")
|
logger.info(f"Loaded schematic from: {file_path}")
|
||||||
return sch
|
return sch
|
||||||
except Exception as e:
|
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
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -56,10 +59,10 @@ class SchematicManager:
|
|||||||
try:
|
try:
|
||||||
# kicad-skip uses write method, not save
|
# kicad-skip uses write method, not save
|
||||||
schematic.write(file_path)
|
schematic.write(file_path)
|
||||||
print(f"Saved schematic to: {file_path}")
|
logger.info(f"Saved schematic to: {file_path}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -72,7 +75,7 @@ class SchematicManager:
|
|||||||
"generator": schematic.generator,
|
"generator": schematic.generator,
|
||||||
# Add other relevant properties if needed
|
# Add other relevant properties if needed
|
||||||
}
|
}
|
||||||
print("Extracted schematic metadata")
|
logger.debug("Extracted schematic metadata")
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -454,10 +454,11 @@ class KiCADInterface:
|
|||||||
"""Create a new schematic"""
|
"""Create a new schematic"""
|
||||||
logger.info("Creating schematic")
|
logger.info("Creating schematic")
|
||||||
try:
|
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", ".")
|
path = params.get("path", ".")
|
||||||
metadata = params.get("metadata", {})
|
metadata = params.get("metadata", {})
|
||||||
|
|
||||||
if not project_name:
|
if not project_name:
|
||||||
return {"success": False, "message": "Project name is required"}
|
return {"success": False, "message": "Project name is required"}
|
||||||
|
|
||||||
|
|||||||
159
src/server.ts
159
src/server.ts
@@ -82,6 +82,8 @@ export class KiCADMcpServer {
|
|||||||
private stdioTransport!: StdioServerTransport;
|
private stdioTransport!: StdioServerTransport;
|
||||||
private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
|
private requestQueue: Array<{ request: any, resolve: Function, reject: Function }> = [];
|
||||||
private processingRequest = false;
|
private processingRequest = false;
|
||||||
|
private responseBuffer: string = '';
|
||||||
|
private currentRequestHandler: { resolve: Function, reject: Function, timeoutHandle: NodeJS.Timeout } | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor for the KiCAD MCP Server
|
* Constructor for the KiCAD MCP Server
|
||||||
@@ -290,7 +292,14 @@ export class KiCADMcpServer {
|
|||||||
logger.error(`Python stderr: ${data.toString()}`);
|
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
|
// Connect server to STDIO transport
|
||||||
logger.info('Connecting MCP server to STDIO transport...');
|
logger.info('Connecting MCP server to STDIO transport...');
|
||||||
try {
|
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
|
* Process the next request in the queue
|
||||||
*/
|
*/
|
||||||
@@ -373,102 +440,56 @@ export class KiCADMcpServer {
|
|||||||
if (this.requestQueue.length === 0 || this.processingRequest) {
|
if (this.requestQueue.length === 0 || this.processingRequest) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set processing flag
|
// Set processing flag
|
||||||
this.processingRequest = true;
|
this.processingRequest = true;
|
||||||
|
|
||||||
// Get the next request
|
// Get the next request
|
||||||
const { request, resolve, reject } = this.requestQueue.shift()!;
|
const { request, resolve, reject } = this.requestQueue.shift()!;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`Processing KiCAD command: ${request.command}`);
|
logger.debug(`Processing KiCAD command: ${request.command}`);
|
||||||
|
|
||||||
// Format the command and parameters as JSON
|
// Format the command and parameters as JSON
|
||||||
const requestStr = JSON.stringify(request);
|
const requestStr = JSON.stringify(request);
|
||||||
|
|
||||||
// Set up response handling
|
|
||||||
let responseData = '';
|
|
||||||
let timeoutHandle: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
// Clear any previous listeners
|
// Clear response buffer for new request
|
||||||
if (this.pythonProcess?.stdout) {
|
this.responseBuffer = '';
|
||||||
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
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a timeout (use command-specific timeout or default)
|
// Set a timeout (use command-specific timeout or default)
|
||||||
const timeoutDuration = request.timeout || 30000;
|
const timeoutDuration = request.timeout || 30000;
|
||||||
timeoutHandle = setTimeout(() => {
|
const timeoutHandle = setTimeout(() => {
|
||||||
logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`);
|
logger.error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`);
|
||||||
|
logger.error(`Buffer contents: ${this.responseBuffer.substring(0, 200)}...`);
|
||||||
|
|
||||||
// Clear listeners
|
// Clear state
|
||||||
if (this.pythonProcess?.stdout) {
|
this.responseBuffer = '';
|
||||||
this.pythonProcess.stdout.removeAllListeners('data');
|
this.currentRequestHandler = null;
|
||||||
this.pythonProcess.stdout.removeAllListeners('end');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset processing flag
|
|
||||||
this.processingRequest = false;
|
this.processingRequest = false;
|
||||||
|
|
||||||
// Process next request
|
|
||||||
setTimeout(() => this.processNextRequest(), 0);
|
|
||||||
|
|
||||||
// Reject the promise
|
// Reject the promise
|
||||||
reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`));
|
reject(new Error(`Command timeout after ${timeoutDuration/1000}s: ${request.command}`));
|
||||||
|
|
||||||
|
// Process next request
|
||||||
|
setTimeout(() => this.processNextRequest(), 0);
|
||||||
}, timeoutDuration);
|
}, timeoutDuration);
|
||||||
|
|
||||||
|
// Store the current request handler
|
||||||
|
this.currentRequestHandler = { resolve, reject, timeoutHandle };
|
||||||
|
|
||||||
// Write the request to the Python process
|
// Write the request to the Python process
|
||||||
logger.debug(`Sending request: ${requestStr}`);
|
logger.debug(`Sending request: ${requestStr}`);
|
||||||
this.pythonProcess?.stdin?.write(requestStr + '\n');
|
this.pythonProcess?.stdin?.write(requestStr + '\n');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error processing request: ${error}`);
|
logger.error(`Error processing request: ${error}`);
|
||||||
|
|
||||||
// Reset processing flag
|
// Reset processing flag
|
||||||
this.processingRequest = false;
|
this.processingRequest = false;
|
||||||
|
this.currentRequestHandler = null;
|
||||||
|
|
||||||
// Process next request
|
// Process next request
|
||||||
setTimeout(() => this.processNextRequest(), 0);
|
setTimeout(() => this.processNextRequest(), 0);
|
||||||
|
|
||||||
// Reject the promise
|
// Reject the promise
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user