@codesys/mcp-toolkit 1.2.4 → 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.
- package/.claude/settings.local.json +3 -1
- package/CHANGELOG.md +14 -0
- package/dist/scripts/session_status.py +162 -141
- package/dist/scripts/start_session.py +222 -63
- package/dist/scripts/stop_session.py +128 -72
- package/dist/sessionManager.js +54 -49
- package/docs/session_management.md +75 -54
- package/package.json +1 -1
package/dist/sessionManager.js
CHANGED
|
@@ -63,11 +63,12 @@ const scriptLoader_1 = require("./scriptLoader");
|
|
|
63
63
|
const DEFAULT_SCRIPT_TIMEOUT = 60000; // 60 seconds
|
|
64
64
|
const DEFAULT_SESSION_IDLE_TIMEOUT = 300000; // 5 minutes
|
|
65
65
|
const DEFAULT_SESSION_POLL_INTERVAL = 1000; // 1 second
|
|
66
|
-
// Session directory and
|
|
66
|
+
// Session directory and file names
|
|
67
67
|
const SESSION_DIR = path.join(os.tmpdir(), 'codesys_session');
|
|
68
|
-
const
|
|
69
|
-
const
|
|
68
|
+
const COMMAND_FILE = 'command.json';
|
|
69
|
+
const RESULT_FILE = 'result.json';
|
|
70
70
|
const STATE_FILE = 'session_state.json';
|
|
71
|
+
const SESSION_LOCK_FILE = 'session.lock';
|
|
71
72
|
// Define expected success/error markers from the Python scripts
|
|
72
73
|
const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
|
|
73
74
|
const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
|
|
@@ -120,11 +121,12 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
120
121
|
this.codesysProfileName = codesysProfileName;
|
|
121
122
|
this.idleTimeoutMs = idleTimeoutMs;
|
|
122
123
|
this.sessionId = (0, uuid_1.v4)();
|
|
123
|
-
// Set up session directory and
|
|
124
|
+
// Set up session directory and files
|
|
124
125
|
this.sessionDir = path.join(SESSION_DIR, this.sessionId);
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
126
|
+
this.commandFilePath = path.join(this.sessionDir, COMMAND_FILE);
|
|
127
|
+
this.resultFilePath = path.join(this.sessionDir, RESULT_FILE);
|
|
127
128
|
this.stateFilePath = path.join(this.sessionDir, STATE_FILE);
|
|
129
|
+
this.lockFilePath = path.join(this.sessionDir, SESSION_LOCK_FILE);
|
|
128
130
|
// Log status changes through event emitter
|
|
129
131
|
this.on(exports.SESSION_EVENTS.STARTED, () => {
|
|
130
132
|
console.error(`SESSION_MANAGER: Session started successfully`);
|
|
@@ -247,8 +249,8 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
247
249
|
// Interpolate parameters
|
|
248
250
|
const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
|
|
249
251
|
SESSION_DIR: this.sessionDir,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
COMMAND_FILE: COMMAND_FILE,
|
|
253
|
+
RESULT_FILE: RESULT_FILE,
|
|
252
254
|
STATE_FILE: STATE_FILE,
|
|
253
255
|
SESSION_ID: this.sessionId,
|
|
254
256
|
});
|
|
@@ -319,10 +321,10 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
319
321
|
const startTime = Date.now();
|
|
320
322
|
while (Date.now() - startTime < timeoutMs) {
|
|
321
323
|
try {
|
|
322
|
-
// Check if
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
324
|
+
// Check if files exist
|
|
325
|
+
const commandFileExists = fs.existsSync(this.commandFilePath);
|
|
326
|
+
const resultFileExists = fs.existsSync(this.resultFilePath);
|
|
327
|
+
if (commandFileExists && resultFileExists) {
|
|
326
328
|
// Read state file to check if session is ready
|
|
327
329
|
const stateContent = yield (0, promises_1.readFile)(this.stateFilePath, 'utf8');
|
|
328
330
|
const stateData = JSON.parse(stateContent);
|
|
@@ -366,14 +368,14 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
366
368
|
}
|
|
367
369
|
try {
|
|
368
370
|
// Check if we have a running session
|
|
369
|
-
if (fs.existsSync(this.sessionDir) && fs.existsSync(this.
|
|
371
|
+
if (fs.existsSync(this.sessionDir) && fs.existsSync(this.commandFilePath)) {
|
|
370
372
|
// Load the stop_session script template
|
|
371
373
|
const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('stop_session');
|
|
372
374
|
// Interpolate parameters
|
|
373
375
|
const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
|
|
374
376
|
SESSION_DIR: this.sessionDir,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
+
COMMAND_FILE: COMMAND_FILE,
|
|
378
|
+
RESULT_FILE: RESULT_FILE,
|
|
377
379
|
STATE_FILE: STATE_FILE,
|
|
378
380
|
SESSION_ID: this.sessionId,
|
|
379
381
|
});
|
|
@@ -524,15 +526,15 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
524
526
|
this.resetIdleTimeout();
|
|
525
527
|
// Mark the session as busy
|
|
526
528
|
this.sessionState = exports.SessionState.BUSY;
|
|
527
|
-
// Check if the command
|
|
528
|
-
if (!fs.existsSync(this.
|
|
529
|
+
// Check if the command file exists
|
|
530
|
+
if (!fs.existsSync(this.commandFilePath)) {
|
|
529
531
|
this.sessionState = exports.SessionState.ERROR;
|
|
530
|
-
throw new Error(`Command
|
|
532
|
+
throw new Error(`Command file not found at ${this.commandFilePath}`);
|
|
531
533
|
}
|
|
532
|
-
// Check if the result
|
|
533
|
-
if (!fs.existsSync(this.
|
|
534
|
+
// Check if the result file exists
|
|
535
|
+
if (!fs.existsSync(this.resultFilePath)) {
|
|
534
536
|
this.sessionState = exports.SessionState.ERROR;
|
|
535
|
-
throw new Error(`Result
|
|
537
|
+
throw new Error(`Result file not found at ${this.resultFilePath}`);
|
|
536
538
|
}
|
|
537
539
|
// Return a promise that will be resolved when script execution completes
|
|
538
540
|
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -571,15 +573,21 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
571
573
|
// Create the command to send
|
|
572
574
|
const command = {
|
|
573
575
|
id: scriptId,
|
|
576
|
+
type: "execute_script",
|
|
574
577
|
script: scriptContent,
|
|
575
578
|
timestamp: Date.now()
|
|
576
579
|
};
|
|
577
|
-
//
|
|
578
|
-
const
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
580
|
+
// Prepare the command data
|
|
581
|
+
const commandData = {
|
|
582
|
+
status: "pending",
|
|
583
|
+
command: command,
|
|
584
|
+
timestamp: Date.now()
|
|
585
|
+
};
|
|
586
|
+
// Send the command by writing to the command file
|
|
587
|
+
fs.writeFileSync(this.commandFilePath, JSON.stringify(commandData));
|
|
588
|
+
console.error(`SESSION_MANAGER: Command written to file`);
|
|
589
|
+
// Start reading the result file
|
|
590
|
+
this.readResultFile(scriptId);
|
|
583
591
|
}
|
|
584
592
|
catch (error) {
|
|
585
593
|
console.error(`SESSION_MANAGER: Error executing script: ${error.message}`);
|
|
@@ -599,12 +607,12 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
599
607
|
});
|
|
600
608
|
}
|
|
601
609
|
/**
|
|
602
|
-
* Read the result
|
|
610
|
+
* Read the result file for a specific script execution
|
|
603
611
|
* @param scriptId The script ID to read results for
|
|
604
612
|
*/
|
|
605
|
-
|
|
606
|
-
console.error(`SESSION_MANAGER: Starting to read result
|
|
607
|
-
// We'll use a polling approach to read from the
|
|
613
|
+
readResultFile(scriptId) {
|
|
614
|
+
console.error(`SESSION_MANAGER: Starting to read result file for script ${scriptId}`);
|
|
615
|
+
// We'll use a polling approach to read from the file
|
|
608
616
|
const pollInterval = setInterval(() => __awaiter(this, void 0, void 0, function* () {
|
|
609
617
|
try {
|
|
610
618
|
if (!this.pendingScripts.has(scriptId)) {
|
|
@@ -612,21 +620,19 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
612
620
|
clearInterval(pollInterval);
|
|
613
621
|
return;
|
|
614
622
|
}
|
|
615
|
-
// Check if the result
|
|
616
|
-
if (!fs.existsSync(this.
|
|
617
|
-
console.error(`SESSION_MANAGER: Result
|
|
623
|
+
// Check if the result file exists
|
|
624
|
+
if (!fs.existsSync(this.resultFilePath)) {
|
|
625
|
+
console.error(`SESSION_MANAGER: Result file not found at ${this.resultFilePath}`);
|
|
618
626
|
return;
|
|
619
627
|
}
|
|
620
|
-
// Read the result
|
|
621
|
-
const resultContent = yield (0, promises_1.readFile)(this.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
if
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const result = JSON.parse(line);
|
|
629
|
-
// Check if this result is for our script
|
|
628
|
+
// Read the result file
|
|
629
|
+
const resultContent = yield (0, promises_1.readFile)(this.resultFilePath, 'utf8');
|
|
630
|
+
try {
|
|
631
|
+
// Parse the result data
|
|
632
|
+
const resultData = JSON.parse(resultContent);
|
|
633
|
+
// Check if the result is complete and for our script
|
|
634
|
+
if (resultData.status === 'complete' && resultData.result) {
|
|
635
|
+
const result = resultData.result;
|
|
630
636
|
if (result.id === scriptId) {
|
|
631
637
|
console.error(`SESSION_MANAGER: Found result for script ${scriptId}`);
|
|
632
638
|
// Remove from pending scripts
|
|
@@ -647,14 +653,13 @@ class CodesysSessionManager extends events_1.EventEmitter {
|
|
|
647
653
|
return;
|
|
648
654
|
}
|
|
649
655
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
656
|
+
}
|
|
657
|
+
catch (parseError) {
|
|
658
|
+
console.error(`SESSION_MANAGER: Error parsing result data: ${parseError}`);
|
|
654
659
|
}
|
|
655
660
|
}
|
|
656
661
|
catch (error) {
|
|
657
|
-
console.error(`SESSION_MANAGER: Error reading result
|
|
662
|
+
console.error(`SESSION_MANAGER: Error reading result file: ${error}`);
|
|
658
663
|
}
|
|
659
664
|
}), 100); // Poll every 100ms
|
|
660
665
|
// Set a timeout to stop polling after the script timeout
|
|
@@ -1,89 +1,110 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
## Benefits
|
|
15
8
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
The toolkit provides three specific commands for managing sessions:
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
### 1. `start_session`
|
|
28
19
|
|
|
29
|
-
|
|
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
|
-
|
|
24
|
+
/mcp codesys_local start_session
|
|
35
25
|
```
|
|
36
26
|
|
|
37
|
-
###
|
|
27
|
+
### 2. `session_status`
|
|
38
28
|
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
/mcp codesys_local stop_session
|
|
53
46
|
```
|
|
54
47
|
|
|
55
|
-
|
|
48
|
+
## How to Use Session Management
|
|
56
49
|
|
|
57
|
-
|
|
50
|
+
### Recommended Workflow
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
63
|
+
3. When you're done working, stop the session:
|
|
64
|
+
```
|
|
65
|
+
/mcp codesys_local stop_session
|
|
66
|
+
```
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
If you forget to stop the session, it will automatically time out after 10 minutes of inactivity.
|
|
66
69
|
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
### Common Issues
|
|
78
80
|
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|