@codesys/mcp-toolkit 1.0.9 → 1.1.2
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 +84 -193
- package/dist/bin.js +54 -22
- package/dist/codesys_interop.js +154 -78
- package/dist/server.js +1394 -1367
- package/package.json +48 -45
- 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);
|
|
@@ -43,152 +55,216 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
43
55
|
};
|
|
44
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
57
|
exports.executeCodesysScript = executeCodesysScript;
|
|
46
|
-
const child_process_1 = require("child_process");
|
|
58
|
+
const child_process_1 = require("child_process");
|
|
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
|
-
* Tries using spawn with shell:true and explicit quoting for cmd.exe.
|
|
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: Using shell: true with careful command string quoting to handle
|
|
75
|
+
* CODESYS argument parsing quirks when launched non-interactively.
|
|
61
76
|
*/
|
|
62
77
|
function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
|
|
63
78
|
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!codesysProfileName)
|
|
68
|
-
throw new Error('CODESYS profile name was not provided
|
|
69
|
-
|
|
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 ---
|
|
70
87
|
const tempDir = os.tmpdir();
|
|
71
88
|
const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
|
|
72
|
-
const tempFilePath = path.join(tempDir, tempFileName);
|
|
89
|
+
const tempFilePath = path.join(tempDir, tempFileName); // Path module uses OS-specific separators ('\' on Windows)
|
|
73
90
|
let output = '';
|
|
91
|
+
let stderrOutput = '';
|
|
74
92
|
let success = false;
|
|
75
93
|
let exitCode = null;
|
|
94
|
+
const codesysDir = path.dirname(codesysExePath); // Directory containing CODESYS.exe
|
|
76
95
|
try {
|
|
77
|
-
const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
|
|
78
|
-
|
|
79
|
-
process.stderr.write(`
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n'); // Normalize line endings
|
|
97
|
+
// <<< --- ADDED SCRIPT CONTENT LOGGING --- >>>
|
|
98
|
+
process.stderr.write(`INTEROP: Script content to be written (first 500 chars):\n`);
|
|
99
|
+
process.stderr.write(`------ START SCRIPT (TEMP FILE) -----\n`);
|
|
100
|
+
process.stderr.write(`${normalizedScriptContent.substring(0, 500)}\n`); // Log the first 500 chars
|
|
101
|
+
process.stderr.write(`------ END SCRIPT SNIPPET (TEMP FILE) -----\n`);
|
|
102
|
+
// <<< --- END SCRIPT CONTENT LOGGING --- >>>
|
|
103
|
+
yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1'); // Write the normalized content
|
|
104
|
+
process.stderr.write(`INTEROP: Temp script written: ${tempFilePath}\n`);
|
|
105
|
+
// --- Construct command string for shell: true ---
|
|
106
|
+
// Quote the executable path itself
|
|
107
|
+
const quotedExePath = `"${codesysExePath}"`;
|
|
108
|
+
// Format arguments exactly as CODESYS seems to want: --option="Value With Spaces"
|
|
109
|
+
// The outer quotes are for the shell parser.
|
|
110
|
+
const profileArg = `--profile="${codesysProfileName}"`;
|
|
111
|
+
const scriptArg = `--runscript="${tempFilePath}"`; // tempFilePath from path.join has correct backslashes for Win
|
|
112
|
+
// Combine into a single string for the shell
|
|
113
|
+
const fullCommandString = `${quotedExePath} ${profileArg} --noUI ${scriptArg}`;
|
|
114
|
+
// Example result: "\"C:\\Program Files\\...\\CODESYS.exe\" --profile=\"CODESYS V3.5 SP21\" --noUI --runscript=\"C:\\Users\\...\\script.py\""
|
|
115
|
+
// --- End command string construction ---
|
|
116
|
+
process.stderr.write(`INTEROP: Spawning command (shell:true): ${fullCommandString}\n`);
|
|
117
|
+
process.stderr.write(`INTEROP ENV: CWD before spawn: ${process.cwd()}\n`);
|
|
118
|
+
process.stderr.write(`INTEROP ENV: Forcing CWD for spawn: ${codesysDir}\n`); // Re-enabled CWD change
|
|
119
|
+
// --- Create modified environment (Re-enabled) ---
|
|
120
|
+
const spawnEnv = Object.assign({}, process.env);
|
|
121
|
+
const pathSeparator = ';'; // Windows
|
|
122
|
+
const originalPath = spawnEnv.PATH || '';
|
|
123
|
+
spawnEnv.PATH = `${codesysDir}${pathSeparator}${originalPath}`; // Prepend CODESYS dir to PATH
|
|
124
|
+
process.stderr.write(`INTEROP ENV: MODIFIED PATH for spawn (prepended): ${spawnEnv.PATH.substring(0, 100)}...\n`); // Re-enabled ENV change
|
|
125
|
+
// --- End modified environment ---
|
|
87
126
|
const spawnResult = yield new Promise((resolve) => {
|
|
88
127
|
let stdoutData = '';
|
|
89
128
|
let stderrData = '';
|
|
90
129
|
const controller = new AbortController();
|
|
91
130
|
const timeoutSignal = controller.signal;
|
|
92
|
-
const timeoutDuration =
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
131
|
+
const timeoutDuration = 60000; // 60 seconds
|
|
132
|
+
// Pass the single command string, empty args array, and shell: true
|
|
133
|
+
const childProcess = (0, child_process_1.spawn)(fullCommandString, [], {
|
|
134
|
+
windowsHide: true,
|
|
135
|
+
signal: timeoutSignal,
|
|
136
|
+
cwd: codesysDir, // Re-enabled CWD change
|
|
137
|
+
env: spawnEnv, // Re-enabled ENV change
|
|
138
|
+
shell: true // USE shell: true
|
|
100
139
|
});
|
|
101
140
|
const timeoutId = setTimeout(() => {
|
|
102
|
-
process.stderr.write(
|
|
141
|
+
process.stderr.write('INTEROP: Process timeout reached.\n');
|
|
103
142
|
controller.abort();
|
|
104
143
|
}, timeoutDuration);
|
|
144
|
+
// --- Event Listeners (stdout, stderr, error, close, abort) ---
|
|
105
145
|
childProcess.stdout.on('data', (data) => {
|
|
106
146
|
const chunk = data.toString();
|
|
107
147
|
stdoutData += chunk;
|
|
108
|
-
process.stderr.write(`
|
|
148
|
+
process.stderr.write(`INTEROP stdout chunk: ${chunk.length > 50 ? chunk.substring(0, 50) + '...' : chunk}\n`);
|
|
109
149
|
});
|
|
110
150
|
childProcess.stderr.on('data', (data) => {
|
|
111
|
-
// **** THIS IS CRITICAL - CAPTURE STDERR ****
|
|
112
151
|
const chunk = data.toString();
|
|
113
152
|
stderrData += chunk;
|
|
114
|
-
|
|
153
|
+
// Check for specific error patterns
|
|
154
|
+
if (chunk.includes('--profile="profile name"')) {
|
|
155
|
+
process.stderr.write(`>>>> INTEROP STDERR DETECTED Profile Error Message: ${chunk}\n`);
|
|
156
|
+
}
|
|
157
|
+
else if (chunk.includes('is not recognized')) {
|
|
158
|
+
process.stderr.write(`>>>> INTEROP STDERR DETECTED 'not recognized' (shell issue?): ${chunk}\n`);
|
|
159
|
+
}
|
|
160
|
+
else if (chunk.includes('SyntaxErrorException')) {
|
|
161
|
+
process.stderr.write(`>>>> INTEROP STDERR DETECTED Syntax Error: ${chunk}\n`);
|
|
162
|
+
} // More specific check
|
|
163
|
+
else {
|
|
164
|
+
process.stderr.write(`INTEROP stderr chunk: ${chunk}\n`);
|
|
165
|
+
}
|
|
115
166
|
});
|
|
116
|
-
childProcess.on('error', (
|
|
167
|
+
childProcess.on('error', (spawnError) => {
|
|
117
168
|
var _a;
|
|
118
169
|
clearTimeout(timeoutId);
|
|
119
|
-
process.stderr.write(`
|
|
120
|
-
resolve({ code: (_a =
|
|
170
|
+
process.stderr.write(`INTEROP SPAWN ERROR (shell:true): ${spawnError.message}\n`);
|
|
171
|
+
resolve({ code: (_a = spawnError.errno) !== null && _a !== void 0 ? _a : 1, stdout: stdoutData, stderr: stderrData, error: spawnError });
|
|
121
172
|
});
|
|
122
173
|
childProcess.on('close', (code) => {
|
|
123
174
|
clearTimeout(timeoutId);
|
|
124
|
-
process.stderr.write(`
|
|
125
|
-
|
|
126
|
-
resolve({ code: code, output: combinedOutput });
|
|
175
|
+
process.stderr.write(`INTEROP: Process closed code: ${code}\n`);
|
|
176
|
+
resolve({ code: code, stdout: stdoutData, stderr: stderrData });
|
|
127
177
|
});
|
|
128
178
|
timeoutSignal.addEventListener('abort', () => {
|
|
129
|
-
process.stderr.write('Abort signal received,
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
179
|
+
process.stderr.write('INTEROP: Abort signal received, attempting to kill process.\n');
|
|
180
|
+
if (!childProcess.killed) {
|
|
181
|
+
if (!childProcess.kill('SIGTERM')) { // Try graceful termination first
|
|
182
|
+
process.stderr.write('INTEROP: SIGTERM failed, attempting SIGKILL in 2s.\n');
|
|
183
|
+
setTimeout(() => { if (!childProcess.killed)
|
|
184
|
+
childProcess.kill('SIGKILL'); }, 2000);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
process.stderr.write('INTEROP: SIGTERM sent.\n');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
resolve({ code: null, stdout: stdoutData, stderr: stderrData + "\nTIMEOUT: Process aborted due to timeout." });
|
|
136
191
|
}, { once: true });
|
|
192
|
+
// --- End Event Listeners ---
|
|
137
193
|
});
|
|
138
|
-
output = spawnResult.
|
|
194
|
+
output = spawnResult.stdout;
|
|
195
|
+
stderrOutput = spawnResult.stderr;
|
|
139
196
|
exitCode = spawnResult.code;
|
|
140
|
-
// Success
|
|
141
|
-
//
|
|
142
|
-
if (
|
|
143
|
-
|
|
197
|
+
// --- Success Determination Logic ---
|
|
198
|
+
success = false; // Assume failure unless proven otherwise
|
|
199
|
+
if (spawnResult.error) {
|
|
200
|
+
process.stderr.write(`INTEROP: Failure determined by spawn error: ${spawnResult.error.message}\n`);
|
|
201
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
202
|
+
stderrOutput = `SCRIPT_ERROR: Spawn failed: ${spawnResult.error.message}\n${stderrOutput}`;
|
|
203
|
+
}
|
|
204
|
+
else if (stderrOutput.includes('is not recognized as an internal or external command')) {
|
|
205
|
+
process.stderr.write("INTEROP: Failure determined by 'not recognized' error in stderr (shell:true quoting issue likely).\n");
|
|
206
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
207
|
+
stderrOutput = `SCRIPT_ERROR: Shell execution failed: ${stderrOutput}`;
|
|
144
208
|
}
|
|
145
|
-
else if (
|
|
146
|
-
|
|
209
|
+
else if (stderrOutput.includes('--profile="profile name"')) {
|
|
210
|
+
process.stderr.write("INTEROP: Failure determined by CODESYS profile error message in stderr.\n");
|
|
211
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
212
|
+
stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`;
|
|
213
|
+
}
|
|
214
|
+
else if (stderrOutput.includes('SyntaxErrorException')) { // Check for syntax error specifically
|
|
215
|
+
process.stderr.write("INTEROP: Failure determined by CODESYS Script Syntax Error.\n");
|
|
216
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
217
|
+
stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`; // Include the syntax error details
|
|
147
218
|
}
|
|
148
219
|
else {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
220
|
+
// No spawn error, no shell error, no profile error, no syntax error -> check markers/exit code
|
|
221
|
+
process.stderr.write(`INTEROP: Checking markers/exit code (Code: ${exitCode})...\n`);
|
|
222
|
+
if (output.includes(SCRIPT_SUCCESS_MARKER) || stderrOutput.includes(SCRIPT_SUCCESS_MARKER)) {
|
|
223
|
+
success = true;
|
|
224
|
+
process.stderr.write("INTEROP: Success determined by SUCCESS marker.\n");
|
|
225
|
+
}
|
|
226
|
+
else if (output.includes(SCRIPT_ERROR_MARKER) || stderrOutput.includes(SCRIPT_ERROR_MARKER)) {
|
|
227
|
+
success = false; // Explicit error marker found
|
|
228
|
+
process.stderr.write("INTEROP: Failure determined by ERROR marker.\n");
|
|
152
229
|
}
|
|
153
230
|
else {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
output = `SCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.\n${output}`;
|
|
231
|
+
// No markers found, rely solely on exit code
|
|
232
|
+
success = exitCode === 0;
|
|
233
|
+
if (success) {
|
|
234
|
+
process.stderr.write(`INTEROP: Success determined by exit code 0 (no markers found).\n`);
|
|
159
235
|
}
|
|
160
236
|
else {
|
|
161
|
-
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
// Handle timeout message addition if necessary (should be added by abort handler)
|
|
167
|
-
if (exitCode === null && !output.includes("TIMEOUT:") && !output.includes("Spawn Error:")) {
|
|
168
|
-
output += `\nProcess may have been killed externally or timed out without proper exit code reporting.`;
|
|
237
|
+
process.stderr.write(`INTEROP: Failure determined by non-zero exit code ${exitCode} (no markers found).\n`);
|
|
238
|
+
// Add generic failure message if stderr doesn't already contain SCRIPT_ERROR
|
|
239
|
+
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
240
|
+
stderrOutput = `SCRIPT_ERROR: Process failed with exit code ${exitCode} (no markers found).\n${stderrOutput}`;
|
|
169
241
|
}
|
|
170
242
|
}
|
|
171
243
|
}
|
|
244
|
+
// --- End Success Determination ---
|
|
172
245
|
}
|
|
173
246
|
catch (error) {
|
|
174
|
-
process.stderr.write(`Error during
|
|
175
|
-
|
|
247
|
+
process.stderr.write(`INTEROP: Error during setup: ${error.message}\n${error.stack}\n`);
|
|
248
|
+
stderrOutput = `SCRIPT_ERROR: Failed during script execution setup: ${error.message}`;
|
|
176
249
|
success = false;
|
|
177
250
|
}
|
|
178
251
|
finally {
|
|
179
|
-
// Cleanup
|
|
252
|
+
// Cleanup: Attempt to delete the temporary script file
|
|
180
253
|
try {
|
|
181
254
|
yield (0, promises_1.unlink)(tempFilePath);
|
|
182
|
-
process.stderr.write(`
|
|
255
|
+
process.stderr.write(`INTEROP: Temp script deleted: ${tempFilePath}\n`);
|
|
183
256
|
}
|
|
184
257
|
catch (cleanupError) {
|
|
185
|
-
process.stderr.write(`Failed to delete
|
|
186
|
-
if (success)
|
|
187
|
-
|
|
188
|
-
}
|
|
258
|
+
process.stderr.write(`INTEROP: Failed to delete temp file ${tempFilePath}: ${cleanupError.message}\n`);
|
|
259
|
+
if (success)
|
|
260
|
+
stderrOutput += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
|
|
189
261
|
}
|
|
190
262
|
}
|
|
191
|
-
|
|
192
|
-
|
|
263
|
+
// Final output processing
|
|
264
|
+
// Combine stderr and stdout only on failure to preserve clean success output
|
|
265
|
+
const finalOutput = success ? output : `${stderrOutput}\n${output}`.trim();
|
|
266
|
+
process.stderr.write(`INTEROP: Final Success: ${success}\n`);
|
|
267
|
+
process.stderr.write(`INTEROP: Final Output Length: ${finalOutput.length}\n---\n`);
|
|
268
|
+
return { success, output: finalOutput };
|
|
193
269
|
});
|
|
194
270
|
}
|