@codesys/mcp-toolkit 1.1.0 → 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 +6 -12
- package/dist/codesys_interop.js +82 -47
- package/dist/server.js +916 -52
- package/package.json +48 -48
package/README.md
CHANGED
|
@@ -132,20 +132,14 @@ When running `codesys-mcp-tool` directly or configuring it, you can use these ar
|
|
|
132
132
|
* **Check Logs:**
|
|
133
133
|
* Claude Desktop logs: `C:\Users\<YourUsername>\AppData\Roaming\Claude\logs\` (Windows)
|
|
134
134
|
|
|
135
|
-
## 📖 Detailed OOP Workflow Guide
|
|
136
|
-
*(This section seems good, keep as is)*
|
|
137
|
-
... (Paste your existing OOP Workflow Guide here) ...
|
|
138
|
-
|
|
139
135
|
## 🤝 Contributing
|
|
140
|
-
|
|
141
|
-
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
136
|
+
Contributions, issues, and feature requests are welcome! Feel free to check the issues page. (Optionally add a CONTRIBUTING.md file with more details).
|
|
142
137
|
|
|
143
138
|
## 📝 License
|
|
144
|
-
|
|
145
|
-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
139
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
146
140
|
|
|
147
141
|
## 🙏 Acknowledgements
|
|
148
|
-
|
|
149
|
-
- The
|
|
150
|
-
-
|
|
151
|
-
|
|
142
|
+
- The CODESYS GmbH team for the powerful CODESYS platform and its scripting engine.
|
|
143
|
+
- The Model Context Protocol project for defining the interaction standard.
|
|
144
|
+
- All contributors and users who help improve this toolkit.
|
|
145
|
+
|
package/dist/codesys_interop.js
CHANGED
|
@@ -55,7 +55,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
55
55
|
};
|
|
56
56
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
57
|
exports.executeCodesysScript = executeCodesysScript;
|
|
58
|
-
const child_process_1 = require("child_process");
|
|
58
|
+
const child_process_1 = require("child_process");
|
|
59
59
|
const promises_1 = require("fs/promises");
|
|
60
60
|
const path = __importStar(require("path"));
|
|
61
61
|
const os = __importStar(require("os"));
|
|
@@ -71,8 +71,8 @@ const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
|
|
|
71
71
|
* @param codesysProfileName The name of the CODESYS profile to use for scripting.
|
|
72
72
|
* @returns A promise resolving to an object containing the success status and the script's output.
|
|
73
73
|
*
|
|
74
|
-
* NOTE:
|
|
75
|
-
*
|
|
74
|
+
* NOTE: Using shell: true with careful command string quoting to handle
|
|
75
|
+
* CODESYS argument parsing quirks when launched non-interactively.
|
|
76
76
|
*/
|
|
77
77
|
function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
|
|
78
78
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -86,37 +86,42 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
86
86
|
// --- End Pre-checks ---
|
|
87
87
|
const tempDir = os.tmpdir();
|
|
88
88
|
const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
|
|
89
|
-
const tempFilePath = path.join(tempDir, tempFileName);
|
|
89
|
+
const tempFilePath = path.join(tempDir, tempFileName); // Path module uses OS-specific separators ('\' on Windows)
|
|
90
90
|
let output = '';
|
|
91
91
|
let stderrOutput = '';
|
|
92
92
|
let success = false;
|
|
93
93
|
let exitCode = null;
|
|
94
94
|
const codesysDir = path.dirname(codesysExePath); // Directory containing CODESYS.exe
|
|
95
95
|
try {
|
|
96
|
-
const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
|
|
97
|
-
|
|
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
|
|
98
104
|
process.stderr.write(`INTEROP: Temp script written: ${tempFilePath}\n`);
|
|
99
|
-
// ---
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// --- End
|
|
110
|
-
process.stderr.write(`INTEROP: Spawning command: ${
|
|
111
|
-
process.stderr.write(`INTEROP: Spawning args: ${JSON.stringify(args)}\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`);
|
|
112
117
|
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 ---
|
|
118
|
+
process.stderr.write(`INTEROP ENV: Forcing CWD for spawn: ${codesysDir}\n`); // Re-enabled CWD change
|
|
119
|
+
// --- Create modified environment (Re-enabled) ---
|
|
115
120
|
const spawnEnv = Object.assign({}, process.env);
|
|
116
121
|
const pathSeparator = ';'; // Windows
|
|
117
122
|
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`);
|
|
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
|
|
120
125
|
// --- End modified environment ---
|
|
121
126
|
const spawnResult = yield new Promise((resolve) => {
|
|
122
127
|
let stdoutData = '';
|
|
@@ -124,16 +129,19 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
124
129
|
const controller = new AbortController();
|
|
125
130
|
const timeoutSignal = controller.signal;
|
|
126
131
|
const timeoutDuration = 60000; // 60 seconds
|
|
127
|
-
|
|
132
|
+
// Pass the single command string, empty args array, and shell: true
|
|
133
|
+
const childProcess = (0, child_process_1.spawn)(fullCommandString, [], {
|
|
128
134
|
windowsHide: true,
|
|
129
135
|
signal: timeoutSignal,
|
|
130
|
-
cwd: codesysDir, //
|
|
131
|
-
env: spawnEnv, //
|
|
132
|
-
shell:
|
|
136
|
+
cwd: codesysDir, // Re-enabled CWD change
|
|
137
|
+
env: spawnEnv, // Re-enabled ENV change
|
|
138
|
+
shell: true // USE shell: true
|
|
133
139
|
});
|
|
134
|
-
const timeoutId = setTimeout(() => {
|
|
140
|
+
const timeoutId = setTimeout(() => {
|
|
141
|
+
process.stderr.write('INTEROP: Process timeout reached.\n');
|
|
142
|
+
controller.abort();
|
|
143
|
+
}, timeoutDuration);
|
|
135
144
|
// --- Event Listeners (stdout, stderr, error, close, abort) ---
|
|
136
|
-
// (Keep the event listener logic the same as the previous version)
|
|
137
145
|
childProcess.stdout.on('data', (data) => {
|
|
138
146
|
const chunk = data.toString();
|
|
139
147
|
stdoutData += chunk;
|
|
@@ -142,9 +150,16 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
142
150
|
childProcess.stderr.on('data', (data) => {
|
|
143
151
|
const chunk = data.toString();
|
|
144
152
|
stderrData += chunk;
|
|
145
|
-
|
|
146
|
-
|
|
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`);
|
|
147
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
|
|
148
163
|
else {
|
|
149
164
|
process.stderr.write(`INTEROP stderr chunk: ${chunk}\n`);
|
|
150
165
|
}
|
|
@@ -152,7 +167,7 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
152
167
|
childProcess.on('error', (spawnError) => {
|
|
153
168
|
var _a;
|
|
154
169
|
clearTimeout(timeoutId);
|
|
155
|
-
process.stderr.write(`INTEROP SPAWN ERROR (shell:
|
|
170
|
+
process.stderr.write(`INTEROP SPAWN ERROR (shell:true): ${spawnError.message}\n`);
|
|
156
171
|
resolve({ code: (_a = spawnError.errno) !== null && _a !== void 0 ? _a : 1, stdout: stdoutData, stderr: stderrData, error: spawnError });
|
|
157
172
|
});
|
|
158
173
|
childProcess.on('close', (code) => {
|
|
@@ -161,11 +176,17 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
161
176
|
resolve({ code: code, stdout: stdoutData, stderr: stderrData });
|
|
162
177
|
});
|
|
163
178
|
timeoutSignal.addEventListener('abort', () => {
|
|
164
|
-
process.stderr.write('INTEROP: Abort signal received,
|
|
165
|
-
if (!childProcess.killed)
|
|
166
|
-
childProcess.kill('SIGTERM')
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
190
|
resolve({ code: null, stdout: stdoutData, stderr: stderrData + "\nTIMEOUT: Process aborted due to timeout." });
|
|
170
191
|
}, { once: true });
|
|
171
192
|
// --- End Event Listeners ---
|
|
@@ -173,35 +194,48 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
173
194
|
output = spawnResult.stdout;
|
|
174
195
|
stderrOutput = spawnResult.stderr;
|
|
175
196
|
exitCode = spawnResult.code;
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
process.stderr.write(
|
|
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}`;
|
|
208
|
+
}
|
|
209
|
+
else if (stderrOutput.includes('--profile="profile name"')) {
|
|
210
|
+
process.stderr.write("INTEROP: Failure determined by CODESYS profile error message in stderr.\n");
|
|
180
211
|
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
181
212
|
stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`;
|
|
182
213
|
}
|
|
183
|
-
else if (
|
|
184
|
-
|
|
185
|
-
process.stderr.write(`INTEROP: Failure determined by spawn error: ${spawnResult.error.message}\n`);
|
|
214
|
+
else if (stderrOutput.includes('SyntaxErrorException')) { // Check for syntax error specifically
|
|
215
|
+
process.stderr.write("INTEROP: Failure determined by CODESYS Script Syntax Error.\n");
|
|
186
216
|
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
187
|
-
stderrOutput = `SCRIPT_ERROR:
|
|
217
|
+
stderrOutput = `SCRIPT_ERROR: ${stderrOutput}`; // Include the syntax error details
|
|
188
218
|
}
|
|
189
219
|
else {
|
|
220
|
+
// No spawn error, no shell error, no profile error, no syntax error -> check markers/exit code
|
|
190
221
|
process.stderr.write(`INTEROP: Checking markers/exit code (Code: ${exitCode})...\n`);
|
|
191
222
|
if (output.includes(SCRIPT_SUCCESS_MARKER) || stderrOutput.includes(SCRIPT_SUCCESS_MARKER)) {
|
|
192
223
|
success = true;
|
|
193
224
|
process.stderr.write("INTEROP: Success determined by SUCCESS marker.\n");
|
|
194
225
|
}
|
|
195
226
|
else if (output.includes(SCRIPT_ERROR_MARKER) || stderrOutput.includes(SCRIPT_ERROR_MARKER)) {
|
|
196
|
-
success = false;
|
|
227
|
+
success = false; // Explicit error marker found
|
|
197
228
|
process.stderr.write("INTEROP: Failure determined by ERROR marker.\n");
|
|
198
229
|
}
|
|
199
230
|
else {
|
|
231
|
+
// No markers found, rely solely on exit code
|
|
200
232
|
success = exitCode === 0;
|
|
201
|
-
if (success)
|
|
233
|
+
if (success) {
|
|
202
234
|
process.stderr.write(`INTEROP: Success determined by exit code 0 (no markers found).\n`);
|
|
235
|
+
}
|
|
203
236
|
else {
|
|
204
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
|
|
205
239
|
if (!stderrOutput.includes(SCRIPT_ERROR_MARKER))
|
|
206
240
|
stderrOutput = `SCRIPT_ERROR: Process failed with exit code ${exitCode} (no markers found).\n${stderrOutput}`;
|
|
207
241
|
}
|
|
@@ -227,6 +261,7 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
|
|
|
227
261
|
}
|
|
228
262
|
}
|
|
229
263
|
// Final output processing
|
|
264
|
+
// Combine stderr and stdout only on failure to preserve clean success output
|
|
230
265
|
const finalOutput = success ? output : `${stderrOutput}\n${output}`.trim();
|
|
231
266
|
process.stderr.write(`INTEROP: Final Success: ${success}\n`);
|
|
232
267
|
process.stderr.write(`INTEROP: Final Output Length: ${finalOutput.length}\n---\n`);
|