@codesys/mcp-toolkit 1.5.6 → 1.5.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,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.5.8] - 2025-05-02
9
+ ### Fixed
10
+ - Fixed null reference error when accessing sessionManager.getState() with --no-session flag
11
+ - Added proper null checks for session manager in session_status, start_session, and stop_session tools
12
+ - Improved error messages when trying to use session management tools with --no-session flag
13
+
14
+ ## [1.5.7] - 2025-05-02
15
+ ### Fixed
16
+ - Added safety timeout to session scripts to prevent CODESYS UI hanging
17
+ - Improved session detection and management to handle zombie sessions
18
+ - Enhanced session cleanup in stop_session.py with more robust error handling
19
+ - Added maximum session runtime (8 hours) to prevent infinite hanging
20
+ - Improved error detection and recovery in session management scripts
21
+
8
22
  ## [1.5.6] - 2025-05-02
9
23
  ### Fixed
10
24
  - Fixed session manager null reference error in interop.ts
Binary file
Binary file
@@ -97,6 +97,9 @@ def check_session_status():
97
97
  except json.JSONDecodeError:
98
98
  print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
99
99
  return False
100
+ except Exception as e:
101
+ print("{0}: Error reading session state file: {1}".format(SCRIPT_ERROR_MARKER, str(e)))
102
+ return False
100
103
 
101
104
  # Check if session is active
102
105
  active = session_state.get('active', False)
@@ -107,13 +110,36 @@ def check_session_status():
107
110
  print("{0}: Session is not active (Status: {1})".format(SCRIPT_SUCCESS_MARKER, status))
108
111
  return False
109
112
 
113
+ # Check for session running too long (> 8 hours)
114
+ start_time = session_state.get('start_time', 0)
115
+ current_time = time.time()
116
+ uptime = current_time - start_time if start_time else 0
117
+ max_allowed_uptime = 60 * 60 * 8 # 8 hours in seconds
118
+
119
+ if uptime > max_allowed_uptime:
120
+ print("WARNING: Session has been running for {0:.1f} hours, exceeding the 8-hour safety limit".format(uptime / 3600))
121
+
122
+ # Update session state to indicate timeout due to excessive runtime
123
+ try:
124
+ session_state['status'] = 'timeout'
125
+ session_state['active'] = False
126
+ session_state['error_reason'] = 'Maximum runtime exceeded'
127
+
128
+ with open(SESSION_STATE_FILE, 'w') as f:
129
+ json.dump(session_state, f, indent=2)
130
+
131
+ print("Updated session state to timeout due to excessive runtime")
132
+ print("{0}: Session is not active (exceeded maximum runtime)".format(SCRIPT_SUCCESS_MARKER))
133
+ return False
134
+ except Exception as e:
135
+ print("Error updating session state: {0}".format(e))
136
+
110
137
  # Check session lock file to see if it's been updated recently
111
138
  lock_is_stale = False
112
139
  if os.path.exists(SESSION_LOCK_FILE):
113
140
  try:
114
141
  with open(SESSION_LOCK_FILE, 'r') as f:
115
142
  lock_time = float(f.read().strip())
116
- current_time = time.time()
117
143
  time_diff = current_time - lock_time
118
144
 
119
145
  # If lock file hasn't been updated in 30 seconds, session might be dead
@@ -213,9 +239,10 @@ def check_session_status():
213
239
 
214
240
  # Include session info
215
241
  session_id = session_state.get('session_id', 'Unknown')
216
- start_time = session_state.get('start_time', 0)
217
- uptime = time.time() - start_time if start_time else 0
218
- uptime_str = "{0:.0f} seconds".format(uptime) if uptime < 60 else "{0:.1f} minutes".format(uptime/60)
242
+ uptime_str = "{0:.0f} seconds".format(uptime) if uptime < 60 else (
243
+ "{0:.1f} minutes".format(uptime/60) if uptime < 3600 else
244
+ "{0:.1f} hours".format(uptime/3600)
245
+ )
219
246
 
220
247
  if session_active:
221
248
  print("{0}: Session is active (ID: {1}, Status: {2}, Uptime: {3}{4})".format(SCRIPT_SUCCESS_MARKER, session_id, status, uptime_str, project_info))
@@ -648,6 +648,11 @@ def command_loop():
648
648
  last_state_update = time.time()
649
649
  loop_count = 0
650
650
  last_heartbeat = time.time()
