@codesys/mcp-toolkit 1.1.0 → 1.1.3

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 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
- *(Keep as is or add details)*
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
- *(Keep as is)*
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
- *(Keep as is)*
149
- - The [CODESYS](https://www.codesys.com/) team for their scripting engine
150
- - [Model Context Protocol](https://github.com/m-ld/model-context-protocol) project
151
- - All contributors who have helped improve this toolkit
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
+
@@ -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"); // Use spawn directly
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: 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.
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
- yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
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
- // --- 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`);
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
- const childProcess = (0, child_process_1.spawn)(command, args, {
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, // Force CWD
131
- env: spawnEnv, // Pass modified environment
132
- shell: false // <<< Ensure shell is false or omitted (default)
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(() => { }, timeoutDuration);
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
- if (chunk.includes('C:\\Program')) {
146
- process.stderr.write(`>>>> INTEROP STDERR DETECTED 'C:\\Program': ${chunk}\n`);
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:false): ${spawnError.message}\n`);
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, killing process.\n');
165
- if (!childProcess.killed)
166
- childProcess.kill('SIGTERM');
167
- setTimeout(() => { if (!childProcess.killed)
168
- childProcess.kill('SIGKILL'); }, 2000);
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
- // 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");
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 (spawnResult.error) {
184
- success = false;
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: Spawn failed: ${spawnResult.error.message}\n${stderrOutput}`;
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`);