@codesys/mcp-toolkit 1.3.0 → 1.4.0

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.
@@ -18,7 +18,9 @@
18
18
  "Bash(npm publish:*)",
19
19
  "Bash(npm publish:*)",
20
20
  "Bash(ls:*)",
21
- "Bash(ls:*)"
21
+ "Bash(ls:*)",
22
+ "Bash(mkdir:*)",
23
+ "Bash(mkdir:*)"
22
24
  ],
23
25
  "deny": []
24
26
  }
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.0] - 2025-05-02
9
+ ### Added
10
+ - Implemented persistent session management functionality
11
+ - Added support for keeping CODESYS open between commands
12
+ - Created file-based IPC for script execution in existing CODESYS instance
13
+ - Improved session reliability with health checks and automatic recovery
14
+ - Added session timeout after 10 minutes of inactivity
15
+ - Created detailed documentation for session management features
16
+
17
+ ## [1.3.0] - 2025-05-02
18
+ ### Changed
19
+ - Refactored codebase for improved reliability and maintainability
20
+ - Enhanced error handling in Python script templates
21
+
8
22
  ## [1.2.4] - 2025-05-02
9
23
  ### Fixed
10
24
  - Fully disabled session management to prevent operations appearing to succeed but not actually running
@@ -1,4 +1,4 @@
1
- import sys, os, json, time
1
+ import sys, os, json, time, traceback
2
2
  import tempfile
3
3
 
4
4
  # Define markers for script results
@@ -26,18 +26,24 @@ def check_session_status():
26
26
  return False
27
27
 
28
28
  # Read session state
29
- with open(SESSION_STATE_FILE, 'r') as f:
30
- try:
29
+ try:
30
+ with open(SESSION_STATE_FILE, 'r') as f:
31
31
  session_state = json.load(f)
32
- except json.JSONDecodeError:
33
- print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
34
- return False
32
+ except json.JSONDecodeError:
33
+ print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
34
+ return False
35
35
 
36
36
  # Check if session is active
37
37
  active = session_state.get('active', False)
38
38
  status = session_state.get('status', 'unknown')
39
39
 
40
+ # If the status indicates the session is not active, return immediately
41
+ if not active or status in ['stopped', 'error', 'stopping', 'timeout']:
42
+ print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
43
+ return False
44
+
40
45
  # Check session lock file to see if it's been updated recently
46
+ lock_is_stale = False
41
47
  if os.path.exists(SESSION_LOCK_FILE):
42
48
  try:
43
49
  with open(SESSION_LOCK_FILE, 'r') as f:
@@ -47,26 +53,33 @@ def check_session_status():
47
53
 
48
54
  # If lock file hasn't been updated in 30 seconds, session might be dead
49
55
  if time_diff > 30:
50
- print(f"{SCRIPT_SUCCESS_MARKER}: Session appears to be inactive (lock file stale by {time_diff:.1f} seconds)")
51
- # But we'll leave the active flag as is, since it might just be busy
56
+ print(f"WARNING: Session lock file is stale by {time_diff:.1f} seconds")
57
+ lock_is_stale = True
52
58
  except Exception as e:
53
59
  print(f"Error reading lock file: {e}")
60
+ lock_is_stale = True
61
+ else:
62
+ print("WARNING: Session lock file not found")
63
+ lock_is_stale = True
54
64
 
55
- # Execute a ping test by sending a simple command
56
- if active and status in ["running", "busy"]:
65
+ # If the lock file is stale but other checks suggest the session is active,
66
+ # we'll perform a ping test to be sure
67
+ ping_success = False
68
+ if status in ["running", "busy"]:
57
69
  # Try to ping the session
58
70
  try:
59
71
  # Check if command file exists
60
72
  if os.path.exists(COMMAND_FILE):
73
+ print("Sending ping command to verify session...")
74
+
61
75
  # Create ping command
62
76
  ping_command = {
63
77
  "id": f"ping_{int(time.time())}",
64
- "type": "execute_script",
65
- "script": "print('SCRIPT_SUCCESS: Ping successful')",
78
+ "type": "ping", # Use specific ping command type
66
79
  "timestamp": time.time()
67
80
  }
68
81
 
69
- # Write command to file
82
+ # Write command to file with pretty-printing
70
83
  command_data = {
71
84
  "status": "pending",
72
85
  "command": ping_command,
@@ -74,12 +87,11 @@ def check_session_status():
74
87
  }
