@codesys/mcp-toolkit 1.4.7 → 1.4.9
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 +17 -0
- package/dist/scripts/create_project.py +33 -17
- package/dist/scripts/start_session.py +104 -7
- package/dist/server.js +30 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.9] - 2025-05-02
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixed critical issue with create_project not succeeding in persistent sessions
|
|
11
|
+
- Added comprehensive debugging for project creation operations
|
|
12
|
+
- Improved result file handling to prevent file access issues
|
|
13
|
+
- Enhanced success detection for project file creation when opening fails
|
|
14
|
+
- Fixed issue with result files not being properly written during critical operations
|
|
15
|
+
- Added atomic file writing with fallback mechanisms for result files
|
|
16
|
+
|
|
17
|
+
## [1.4.8] - 2025-05-02
|
|
18
|
+
### Fixed
|
|
19
|
+
- Fixed critical bug with project creation in persistent sessions
|
|
20
|
+
- Added SKIP_OPEN option to create_project script for session compatibility
|
|
21
|
+
- Improved handling of project creation in sessions to avoid "project in use" errors
|
|
22
|
+
- Added better error recovery when project file exists but can't be opened in a session
|
|
23
|
+
- Added helpful user messages explaining session limitations with project operations
|
|
24
|
+
|
|
8
25
|
## [1.4.7] - 2025-05-02
|
|
9
26
|
### Fixed
|
|
10
27
|
- Fixed create_project freezing in persistent sessions
|
|
@@ -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)
|
|
@@ -287,8 +287,28 @@ def execute_command(command_data):
|
|
|
287
287
|
# For project creation/opening, we'll execute the script with some guards
|
|
288
288
|
print("Executing critical script with guards...")
|
|
289
289
|
try:
|
|
290
|
-
# Execute the script
|
|
290
|
+
# Execute the script in a more controlled manner with debugging
|
|
291
|
+
print("DEBUG-CRITICAL: About to execute project creation/open script")
|
|
292
|
+
# First write project ops directly to the result file to prevent hanging
|
|
293
|
+
try:
|
|
294
|
+
with open(RESULT_FILE, 'w') as f:
|
|
295
|
+
result_data = {
|
|
296
|
+
"status": "in_progress",
|
|
297
|
+
"result": {
|
|
298
|
+
"id": command.get("id", "unknown"),
|
|
299
|
+
"success": False,
|
|
300
|
+
"output": "Project creation/opening operation in progress...",
|
|
301
|
+
},
|
|
302
|
+
"timestamp": time.time()
|
|
303
|
+
}
|
|
304
|
+
json.dump(result_data, f, indent=2)
|
|
305
|
+
print("DEBUG-CRITICAL: Created preliminary result file")
|
|
306
|
+
except Exception as rf_err:
|
|
307
|
+
print("DEBUG-CRITICAL: Error creating preliminary result: {0}".format(rf_err))
|
|
308
|
+
|
|
309
|
+
# Now execute the script
|
|
291
310
|
exec(script, local_globals)
|
|
311
|
+
print("DEBUG-CRITICAL: Script executed successfully")
|
|
292
312
|
except Exception as e:
|
|
293
313
|
# If the script fails, capture the error
|
|
294
314
|
print("ERROR in critical script execution: {0}".format(e))
|
|
@@ -315,6 +335,32 @@ def execute_command(command_data):
|
|
|
315
335
|
stdout_output = stdout_buffer.getvalue()
|
|
316
336
|
stderr_output = stderr_buffer.getvalue()
|
|
317
337
|
|
|
338
|
+
# Special handling for project creation - always check if we can find success markers
|
|
339
|
+
# in partial output even if script appears to have issues
|
|
340
|
+
if has_project_create:
|
|
341
|
+
# Look specifically for the file copy completion
|
|
342
|
+
if "File copy VERIFIED - file exists at destination" in stdout_output or "DEBUG: File copy complete" in stdout_output:
|
|
343
|
+
print("IMPORTANT: Detected successful file copy in project creation script")
|
|
344
|
+
project_path = ""
|
|
345
|
+
|
|
346
|
+
# Try to extract the project path from output
|
|
347
|
+
project_path_match = None
|
|
348
|
+
for line in stdout_output.split('\n'):
|
|
349
|
+
if "DEBUG: Target Path =" in line:
|
|
350
|
+
project_path_match = line.split('=')[1].strip()
|
|
351
|
+
break
|
|
352
|
+
|
|
353
|
+
if project_path_match:
|
|
354
|
+
project_path = project_path_match
|
|
355
|
+
print("IMPORTANT: Found project path: {0}".format(project_path))
|
|
356
|
+
|
|
357
|
+
# Even if there was an error later (like opening), consider this a partial success
|
|
358
|
+
if "Error creating project" in stdout_output and "currently in use" in stdout_output:
|
|
359
|
+
print("IMPORTANT: Project file was created but couldn't be opened due to 'in use' error")
|
|
360
|
+
# Combine success and error markers
|
|
361
|
+
stdout_output += "\nSCRIPT_SUCCESS: Project file was created at {0} but could not be opened because it is in use by another CODESYS instance.".format(project_path)
|
|
362
|
+
result["success"] = True
|
|
363
|
+
|
|
318
364
|
# Check for success/error markers
|
|
319
365
|
if SCRIPT_SUCCESS_MARKER in stdout_output or SCRIPT_SUCCESS_MARKER in stderr_output:
|
|
320
366
|
result["success"] = True
|
|
@@ -323,9 +369,18 @@ def execute_command(command_data):
|
|
|
323
369
|
result["success"] = False
|
|
324
370
|
print("Found error marker in script output")
|
|
325
371
|
else:
|
|
326
|
-
# No explicit markers, assume success
|
|
327
|
-
|
|
328
|
-
|
|
372
|
+
# No explicit markers, assume success for non-critical operations
|
|
373
|
+
# For critical operations like project creation, be more careful
|
|
374
|
+
if has_project_create:
|
|
375
|
+
if "File copy complete" in stdout_output:
|
|
376
|
+
result["success"] = True
|
|
377
|
+
print("Project creation seems successful based on file copy completion")
|
|
378
|
+
else:
|
|
379
|
+
result["success"] = False
|
|
380
|
+
print("Project creation script lacks success markers and file copy confirmation")
|
|
381
|
+
else:
|
|
382
|
+
result["success"] = True
|
|
383
|
+
print("No markers found in script output, assuming success")
|
|
329
384
|
|
|
330
385
|
# Tally output length to check if output is reasonable
|
|
331
386
|
print("Output length: stdout={0} chars, stderr={1} chars".format(
|
|
@@ -450,7 +505,7 @@ def check_command():
|
|
|
450
505
|
result, stop_requested = execute_command(command)
|
|
451
506
|
print("Command execution complete, stop_requested: {0}".format(stop_requested))
|
|
452
507
|
|
|
453
|
-
# Write the result
|
|
508
|
+
# Write the result with extra safety measures
|
|
454
509
|
result_data = {
|
|
455
510
|
"status": "complete",
|
|
456
511
|
"result": result,
|
|
@@ -458,8 +513,50 @@ def check_command():
|
|
|
458
513
|
}
|
|
459
514
|
|
|
460
515
|
print("Writing result to {0}".format(RESULT_FILE))
|
|
461
|
-
|
|
462
|
-
|
|
516
|
+
sys.stdout.flush()
|
|
517
|
+
|
|
518
|
+
# First, write to a temporary file to avoid corruption
|
|
519
|
+
temp_result_file = RESULT_FILE + ".tmp"
|
|
520
|
+
try:
|
|
521
|
+
with open(temp_result_file, 'w') as f:
|
|
522
|
+
json.dump(result_data, f, indent=2)
|
|
523
|
+
print("Temporary result file written successfully")
|
|
524
|
+
|
|
525
|
+
# If temporary write succeeded, move to the real file
|
|
526
|
+
if os.path.exists(temp_result_file):
|
|
527
|
+
# On Windows, we may need to remove the destination first
|
|
528
|
+
if os.path.exists(RESULT_FILE):
|
|
529
|
+
try:
|
|
530
|
+
os.remove(RESULT_FILE)
|
|
531
|
+
except Exception as rm_err:
|
|
532
|
+
print("Warning: Could not remove existing result file: {0}".format(rm_err))
|
|
533
|
+
|
|
534
|
+
# Now rename the temp file to the real file
|
|
535
|
+
os.rename(temp_result_file, RESULT_FILE)
|
|
536
|
+
print("Result file updated successfully")
|
|
537
|
+
else:
|
|
538
|
+
print("ERROR: Temporary result file not found after writing!")
|
|
539
|
+
# Try direct write as fallback
|
|
540
|
+
with open(RESULT_FILE, 'w') as f:
|
|
541
|
+
json.dump(result_data, f, indent=2)
|
|
542
|
+
except Exception as result_err:
|
|
543
|
+
print("ERROR writing result file: {0}".format(result_err))
|
|
544
|
+
traceback.print_exc()
|
|
545
|
+
sys.stdout.flush()
|
|
546
|
+
|
|
547
|
+
# Desperate attempt - try direct write with minimal content
|
|
548
|
+
try:
|
|
549
|
+
with open(RESULT_FILE, 'w') as f:
|
|
550
|
+
f.write('{"status":"complete","result":{"id":"' +
|
|
551
|
+
result["id"] + '","success":' +
|
|
552
|
+
('true' if result["success"] else 'false') +
|
|
553
|
+
',"output":"Result writing failed but operation completed"},"timestamp":' +
|
|
554
|
+
str(time.time()) + '}')
|
|
555
|
+
print("Emergency minimal result file written")
|
|
556
|
+
except Exception as emergency_err:
|
|
557
|
+
print("CRITICAL ERROR: Even emergency result write failed: {0}".format(emergency_err))
|
|
558
|
+
|
|
559
|
+
sys.stdout.flush()
|
|
463
560
|
|
|
464
561
|
# Reset command file
|
|
465
562
|
command_data = {
|
package/dist/server.js
CHANGED
|
@@ -377,9 +377,14 @@ function startMcpServer(config) {
|
|
|
377
377
|
const normalizedTargetPath = absPath.replace(/\\/g, '/');
|
|
378
378
|
const normalizedTemplatePath = templatePath.replace(/\\/g, '/');
|
|
379
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);
|
|
380
384
|
const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
|
|
381
385
|
'PROJECT_FILE_PATH': normalizedTargetPath,
|
|
382
|
-
'TEMPLATE_PROJECT_PATH': normalizedTemplatePath
|
|
386
|
+
'TEMPLATE_PROJECT_PATH': normalizedTemplatePath,
|
|
387
|
+
'SKIP_OPEN': inSession ? 'true' : 'false'
|
|
383
388
|
});
|
|
384
389
|
console.error(">>> create_project (copy-then-open): PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
|
|
385
390
|
// Use a longer timeout for project creation operations in sessions (120 seconds)
|
|
@@ -427,6 +432,30 @@ function startMcpServer(config) {
|
|
|
427
432
|
}
|
|
428
433
|
}
|
|
429
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
|
+
}
|
|
430
459
|
return {
|
|
431
460
|
content: [{
|
|
432
461
|
type: "text",
|