@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 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
- try:
258
- # Execute the script
259
- exec(script, local_globals)
260
- finally:
261
- # Restore original stdout and stderr
262
- sys.stdout = old_stdout
263
- sys.stderr = old_stderr
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.0", // Updated version for refactored code
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
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName, sessionManager);
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: [{
@@ -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
- console.error(`SESSION_MANAGER: Result file not found at ${this.resultFilePath}`);
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "description": "Model Context Protocol (MCP) server for CODESYS automation platform",
5
5
  "main": "dist/server.js",
6
6
  "bin": {