651
+ start_time = time.time()
652
+ error_count = 0 # Track consecutive errors
653
+
654
+ # Maximum session runtime to prevent infinite hanging (8 hours)
655
+ max_session_runtime = 60 * 60 * 8
651
656
 
652
657
  print("Starting command loop...")
653
658
  print("Command file: {0}".format(COMMAND_FILE))
@@ -656,6 +661,7 @@ def command_loop():
656
661
  print("Session state file: {0}".format(SESSION_STATE_FILE))
657
662
  print("Poll interval: {0} seconds".format(POLL_INTERVAL))
658
663
  print("Max inactivity time: {0} seconds".format(MAX_INACTIVITY_TIME))
664
+ print("Max session runtime: {0} hours".format(max_session_runtime / 3600))
659
665
  sys.stdout.flush() # Ensure initialization output is visible immediately
660
666
 
661
667
  # Set up initial session state files
@@ -693,6 +699,17 @@ def command_loop():
693
699
  loop_count += 1
694
700
  current_time = time.time()
695
701
 
702
+ # Check for absolute maximum session runtime to prevent infinite hanging
703
+ session_runtime = current_time - start_time
704
+ if session_runtime > max_session_runtime:
705
+ print("SAFETY: Maximum session run time of {0} hours reached".format(max_session_runtime / 3600))
706
+ print("SAFETY: This is to prevent CODESYS from hanging indefinitely")
707
+ print("SAFETY: Session will now terminate")
708
+ update_session_state("timeout")
709
+ sys.stdout.flush()
710
+ stop_requested = True
711
+ break
712
+
696
713
  # Update session lock file to indicate we're alive
697
714
  with open(SESSION_LOCK_FILE, 'w') as f:
698
715
  f.write(str(current_time))
@@ -700,6 +717,7 @@ def command_loop():
700
717
  # Print heartbeat message every 60 seconds
701
718
  if current_time - last_heartbeat > 60:
702
719
  print("Command loop heartbeat - still alive (iteration {0})".format(loop_count))
720
+ print("Session running for {0:.1f} minutes".format(session_runtime / 60))
703
721
  last_heartbeat = current_time
704
722
  sys.stdout.flush()
705
723
 
@@ -723,6 +741,9 @@ def command_loop():
723
741
  last_state_update = current_time
724
742
  sys.stdout.flush()
725
743
 
744
+ # Reset error count on successful iteration
745
+ error_count = 0
746
+
726
747
  # Sleep for a bit
727
748
  time.sleep(POLL_INTERVAL)
728
749
 
@@ -730,8 +751,19 @@ def command_loop():
730
751
  print("Error in command loop: {0}".format(e))
731
752
  traceback.print_exc()
732
753
  sys.stdout.flush()
733
- # Sleep for a bit before retrying
734
- time.sleep(POLL_INTERVAL)
754
+
755
+ # Increment error count
756
+ error_count += 1
757
+
758
+ # If we're getting repeated errors, consider breaking the loop
759
+ if error_count > 10:
760
+ print("SAFETY: Too many consecutive errors ({0}), stopping session".format(error_count))
761
+ update_session_state("error")
762
+ sys.stdout.flush()
763
+ stop_requested = True
764
+ else:
765
+ # Sleep for a bit before retrying
766
+ time.sleep(POLL_INTERVAL)
735
767
 
736
768
  print("Command loop stopped after {0} iterations".format(loop_count))
737
769
  sys.stdout.flush()
@@ -85,65 +85,157 @@ print("SESSION_LOCK_FILE: {0}".format(SESSION_LOCK_FILE))
85
85
  def stop_session():
86
86
  """Stop a running CODESYS session"""
87
87
  try:
88
+ print("Attempting to stop CODESYS session")
89
+
88
90
  # Check if session directory exists
89
91
  if not os.path.exists(SESSION_DIR):
