@codesys/mcp-toolkit 1.1.11 → 1.1.13

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,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.1.13] - 2025-05-06
9
+
10
+ ### Fixed
11
+ - Fixed remaining f-string formatting in create_pou.py script
12
+ - Restored compatibility with older Python versions in path formatting
13
+
14
+ ## [1.1.12] - 2025-05-06
15
+
16
+ ### Fixed
17
+ - Completely rewrote path handling to avoid excessive backslash escaping
18
+ - Added multiple fallback methods for opening CODESYS projects
19
+ - Removed use of non-existent get_version_info API
20
+ - Made project structure tool more resilient to various CODESYS API patterns
21
+
8
22
  ## [1.1.11] - 2025-05-06
9
23
 
10
24
  ### Fixed
@@ -40,22 +40,22 @@ try:
40
40
  # Get project name from file path to build the likely full path
41
41
  project_name = os.path.splitext(os.path.basename(PROJECT_FILE_PATH))[0]
42
42
  potential_paths = [
43
- PARENT_PATH_REL, # Original "Application"
44
- f"{project_name}.{PARENT_PATH_REL}", # "projectName.Application"
45
- f"{project_name}/{PARENT_PATH_REL}", # "projectName/Application"
46
- f"{project_name}" # Just the project name itself might work
43
+ PARENT_PATH_REL, # Original "Application"
44
+ "%s.%s" % (project_name, PARENT_PATH_REL), # "projectName.Application"
45
+ "%s/%s" % (project_name, PARENT_PATH_REL), # "projectName/Application"
46
+ project_name # Just the project name itself might work
47
47
  ]
48
48
 
49
- print(f"DEBUG: Parent path is simply 'Application', trying several variants to find it")
49
+ print("DEBUG: Parent path is simply 'Application', trying several variants to find it")
50
50
 
51
51
  # Try each potential path until one works
52
52
  parent_object = None
53
53
  for path in potential_paths:
54
- print(f"DEBUG: Attempting to find parent with path: '{path}'")
54
+ print("DEBUG: Attempting to find parent with path: '%s'" % path)
55
55
  parent_candidate = find_object_by_path_robust(primary_project, path, "parent container")
56
56
  if parent_candidate:
57
57
  parent_object = parent_candidate
58
- print(f"DEBUG: Successfully found parent using path: '{path}'")
58
+ print("DEBUG: Successfully found parent using path: '%s'" % path)
59
59
  break
60
60
 
61
61
  if not parent_object:
@@ -66,14 +66,14 @@ try:
66
66
  app = primary_project.active_application
67
67
  if app:
68
68
  parent_object = app
69
- print(f"DEBUG: Found application object directly: {app.get_name()}")
69
+ print("DEBUG: Found application object directly: %s" % app.get_name())
70
70
  if not parent_object and hasattr(primary_project, 'find'):
71
71
  apps = primary_project.find("Application", True)
72
72
  if apps:
73
73
  parent_object = apps[0]
74
- print(f"DEBUG: Found application via search: {parent_object.get_name()}")
74
+ print("DEBUG: Found application via search: %s" % parent_object.get_name())
75
75
  except Exception as e:
76
- print(f"ERROR: Direct application access also failed: {e}")
76
+ print("ERROR: Direct application access also failed: %s" % e)
77
77
  else:
78
78
  # Use the provided path normally
79
79
  parent_object = find_object_by_path_robust(primary_project, PARENT_PATH_REL, "parent container")
@@ -8,31 +8,22 @@ import traceback
8
8
  MAX_RETRIES = 3
9
9
  RETRY_DELAY = 2.0 # seconds (use float for time.sleep)
10
10
 
11
- # Special handling for Windows paths
12
- def sanitize_windows_path(path_str):
13
- """Properly sanitize Windows paths for CODESYS scripting"""
14
- # If it's already a Python raw string literal, return it as is
15
- if path_str.startswith('r"') or path_str.startswith("r'"):
16
- return path_str
17
-
18
- # Make Windows paths with backslashes work correctly when passed to CODESYS
19
- # We use actual backslashes (which will be doubled by Python raw string)
20
- sanitized = path_str.replace('\\', '\\\\')
21
-
22
- # Clean up any extraneous quotes
23
- sanitized = sanitized.strip('"\'')
24
-
25
- print("DEBUG: Sanitized path from '%s' to '%s'" % (path_str, sanitized))
26
- return sanitized
11
+ # Basic path cleanup without excessive escaping
12
+ def clean_path(path_str):
13
+ """Clean up a path for CODESYS scripting without excessive escaping"""
14
+ # Simply remove any extraneous quotes
15
+ cleaned = path_str.strip('"\'')
16
+ print("DEBUG: Cleaned path: '%s'" % cleaned)
17
+ return cleaned
27
18
 
