@codesys/mcp-toolkit 1.0.9 → 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 +115 -74
- 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);
|
|
@@ -43,152 +55,181 @@ 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"); // 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
|
-
* 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: 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
|
-
|
|
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
89
|
const tempFilePath = path.join(tempDir, tempFileName);
|
|
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
96
|
const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
|
|
78
97
|
yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
|
|
79
|
-
process.stderr.write(`
|
|
80
|
-
// ---
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
process.stderr.write(`INTEROP: Temp script written: ${tempFilePath}\n`);
|
|
99
|
+
// --- Use spawn arguments array ---
|
|
100
|
+
const command = codesysExePath;
|
|
101
|
+
const args = [
|
|
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}"`
|
|
105
|
+
'--noUI',
|
|
106
|
+
`--runscript=${tempFilePath}`
|
|
107
|
+
// If CODESYS needs quotes around the value *itself*: `--runscript="${tempFilePath}"`
|
|
108
|
+
];
|
|
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 ---
|
|
87
121
|
const spawnResult = yield new Promise((resolve) => {
|
|
88
122
|
let stdoutData = '';
|
|
89
123
|
let stderrData = '';
|
|
90
124
|
const controller = new AbortController();
|
|
91
125
|
const timeoutSignal = controller.signal;
|
|
92
|
-
const timeoutDuration =
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
signal: timeoutSignal
|
|
126
|
+
const timeoutDuration = 60000; // 60 seconds
|
|
127
|
+
const childProcess = (0, child_process_1.spawn)(command, args, {
|
|
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)
|
|
100
133
|
});
|
|
101
|
-
const timeoutId = setTimeout(() => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}, 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)
|
|
105
137
|
childProcess.stdout.on('data', (data) => {
|
|
106
138
|
const chunk = data.toString();
|
|
107
139
|
stdoutData += chunk;
|
|
108
|
-
process.stderr.write(`
|
|
140
|
+
process.stderr.write(`INTEROP stdout chunk: ${chunk.length > 50 ? chunk.substring(0, 50) + '...' : chunk}\n`);
|
|
109
141
|
});
|
|
110
142
|
childProcess.stderr.on('data', (data) => {
|
|
111
|
-
// **** THIS IS CRITICAL - CAPTURE STDERR ****
|
|
112
143
|
const chunk = data.toString();
|
|
113
144
|
stderrData += chunk;
|
|
114
|
-
|
|
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
|
+
}
|
|
115
151
|
});
|
|
116
|
-
childProcess.on('error', (
|
|
152
|
+
childProcess.on('error', (spawnError) => {
|
|
117
153
|
var _a;
|
|
118
154
|
clearTimeout(timeoutId);
|
|
119
|
-
process.stderr.write(`
|
|
120
|
-
resolve({ code: (_a =
|
|
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 });
|
|
121
157
|
});
|
|
122
158
|
childProcess.on('close', (code) => {
|
|
123
159
|
clearTimeout(timeoutId);
|
|
124
|
-
process.stderr.write(`
|
|
125
|
-
|
|
126
|
-
resolve({ code: code, output: combinedOutput });
|
|
160
|
+
process.stderr.write(`INTEROP: Process closed code: ${code}\n`);
|
|
161
|
+
resolve({ code: code, stdout: stdoutData, stderr: stderrData });
|
|
127
162
|
});
|
|
128
163
|
timeoutSignal.addEventListener('abort', () => {
|
|
129
|
-
process.stderr.write('Abort signal received, killing process
|
|
130
|
-
// Kill logic remains the same
|
|
164
|
+
process.stderr.write('INTEROP: Abort signal received, killing process.\n');
|
|
131
165
|
if (!childProcess.killed)
|
|
132
166
|
childProcess.kill('SIGTERM');
|
|
133
167
|
setTimeout(() => { if (!childProcess.killed)
|
|
134
168
|
childProcess.kill('SIGKILL'); }, 2000);
|
|
135
|
-
resolve({ code: null,
|
|
169
|
+
resolve({ code: null, stdout: stdoutData, stderr: stderrData + "\nTIMEOUT: Process aborted due to timeout." });
|
|
136
170
|
}, { once: true });
|
|
171
|
+
// --- End Event Listeners ---
|
|
137
172
|
});
|
|
138
|
-
output = spawnResult.
|
|
173
|
+
output = spawnResult.stdout;
|
|
174
|
+
stderrOutput = spawnResult.stderr;
|
|
139
175
|
exitCode = spawnResult.code;
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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}`;
|
|
144
182
|
}
|
|
145
|
-
else if (
|
|
183
|
+
else if (spawnResult.error) {
|
|
146
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}`;
|
|
147
188
|
}
|
|
148
189
|
else {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
success =
|
|
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");
|
|
152
194
|
}
|
|
153
|
-
else {
|
|
195
|
+
else if (output.includes(SCRIPT_ERROR_MARKER) || stderrOutput.includes(SCRIPT_ERROR_MARKER)) {
|
|
154
196
|
success = false;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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`);
|
|
160
203
|
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.`;
|
|
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}`;
|
|
169
207
|
}
|
|
170
208
|
}
|
|
171
209
|
}
|
|
210
|
+
// --- End Success Determination ---
|
|
172
211
|
}
|
|
173
212
|
catch (error) {
|
|
174
|
-
process.stderr.write(`Error during
|
|
175
|
-
|
|
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}`;
|
|
176
215
|
success = false;
|
|
177
216
|
}
|
|
178
217
|
finally {
|
|
179
|
-
// Cleanup
|
|
218
|
+
// Cleanup: Attempt to delete the temporary script file
|
|
180
219
|
try {
|
|
181
220
|
yield (0, promises_1.unlink)(tempFilePath);
|
|
182
|
-
process.stderr.write(`
|
|
221
|
+
process.stderr.write(`INTEROP: Temp script deleted: ${tempFilePath}\n`);
|
|
183
222
|
}
|
|
184
223
|
catch (cleanupError) {
|
|
185
|
-
process.stderr.write(`Failed to delete
|
|
186
|
-
if (success)
|
|
187
|
-
|
|
188
|
-
}
|
|
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}`;
|
|
189
227
|
}
|
|
190
228
|
}
|
|
191
|
-
|
|
192
|
-
|
|
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 };
|
|
193
234
|
});
|
|
194
235
|
}
|