75
88
 
76
89
  with open(COMMAND_FILE, 'w') as f:
77
- json.dump(command_data, f)
90
+ json.dump(command_data, f, indent=2)
78
91
 
79
92
  # Wait for result (with short timeout)
80
93
  timeout = 5 # seconds
81
94
  start_time = time.time()
82
- ping_success = False
83
95
 
84
96
  while not ping_success and (time.time() - start_time) < timeout:
85
97
  # Check if result file exists
@@ -95,16 +107,37 @@ def check_session_status():
95
107
  result = result_data.get("result")
96
108
  if result.get('id') == ping_command['id'] and result.get('success', False):
97
109
  ping_success = True
110
+ print("Ping successful, session is responsive")
98
111
  break
99
112
  except Exception as e:
100
113
  print(f"Error reading result: {e}")
101
114
  time.sleep(0.5)
102
115
 
103
116
  if not ping_success:
104
- print(f"{SCRIPT_SUCCESS_MARKER}: Session appears to be unresponsive to ping")
105
- return False
117
+ print(f"WARNING: Session did not respond to ping within {timeout} seconds")
118
+
119
+ # Reset command file since ping failed
120
+ try:
121
+ command_data = {
122
+ "status": "ready",
123
+ "command": None,
124
+ "timestamp": time.time()
125
+ }
126
+
127
+ with open(COMMAND_FILE, 'w') as f:
128
+ json.dump(command_data, f, indent=2)
129
+
130
+ print("Reset command file after failed ping")
131
+ except Exception as e:
132
+ print(f"Error resetting command file: {e}")
133
+ else:
134
+ print("WARNING: Command file not found, cannot ping session")
106
135
  except Exception as e:
107
136
  print(f"Error during ping test: {e}")
137
+ traceback.print_exc()
138
+
139
+ # Make final determination based on all checks
140
+ session_active = active and (not lock_is_stale or ping_success) and status in ["running", "busy"]
108
141
 
109
142
  # Build status message
110
143
  project_info = ""
@@ -113,15 +146,39 @@ def check_session_status():
113
146
  project_path = session_state.get('project_path', 'Unknown')
114
147
  project_info = f", Project: {project_name} ({project_path})"
115
148
 
116
- if active and status in ["running", "busy"]:
117
- print(f"{SCRIPT_SUCCESS_MARKER}: Session is active (Status: {status}{project_info})")
149
+ # Include session info
150
+ session_id = session_state.get('session_id', 'Unknown')
151
+ start_time = session_state.get('start_time', 0)
152
+ uptime = time.time() - start_time if start_time else 0
153
+ uptime_str = f"{uptime:.0f} seconds" if uptime < 60 else f"{uptime/60:.1f} minutes"
154
+
155
+ if session_active:
156
+ print(f"{SCRIPT_SUCCESS_MARKER}: Session is active (ID: {session_id}, Status: {status}, Uptime: {uptime_str}{project_info})")
118
157
  return True
119
158
  else:
120
- print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
159
+ # If we get here, session may be in a state where files exist but process is dead
160
+ if active and status in ["running", "busy"] and lock_is_stale and not ping_success:
161
+ print(f"{SCRIPT_SUCCESS_MARKER}: Session appears to be zombie (ID: {session_id}, Status: {status} but unresponsive)")
162
+ # Update session state to error so it will be considered inactive
163
+ try:
164
+ session_state['status'] = 'error'
165
+ session_state['active'] = False
166
+ session_state['error_reason'] = 'Unresponsive to ping'
167
+
168
+ with open(SESSION_STATE_FILE, 'w') as f:
169
+ json.dump(session_state, f, indent=2)
170
+
171
+ print("Updated session state to error due to unresponsiveness")
172
+ except Exception as e:
173
+ print(f"Error updating session state: {e}")
174
+ else:
175
+ print(f"{SCRIPT_SUCCESS_MARKER}: Session is not active (Status: {status})")
176
+
121
177
  return False
122
178
 
123
179
  except Exception as e:
124
180
  print(f"{SCRIPT_ERROR_MARKER}: Error checking session status - {str(e)}")
181
+ traceback.print_exc()
125
182
  return False
126
183
 
127
184
  # Main script
@@ -1,4 +1,4 @@
1
- import sys, os, json, time, tempfile
1
+ import sys, os, json, time, tempfile, traceback
2
2
  import scriptengine as script_engine