28
19
  def ensure_project_open(target_project_path):
29
20
  print("DEBUG: Ensuring project is open: %s" % target_project_path)
30
21
 
31
- # First sanitize the path for proper Windows path handling
32
- sanitized_path = sanitize_windows_path(target_project_path)
22
+ # Just clean the path without adding escapes
23
+ path_to_use = clean_path(target_project_path)
33
24
 
34
25
  # Normalize target path once
35
- normalized_target_path = os.path.normcase(os.path.abspath(sanitized_path))
26
+ normalized_target_path = path_to_use
36
27
 
37
28
  for attempt in range(MAX_RETRIES):
38
29
  print("DEBUG: Ensure project attempt %d/%d for %s" % (attempt + 1, MAX_RETRIES, normalized_target_path))
@@ -6,6 +6,72 @@ def ensure_project_open(target_project_path):
6
6
  # Function implementation will be included from ensure_project_open.py
7
7
  pass
8
8
 
9
+ # Simpler version that directly uses script_engine.projects.open
10
+ def direct_project_open(project_path):
11
+ print("DEBUG: Attempting to open project directly: %s" % project_path)
12
+ try:
13
+ # Clean up any double backslashes
14
+ clean_path = project_path.replace('\\\\', '\\').strip('"\'')
15
+ print("DEBUG: Using path: %s" % clean_path)
16
+
17
+ # First check if the project is already open
18
+ try:
19
+ current = script_engine.get_current_project()
20
+ if current and hasattr(current, 'get_path'):
21
+ current_path = current.get_path()
22
+ if current_path == clean_path:
23
+ print("DEBUG: Project already open: %s" % clean_path)
24
+ return current
25
+ else:
26
+ print("DEBUG: Different project open. Closing: %s" % current_path)
27
+ script_engine.close_project()
28
+ except Exception as e:
29
+ print("DEBUG: Error checking current project: %s" % e)
30
+
31
+ # Try to open the project
32
+ try:
33
+ # Try different ways of opening the project
34
+ print("DEBUG: Trying script_engine.open_project")
35
+ project = script_engine.open_project(clean_path)
36
+ if project:
37
+ print("DEBUG: Project opened successfully with open_project")
38
+ return project
39
+ except Exception as e:
40
+ print("DEBUG: Error with open_project: %s" % e)
41
+
42
+ # Try alternative method
43
+ try:
44
+ print("DEBUG: Trying script_engine.projects.open")
45
+ project = script_engine.projects.open(clean_path)
46
+ if project:
47
+ print("DEBUG: Project opened successfully with projects.open")
48
+ return project
49
+ except Exception as e:
50
+ print("DEBUG: Error with projects.open: %s" % e)
51
+
52
+ # One more attempt if available
53
+ try:
54
+ print("DEBUG: Trying primary project access")
55
+ project = script_engine.projects.primary
56
+ if project:
57
+ print("DEBUG: Got primary project")
58
+ if hasattr(project, 'get_path'):
59
+ path = project.get_path()
60
+ print("DEBUG: Primary project path: %s" % path)
61
+ if path == clean_path:
62
+ return project
63
+ return project # Return anyway as fallback
64
+ except Exception as e:
65
+ print("DEBUG: Error getting primary project: %s" % e)
66
+
67
+ print("DEBUG: All attempts to open project failed")
68
+ return None
69
+
70
+ except Exception as e:
71
+ print("DEBUG: Unexpected error in direct_project_open: %s" % e)
72
+ traceback.print_exc()
73
+ return None
74
+
9
75
  def get_object_structure(obj, indent=0, max_depth=10): # Add max_depth
10
76
  lines = []; indent_str = " " * indent
11
77
  if obj is None:
@@ -55,37 +121,33 @@ def get_object_structure(obj, indent=0, max_depth=10): # Add max_depth
55
121
  traceback.print_exc() # Print detailed error for this node
56
122
  return lines
57
123
 
