@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 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
- project = script_engine.projects.open(PROJECT_FILE_PATH, update_flags=update_mode)
75
- print("DEBUG: script_engine.projects.open returned: %s" % project)
76
- if project:
77
- print("DEBUG: Pausing briefly after open...")
78
- time.sleep(1.0) # Allow CODESYS to potentially initialize things
79
- try:
80
- print("DEBUG: Explicitly saving project after opening copy...")
81
- project.save();
82
- print("DEBUG: Project save after opening copy succeeded.")
83
- except Exception as save_err:
84
- print("WARN: Explicit save after opening copy failed: %s" % save_err)
85
- # Decide if this is critical - maybe not, but good to know.
86
- print("Project Created from Template Copy at: %s" % PROJECT_FILE_PATH)
87
- print("SCRIPT_SUCCESS: Project copied from template and opened successfully.")
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
- 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))
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
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName, sessionManager);
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",
@@ -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.6",
3
+ "version": "1.4.8",
4
4
  "description": "Model Context Protocol (MCP) server for CODESYS automation platform",
5
5
  "main": "dist/server.js",
6
6
  "bin": {