90
- print("{0}: No session directory found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_DIR))
91
- return False
92
+ print("{0}: No session directory found at {1}".format(SCRIPT_SUCCESS_MARKER, SESSION_DIR))
93
+ # Consider this a success since there's no session to stop
94
+ return True
92
95
 
93
96
  # Check if session state file exists
94
97
  if not os.path.exists(SESSION_STATE_FILE):
95
- print("{0}: No session state file found at {1}".format(SCRIPT_ERROR_MARKER, SESSION_STATE_FILE))
96
- return False
98
+ print("{0}: No session state file found at {1}".format(SCRIPT_SUCCESS_MARKER, SESSION_STATE_FILE))
99
+ # No state file means no active session - clear any other potential session files
100
+ try:
101
+ for filename in os.listdir(SESSION_DIR):
102
+ filepath = os.path.join(SESSION_DIR, filename)
103
+ try:
104
+ if os.path.isfile(filepath):
105
+ os.unlink(filepath)
106
+ print("Removed orphaned session file: {0}".format(filepath))
107
+ except Exception as e:
108
+ print("Error removing file {0}: {1}".format(filepath, e))
109
+ except Exception as e:
110
+ print("Error cleaning session directory: {0}".format(e))
111
+
112
+ return True
97
113
 
98
114
  # Read session state
99
115
  try:
100
116
  with open(SESSION_STATE_FILE, 'r') as f:
101
117
  session_state = json.load(f)
102
- except json.JSONDecodeError:
103
- print("{0}: Invalid session state file".format(SCRIPT_ERROR_MARKER))
104
- return False
105
-
106
- # Check if session is active
107
- if not session_state.get('active', False) or session_state.get('status') == 'stopped':
108
- print("{0}: Session is already stopped".format(SCRIPT_SUCCESS_MARKER))
118
+ except Exception as e:
119
+ print("Error reading session state file: {0}".format(e))
120
+ # Force cleanup regardless of the error
121
+ try:
122
+ for filename in os.listdir(SESSION_DIR):
123
+ filepath = os.path.join(SESSION_DIR, filename)
124
+ try:
125
+ if os.path.isfile(filepath):
126
+ os.unlink(filepath)
127
+ print("Removed session file due to corrupt state: {0}".format(filepath))
128
+ except Exception as rm_err:
129
+ print("Error removing file {0}: {1}".format(filepath, rm_err))
130
+ except Exception as dir_err:
131
+ print("Error cleaning session directory: {0}".format(dir_err))
132
+
133
+ print("{0}: Session files cleaned up forcefully due to corrupt state".format(SCRIPT_SUCCESS_MARKER))
109
134
  return True
110
135
 
111
- # Check if command file exists
112
- if not os.path.exists(COMMAND_FILE):
113
- print("{0}: Command file not found at {1}".format(SCRIPT_ERROR_MARKER, COMMAND_FILE))
114
- return False
136
+ # Check if session is already marked as inactive
137
+ if not session_state.get('active', False) or session_state.get('status') in ['stopped', 'error', 'timeout']:
138
+ print("{0}: Session is already marked as {1}".format(
139
+ SCRIPT_SUCCESS_MARKER, session_state.get('status', 'inactive')))
140
+
141
+ # Remove session files to ensure a clean state
142
+ try:
143
+ for filename in os.listdir(SESSION_DIR):
144
+ filepath = os.path.join(SESSION_DIR, filename)
145
+ try:
146
+ if os.path.isfile(filepath):
147
+ os.unlink(filepath)
148
+ print("Removed session file for cleanup: {0}".format(filepath))
149
+ except Exception as e:
150
+ print("Error removing file {0}: {1}".format(filepath, e))
151
+ except Exception as e:
152
+ print("Error cleaning session directory: {0}".format(e))
153
+
154
+ return True
115
155
 
116
- # Try to read the lock file to check if session is still alive
117
- lock_time = 0
156
+ # Get lock file age to check if session is responsive
157
+ lock_is_stale = False
158
+ current_time = time.time()
159
+
118
160
  try:
119
161
  if os.path.exists(SESSION_LOCK_FILE):
120
162
  with open(SESSION_LOCK_FILE, 'r') as f:
121
163
  lock_time = float(f.read().strip())
164
+ lock_age = current_time - lock_time
122
165
 
123
- current_time = time.time()
124
- if (current_time - lock_time) > 30:
125
- print("WARNING: Session lock file is stale ({0:.1f} seconds old). Session may have crashed.".format(current_time - lock_time))
166
+ if lock_age > 30:
167
+ print("WARNING: Session lock file is stale ({0:.1f} seconds old). Session may have crashed.".format(lock_age))
168
+ lock_is_stale = True
169
+ else:
170
+ print("WARNING: Session lock file not found. Session may have crashed.")
171
+ lock_is_stale = True
126
172
  except Exception as e:
127
173
  print("Warning: Error reading lock file: {0}".format(e))
174
+ lock_is_stale = True
128
175
 
129
- # Create stop command
130
- stop_command = {
131
- "id": "stop_{0}".format(int(time.time())),
132
- "type": "stop",
133
- "timestamp": time.time()
134
- }
176
+ # If the command file doesn't exist, we have a zombie session
177
+ if not os.path.exists(COMMAND_FILE):
178
+ print("WARNING: Command file not found. Session is in a zombie state.")
179
+ lock_is_stale = True
180
+
181
+ # If the session appears to be a zombie (stale lock), don't try to send a command
182
+ # Just force cleanup immediately
183
+ if lock_is_stale:
184
+ print("Session appears to be unresponsive. Skipping command and forcing cleanup.")
185
+ try:
186
+ # Update state file first
187
+ try:
188
+ session_state['active'] = False
189
+ session_state['status'] = "error"
190
+ session_state['end_time'] = current_time
191
+ session_state['error_reason'] = "Unresponsive session, forced cleanup"
192
+
193
+ with open(SESSION_STATE_FILE, 'w') as f:
194
+ json.dump(session_state, f, indent=2)
195
+ print("Updated session state to error before cleanup")
196
+ except Exception as state_err:
197
+ print("Warning: Failed to update session state file: {0}".format(state_err))
198
+
199
+ # Remove all session files
200
+ for filename in os.listdir(SESSION_DIR):
201
+ filepath = os.path.join(SESSION_DIR, filename)
202
+ try:
203
+ if os.path.isfile(filepath):
204
+ os.unlink(filepath)
205
+ print("Removed zombie session file: {0}".format(filepath))
206
+ except Exception as e:
207
+ print("Error removing file {0}: {1}".format(filepath, e))
208
+
209
+ print("{0}: Zombie session files cleaned up forcefully".format(SCRIPT_SUCCESS_MARKER))
210
+ return True
211
+ except Exception as e:
212
+ print("{0}: Failed to force cleanup - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
213
+ return False
214
+
215
+ # For responsive sessions, try to send a stop command first
216
+ try:
217
+ # Create stop command
218
+ stop_command = {
219
+ "id": "stop_{0}".format(int(time.time())),
220
+ "type": "stop",
221
+ "timestamp": current_time
222
+ }
135
223
 
136
- # Write command to file with pretty-printing
137
- command_data = {
138
- "status": "pending",
139
- "command": stop_command,
140
- "timestamp": time.time()
141
- }
224
+ # Write command to file with pretty-printing
225
+ command_data = {
226
+ "status": "pending",
227
+ "command": stop_command,
228
+ "timestamp": current_time
229
+ }
142
230
 
143
- with open(COMMAND_FILE, 'w') as f:
144
- json.dump(command_data, f, indent=2)
231
+ print("Writing stop command to command file: {0}".format(COMMAND_FILE))
232
+ with open(COMMAND_FILE, 'w') as f:
233
+ json.dump(command_data, f, indent=2)
145
234
 
146
- print("Stop command sent to session")
235
+ print("Stop command sent to session")
236
+ except Exception as cmd_err:
237
+ print("Error sending stop command: {0}".format(cmd_err))
238
+ # Continue to forced cleanup
147
239
 
148
240
  # Wait for result (with timeout)
149
241
  timeout = 15 # seconds (increased timeout for more reliability)
@@ -181,35 +273,47 @@ def stop_session():
181
273
 
182
274
  with open(SESSION_STATE_FILE, 'w') as f:
183
275
  json.dump(session_state, f, indent=2)
276
+ print("Updated session state to stopped")
184
277
  except Exception as e:
185
278
  print("Warning: Failed to update session state file: {0}".format(e))
186
279
 
187
- if result_received:
188
- print("{0}: Session stopped successfully".format(SCRIPT_SUCCESS_MARKER))
280
+ # Remove all session files for a clean state
281
+ print("Cleaning up all session files")
282
+ try:
283
+ for filename in os.listdir(SESSION_DIR):
284
+ filepath = os.path.join(SESSION_DIR, filename)
285
+ try:
286
+ if os.path.isfile(filepath):
287
+ os.unlink(filepath)
288
+ print("Removed session file: {0}".format(filepath))
289
+ except Exception as e:
290
+ print("Error removing file {0}: {1}".format(filepath, e))
291
+
292
+ print("{0}: Session stopped and files cleaned up successfully".format(SCRIPT_SUCCESS_MARKER))
189
293
  return True
190
- else:
191
- # If we didn't get a response, the session may be hung
192
- # Force a cleanup by removing session files
193
- print("Warning: Session did not respond to stop command. Forcing cleanup.")
194
- try:
195
- # Try to remove all session files to ensure a clean state for next session
294
+ except Exception as e:
295
+ print("{0}: Error during final cleanup - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
296
+ return False
297
+
298
+ except Exception as e:
299
+ print("{0}: Error stopping session - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
300
+ traceback.print_exc()
301
+
302
+ # Try one final desperate cleanup even if everything else failed
303
+ try:
304
+ if os.path.exists(SESSION_DIR):
196
305
  for filename in os.listdir(SESSION_DIR):
197
306
  filepath = os.path.join(SESSION_DIR, filename)
198
307
  try:
199
308
  if os.path.isfile(filepath):
200
309
  os.unlink(filepath)
201
- except Exception as e:
202
- print("Error removing file {0}: {1}".format(filepath, e))
203
-
204
- print("{0}: Session files cleaned up forcefully".format(SCRIPT_SUCCESS_MARKER))
205
- return True
206
- except Exception as e:
207
- print("{0}: Failed to force cleanup - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
208
- return False
209
-
210
- except Exception as e:
211
- print("{0}: Error stopping session - {1}".format(SCRIPT_ERROR_MARKER, str(e)))
212
- traceback.print_exc()
310
+ print("Emergency cleanup: Removed session file: {0}".format(filepath))
311
+ except Exception:
312
+ pass
313
+ print("Emergency session cleanup attempted after error")
314
+ except Exception:
315
+ pass
316
+
213
317
  return False
214
318
 
215
319
  # Main script
package/dist/server.js CHANGED
@@ -177,7 +177,7 @@ function startMcpServer(config) {
177
177
  console.error("SERVER.TS: Initializing McpServer...");
178
178
  const server = new mcp_js_1.McpServer({
179
179
  name: "CODESYS Control MCP Server",
180
- version: "1.8.1", // Updated version for session fixes
180
+ version: "1.8.3", // Updated version with null reference fixes for session management
181
181
  capabilities: {
182
182
  resources: { listChanged: true },
183
183
  tools: { listChanged: true }
@@ -800,6 +800,17 @@ function startMcpServer(config) {
800
800
  // --- Session Management Tools ---
801
801
  server.tool("start_session", "Starts a persistent CODESYS session for improved performance with multiple operations", {}, () => __awaiter(this, void 0, void 0, function* () {
802
802
  console.error("Tool call: start_session");
803
+ // First check if session manager is defined
804
+ if (!sessionManager) {
805
+ console.error("Tool call: start_session - session manager is not defined");
806
+ return {
807
+ content: [{
808
+ type: "text",
809
+ text: "Session management is disabled. Use the toolkit without the --no-session flag to enable session management."
810
+ }],
811
+ isError: true
812
+ };
813
+ }
803
814
  try {
804
815
  const currentState = sessionManager.getState();
805
816
  if (currentState === sessionManager_1.SessionState.RUNNING || currentState === sessionManager_1.SessionState.BUSY) {
@@ -842,6 +853,17 @@ function startMcpServer(config) {
842
853
  }));
843
854
  server.tool("stop_session", "Stops the persistent CODESYS session", {}, () => __awaiter(this, void 0, void 0, function* () {
844
855
  console.error("Tool call: stop_session");
856
+ // First check if session manager is defined
857
+ if (!sessionManager) {
858
+ console.error("Tool call: stop_session - session manager is not defined");
859
+ return {
860
+ content: [{
861
+ type: "text",
862
+ text: "Session management is disabled. No active session to stop."
863
+ }],
864
+ isError: false // Not an error, as there's no session to stop
865
+ };
866
+ }
845
867
  try {
846
868
  const currentState = sessionManager.getState();
847
869
  if (currentState === sessionManager_1.SessionState.STOPPED) {
@@ -884,6 +906,17 @@ function startMcpServer(config) {
884
906
  }));
885
907
  server.tool("session_status", "Returns the status of the CODESYS session", {}, () => __awaiter(this, void 0, void 0, function* () {
886
908
  console.error("Tool call: session_status");
909
+ // First check if session manager is defined
910
+ if (!sessionManager) {
911
+ console.error("Tool call: session_status - session manager is not defined");
912
+ return {
913
+ content: [{
914
+ type: "text",
915
+ text: "Session management is disabled. Status: DISABLED"
916
+ }],
917
+ isError: false
918
+ };
919
+ }
887
920
  try {
888
921
  const state = sessionManager.getState();
889
922
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "1.5.6",
3
+ "version": "1.5.8",
4
4
  "description": "Model Context Protocol (MCP) server for CODESYS automation platform",
5
5
  "main": "dist/server.js",
6
6
  "bin": {