58
- # Normalize Windows paths for CODESYS scripting
59
- def normalize_path(path_str):
60
- # Replace backslashes with double backslashes for Python string in CODESYS
61
- normalized = path_str.replace('\\', '\\\\')
62
- # Remove any quotes that might be present
63
- normalized = normalized.strip('"\'')
64
- print("DEBUG: Normalized path from '%s' to '%s'" % (path_str, normalized))
65
- return normalized
124
+ # Simple path cleanup function - CODESYS doesn't need double-escaped paths
125
+ def clean_path(path_str):
126
+ # Just remove any quotes that might be present
127
+ cleaned = path_str.strip('"\'')
128
+ print("DEBUG: Cleaned path: '%s'" % cleaned)
129
+ return cleaned
66
130
 
67
131
  try:
68
- # Normalize the project path for CODESYS
69
- normalized_path = normalize_path(PROJECT_FILE_PATH)
70
- print("DEBUG: Getting structure for project path: '%s'" % normalized_path)
132
+ # Just use the path directly without escaping
133
+ project_path = PROJECT_FILE_PATH.strip('"\'')
134
+ print("DEBUG: Getting structure for project: '%s'" % project_path)
71
135
 
72
- # First confirm that CODESYS is running properly
73
- print("DEBUG: CODESYS version info:")
74
- try:
75
- version_info = script_engine.get_version_info()
76
- print("DEBUG: CODESYS Version: %s" % version_info)
77
- except Exception as v_err:
78
- print("DEBUG: Error getting CODESYS version: %s" % v_err)
136
+ print("DEBUG: Opening project directly...")
137
+ # Try our direct method first
138
+ primary_project = direct_project_open(project_path)
79
139
 
80
- print("DEBUG: Opening project...")
81
- primary_project = ensure_project_open(normalized_path)
140
+ # If that fails, try the standard method
141
+ if not primary_project:
142
+ print("DEBUG: Direct open failed, trying ensure_project_open...")
143
+ primary_project = ensure_project_open(project_path)
82
144
 
83
145
  if primary_project is None:
84
146
  print("DEBUG: ERROR - Project object is None after ensure_project_open")
85
- raise ValueError("Failed to open project: %s - project object is None" % normalized_path)
147
+ raise ValueError("Failed to open project: %s - project object is None" % project_path)
86
148
 
87
149
  print("DEBUG: Project object type: %s" % type(primary_project).__name__)
88
- project_name = os.path.basename(normalized_path)
150
+ project_name = os.path.basename(project_path)
89
151
 
90
152
  if hasattr(primary_project, 'get_name'):
91
153
  print("DEBUG: Project name from object: %s" % primary_project.get_name())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "description": "Model Context Protocol (MCP) server for CODESYS automation platform",
5
5
  "main": "dist/server.js",