3
3
 
4
4
  # Define markers for script results
@@ -11,43 +11,71 @@ SESSION_STATE_FILE = os.path.join(SESSION_DIR, "session_state.json")
11
11
  COMMAND_FILE = os.path.join(SESSION_DIR, "command.json")
12
12
  RESULT_FILE = os.path.join(SESSION_DIR, "result.json")
13
13
  SESSION_LOCK_FILE = os.path.join(SESSION_DIR, "session.lock")
14
- POLL_INTERVAL = 0.5 # seconds
14
+ POLL_INTERVAL = 0.1 # seconds (faster polling for better responsiveness)
15
+ MAX_INACTIVITY_TIME = 600 # seconds (10 minutes of inactivity before auto-shutdown)
15
16
 
16
17
  def setup_session_dir():
17
18
  """Create the session directory and initial files"""
18
19
  try:
20
+ # Clean up any existing session first
21
+ if os.path.exists(SESSION_DIR):
22
+ try:
23
+ # Try to read existing session state
24
+ if os.path.exists(SESSION_STATE_FILE):
25
+ try:
26
+ with open(SESSION_STATE_FILE, 'r') as f:
27
+ old_state = json.load(f)
28
+ print(f"Cleaning up previous session (ID: {old_state.get('session_id', 'unknown')})")
29
+ except:
30
+ pass
31
+
32
+ # Remove all files in the directory
33
+ for filename in os.listdir(SESSION_DIR):
34
+ filepath = os.path.join(SESSION_DIR, filename)
35
+ try:
36
+ if os.path.isfile(filepath):
37
+ os.unlink(filepath)
38
+ except Exception as e:
39
+ print(f"Error removing file {filepath}: {e}")
40
+ except Exception as e:
41
+ print(f"Error cleaning up previous session: {e}")
42
+
19
43
  # Create session directory
20
44
  if not os.path.exists(SESSION_DIR):
21
45
  os.makedirs(SESSION_DIR)
22
46
 
23
47
  # Initialize session state
48
+ session_id = str(time.time()) + "_" + str(os.getpid())
24
49
  session_state = {
25
50
  "active": True,
26
- "session_id": str(time.time()),
51
+ "session_id": session_id,
27
52
  "start_time": time.time(),
53
+ "last_activity": time.time(),
28
54
  "status": "running",
29
55
  "project_open": False,
30
56
  "project_name": "",
31
- "project_path": ""
57
+ "project_path": "",
58
+ "version": "1.0"
32
59
  }
33
60
 
34
61
  # Write session state
35
62
  with open(SESSION_STATE_FILE, 'w') as f:
36
- json.dump(session_state, f)
63
+ json.dump(session_state, f, indent=2)
37
64
 
38
65
  # Create empty command file
39
66
  with open(COMMAND_FILE, 'w') as f:
40
- json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f)
67
+ json.dump({"status": "ready", "command": None, "timestamp": time.time()}, f, indent=2)
41
68
 
42
69
  # Create empty result file
43
70
  with open(RESULT_FILE, 'w') as f:
44
- json.dump({"status": "none", "result": None, "timestamp": time.time()}, f)
71
+ json.dump({"status": "none", "result": None, "timestamp": time.time()}, f, indent=2)
45
72
 
46
73
  # Create lock file
47
74
  with open(SESSION_LOCK_FILE, 'w') as f:
48
75
  f.write(str(time.time()))
49
76
 
50
77
  print(f"Session directory setup at: {SESSION_DIR}")
78
+ print(f"Session ID: {session_id}")
51
79
  return True
52
80
  except Exception as e:
53
81
  print(f"Failed to set up session directory: {e}")
@@ -78,8 +106,19 @@ def update_session_state(status="running"):
78
106
  project_name = "Unnamed (Path Error)"
79
107
 
80
108
  # Read current state file
81
- with open(SESSION_STATE_FILE, 'r') as f:
82
- session_state = json.load(f)
109
+ try:
110
+ with open(SESSION_STATE_FILE, 'r') as f:
111
+ session_state = json.load(f)
112
+ except (FileNotFoundError, json.JSONDecodeError):
113
+ # State file doesn't exist or is invalid, create a new one
114
+ session_id = str(time.time()) + "_" + str(os.getpid())
115
+ session_state = {
116
+ "active": True,
117
+ "session_id": session_id,
118
+ "start_time": time.time(),
119
+ "status": status,
120
+ "version": "1.0"
121
+ }
83
122
 
