@codesys/mcp-toolkit 1.4.5 → 1.4.7
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 +15 -0
- package/dist/scripts/start_session.py +93 -8
- package/dist/server.js +45 -2
- package/dist/sessionManager.js +42 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,21 @@ 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.7] - 2025-05-02
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixed create_project freezing in persistent sessions
|
|
11
|
+
- Added special detection and handling for project creation operations
|
|
12
|
+
- Extended timeout for project creation to 120 seconds (from 60 seconds)
|
|
13
|
+
- Added graceful handling of timeouts with partial success detection
|
|
14
|
+
- Improved debugging and monitoring of long-running operations in sessions
|
|
15
|
+
|
|
16
|
+
## [1.4.6] - 2025-05-02
|
|
17
|
+
### Fixed
|
|
18
|
+
- Fixed session management "hanging" issue with start_session
|
|
19
|
+
- Added stdout flushing in Python scripts to ensure output is visible
|
|
20
|
+
- Improved session script logging for better debugging
|
|
21
|
+
- Added more detailed state information in session scripts
|
|
22
|
+
|
|
8
23
|
## [1.4.5] - 2025-05-02
|
|
9
24
|
### Fixed
|
|
10
25
|
- Completely redesigned path handling for Windows paths
|
|
@@ -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))
|
|
@@ -387,6 +446,7 @@ def check_command():
|
|
|
387
446
|
|
|
388
447
|
# Execute the command
|
|
389
448
|
print("Executing command: {0}".format(command.get("type", "unknown")))
|
|
449
|
+
sys.stdout.flush() # Ensure output is flushed before potentially long-running operation
|
|
390
450
|
result, stop_requested = execute_command(command)
|
|
391
451
|
print("Command execution complete, stop_requested: {0}".format(stop_requested))
|
|
392
452
|
|
|
@@ -423,6 +483,7 @@ def check_command():
|
|
|
423
483
|
except Exception as cmd_err:
|
|
424
484
|
print("Error processing command: {0}".format(cmd_err))
|
|
425
485
|
traceback.print_exc()
|
|
486
|
+
sys.stdout.flush() # Ensure error output is flushed
|
|
426
487
|
|
|
427
488
|
# Try to reset the command file
|
|
428
489
|
try:
|
|
@@ -448,6 +509,7 @@ def check_command():
|
|
|
448
509
|
except Exception as e:
|
|
449
510
|
print("Error in check_command: {0}".format(e))
|
|
450
511
|
traceback.print_exc()
|
|
512
|
+
sys.stdout.flush() # Ensure error output is flushed
|
|
451
513
|
return False
|
|
452
514
|
|
|
453
515
|
def update_last_activity():
|
|
@@ -497,6 +559,7 @@ def command_loop():
|
|
|
497
559
|
print("Session state file: {0}".format(SESSION_STATE_FILE))
|
|
498
560
|
print("Poll interval: {0} seconds".format(POLL_INTERVAL))
|
|
499
561
|
print("Max inactivity time: {0} seconds".format(MAX_INACTIVITY_TIME))
|
|
562
|
+
sys.stdout.flush() # Ensure initialization output is visible immediately
|
|
500
563
|
|
|
501
564
|
# Set up initial session state files
|
|
502
565
|
try:
|
|
@@ -519,9 +582,14 @@ def command_loop():
|
|
|
519
582
|
"timestamp": time.time()
|
|
520
583
|
}, f, indent=2)
|
|
521
584
|
print("Created initial result file")
|
|
585
|
+
sys.stdout.flush()
|
|
522
586
|
except Exception as setup_err:
|
|
523
587
|
print("Error setting up initial files: {0}".format(setup_err))
|
|
524
588
|
traceback.print_exc()
|
|
589
|
+
sys.stdout.flush()
|
|
590
|
+
|
|
591
|
+
print("Entering main command loop...")
|
|
592
|
+
sys.stdout.flush()
|
|
525
593
|
|
|
526
594
|
while not stop_requested:
|
|
527
595
|
try:
|
|
@@ -536,24 +604,27 @@ def command_loop():
|
|
|
536
604
|
if current_time - last_heartbeat > 60:
|
|
537
605
|
print("Command loop heartbeat - still alive (iteration {0})".format(loop_count))
|
|
538
606
|
last_heartbeat = current_time
|
|
607
|
+
sys.stdout.flush()
|
|
539
608
|
|
|
540
609
|
# Check for new commands
|
|
541
|
-
print("Checking for commands...")
|
|
542
610
|
stop_requested = check_command()
|
|
543
611
|
if stop_requested:
|
|
544
612
|
print("Stop was requested by a command")
|
|
613
|
+
sys.stdout.flush()
|
|
545
614
|
|
|
546
615
|
# Check for inactivity timeout
|
|
547
616
|
if check_inactivity_timeout():
|
|
548
617
|
print("Session timeout due to inactivity")
|
|
549
618
|
update_session_state("timeout")
|
|
550
619
|
stop_requested = True
|
|
620
|
+
sys.stdout.flush()
|
|
551
621
|
|
|
552
622
|
# Periodically update session state (every 30 seconds)
|
|
553
623
|
if (current_time - last_state_update) > 30:
|
|
554
624
|
print("Periodic session state update")
|
|
555
625
|
update_session_state()
|
|
556
626
|
last_state_update = current_time
|
|
627
|
+
sys.stdout.flush()
|
|
557
628
|
|
|
558
629
|
# Sleep for a bit
|
|
559
630
|
time.sleep(POLL_INTERVAL)
|
|
@@ -561,34 +632,48 @@ def command_loop():
|
|
|
561
632
|
except Exception as e:
|
|
562
633
|
print("Error in command loop: {0}".format(e))
|
|
563
634
|
traceback.print_exc()
|
|
635
|
+
sys.stdout.flush()
|
|
564
636
|
# Sleep for a bit before retrying
|
|
565
637
|
time.sleep(POLL_INTERVAL)
|
|
566
638
|
|
|
567
639
|
print("Command loop stopped after {0} iterations".format(loop_count))
|
|
640
|
+
sys.stdout.flush()
|
|
568
641
|
|
|
569
642
|
# Main script
|
|
570
643
|
def main():
|
|
571
644
|
try:
|
|
645
|
+
print("Starting CODESYS automation session...")
|
|
646
|
+
sys.stdout.flush()
|
|
647
|
+
|
|
572
648
|
# Setup session directory and files
|
|
649
|
+
print("Setting up session directory...")
|
|
650
|
+
sys.stdout.flush()
|
|
573
651
|
if not setup_session_dir():
|
|
652
|
+
print("Failed to set up session directory")
|
|
653
|
+
sys.stdout.flush()
|
|
574
654
|
sys.exit(1)
|
|
575
655
|
|
|
576
656
|
# Update initial session state
|
|
657
|
+
print("Updating initial session state...")
|
|
658
|
+
sys.stdout.flush()
|
|
577
659
|
update_session_state()
|
|
578
660
|
|
|
579
661
|
# Start command loop
|
|
580
662
|
print("{0}: CODESYS session started".format(SCRIPT_SUCCESS_MARKER))
|
|
663
|
+
sys.stdout.flush()
|
|
581
664
|
command_loop()
|
|
582
665
|
|
|
583
666
|
# Update final state
|
|
584
667
|
update_session_state("stopped")
|
|
585
668
|
|
|
586
669
|
print("{0}: CODESYS session stopped cleanly".format(SCRIPT_SUCCESS_MARKER))
|
|
670
|
+
sys.stdout.flush()
|
|
587
671
|
sys.exit(0)
|
|
588
672
|
except Exception as e:
|
|
589
673
|
print("Error in session: {0}".format(e))
|
|
590
674
|
print("{0}: Session failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
591
675
|
traceback.print_exc()
|
|
676
|
+
sys.stdout.flush()
|
|
592
677
|
sys.exit(1)
|
|
593
678
|
|
|
594
679
|
if __name__ == "__main__":
|
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 ---
|
|
@@ -130,7 +131,7 @@ function startMcpServer(config) {
|
|
|
130
131
|
console.error("SERVER.TS: Initializing McpServer...");
|
|
131
132
|
const server = new mcp_js_1.McpServer({
|
|
132
133
|
name: "CODESYS Control MCP Server",
|
|
133
|
-
version: "1.8.
|
|
134
|
+
version: "1.8.1", // Updated version for session fixes
|
|
134
135
|
capabilities: {
|
|
135
136
|
resources: { listChanged: true },
|
|
136
137
|
tools: { listChanged: true }
|
|
@@ -381,8 +382,50 @@ function startMcpServer(config) {
|
|
|
381
382
|
'TEMPLATE_PROJECT_PATH': normalizedTemplatePath
|
|
382
383
|
});
|
|
383
384
|
console.error(">>> create_project (copy-then-open): PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
|
|
384
|
-
|
|
385
|
+
// Use a longer timeout for project creation operations in sessions (120 seconds)
|
|
386
|
+
// This helps prevent timing out on slower systems or when CODESYS is busy
|
|
387
|
+
let result;
|
|
388
|
+
// Check if we're running in a session
|
|
389
|
+
if (sessionManager &&
|
|
390
|
+
(sessionManager.getState() === sessionManager_1.SessionState.RUNNING ||
|
|
391
|
+
sessionManager.getState() === sessionManager_1.SessionState.BUSY)) {
|
|
392
|
+
console.error(">>> create_project: Using session with extended timeout (120s)");
|
|
393
|
+
try {
|
|
394
|
+
// Execute script directly through session manager to specify longer timeout
|
|
395
|
+
const scriptResult = yield sessionManager.executeScript(scriptContent, 120000);
|
|
396
|
+
result = {
|
|
397
|
+
success: scriptResult.success,
|
|
398
|
+
output: scriptResult.output
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch (sessionError) {
|
|
402
|
+
console.error(`>>> create_project: Session execution failed, falling back to one-time execution: ${sessionError.message}`);
|
|
403
|
+
// Fall back to one-time execution
|
|
404
|
+
result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
// Standard execution without a session
|
|
409
|
+
result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName, sessionManager);
|
|
410
|
+
}
|
|
385
411
|
console.error(">>> create_project (copy-then-open): EXECUTION RESULT:", JSON.stringify(result));
|
|
412
|
+
// Check if there was a timeout but the project might still have been created
|
|
413
|
+
if (!result.success && result.output.includes("timed out") && result.output.includes("project file might still have been created")) {
|
|
414
|
+
console.error(">>> create_project: Script timed out but project might have been created. Checking if file exists.");
|
|
415
|
+
// Check if the project file was actually created despite the timeout
|
|
416
|
+
const fileExists = fs.existsSync(absPath);
|
|
417
|
+
if (fileExists) {
|
|
418
|
+
console.error(`>>> create_project: Project file ${absPath} exists despite timeout.`);
|
|
419
|
+
return {
|
|
420
|
+
content: [{
|
|
421
|
+
type: "text",
|
|
422
|
+
text: `Project file was created at ${absPath}, but the operation timed out before completion. ` +
|
|
423
|
+
`The project might be usable, but you may need to manually open it in CODESYS.`
|
|
424
|
+
}],
|
|
425
|
+
isError: true // Still mark as error because operation didn't complete properly
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
386
429
|
const success = result.success && result.output.includes("SCRIPT_SUCCESS");
|
|
387
430
|
return {
|
|
388
431
|
content: [{
|
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
|
/**
|