@codesys/mcp-toolkit 1.4.6 → 1.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/scripts/create_project.py +33 -17
- package/dist/scripts/start_session.py +66 -7
- package/dist/server.js +74 -2
- package/dist/sessionManager.js +42 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.8] - 2025-05-02
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixed critical bug with project creation in persistent sessions
|
|
11
|
+
- Added SKIP_OPEN option to create_project script for session compatibility
|
|
12
|
+
- Improved handling of project creation in sessions to avoid "project in use" errors
|
|
13
|
+
- Added better error recovery when project file exists but can't be opened in a session
|
|
14
|
+
- Added helpful user messages explaining session limitations with project operations
|
|
15
|
+
|
|
16
|
+
## [1.4.7] - 2025-05-02
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fixed create_project freezing in persistent sessions
|
|
19
|
+
- Added special detection and handling for project creation operations
|
|
20
|
+
- Extended timeout for project creation to 120 seconds (from 60 seconds)
|
|
21
|
+
- Added graceful handling of timeouts with partial success detection
|
|
22
|
+
- Improved debugging and monitoring of long-running operations in sessions
|
|
23
|
+
|
|
8
24
|
## [1.4.6] - 2025-05-02
|
|
9
25
|
### Fixed
|
|
10
26
|
- Fixed session management "hanging" issue with start_session
|
|
@@ -4,6 +4,8 @@ import sys, scriptengine as script_engine, os, shutil, time, traceback
|
|
|
4
4
|
# Paths will be correctly escaped in the template substitution
|
|
5
5
|
TEMPLATE_PROJECT_PATH = '{TEMPLATE_PROJECT_PATH}'
|
|
6
6
|
PROJECT_FILE_PATH = '{PROJECT_FILE_PATH}'
|
|
7
|
+
# Optional flag to skip opening the project after creation (useful for sessions)
|
|
8
|
+
SKIP_OPEN = '{SKIP_OPEN}' == 'true'
|
|
7
9
|
|
|
8
10
|
# Print raw paths for debugging
|
|
9
11
|
print("DEBUG: Template path (raw): %s" % repr(TEMPLATE_PROJECT_PATH))
|
|
@@ -61,6 +63,13 @@ try:
|
|
|
61
63
|
|
|
62
64
|
print("DEBUG: File copy complete.")
|
|
63
65
|
|
|
66
|
+
# For sessions, we may want to skip opening the project to avoid conflicts
|
|
67
|
+
if SKIP_OPEN:
|
|
68
|
+
print("DEBUG: SKIP_OPEN is set to true, skipping project open step.")
|
|
69
|
+
print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH)
|
|
70
|
+
print("SCRIPT_SUCCESS: Project copied from template (open step skipped for session compatibility).")
|
|
71
|
+
sys.exit(0)
|
|
72
|
+
|
|
64
73
|
# 2. Open the newly copied project file
|
|
65
74
|
print("DEBUG: Opening the copied project: %s" % PROJECT_FILE_PATH)
|
|
66
75
|
# Set flags for silent opening
|
|
@@ -71,24 +80,31 @@ try:
|
|
|
71
80
|
# print("WARN: VersionUpdateFlags not found, using integer flags for open (1 | 2 = 3).")
|
|
72
81
|
# update_mode = 3
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
try:
|
|
84
|
+
project = script_engine.projects.open(PROJECT_FILE_PATH, update_flags=update_mode)
|
|
85
|
+
print("DEBUG: script_engine.projects.open returned: %s" % project)
|
|
86
|
+
if project:
|
|
87
|
+
print("DEBUG: Pausing briefly after open...")
|
|
88
|
+
time.sleep(1.0) # Allow CODESYS to potentially initialize things
|
|
89
|
+
try:
|
|
90
|
+
print("DEBUG: Explicitly saving project after opening copy...")
|
|
91
|
+
project.save();
|
|
92
|
+
print("DEBUG: Project save after opening copy succeeded.")
|
|
93
|
+
except Exception as save_err:
|
|
94
|
+
print("WARN: Explicit save after opening copy failed: %s" % save_err)
|
|
95
|
+
# Decide if this is critical - maybe not, but good to know.
|
|
96
|
+
print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH)
|
|
97
|
+
print("SCRIPT_SUCCESS: Project copied from template and opened successfully.")
|
|
98
|
+
sys.exit(0)
|
|
99
|
+
else:
|
|
100
|
+
error_message = "Failed to open project copy %s after copying template %s. projects.open returned None." % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH)
|
|
101
|
+
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
102
|
+
except Exception as open_err:
|
|
103
|
+
print("WARN: Error opening project after creation: %s" % open_err)
|
|
104
|
+
# Even though we couldn't open it, the project was still created successfully
|
|
105
|
+
print("Project Created from Template Copy at: %s (but could not be opened automatically)" % PROJECT_FILE_PATH)
|
|
106
|
+
print("SCRIPT_SUCCESS: Project copied from template (but open operation failed: %s)." % open_err)
|
|
88
107
|
sys.exit(0)
|
|
89
|
-
else:
|
|
90
|
-
error_message = "Failed to open project copy %s after copying template %s. projects.open returned None." % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH)
|
|
91
|
-
print(error_message); print("SCRIPT_ERROR: %s" % error_message); sys.exit(1)
|
|
92
108
|
except Exception as e:
|
|
93
109
|
detailed_error = traceback.format_exc()
|
|
94
110
|
error_message = "Error creating project '%s' from template '%s': %s\\n%s" % (PROJECT_FILE_PATH, TEMPLATE_PROJECT_PATH, e, detailed_error)
|
|
@@ -229,9 +229,34 @@ def execute_command(command_data):
|
|
|
229
229
|
"error": ""
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
print("Executing command type: {0} with ID: {1}".format(cmd_type, cmd_id))
|
|
233
|
+
sys.stdout.flush()
|
|
234
|
+
|
|
232
235
|
if cmd_type == "execute_script" and script:
|
|
233
236
|
# Execute the script in the current CODESYS context
|
|
234
237
|
try:
|
|
238
|
+
# Log the first few lines of the script for debugging
|
|
239
|
+
script_lines = script.split("\n")
|
|
240
|
+
print("Script preview (first 3 lines):")
|
|
241
|
+
for i in range(min(3, len(script_lines))):
|
|
242
|
+
print(" {0}: {1}".format(i+1, script_lines[i]))
|
|
243
|
+
sys.stdout.flush()
|
|
244
|
+
|
|
245
|
+
print("Script length: {0} chars, {1} lines".format(len(script), len(script_lines)))
|
|
246
|
+
sys.stdout.flush()
|
|
247
|
+
|
|
248
|
+
# Check for critical operations in the script
|
|
249
|
+
has_project_create = "copy2" in script or "shutil.copy" in script
|
|
250
|
+
has_project_open = "projects.open" in script
|
|
251
|
+
|
|
252
|
+
if has_project_create:
|
|
253
|
+
print("WARNING: Script contains project creation operations - these may require special handling")
|
|
254
|
+
sys.stdout.flush()
|
|
255
|
+
|
|
256
|
+
if has_project_open:
|
|
257
|
+
print("WARNING: Script contains project open operations - these may require special handling")
|
|
258
|
+
sys.stdout.flush()
|
|
259
|
+
|
|
235
260
|
# Create a temporary globals dictionary with the current globals
|
|
236
261
|
# This allows us to isolate the script execution somewhat
|
|
237
262
|
local_globals = globals().copy()
|
|
@@ -250,17 +275,41 @@ def execute_command(command_data):
|
|
|
250
275
|
old_stdout = sys.stdout
|
|
251
276
|
old_stderr = sys.stderr
|
|
252
277
|
|
|
278
|
+
print("Execution starting - output will be captured")
|
|
279
|
+
sys.stdout.flush()
|
|
280
|
+
|
|
253
281
|
# Replace with our buffers
|
|
254
282
|
sys.stdout = stdout_buffer
|
|
255
283
|
sys.stderr = stderr_buffer
|
|
256
284
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
285
|
+
# For critical operations that might deadlock, we need to be more careful
|
|
286
|
+
if has_project_create or has_project_open:
|
|
287
|
+
# For project creation/opening, we'll execute the script with some guards
|
|
288
|
+
print("Executing critical script with guards...")
|
|
289
|
+
try:
|
|
290
|
+
# Execute the script
|
|
291
|
+
exec(script, local_globals)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
# If the script fails, capture the error
|
|
294
|
+
print("ERROR in critical script execution: {0}".format(e))
|
|
295
|
+
traceback.print_exc()
|
|
296
|
+
|
|
297
|
+
print("Critical script execution finished")
|
|
298
|
+
else:
|
|
299
|
+
# For normal scripts, execute normally
|
|
300
|
+
try:
|
|
301
|
+
# Execute the script
|
|
302
|
+
exec(script, local_globals)
|
|
303
|
+
finally:
|
|
304
|
+
# No special handling needed
|
|
305
|
+
pass
|
|
306
|
+
|
|
307
|
+
# Restore original stdout and stderr
|
|
308
|
+
sys.stdout = old_stdout
|
|
309
|
+
sys.stderr = old_stderr
|
|
310
|
+
|
|
311
|
+
print("Execution completed - processing results")
|
|
312
|
+
sys.stdout.flush()
|
|
264
313
|
|
|
265
314
|
# Get the captured output
|
|
266
315
|
stdout_output = stdout_buffer.getvalue()
|
|
@@ -269,18 +318,28 @@ def execute_command(command_data):
|
|
|
269
318
|
# Check for success/error markers
|
|
270
319
|
if SCRIPT_SUCCESS_MARKER in stdout_output or SCRIPT_SUCCESS_MARKER in stderr_output:
|
|
271
320
|
result["success"] = True
|
|
321
|
+
print("Found success marker in script output")
|
|
272
322
|
elif SCRIPT_ERROR_MARKER in stdout_output or SCRIPT_ERROR_MARKER in stderr_output:
|
|
273
323
|
result["success"] = False
|
|
324
|
+
print("Found error marker in script output")
|
|
274
325
|
else:
|
|
275
326
|
# No explicit markers, assume success
|
|
276
327
|
result["success"] = True
|
|
328
|
+
print("No markers found in script output, assuming success")
|
|
329
|
+
|
|
330
|
+
# Tally output length to check if output is reasonable
|
|
331
|
+
print("Output length: stdout={0} chars, stderr={1} chars".format(
|
|
332
|
+
len(stdout_output), len(stderr_output)))
|
|
277
333
|
|
|
278
334
|
result["output"] = stdout_output
|
|
279
335
|
if stderr_output:
|
|
280
336
|
result["error"] = stderr_output
|
|
337
|
+
|
|
338
|
+
sys.stdout.flush()
|
|
281
339
|
|
|
282
340
|
except Exception as e:
|
|
283
341
|
traceback.print_exc()
|
|
342
|
+
sys.stdout.flush()
|
|
284
343
|
result["success"] = False
|
|
285
344
|
result["error"] = "Error executing script: {0}\n{1}".format(str(e), traceback.format_exc())
|
|
286
345
|
result["output"] = "{0}: {1}".format(SCRIPT_ERROR_MARKER, str(e))
|
package/dist/server.js
CHANGED
|
@@ -66,6 +66,7 @@ const zod_1 = require("zod");
|
|
|
66
66
|
const codesys_interop_1 = require("./codesys_interop");
|
|
67
67
|
const path = __importStar(require("path"));
|
|
68
68
|
const promises_1 = require("fs/promises");
|
|
69
|
+
const fs = __importStar(require("fs"));
|
|
69
70
|
const scriptLoader_1 = require("./scriptLoader");
|
|
70
71
|
const sessionManager_1 = require("./sessionManager");
|
|
71
72
|
// --- Wrap server logic in an exported function ---
|
|
@@ -376,14 +377,85 @@ function startMcpServer(config) {
|
|
|
376
377
|
const normalizedTargetPath = absPath.replace(/\\/g, '/');
|
|
377
378
|
const normalizedTemplatePath = templatePath.replace(/\\/g, '/');
|
|
378
379
|
console.error(`Using normalized paths for script: Target=${normalizedTargetPath}, Template=${normalizedTemplatePath}`);
|
|
380
|
+
// When running in a session, skip the open step to avoid conflicts
|
|
381
|
+
const inSession = sessionManager &&
|
|
382
|
+
(sessionManager.getState() === sessionManager_1.SessionState.RUNNING ||
|
|
383
|
+
sessionManager.getState() === sessionManager_1.SessionState.BUSY);
|
|
379
384
|
const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
|
|
380
385
|
'PROJECT_FILE_PATH': normalizedTargetPath,
|
|
381
|
-
'TEMPLATE_PROJECT_PATH': normalizedTemplatePath
|
|
386
|
+
'TEMPLATE_PROJECT_PATH': normalizedTemplatePath,
|
|
387
|
+
'SKIP_OPEN': inSession ? 'true' : 'false'
|
|
382
388
|
});
|
|
383
389
|
console.error(">>> create_project (copy-then-open): PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
|
|
384
|
-
|
|
390
|
+
// Use a longer timeout for project creation operations in sessions (120 seconds)
|
|
391
|
+
// This helps prevent timing out on slower systems or when CODESYS is busy
|
|
392
|
+
let result;
|
|
393
|
+
// Check if we're running in a session
|
|
394
|
+
if (sessionManager &&
|
|
395
|
+
(sessionManager.getState() === sessionManager_1.SessionState.RUNNING ||
|
|
396
|
+
sessionManager.getState() === sessionManager_1.SessionState.BUSY)) {
|
|
397
|
+
console.error(">>> create_project: Using session with extended timeout (120s)");
|
|
398
|
+
try {
|
|
399
|
+
// Execute script directly through session manager to specify longer timeout
|
|
400
|
+
const scriptResult = yield sessionManager.executeScript(scriptContent, 120000);
|
|
401
|
+
result = {
|
|
402
|
+
success: scriptResult.success,
|
|
403
|
+
output: scriptResult.output
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
catch (sessionError) {
|
|
407
|
+
console.error(`>>> create_project: Session execution failed, falling back to one-time execution: ${sessionError.message}`);
|
|
408
|
+
// Fall back to one-time execution
|
|
409
|
+
result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// Standard execution without a session
|
|
414
|
+
result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName, sessionManager);
|
|
415
|
+
}
|
|
385
416
|
console.error(">>> create_project (copy-then-open): EXECUTION RESULT:", JSON.stringify(result));
|
|
417
|
+
// Check if there was a timeout but the project might still have been created
|
|
418
|
+
if (!result.success && result.output.includes("timed out") && result.output.includes("project file might still have been created")) {
|
|
419
|
+
console.error(">>> create_project: Script timed out but project might have been created. Checking if file exists.");
|
|
420
|
+
// Check if the project file was actually created despite the timeout
|
|
421
|
+
const fileExists = fs.existsSync(absPath);
|
|
422
|
+
if (fileExists) {
|
|
423
|
+
console.error(`>>> create_project: Project file ${absPath} exists despite timeout.`);
|
|
424
|
+
return {
|
|
425
|
+
content: [{
|
|
426
|
+
type: "text",
|
|
427
|
+
text: `Project file was created at ${absPath}, but the operation timed out before completion. ` +
|
|
428
|
+
`The project might be usable, but you may need to manually open it in CODESYS.`
|
|
429
|
+
}],
|
|
430
|
+
isError: true // Still mark as error because operation didn't complete properly
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
386
434
|
const success = result.success && result.output.includes("SCRIPT_SUCCESS");
|
|
435
|
+
// Special handling for cases where project was created but couldn't be opened automatically
|
|
436
|
+
if (success && inSession && result.output.includes("open operation failed")) {
|
|
437
|
+
return {
|
|
438
|
+
content: [{
|
|
439
|
+
type: "text",
|
|
440
|
+
text: `Project created from template: ${absPath}\n` +
|
|
441
|
+
`Note: The project was created successfully, but couldn't be opened automatically because you're in a session.\n` +
|
|
442
|
+
`You can use open_project to open it now, or create a POU directly with create_pou.`
|
|
443
|
+
}],
|
|
444
|
+
isError: false
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
// Special handling for skipped open operations in sessions
|
|
448
|
+
if (success && inSession && result.output.includes("open step skipped for session compatibility")) {
|
|
449
|
+
return {
|
|
450
|
+
content: [{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: `Project created from template: ${absPath}\n` +
|
|
453
|
+
`Note: The project opening step was skipped because you're in a session.\n` +
|
|
454
|
+
`You can use open_project to open it now, or create a POU directly with create_pou.`
|
|
455
|
+
}],
|
|
456
|
+
isError: false
|
|
457
|
+
};
|
|
458
|
+
}
|
|
387
459
|
return {
|
|
388
460
|
content: [{
|
|
389
461
|
type: "text",
|
package/dist/sessionManager.js
CHANGED
|
@@ -667,9 +667,19 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
667
667
|
*/
|
|
668
668
|
readResultFile(scriptId) {
|
|
669
669
|
console.error(`SESSION_MANAGER: Starting to read result file for script ${scriptId}`);
|
|
670
|
+
// Count polling attempts for logging purposes
|
|
671
|
+
let pollCount = 0;
|
|
672
|
+
let lastLogTime = Date.now();
|
|
670
673
|
// We'll use a polling approach to read from the file
|
|
671
674
|
const pollInterval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
672
675
|
try {
|
|
676
|
+
pollCount++;
|
|
677
|
+
const now = Date.now();
|
|
678
|
+
// Log progress every 5 seconds to show we're still working
|
|
679
|
+
if (now - lastLogTime > 5000) {
|
|
680
|
+
console.error(`SESSION_MANAGER: Still polling for result of script ${scriptId}, attempt ${pollCount}`);
|
|
681
|
+
lastLogTime = now;
|
|
682
|
+
}
|
|
673
683
|
if (!this.pendingScripts.has(scriptId)) {
|
|
674
684
|
console.error(`SESSION_MANAGER: Script ${scriptId} is no longer pending, stopping poll`);
|
|
675
685
|
clearInterval(pollInterval);
|
|
@@ -677,7 +687,9 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
677
687
|
}
|
|
678
688
|
// Check if the result file exists
|
|
679
689
|
if (!fs.existsSync(this.resultFilePath)) {
|
|
680
|
-
|
|
690
|
+
if (pollCount % 50 === 0) { // Log less frequently
|
|
691
|
+
console.error(`SESSION_MANAGER: Result file not found at ${this.resultFilePath} (attempt ${pollCount})`);
|
|
692
|
+
}
|
|
681
693
|
return;
|
|
682
694
|
}
|
|
683
695
|
// Read the result file
|
|
@@ -689,7 +701,7 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
689
701
|
if (resultData.status === 'complete' && resultData.result) {
|
|
690
702
|
const result = resultData.result;
|
|
691
703
|
if (result.id === scriptId) {
|
|
692
|
-
console.error(`SESSION_MANAGER: Found result for script ${scriptId}`);
|
|
704
|
+
console.error(`SESSION_MANAGER: Found result for script ${scriptId} after ${pollCount} polls`);
|
|
693
705
|
// Remove from pending scripts
|
|
694
706
|
if (this.pendingScripts.has(scriptId)) {
|
|
695
707
|
const pending = this.pendingScripts.get(scriptId);
|
|
@@ -717,9 +729,36 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
717
729
|
console.error(`SESSION_MANAGER: Error reading result file: ${error}`);
|
|
718
730
|
}
|
|
719
731
|
}), 100); // Poll every 100ms
|
|
720
|
-
// Set a timeout to stop polling after the script timeout
|
|
732
|
+
// Set a timeout to stop polling and forcibly resolve/reject the script after the script timeout
|
|
721
733
|
setTimeout(() => {
|
|
734
|
+
console.error(`SESSION_MANAGER: Max polling time reached for script ${scriptId}, cleaning up`);
|
|
722
735
|
clearInterval(pollInterval);
|
|
736
|
+
// If the script is still pending, force resolve/reject with timeout error
|
|
737
|
+
if (this.pendingScripts.has(scriptId)) {
|
|
738
|
+
const pending = this.pendingScripts.get(scriptId);
|
|
739
|
+
this.pendingScripts.delete(scriptId);
|
|
740
|
+
// Create a timeout result
|
|
741
|
+
const timeoutResult = {
|
|
742
|
+
id: scriptId,
|
|
743
|
+
success: false,
|
|
744
|
+
output: `SCRIPT_ERROR: Script execution timed out after reaching maximum polling time. This typically happens when:\n` +
|
|
745
|
+
`1. The script is stuck in an infinite loop\n` +
|
|
746
|
+
`2. The script has triggered a deadlock in CODESYS\n` +
|
|
747
|
+
`3. A CODESYS dialog or error popup is blocking execution\n` +
|
|
748
|
+
`4. The project creation or opening is taking too long\n\n` +
|
|
749
|
+
`If this is a project creation script, the project file might still have been created successfully.\n` +
|
|
750
|
+
`Check the CODESYS UI to see if a project was created and manually close any dialog boxes.`
|
|
751
|
+
};
|
|
752
|
+
// Resolve with the timeout result rather than rejecting
|
|
753
|
+
// This allows the session to continue operating instead of going into error state
|
|
754
|
+
console.error(`SESSION_MANAGER: Resolving script ${scriptId} with timeout result`);
|
|
755
|
+
pending.resolve(timeoutResult);
|
|
756
|
+
}
|
|
757
|
+
// Check if we need to update session state
|
|
758
|
+
if (this.pendingScripts.size === 0) {
|
|
759
|
+
console.error(`SESSION_MANAGER: No more pending scripts, setting state to RUNNING`);
|
|
760
|
+
this.sessionState = exports.SessionState.RUNNING;
|
|
761
|
+
}
|
|
723
762
|
}, 60000); // 1 minute max polling time
|
|
724
763
|
}
|
|
725
764
|
/**
|