84
123
  # Update state
85
124
  session_state["project_open"] = project_open
@@ -90,11 +129,12 @@ def update_session_state(status="running"):
90
129
 
91
130
  # Write updated state
92
131
  with open(SESSION_STATE_FILE, 'w') as f:
93
- json.dump(session_state, f)
132
+ json.dump(session_state, f, indent=2)
94
133
 
95
134
  return True
96
135
  except Exception as e:
97
136
  print(f"Failed to update session state: {e}")
137
+ traceback.print_exc()
98
138
  return False
99
139
 
100
140
  def execute_command(command_data):
@@ -114,6 +154,10 @@ def execute_command(command_data):
114
154
  if cmd_type == "execute_script" and script:
115
155
  # Execute the script in the current CODESYS context
116
156
  try:
157
+ # Create a temporary globals dictionary with the current globals
158
+ # This allows us to isolate the script execution somewhat
159
+ local_globals = globals().copy()
160
+
117
161
  # Capture output by redirecting stdout/stderr
118
162
  import io
119
163
  from contextlib import redirect_stdout, redirect_stderr
@@ -123,7 +167,7 @@ def execute_command(command_data):
123
167
 
124
168
  with redirect_stdout(stdout_buffer), redirect_stderr(stderr_buffer):
125
169
  # Execute the script
126
- exec(script, globals())
170
+ exec(script, local_globals)
127
171
 
128
172
  # Get the captured output
129
173
  stdout_output = stdout_buffer.getvalue()
@@ -143,8 +187,9 @@ def execute_command(command_data):
143
187
  result["error"] = stderr_output
144
188
 
145
189
  except Exception as e:
190
+ traceback.print_exc()
146
191
  result["success"] = False
147
- result["error"] = f"Error executing script: {str(e)}"
192
+ result["error"] = f"Error executing script: {str(e)}\n{traceback.format_exc()}"
148
193
  result["output"] = f"{SCRIPT_ERROR_MARKER}: {str(e)}"
149
194
 
150
195
  elif cmd_type == "stop":
@@ -158,6 +203,11 @@ def execute_command(command_data):
158
203
  # Return the result and signal to stop the loop
159
204
  return result, True
160
205
 
206
+ elif cmd_type == "ping":
207
+ # Simple ping command to check if session is alive
208
+ result["success"] = True
209
+ result["output"] = f"{SCRIPT_SUCCESS_MARKER}: Session is active"
210
+
161
211
  else:
162
212
  result["success"] = False
163
213
  result["error"] = f"Unknown command type: {cmd_type}"
@@ -169,19 +219,31 @@ def execute_command(command_data):
169
219
  return result, False
170
220
 
171
221
  except Exception as e:
222
+ traceback.print_exc()
172
223
  return {
173
224
  "id": command_data.get("id", "unknown"),
174
225
  "success": False,
175
226
  "output": f"{SCRIPT_ERROR_MARKER}: Command execution failed",
176
- "error": str(e)
227
+ "error": f"{str(e)}\n{traceback.format_exc()}"
177
228
  }, False
178
229
 
179
230
  def check_command():
180
231
  """Check if there is a new command to execute"""
181
232
  try:
233
+ # If command file doesn't exist, return immediately
234
+ if not os.path.exists(COMMAND_FILE):
235
+ return False
236
+
182
237
  # Read the command file
183
- with open(COMMAND_FILE, 'r') as f:
184
- command_data = json.load(f)
238
+ try:
239
+ with open(COMMAND_FILE, 'r') as f:
240
+ command_data = json.load(f)
241
+ except json.JSONDecodeError:
242
+ # If command file is corrupted, reset it
243
+ command_data = {"status": "ready", "command": None, "timestamp": time.time()}
244
+ with open(COMMAND_FILE, 'w') as f:
245
+ json.dump(command_data, f, indent=2)
246
+ return False
185
247
 
186
248
  # Check if there's a new command
187
249
  if command_data.get("status") == "pending" and command_data.get("command"):
@@ -191,7 +253,10 @@ def check_command():
191
253
  # Mark as processing
192
254
  command_data["status"] = "processing"
193
255
  with open(COMMAND_FILE, 'w') as f:
194
- json.dump(command_data, f)
256
+ json.dump(command_data, f, indent=2)
257
+
258
+ # Update session state to busy
259
+ update_session_state("busy")
195
260
 