6
6
  "bin": {
@@ -40,22 +40,22 @@ try:
40
40
  # Get project name from file path to build the likely full path
41
41
  project_name = os.path.splitext(os.path.basename(PROJECT_FILE_PATH))[0]
42
42
  potential_paths = [
43
- PARENT_PATH_REL, # Original "Application"
44
- f"{project_name}.{PARENT_PATH_REL}", # "projectName.Application"
45
- f"{project_name}/{PARENT_PATH_REL}", # "projectName/Application"
46
- f"{project_name}" # Just the project name itself might work
43
+ PARENT_PATH_REL, # Original "Application"
44
+ "%s.%s" % (project_name, PARENT_PATH_REL), # "projectName.Application"
45
+ "%s/%s" % (project_name, PARENT_PATH_REL), # "projectName/Application"
46
+ project_name # Just the project name itself might work
47
47
  ]
48
48
 
49
- print(f"DEBUG: Parent path is simply 'Application', trying several variants to find it")
49
+ print("DEBUG: Parent path is simply 'Application', trying several variants to find it")
50
50
 
51
51
  # Try each potential path until one works
52
52
  parent_object = None
53
53
  for path in potential_paths:
54
- print(f"DEBUG: Attempting to find parent with path: '{path}'")
54
+ print("DEBUG: Attempting to find parent with path: '%s'" % path)
55
55
  parent_candidate = find_object_by_path_robust(primary_project, path, "parent container")
56
56
  if parent_candidate:
57
57
  parent_object = parent_candidate
58
- print(f"DEBUG: Successfully found parent using path: '{path}'")
58
+ print("DEBUG: Successfully found parent using path: '%s'" % path)
59
59
  break
60
60
 
61
61
  if not parent_object:
@@ -66,14 +66,14 @@ try:
66
66
  app = primary_project.active_application
67
67
  if app:
68
68
  parent_object = app
69
- print(f"DEBUG: Found application object directly: {app.get_name()}")
69
+ print("DEBUG: Found application object directly: %s" % app.get_name())
70
70
  if not parent_object and hasattr(primary_project, 'find'):
71
71
  apps = primary_project.find("Application", True)
72
72
  if apps:
73
73
  parent_object = apps[0]
74
- print(f"DEBUG: Found application via search: {parent_object.get_name()}")
74
+ print("DEBUG: Found application via search: %s" % parent_object.get_name())
75
75
  except Exception as e:
76
- print(f"ERROR: Direct application access also failed: {e}")
76
+ print("ERROR: Direct application access also failed: %s" % e)
77
77
  else:
78
78
  # Use the provided path normally
79
79
  parent_object = find_object_by_path_robust(primary_project, PARENT_PATH_REL, "parent container")
@@ -8,31 +8,22 @@ import traceback
8
8
  MAX_RETRIES = 3
9
9
  RETRY_DELAY = 2.0 # seconds (use float for time.sleep)
10
10
 
11
- # Special handling for Windows paths
12
- def sanitize_windows_path(path_str):
13
- """Properly sanitize Windows paths for CODESYS scripting"""
14
- # If it's already a Python raw string literal, return it as is
15
- if path_str.startswith('r"') or path_str.startswith("r'"):
16
- return path_str
17
-
18
- # Make Windows paths with backslashes work correctly when passed to CODESYS
19
- # We use actual backslashes (which will be doubled by Python raw string)
20
- sanitized = path_str.replace('\\', '\\\\')
21
-
22
- # Clean up any extraneous quotes
23
- sanitized = sanitized.strip('"\'')
24
-
25
- print("DEBUG: Sanitized path from '%s' to '%s'" % (path_str, sanitized))
26
- return sanitized
11
+ # Basic path cleanup without excessive escaping
12
+ def clean_path(path_str):
13
+ """Clean up a path for CODESYS scripting without excessive escaping"""
14
+ # Simply remove any extraneous quotes
15
+ cleaned = path_str.strip('"\'')
16
+ print("DEBUG: Cleaned path: '%s'" % cleaned)
17
+ return cleaned
27
18
 
28
19
  def ensure_project_open(target_project_path):
29
20
  print("DEBUG: Ensuring project is open: %s" % target_project_path)
30
21
 
31
- # First sanitize the path for proper Windows path handling
32
- sanitized_path = sanitize_windows_path(target_project_path)
22
+ # Just clean the path without adding escapes
23
+ path_to_use = clean_path(target_project_path)
33
24
 
34
25
  # Normalize target path once
35
- normalized_target_path = os.path.normcase(os.path.abspath(sanitized_path))
26
+ normalized_target_path = path_to_use
36
27
 
37
28
  for attempt in range(MAX_RETRIES):
38
29
  print("DEBUG: Ensure project attempt %d/%d for %s" % (attempt + 1, MAX_RETRIES, normalized_target_path))
@@ -6,6 +6,72 @@ def ensure_project_open(target_project_path):
6
6
  # Function implementation will be included from ensure_project_open.py
7
7
  pass
8
8
 
9
+ # Simpler version that directly uses script_engine.projects.open
10
+ def direct_project_open(project_path):
11
+ print("DEBUG: Attempting to open project directly: %s" % project_path)
12
+ try:
13
+ # Clean up any double backslashes
14
+ clean_path = project_path.replace('\\\\', '\\').strip('"\'')
15
+ print("DEBUG: Using path: %s" % clean_path)
16
+
17
+ # First check if the project is already open
18
+ try:
19
+ current = script_engine.get_current_project()
20
+ if current and hasattr(current, 'get_path'):
21
+ current_path = current.get_path()
22
+ if current_path == clean_path:
23
+ print("DEBUG: Project already open: %s" % clean_path)
24
+ return current
25
+ else:
26
+ print("DEBUG: Different project open. Closing: %s" % current_path)
27
+ script_engine.close_project()
28
+ except Exception as e:
29
+ print("DEBUG: Error checking current project: %s" % e)
30
+
31
+ # Try to open the project
32
+ try:
33
+ # Try different ways of opening the project
34
+ print("DEBUG: Trying script_engine.open_project")
35
+ project = script_engine.open_project(clean_path)
36
+ if project:
37
+ print("DEBUG: Project opened successfully with open_project")
38
+ return project
39
+ except Exception as e:
40
+ print("DEBUG: Error with open_project: %s" % e)
41
+
42
+ # Try alternative method
43
+ try:
44
+ print("DEBUG: Trying script_engine.projects.open")
45
+ project = script_engine.projects.open(clean_path)
46
+ if project:
47
+ print("DEBUG: Project opened successfully with projects.open")
48
+ return project
49
+ except Exception as e:
50
+ print("DEBUG: Error with projects.open: %s" % e)
51
+
52
+ # One more attempt if available
53
+ try:
54
+ print("DEBUG: Trying primary project access")
55
+ project = script_engine.projects.primary
56
+ if project:
57
+ print("DEBUG: Got primary project")
58
+ if hasattr(project, 'get_path'):
59
+ path = project.get_path()
60
+ print("DEBUG: Primary project path: %s" % path)
61
+ if path == clean_path:
62
+ return project
63
+ return project # Return anyway as fallback
64
+ except Exception as e:
65
+ print("DEBUG: Error getting primary project: %s" % e)
66
+
67
+ print("DEBUG: All attempts to open project failed")
68
+ return None
69
+
70
+ except Exception as e:
71
+ print("DEBUG: Unexpected error in direct_project_open: %s" % e)
72
+ traceback.print_exc()
73
+ return None
74
+
9
75
  def get_object_structure(obj, indent=0, max_depth=10): # Add max_depth
10
76
  lines = []; indent_str = " " * indent
11
77
  if obj is None:
@@ -55,37 +121,33 @@ def get_object_structure(obj, indent=0, max_depth=10): # Add max_depth
55
121
  traceback.print_exc() # Print detailed error for this node
56
122
  return lines
57
123
 
58
- # Normalize Windows paths for CODESYS scripting
59
- def normalize_path(path_str):
60
- # Replace backslashes with double backslashes for Python string in CODESYS
61
- normalized = path_str.replace('\\', '\\\\')
62
- # Remove any quotes that might be present
63
- normalized = normalized.strip('"\'')
64
- print("DEBUG: Normalized path from '%s' to '%s'" % (path_str, normalized))
65
- return normalized
124
+ # Simple path cleanup function - CODESYS doesn't need double-escaped paths
125
+ def clean_path(path_str):
126
+ # Just remove any quotes that might be present
127
+ cleaned = path_str.strip('"\'')
128
+ print("DEBUG: Cleaned path: '%s'" % cleaned)
129
+ return cleaned
66
130
 
67
131
  try:
68
- # Normalize the project path for CODESYS
69
- normalized_path = normalize_path(PROJECT_FILE_PATH)
70
- print("DEBUG: Getting structure for project path: '%s'" % normalized_path)
132
+ # Just use the path directly without escaping
133
+ project_path = PROJECT_FILE_PATH.strip('"\'')
134
+ print("DEBUG: Getting structure for project: '%s'" % project_path)
71
135
 
72
- # First confirm that CODESYS is running properly
73
- print("DEBUG: CODESYS version info:")
74
- try:
75
- version_info = script_engine.get_version_info()
76
- print("DEBUG: CODESYS Version: %s" % version_info)
77
- except Exception as v_err:
78
- print("DEBUG: Error getting CODESYS version: %s" % v_err)
136
+ print("DEBUG: Opening project directly...")
137
+ # Try our direct method first
138
+ primary_project = direct_project_open(project_path)
79
139
 
80
- print("DEBUG: Opening project...")
81
- primary_project = ensure_project_open(normalized_path)
140
+ # If that fails, try the standard method
141
+ if not primary_project:
142
+ print("DEBUG: Direct open failed, trying ensure_project_open...")
143
+ primary_project = ensure_project_open(project_path)
82
144
 
83
145
  if primary_project is None:
84
146
  print("DEBUG: ERROR - Project object is None after ensure_project_open")
85
- raise ValueError("Failed to open project: %s - project object is None" % normalized_path)
147
+ raise ValueError("Failed to open project: %s - project object is None" % project_path)
86
148
 
87
149
  print("DEBUG: Project object type: %s" % type(primary_project).__name__)
88
- project_name = os.path.basename(normalized_path)
150
+ project_name = os.path.basename(project_path)
89
151
 
90
152
  if hasattr(primary_project, 'get_name'):
91
153
  print("DEBUG: Project name from object: %s" % primary_project.get_name())