@codesys/mcp-toolkit 1.0.8 → 1.1.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/README.md +83 -186
- package/dist/bin.js +54 -22
- package/dist/codesys_interop.js +118 -98
- package/dist/server.js +541 -1378
- package/package.json +13 -10
- package/direct-codesys-mcp.js +0 -150
package/dist/codesys_interop.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CODESYS Interop Module
|
|
4
|
+
* Handles direct interaction with the CODESYS executable via command-line scripts.
|
|
5
|
+
*
|
|
6
|
+
* This module manages:
|
|
7
|
+
* - Creating temporary Python script files
|
|
8
|
+
* - Executing them via CODESYS's scripting engine
|
|
9
|
+
* - Capturing and processing results
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: Path handling for Windows is critical - paths with spaces require
|
|
12
|
+
* special handling to avoid the 'C:\Program' not recognized error.
|
|
13
|
+
*/
|
|
2
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
15
|
if (k2 === undefined) k2 = k;
|
|
4
16
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -47,169 +59,177 @@ const child_process_1 = require("child_process"); // Use spawn directly
|
|
|
47
59
|
const promises_1 = require("fs/promises");
|
|
48
60
|
const path = __importStar(require("path"));
|
|
49
61
|
const os = __importStar(require("os"));
|
|
62
|
+
const fs = __importStar(require("fs")); // Import fs for existsSync check
|
|
50
63
|
// Define expected success/error markers from the Python scripts
|
|
51
64
|
const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
|
|
52
65
|
const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
|
|
53
66
|
/**
|
|
54
67
|
* Executes a CODESYS Python script using the command line interface.
|
|
55
|
-
* Uses spawn for better handling of paths with spaces.
|
|
56
68
|
*
|
|
57
69
|
* @param scriptContent The Python script code to execute.
|
|
58
|
-
* @param codesysExePath The full path to the CODESYS.exe executable.
|
|
70
|
+
* @param codesysExePath The full path to the CODESYS.exe executable (can contain spaces).
|
|
59
71
|
* @param codesysProfileName The name of the CODESYS profile to use for scripting.
|
|
60
72
|
* @returns A promise resolving to an object containing the success status and the script's output.
|
|
73
|
+
*
|
|
74
|
+
* NOTE: This function uses spawn WITHOUT shell:true for robustness with Windows paths containing spaces.
|
|
75
|
+
* The approach uses modified environment variables and working directory to ensure proper execution.
|
|
61
76
|
*/
|
|
62
77
|
function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
|
|
63
78
|
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
//
|
|
65
|
-
if (!codesysExePath)
|
|
66
|
-
throw new Error('CODESYS executable path was not provided
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
79
|
+
// --- Pre-checks ---
|
|
80
|
+
if (!codesysExePath)
|
|
81
|
+
throw new Error('CODESYS executable path was not provided.');
|
|
82
|
+
if (!codesysProfileName)
|
|
83
|
+
throw new Error('CODESYS profile name was not provided.');
|
|
84
|
+
if (!fs.existsSync(codesysExePath))
|
|
85
|
+
throw new Error(`CODESYS executable not found at provided path: ${codesysExePath}`);
|
|
86
|
+
// --- End Pre-checks ---
|
|
72
87
|
const tempDir = os.tmpdir();
|
|
73
|
-
// Ensure temp file name is safe for command line (though spawn handles args better)
|
|
74
88
|
const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
|
|
75
89
|
const tempFilePath = path.join(tempDir, tempFileName);
|
|
76
90
|
let output = '';
|
|
91
|
+
let stderrOutput = '';
|
|
77
92
|
let success = false;
|
|
78
93
|
let exitCode = null;
|
|
94
|
+
const codesysDir = path.dirname(codesysExePath); // Directory containing CODESYS.exe
|
|
79
95
|
try {
|
|
80
|
-
// Normalize line endings to LF (\n)
|
|
81
96
|
const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
|
|
82
|
-
// Write the script content to the temporary file using latin1 encoding
|
|
83
97
|
yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
|
|
84
|
-
process.stderr.write(`
|
|
85
|
-
// --- Use spawn ---
|
|
86
|
-
const command = codesysExePath;
|
|
98
|
+
process.stderr.write(`INTEROP: Temp script written: ${tempFilePath}\n`);
|
|
99
|
+
// --- Use spawn arguments array ---
|
|
100
|
+
const command = codesysExePath;
|
|
87
101
|
const args = [
|
|
88
102
|
`--profile=${codesysProfileName}`, // Format expected by CODESYS.exe
|
|
103
|
+
// Use = for profile? Or quotes? Check CODESYS docs. Assume '=' for now.
|
|
104
|
+
// If CODESYS needs quotes around the value *itself*: `--profile="${codesysProfileName}"`
|
|
89
105
|
'--noUI',
|
|
90
|
-
`--runscript=${tempFilePath}`
|
|
91
|
-
// If CODESYS
|
|
92
|
-
// `--profile="${codesysProfileName}"`,
|
|
93
|
-
// `--runscript="${tempFilePath}"`,
|
|
94
|
-
// But usually, spawn handles the argument separation correctly without needing inner quotes unless the *program itself* requires them.
|
|
95
|
-
// Test without inner quotes first.
|
|
106
|
+
`--runscript=${tempFilePath}`
|
|
107
|
+
// If CODESYS needs quotes around the value *itself*: `--runscript="${tempFilePath}"`
|
|
96
108
|
];
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
// --- End spawn arguments ---
|
|
110
|
+
process.stderr.write(`INTEROP: Spawning command: ${command}\n`);
|
|
111
|
+
process.stderr.write(`INTEROP: Spawning args: ${JSON.stringify(args)}\n`);
|
|
112
|
+
process.stderr.write(`INTEROP ENV: CWD before spawn: ${process.cwd()}\n`);
|
|
113
|
+
process.stderr.write(`INTEROP ENV: Forcing CWD for spawn: ${codesysDir}\n`);
|
|
114
|
+
// --- Create modified environment ---
|
|
115
|
+
const spawnEnv = Object.assign({}, process.env);
|
|
116
|
+
const pathSeparator = ';'; // Windows
|
|
117
|
+
const originalPath = spawnEnv.PATH || '';
|
|
118
|
+
spawnEnv.PATH = `${codesysDir}${pathSeparator}${originalPath}`;
|
|
119
|
+
process.stderr.write(`INTEROP ENV: MODIFIED PATH for spawn (prepended): ${spawnEnv.PATH.substring(0, 100)}...\n`);
|
|
120
|
+
// --- End modified environment ---
|
|
121
|
+
const spawnResult = yield new Promise((resolve) => {
|
|
99
122
|
let stdoutData = '';
|
|
100
123
|
let stderrData = '';
|
|
101
|
-
// Spawn the process. Detached:false is default. Timeout via AbortController recommended for spawn.
|
|
102
124
|
const controller = new AbortController();
|
|
103
125
|
const timeoutSignal = controller.signal;
|
|
104
|
-
const timeoutDuration =
|
|
126
|
+
const timeoutDuration = 60000; // 60 seconds
|
|
105
127
|
const childProcess = (0, child_process_1.spawn)(command, args, {
|
|
106
|
-
windowsHide: true,
|
|
107
|
-
signal: timeoutSignal
|
|
128
|
+
windowsHide: true,
|
|
129
|
+
signal: timeoutSignal,
|
|
130
|
+
cwd: codesysDir, // Force CWD
|
|
131
|
+
env: spawnEnv, // Pass modified environment
|
|
132
|
+
shell: false // <<< Ensure shell is false or omitted (default)
|
|
108
133
|
});
|
|
109
|
-
const timeoutId = setTimeout(() => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}, timeoutDuration);
|
|
134
|
+
const timeoutId = setTimeout(() => { }, timeoutDuration);
|
|
135
|
+
// --- Event Listeners (stdout, stderr, error, close, abort) ---
|
|
136
|
+
// (Keep the event listener logic the same as the previous version)
|
|
113
137
|
childProcess.stdout.on('data', (data) => {
|
|
114
138
|
const chunk = data.toString();
|
|
115
139
|
stdoutData += chunk;
|
|
116
|
-
process.stderr.write(`
|
|
140
|
+
process.stderr.write(`INTEROP stdout chunk: ${chunk.length > 50 ? chunk.substring(0, 50) + '...' : chunk}\n`);
|
|
117
141
|
});
|
|
118
142
|
childProcess.stderr.on('data', (data) => {
|
|
119
143
|
const chunk = data.toString();
|
|
120
144
|
stderrData += chunk;
|
|
121
|
-
|
|
145
|
+
if (chunk.includes('C:\\Program')) {
|
|
146
|
+
process.stderr.write(`>>>> INTEROP STDERR DETECTED 'C:\\Program': ${chunk}\n`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
process.stderr.write(`INTEROP stderr chunk: ${chunk}\n`);
|
|
150
|
+
}
|
|
122
151
|
});
|
|
123
|
-
childProcess.on('error', (
|
|
152
|
+
childProcess.on('error', (spawnError) => {
|
|
124
153
|
var _a;
|
|
125
|
-
clearTimeout(timeoutId);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// Don't reject here, resolve with error info like exec did
|
|
129
|
-
resolve({ code: (_a = error.errno) !== null && _a !== void 0 ? _a : 1, output: stdoutData + stderrData + `\nSpawn Error: ${error.message}` });
|
|
154
|
+
clearTimeout(timeoutId);
|
|
155
|
+
process.stderr.write(`INTEROP SPAWN ERROR (shell:false): ${spawnError.message}\n`);
|
|
156
|
+
resolve({ code: (_a = spawnError.errno) !== null && _a !== void 0 ? _a : 1, stdout: stdoutData, stderr: stderrData, error: spawnError });
|
|
130
157
|
});
|
|
131
158
|
childProcess.on('close', (code) => {
|
|
132
|
-
clearTimeout(timeoutId);
|
|
133
|
-
process.stderr.write(`
|
|
134
|
-
|
|
135
|
-
resolve({ code: code, output: combinedOutput });
|
|
159
|
+
clearTimeout(timeoutId);
|
|
160
|
+
process.stderr.write(`INTEROP: Process closed code: ${code}\n`);
|
|
161
|
+
resolve({ code: code, stdout: stdoutData, stderr: stderrData });
|
|
136
162
|
});
|
|
137
|
-
// Handle the timeout signal specifically
|
|
138
163
|
timeoutSignal.addEventListener('abort', () => {
|
|
139
|
-
process.stderr.write('Abort signal received, killing process
|
|
140
|
-
if (!childProcess.killed)
|
|
141
|
-
childProcess.kill('SIGTERM');
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
}, 2000); // 2 second grace period
|
|
149
|
-
}
|
|
150
|
-
// Resolve with timeout information. The 'close' event might still fire later with a specific code.
|
|
151
|
-
// We already logged the timeout, let the 'close' handler report the final code.
|
|
152
|
-
// If 'close' doesn't fire after kill, it might indicate a deeper issue.
|
|
153
|
-
// We could resolve here with a timeout indicator, but letting 'close' handle it is cleaner if it works.
|
|
154
|
-
// If 'close' never fires after abort/kill, the promise might hang. Let's add a failsafe resolve in the listener.
|
|
155
|
-
// Revised: Resolve directly on abort to ensure promise completion.
|
|
156
|
-
resolve({ code: null, output: stdoutData + stderrData + "\nTIMEOUT: CODESYS process aborted due to timeout." });
|
|
157
|
-
}, { once: true }); // Ensure listener is called only once
|
|
164
|
+
process.stderr.write('INTEROP: Abort signal received, killing process.\n');
|
|
165
|
+
if (!childProcess.killed)
|
|
166
|
+
childProcess.kill('SIGTERM');
|
|
167
|
+
setTimeout(() => { if (!childProcess.killed)
|
|
168
|
+
childProcess.kill('SIGKILL'); }, 2000);
|
|
169
|
+
resolve({ code: null, stdout: stdoutData, stderr: stderrData + "\nTIMEOUT: Process aborted due to timeout." });
|
|
170
|
+
}, { once: true });
|
|
171
|
+
// --- End Event Listeners ---
|
|
158
172
|
});
|
|
159
|
-
output = spawnResult.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
output = spawnResult.stdout;
|
|
174
|
+
stderrOutput = spawnResult.stderr;
|
|
175
|
+
exitCode = spawnResult.code;
|
|
176
|
+
// success determination logic
|
|
177
|
+
if (stderrOutput.includes("'C:\\Program' is not recognized")) {
|
|
178
|
+
success = false;
|
|
179
|
+
process.stderr.write("INTEROP: Failure determined by 'C:\\Program' error in stderr.\n");
|
|
180
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
181
|
+
stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`;
|
|
164
182
|
}
|
|
165
|
-
else if (
|
|
183
|
+
else if (spawnResult.error) {
|
|
166
184
|
success = false;
|
|
185
|
+
process.stderr.write(`INTEROP: Failure determined by spawn error: ${spawnResult.error.message}\n`);
|
|
186
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
187
|
+
stderrOutput = `SCRIPT_ERROR: Spawn failed: ${spawnResult.error.message}\n${stderrOutput}`;
|
|
167
188
|
}
|
|
168
189
|
else {
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
process.stderr.write(`INTEROP: Checking markers/exit code (Code: ${exitCode})...\n`);
|
|
191
|
+
if (output.includes(SCRIPT_SUCCESS_MARKER) || stderrOutput.includes(SCRIPT_SUCCESS_MARKER)) {
|
|
192
|
+
success = true;
|
|
193
|
+
process.stderr.write("INTEROP: Success determined by SUCCESS marker.\n");
|
|
173
194
|
}
|
|
174
|
-
else {
|
|
195
|
+
else if (output.includes(SCRIPT_ERROR_MARKER) || stderrOutput.includes(SCRIPT_ERROR_MARKER)) {
|
|
175
196
|
success = false;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
197
|
+
process.stderr.write("INTEROP: Failure determined by ERROR marker.\n");
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
success = exitCode === 0;
|
|
201
|
+
if (success)
|
|
202
|
+
process.stderr.write(`INTEROP: Success determined by exit code 0 (no markers found).\n`);
|
|
180
203
|
else {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (output.includes("TIMEOUT: CODESYS process aborted")) {
|
|
185
|
-
// Message already added by the abort handler
|
|
186
|
-
}
|
|
187
|
-
else if (exitCode === null && !output.includes("Spawn Error:")) {
|
|
188
|
-
// If exit code is null but not due to spawn error or handled timeout, add generic killed message
|
|
189
|
-
output += `\nProcess may have been killed externally or timed out without proper exit code reporting.`;
|
|
204
|
+
process.stderr.write(`INTEROP: Failure determined by non-zero exit code ${exitCode} (no markers found).\n`);
|
|
205
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
206
|
+
stderrOutput = `SCRIPT_ERROR: Process failed with exit code ${exitCode} (no markers found).\n${stderrOutput}`;
|
|
190
207
|
}
|
|
191
208
|
}
|
|
192
209
|
}
|
|
210
|
+
// --- End Success Determination ---
|
|
193
211
|
}
|
|
194
212
|
catch (error) {
|
|
195
|
-
process.stderr.write(`Error
|
|
196
|
-
|
|
213
|
+
process.stderr.write(`INTEROP: Error during setup: ${error.message}\n${error.stack}\n`);
|
|
214
|
+
stderrOutput = `SCRIPT_ERROR: Failed during script execution setup: ${error.message}`;
|
|
197
215
|
success = false;
|
|
198
216
|
}
|
|
199
217
|
finally {
|
|
200
|
-
//
|
|
218
|
+
// Cleanup: Attempt to delete the temporary script file
|
|
201
219
|
try {
|
|
202
220
|
yield (0, promises_1.unlink)(tempFilePath);
|
|
203
|
-
process.stderr.write(`
|
|
221
|
+
process.stderr.write(`INTEROP: Temp script deleted: ${tempFilePath}\n`);
|
|
204
222
|
}
|
|
205
223
|
catch (cleanupError) {
|
|
206
|
-
process.stderr.write(`Failed to delete
|
|
207
|
-
if (success)
|
|
208
|
-
|
|
209
|
-
}
|
|
224
|
+
process.stderr.write(`INTEROP: Failed to delete temp file ${tempFilePath}: ${cleanupError.message}\n`);
|
|
225
|
+
if (success)
|
|
226
|
+
stderrOutput += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
|
-
|
|
213
|
-
|
|
229
|
+
// Final output processing
|
|
230
|
+
const finalOutput = success ? output : `${stderrOutput}\n${output}`.trim();
|
|
231
|
+
process.stderr.write(`INTEROP: Final Success: ${success}\n`);
|
|
232
|
+
process.stderr.write(`INTEROP: Final Output Length: ${finalOutput.length}\n---\n`);
|
|
233
|
+
return { success, output: finalOutput };
|
|
214
234
|
});
|
|
215
235
|
}
|