196
261
  # Execute the command
197
262
  result, stop_requested = execute_command(command)
@@ -204,7 +269,7 @@ def check_command():
204
269
  }
205
270
 
206
271
  with open(RESULT_FILE, 'w') as f:
207
- json.dump(result_data, f)
272
+ json.dump(result_data, f, indent=2)
208
273
 
209
274
  # Reset command file
210
275
  command_data = {
@@ -214,18 +279,60 @@ def check_command():
214
279
  }
215
280
 
216
281
  with open(COMMAND_FILE, 'w') as f:
217
- json.dump(command_data, f)
282
+ json.dump(command_data, f, indent=2)
283
+
284
+ # Update session state to running again
285
+ if not stop_requested:
286
+ update_session_state("running")
287
+
288
+ # Update last activity time
289
+ update_last_activity()
218
290
 
219
291
  return stop_requested
220
292
 
221
293
  return False
222
294
  except Exception as e:
223
295
  print(f"Error checking command: {e}")
296
+ traceback.print_exc()
297
+ return False
298
+
299
+ def update_last_activity():
300
+ """Update the last activity timestamp in the session state"""
301
+ try:
302
+ if os.path.exists(SESSION_STATE_FILE):
303
+ with open(SESSION_STATE_FILE, 'r') as f:
304
+ state = json.load(f)
305
+
306
+ state["last_activity"] = time.time()
307
+
308
+ with open(SESSION_STATE_FILE, 'w') as f:
309
+ json.dump(state, f, indent=2)
310
+ except Exception as e:
311
+ print(f"Error updating last activity: {e}")
312
+
313
+ def check_inactivity_timeout():
314
+ """Check if session has been inactive for too long"""
315
+ try:
316
+ if os.path.exists(SESSION_STATE_FILE):
317
+ with open(SESSION_STATE_FILE, 'r') as f:
318
+ state = json.load(f)
319
+
320
+ last_activity = state.get("last_activity", state.get("start_time", time.time()))
321
+ current_time = time.time()
322
+
323
+ if (current_time - last_activity) > MAX_INACTIVITY_TIME:
324
+ print(f"Session inactive for {current_time - last_activity:.1f} seconds, shutting down")
325
+ return True
326
+
327
+ return False
328
+ except Exception as e:
329
+ print(f"Error checking inactivity timeout: {e}")
224
330
  return False
225
331
 
226
332
  def command_loop():
227
333
  """Main loop to check for and execute commands"""
228
334
  stop_requested = False
335
+ last_state_update = time.time()
229
336
 
230
337
  print(f"Starting command loop...")
231
338
  print(f"Command file: {COMMAND_FILE}")
@@ -240,11 +347,24 @@ def command_loop():
240
347
  # Check for new commands
241
348
  stop_requested = check_command()
242
349
 
350
+ # Check for inactivity timeout
351
+ if check_inactivity_timeout():
352
+ print("Session timeout due to inactivity")
353
+ update_session_state("timeout")
354
+ stop_requested = True
355
+
356
+ # Periodically update session state (every 30 seconds)
357
+ current_time = time.time()
358
+ if (current_time - last_state_update) > 30:
359
+ update_session_state()
360
+ last_state_update = current_time
361
+
243
362
  # Sleep for a bit
244
363
  time.sleep(POLL_INTERVAL)
245
364
 
246
365
  except Exception as e:
247
366
  print(f"Error in command loop: {e}")
367
+ traceback.print_exc()
248
368
  # Sleep for a bit
249
369
  time.sleep(POLL_INTERVAL)
250
370
 
@@ -272,6 +392,7 @@ def main():
272
392
  except Exception as e:
273
393
  print(f"Error in session: {e}")
274
394
  print(f"{SCRIPT_ERROR_MARKER}: Session failed - {str(e)}")
395
+ traceback.print_exc()
275
396
  sys.exit(1)
276
397
 
277
398
  if __name__ == "__main__":
@@ -1,4 +1,4 @@
1
- import sys, os, json, time
1
+ import sys, os, json, time, traceback
2
2
  import tempfile
3
3
 
4
4
  # Define markers for script results
@@ -26,15 +26,15 @@ def stop_session():
26
26
  return False
27
27
 
28
28
  # Read session state
29
- with open(SESSION_STATE_FILE, 'r') as f:
30
- try:
29
+ try:
30
+ with open(SESSION_STATE_FILE, 'r') as f:
31
31
  session_state = json.load(f)
