@codesys/mcp-toolkit 1.4.4 → 1.4.6
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 +14 -0
- package/dist/scriptLoader.js +8 -13
- package/dist/scripts/create_project.py +7 -28
- package/dist/scripts/start_session.py +27 -1
- package/dist/server.js +7 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ 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.6] - 2025-05-02
|
|
9
|
+
### Fixed
|
|
10
|
+
- Fixed session management "hanging" issue with start_session
|
|
11
|
+
- Added stdout flushing in Python scripts to ensure output is visible
|
|
12
|
+
- Improved session script logging for better debugging
|
|
13
|
+
- Added more detailed state information in session scripts
|
|
14
|
+
|
|
15
|
+
## [1.4.5] - 2025-05-02
|
|
16
|
+
### Fixed
|
|
17
|
+
- Completely redesigned path handling for Windows paths
|
|
18
|
+
- Removed raw string (r'...') approach for more direct path handling
|
|
19
|
+
- Implemented path normalization using forward slashes for cross-platform compatibility
|
|
20
|
+
- Fixed create_project script to properly handle Windows paths in IronPython environment
|
|
21
|
+
|
|
8
22
|
## [1.4.4] - 2025-05-02
|
|
9
23
|
### Fixed
|
|
10
24
|
- Fixed "unexpected token 'C'" error in create_project script
|
package/dist/scriptLoader.js
CHANGED
|
@@ -121,19 +121,14 @@ function interpolateScript(template, params) {
|
|
|
121
121
|
let result = template;
|
|
122
122
|
// Replace each parameter in the template
|
|
123
123
|
for (const [key, value] of Object.entries(params)) {
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
console.error(`SCRIPT_LOADER: Converted to Python raw string: ${escapedValue}`);
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
// For non-path values, just escape backslashes normally
|
|
136
|
-
escapedValue = String(value).replace(/\\/g, '\\\\');
|
|
124
|
+
// Get the escaped value, handling special characters
|
|
125
|
+
// Note: We've changed our approach to use forward slashes everywhere
|
|
126
|
+
// and handle the path normalization in each script instead of using r-strings
|
|
127
|
+
let escapedValue = String(value);
|
|
128
|
+
// If value has single quotes, escape them
|
|
129
|
+
if (escapedValue.includes("'")) {
|
|
130
|
+
escapedValue = escapedValue.replace(/'/g, "\\'");
|
|
131
|
+
console.error(`SCRIPT_LOADER: Escaped single quotes in value: ${escapedValue}`);
|
|
137
132
|
}
|
|
138
133
|
// Replace {PARAM_NAME} with the escaped value
|
|
139
134
|
const paramPattern = new RegExp(`\\{${key}\\}`, 'g');
|
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
import sys, scriptengine as script_engine, os, shutil, time, traceback
|
|
2
|
-
# Placeholders for paths - note that these might be prefixed with r' for raw strings
|
|
3
|
-
# from the TypeScript/Node.js side due to Windows path handling
|
|
4
|
-
TEMPLATE_PROJECT_PATH_RAW = '{TEMPLATE_PROJECT_PATH}' # Path to Standard.project
|
|
5
|
-
PROJECT_FILE_PATH_RAW = '{PROJECT_FILE_PATH}' # Path for the new project (Target Path)
|
|
6
2
|
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if path_str.startswith("r'") and path_str.endswith("'"):
|
|
12
|
-
try:
|
|
13
|
-
# Extract the content between r' and '
|
|
14
|
-
return path_str[2:-1]
|
|
15
|
-
except Exception as e:
|
|
16
|
-
print("DEBUG: Error extracting raw string content: %s" % e)
|
|
17
|
-
return path_str
|
|
18
|
-
return path_str
|
|
3
|
+
# Direct path assignment - no more raw string handling
|
|
4
|
+
# Paths will be correctly escaped in the template substitution
|
|
5
|
+
TEMPLATE_PROJECT_PATH = '{TEMPLATE_PROJECT_PATH}'
|
|
6
|
+
PROJECT_FILE_PATH = '{PROJECT_FILE_PATH}'
|
|
19
7
|
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
PROJECT_FILE_PATH = extract_path(PROJECT_FILE_PATH_RAW)
|
|
24
|
-
print("DEBUG: Processed paths:")
|
|
25
|
-
print("DEBUG: TEMPLATE_PROJECT_PATH_RAW: %s" % TEMPLATE_PROJECT_PATH_RAW)
|
|
26
|
-
print("DEBUG: TEMPLATE_PROJECT_PATH (processed): %s" % TEMPLATE_PROJECT_PATH)
|
|
27
|
-
print("DEBUG: PROJECT_FILE_PATH_RAW: %s" % PROJECT_FILE_PATH_RAW)
|
|
28
|
-
print("DEBUG: PROJECT_FILE_PATH (processed): %s" % PROJECT_FILE_PATH)
|
|
29
|
-
except Exception as path_proc_err:
|
|
30
|
-
print("CRITICAL: Error processing path variables: %s" % path_proc_err)
|
|
31
|
-
traceback.print_exc()
|
|
8
|
+
# Print raw paths for debugging
|
|
9
|
+
print("DEBUG: Template path (raw): %s" % repr(TEMPLATE_PROJECT_PATH))
|
|
10
|
+
print("DEBUG: Project path (raw): %s" % repr(PROJECT_FILE_PATH))
|
|
32
11
|
|
|
33
12
|
print("DEBUG: Processed TEMPLATE_PROJECT_PATH: %s" % TEMPLATE_PROJECT_PATH)
|
|
34
13
|
print("DEBUG: Processed PROJECT_FILE_PATH: %s" % PROJECT_FILE_PATH)
|
|
@@ -387,6 +387,7 @@ def check_command():
|
|
|
387
387
|
|
|
388
388
|
# Execute the command
|
|
389
389
|
print("Executing command: {0}".format(command.get("type", "unknown")))
|
|
390
|
+
sys.stdout.flush() # Ensure output is flushed before potentially long-running operation
|
|
390
391
|
result, stop_requested = execute_command(command)
|
|
391
392
|
print("Command execution complete, stop_requested: {0}".format(stop_requested))
|
|
392
393
|
|
|
@@ -423,6 +424,7 @@ def check_command():
|
|
|
423
424
|
except Exception as cmd_err:
|
|
424
425
|
print("Error processing command: {0}".format(cmd_err))
|
|
425
426
|
traceback.print_exc()
|
|
427
|
+
sys.stdout.flush() # Ensure error output is flushed
|
|
426
428
|
|
|
427
429
|
# Try to reset the command file
|
|
428
430
|
try:
|
|
@@ -448,6 +450,7 @@ def check_command():
|
|
|
448
450
|
except Exception as e:
|
|
449
451
|
print("Error in check_command: {0}".format(e))
|
|
450
452
|
traceback.print_exc()
|
|
453
|
+
sys.stdout.flush() # Ensure error output is flushed
|
|
451
454
|
return False
|
|
452
455
|
|
|
453
456
|
def update_last_activity():
|
|
@@ -497,6 +500,7 @@ def command_loop():
|
|
|
497
500
|
print("Session state file: {0}".format(SESSION_STATE_FILE))
|
|
498
501
|
print("Poll interval: {0} seconds".format(POLL_INTERVAL))
|
|
499
502
|
print("Max inactivity time: {0} seconds".format(MAX_INACTIVITY_TIME))
|
|
503
|
+
sys.stdout.flush() # Ensure initialization output is visible immediately
|
|
500
504
|
|
|
501
505
|
# Set up initial session state files
|
|
502
506
|
try:
|
|
@@ -519,9 +523,14 @@ def command_loop():
|
|
|
519
523
|
"timestamp": time.time()
|
|
520
524
|
}, f, indent=2)
|
|
521
525
|
print("Created initial result file")
|
|
526
|
+
sys.stdout.flush()
|
|
522
527
|
except Exception as setup_err:
|
|
523
528
|
print("Error setting up initial files: {0}".format(setup_err))
|
|
524
529
|
traceback.print_exc()
|
|
530
|
+
sys.stdout.flush()
|
|
531
|
+
|
|
532
|
+
print("Entering main command loop...")
|
|
533
|
+
sys.stdout.flush()
|
|
525
534
|
|
|
526
535
|
while not stop_requested:
|
|
527
536
|
try:
|
|
@@ -536,24 +545,27 @@ def command_loop():
|
|
|
536
545
|
if current_time - last_heartbeat > 60:
|
|
537
546
|
print("Command loop heartbeat - still alive (iteration {0})".format(loop_count))
|
|
538
547
|
last_heartbeat = current_time
|
|
548
|
+
sys.stdout.flush()
|
|
539
549
|
|
|
540
550
|
# Check for new commands
|
|
541
|
-
print("Checking for commands...")
|
|
542
551
|
stop_requested = check_command()
|
|
543
552
|
if stop_requested:
|
|
544
553
|
print("Stop was requested by a command")
|
|
554
|
+
sys.stdout.flush()
|
|
545
555
|
|
|
546
556
|
# Check for inactivity timeout
|
|
547
557
|
if check_inactivity_timeout():
|
|
548
558
|
print("Session timeout due to inactivity")
|
|
549
559
|
update_session_state("timeout")
|
|
550
560
|
stop_requested = True
|
|
561
|
+
sys.stdout.flush()
|
|
551
562
|
|
|
552
563
|
# Periodically update session state (every 30 seconds)
|
|
553
564
|
if (current_time - last_state_update) > 30:
|
|
554
565
|
print("Periodic session state update")
|
|
555
566
|
update_session_state()
|
|
556
567
|
last_state_update = current_time
|
|
568
|
+
sys.stdout.flush()
|
|
557
569
|
|
|
558
570
|
# Sleep for a bit
|
|
559
571
|
time.sleep(POLL_INTERVAL)
|
|
@@ -561,34 +573,48 @@ def command_loop():
|
|
|
561
573
|
except Exception as e:
|
|
562
574
|
print("Error in command loop: {0}".format(e))
|
|
563
575
|
traceback.print_exc()
|
|
576
|
+
sys.stdout.flush()
|
|
564
577
|
# Sleep for a bit before retrying
|
|
565
578
|
time.sleep(POLL_INTERVAL)
|
|
566
579
|
|
|
567
580
|
print("Command loop stopped after {0} iterations".format(loop_count))
|
|
581
|
+
sys.stdout.flush()
|
|
568
582
|
|
|
569
583
|
# Main script
|
|
570
584
|
def main():
|
|
571
585
|
try:
|
|
586
|
+
print("Starting CODESYS automation session...")
|
|
587
|
+
sys.stdout.flush()
|
|
588
|
+
|
|
572
589
|
# Setup session directory and files
|
|
590
|
+
print("Setting up session directory...")
|
|
591
|
+
sys.stdout.flush()
|
|
573
592
|
if not setup_session_dir():
|
|
593
|
+
print("Failed to set up session directory")
|
|
594
|
+
sys.stdout.flush()
|
|
574
595
|
sys.exit(1)
|
|
575
596
|
|
|
576
597
|
# Update initial session state
|
|
598
|
+
print("Updating initial session state...")
|
|
599
|
+
sys.stdout.flush()
|
|
577
600
|
update_session_state()
|
|
578
601
|
|
|
579
602
|
# Start command loop
|
|
580
603
|
print("{0}: CODESYS session started".format(SCRIPT_SUCCESS_MARKER))
|
|
604
|
+
sys.stdout.flush()
|
|
581
605
|
command_loop()
|
|
582
606
|
|
|
583
607
|
# Update final state
|
|
584
608
|
update_session_state("stopped")
|
|
585
609
|
|
|
586
610
|
print("{0}: CODESYS session stopped cleanly".format(SCRIPT_SUCCESS_MARKER))
|
|
611
|
+
sys.stdout.flush()
|
|
587
612
|
sys.exit(0)
|
|
588
613
|
except Exception as e:
|
|
589
614
|
print("Error in session: {0}".format(e))
|
|
590
615
|
print("{0}: Session failed - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
|
|
591
616
|
traceback.print_exc()
|
|
617
|
+
sys.stdout.flush()
|
|
592
618
|
sys.exit(1)
|
|
593
619
|
|
|
594
620
|
if __name__ == "__main__":
|
package/dist/server.js
CHANGED
|
@@ -130,7 +130,7 @@ function startMcpServer(config) {
|
|
|
130
130
|
console.error("SERVER.TS: Initializing McpServer...");
|
|
131
131
|
const server = new mcp_js_1.McpServer({
|
|
132
132
|
name: "CODESYS Control MCP Server",
|
|
133
|
-
version: "1.8.
|
|
133
|
+
version: "1.8.1", // Updated version for session fixes
|
|
134
134
|
capabilities: {
|
|
135
135
|
resources: { listChanged: true },
|
|
136
136
|
tools: { listChanged: true }
|
|
@@ -372,9 +372,13 @@ function startMcpServer(config) {
|
|
|
372
372
|
try {
|
|
373
373
|
// Load script template and interpolate parameters
|
|
374
374
|
const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('create_project');
|
|
375
|
+
// Normalize paths for more consistent handling between platforms
|
|
376
|
+
const normalizedTargetPath = absPath.replace(/\\/g, '/');
|
|
377
|
+
const normalizedTemplatePath = templatePath.replace(/\\/g, '/');
|
|
378
|
+
console.error(`Using normalized paths for script: Target=${normalizedTargetPath}, Template=${normalizedTemplatePath}`);
|
|
375
379
|
const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
|
|
376
|
-
'PROJECT_FILE_PATH':
|
|
377
|
-
'TEMPLATE_PROJECT_PATH':
|
|
380
|
+
'PROJECT_FILE_PATH': normalizedTargetPath,
|
|
381
|
+
'TEMPLATE_PROJECT_PATH': normalizedTemplatePath
|
|
378
382
|
});
|
|
379
383
|
console.error(">>> create_project (copy-then-open): PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
|
|
380
384
|
const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName, sessionManager);
|