32
- except json.JSONDecodeError:
33
- print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
34
- return False
32
+ except json.JSONDecodeError:
33
+ print(f"{SCRIPT_ERROR_MARKER}: Invalid session state file")
34
+ return False
35
35
 
36
36
  # Check if session is active
37
- if not session_state.get('active', False):
37
+ if not session_state.get('active', False) or session_state.get('status') == 'stopped':
38
38
  print(f"{SCRIPT_SUCCESS_MARKER}: Session is already stopped")
39
39
  return True
40
40
 
@@ -43,6 +43,19 @@ def stop_session():
43
43
  print(f"{SCRIPT_ERROR_MARKER}: Command file not found at {COMMAND_FILE}")
44
44
  return False
45
45
 
46
+ # Try to read the lock file to check if session is still alive
47
+ lock_time = 0
48
+ try:
49
+ if os.path.exists(SESSION_LOCK_FILE):
50
+ with open(SESSION_LOCK_FILE, 'r') as f:
51
+ lock_time = float(f.read().strip())
52
+
53
+ current_time = time.time()
54
+ if (current_time - lock_time) > 30:
55
+ print(f"WARNING: Session lock file is stale ({current_time - lock_time:.1f} seconds old). Session may have crashed.")
56
+ except Exception as e:
57
+ print(f"Warning: Error reading lock file: {e}")
58
+
46
59
  # Create stop command
47
60
  stop_command = {
48
61
  "id": f"stop_{int(time.time())}",
@@ -50,7 +63,7 @@ def stop_session():
50
63
  "timestamp": time.time()
51
64
  }
52
65
 
53
- # Write command to file
66
+ # Write command to file with pretty-printing
54
67
  command_data = {
55
68
  "status": "pending",
56
69
  "command": stop_command,
@@ -58,12 +71,12 @@ def stop_session():
58
71
  }
59
72
 
60
73
  with open(COMMAND_FILE, 'w') as f:
61
- json.dump(command_data, f)
74
+ json.dump(command_data, f, indent=2)
62
75
 
63
76
  print(f"Stop command sent to session")
64
77
 
65
78
  # Wait for result (with timeout)
66
- timeout = 10 # seconds
79
+ timeout = 15 # seconds (increased timeout for more reliability)
67
80
  start_time = time.time()
68
81
  result_received = False
69
82
 
@@ -87,26 +100,46 @@ def stop_session():
87
100
  print(f"Error reading result: {e}")
88
101
  time.sleep(0.5)
89
102
 
90
- # Update session state to indicate it's stopped
91
- with open(SESSION_STATE_FILE, 'r') as f:
92
- session_state = json.load(f)
93
-
94
- session_state['active'] = False
95
- session_state['status'] = "stopped"
96
- session_state['end_time'] = time.time()
103
+ # Update session state to indicate it's stopped regardless of result
104
+ try:
105
+ with open(SESSION_STATE_FILE, 'r') as f:
106
+ session_state = json.load(f)
107
+
108
+ session_state['active'] = False
109
+ session_state['status'] = "stopped"
110
+ session_state['end_time'] = time.time()
97
111
 
98
- with open(SESSION_STATE_FILE, 'w') as f:
99
- json.dump(session_state, f)
112
+ with open(SESSION_STATE_FILE, 'w') as f:
113
+ json.dump(session_state, f, indent=2)
114
+ except Exception as e:
115
+ print(f"Warning: Failed to update session state file: {e}")
100
116
 
101
117
  if result_received:
102
118
  print(f"{SCRIPT_SUCCESS_MARKER}: Session stopped successfully")
119
+ return True
103
120
  else:
104
- print(f"{SCRIPT_ERROR_MARKER}: Timeout waiting for session to stop")
105
-
106
- return result_received
121
+ # If we didn't get a response, the session may be hung
122
+ # Force a cleanup by removing session files
123
+ print(f"Warning: Session did not respond to stop command. Forcing cleanup.")
124
+ try:
125
+ # Try to remove all session files to ensure a clean state for next session
126
+ for filename in os.listdir(SESSION_DIR):
127
+ filepath = os.path.join(SESSION_DIR, filename)
128
+ try:
129
+ if os.path.isfile(filepath):
130
+ os.unlink(filepath)
131
+ except Exception as e:
132
+ print(f"Error removing file {filepath}: {e}")
133
+
134
+ print(f"{SCRIPT_SUCCESS_MARKER}: Session files cleaned up forcefully")
135
+ return True
136
+ except Exception as e:
137
+ print(f"{SCRIPT_ERROR_MARKER}: Failed to force cleanup - {str(e)}")
138
+ return False
107
139
 
108
140
  except Exception as e:
109
141
  print(f"{SCRIPT_ERROR_MARKER}: Error stopping session - {str(e)}")
142
+ traceback.print_exc()
110
143
  return False
111
144
 
112
145
  # Main script
@@ -1,89 +1,110 @@
1
- # CODESYS MCP Session Management (Not Yet Implemented)
2
-
3
- > **IMPORTANT:** Session management is currently disabled in version 1.2.4.
4
- > The `start_session` and `stop_session` commands exist but are non-functional.
5
- > All operations will run in standard mode (starting and stopping CODESYS for each operation).
6
- > This feature will be fully implemented in a future release.
1
+ # Session Management in CODESYS MCP Toolkit
7
2
 
8
3
  ## Overview
9
4
 
10
- The CODESYS MCP Toolkit now supports persistent sessions, which significantly improves performance when executing multiple operations in sequence. This document explains how to use the session management functionality.
11
-
12
- ## Why Session Management?
5
+ Starting from version 1.4.0, the CODESYS MCP Toolkit supports persistent sessions. This feature significantly improves performance by allowing a single instance of CODESYS to stay open while multiple commands are executed, then closing it when explicitly requested or after a period of inactivity.
13
6
 
14
- Previously, each MCP command would start a new CODESYS process, execute the operation, and then shut down the process. This approach is inefficient when performing multiple operations, such as:
7
+ ## Benefits
15
8
 
16
- - Creating multiple POUs
17
- - Editing multiple code sections
18
- - Complex project setup with many steps
9
+ - **Faster execution**: Eliminates the overhead of starting and stopping CODESYS for each command
10
+ - **Shared context**: Commands executed in the same session share the same CODESYS runtime context
11
+ - **Resource efficiency**: Reduces CPU and memory usage by avoiding repeated process creation
12
+ - **Automatic timeout**: Sessions automatically shut down after a period of inactivity (10 minutes by default)
19
13
 
20
- With session management, you can:
21
- 1. Start a CODESYS session once
22
- 2. Execute multiple operations without restarting CODESYS
23
- 3. Close the session when you're finished
14
+ ## Session Management Commands
24
15
 
25
- This can reduce execution time from minutes to seconds for complex tasks.
16
+ The toolkit provides three specific commands for managing sessions:
26
17
 
27
- ## Using Session Management with Claude
18
+ ### 1. `start_session`
28
19
 
29
- You can now use the `start_session` and `stop_session` commands in your Claude interactions with CODESYS:
30
-
31
- ### Starting a Session
20
+ Starts a new persistent CODESYS session. If a session is already running, this command reports that fact without starting another.
32
21
 
22
+ **Example**:
33
23
  ```
34
- Using the CODESYS Local server, run the start_session command to open a persistent CODESYS session.
24
+ /mcp codesys_local start_session
35
25
  ```
36
26
 
37
- ### Using the Session
27
+ ### 2. `session_status`
38
28
 
39
- Once the session is started, all subsequent operations will use the persistent session automatically:
29
+ Checks if a session is currently active and returns information about it, including:
30
+ - Current state (running, busy, stopped, etc.)
31
+ - How long the session has been running
32
+ - Which project is open in the session (if any)
40
33
 
34
+ **Example**:
41
35
  ```
42
- Using the CODESYS Local server, create a new project at C:/MyProjects/Test.project
36
+ /mcp codesys_local session_status
43
37
  ```
44
38
 
45
- ```
46
- Using the CODESYS Local server, add a POU named MainProgram of type Program with ST language
47
- ```
39
+ ### 3. `stop_session`
48
40
 
49
- ### Checking Session Status
41
+ Stops an active session and cleans up session resources. If no session is running, this command reports that fact without taking any action.
50
42
 
43
+ **Example**:
51
44
  ```
52
- Using the CODESYS Local server, run the session_status command to check the current state of the CODESYS session.
45
+ /mcp codesys_local stop_session
53
46
  ```
54
47
 
55
- ### Stopping the Session
48
+ ## How to Use Session Management
56
49
 
57
- When you're done with all operations, you should close the session to free up resources:
50
+ ### Recommended Workflow
58
51
 
59
- ```
60
- Using the CODESYS Local server, run the stop_session command to close the CODESYS session.
61
- ```
52
+ 1. Start a session at the beginning of your work:
53
+ ```
54
+ /mcp codesys_local start_session
55
+ ```
56
+
57
+ 2. Execute all your commands normally - they will automatically use the active session:
58
+ ```
59
+ /mcp codesys_local open_project --filePath "C:/path/to/project.project"
60
+ /mcp codesys_local create_pou --projectFilePath "C:/path/to/project.project" --name "MyPOU" --type "FunctionBlock" --language "ST" --parentPath "Application"
61
+ ```
62
62
 
63
- ## Recommended Workflow
63
+ 3. When you're done working, stop the session:
64
+ ```
65
+ /mcp codesys_local stop_session
66
+ ```
64
67
 
65
- For the best performance, we recommend the following workflow when working with CODESYS projects:
68
+ If you forget to stop the session, it will automatically time out after 10 minutes of inactivity.
66
69
 
67
- 1. Start the session: `Using the CODESYS Local server, run the start_session command to open a persistent CODESYS session.`
68
- 2. Execute all required operations (create project, create POUs, edit code, etc.)
69
- 3. Stop the session: `Using the CODESYS Local server, run the stop_session command to close the CODESYS session.`
70
+ ### Session Behavior
70
71
 
71
- ## Technical Notes
72
+ - All commands automatically use an active session if one exists
73
+ - If no session exists when a command is run, it will execute without a session (traditional mode)
74
+ - You can explicitly start a session before running commands to improve performance
75
+ - Sessions maintain state between commands, including open projects
72
76
 
73
- - Sessions will automatically close after 5 minutes of inactivity to prevent resource leaks
74
- - The session status can be in one of the following states: `IDLE`, `STARTING`, `RUNNING`, `BUSY`, `STOPPING`, `STOPPED`, or `ERROR`
75
- - If a session operation fails, the system will automatically fall back to the standard one-time execution mode
77
+ ## Troubleshooting
76
78
 
77
- ## Limitations
79
+ ### Common Issues
78
80
 
79
- - The session management is still experimental and may not work with all CODESYS operations
80
- - Only one session can be active at a time per MCP server instance
81
- - Some operations that modify the CODESYS environment itself may require restarting the session
81
+ #### Session Won't Start
82
82
 
83
- ## Troubleshooting
83
+ If you can't start a session, check:
84
+ - Make sure CODESYS is properly installed and accessible at the path specified in your configuration
85
+ - Verify that the CODESYS profile exists and is valid
86
+ - Check if there are any error messages in the client's output
87
+
88
+ #### Session Appears Unresponsive
89
+
90
+ If a session seems stuck:
91
+ 1. Check its status: `/mcp codesys_local session_status`
92
+ 2. If it reports as active but not responding, try stopping it: `/mcp codesys_local stop_session`
93
+ 3. If it still doesn't respond, the toolkit includes a forced cleanup mechanism that will activate automatically when you try to stop a non-responsive session
94
+
95
+ #### Session Files Remain After Stopping
96
+
97
+ Session state is stored in a temporary directory (`[temp]/codesys_session/`). These files should be cleaned up automatically when a session stops, but in rare cases (like if the process is killed abruptly), they might remain. The toolkit will clean up old files when starting a new session.
98
+
99
+ ## Technical Details
100
+
101
+ The session management system works by:
84
102
 
85
- If you encounter issues with session management:
103
+ 1. Starting a CODESYS instance with a special Python script that enters a command loop
104
+ 2. Maintaining session state in files within a temporary directory
105
+ 3. Communicating with the running session via file-based IPC (Inter-Process Communication)
106
+ 4. Executing scripts within the context of the running CODESYS instance
107
+ 5. Automatically detecting and handling session health issues
86
108
 
87
- 1. Check the session status: `Using the CODESYS Local server, run the session_status command`
88
- 2. If the status is `ERROR`, try stopping the session and starting a new one
89
- 3. If problems persist, fall back to using standard operations without explicit session management
109
+ Session files are stored in the system's temporary directory in a subfolder called `codesys_session`.
110
+ EOF < /dev/null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Model Context Protocol (MCP) server for CODESYS automation platform",
5
5
  "main": "dist/server.js",
6
6
  "bin": {