@codesys/mcp-toolkit 1.0.6 → 1.0.8

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.
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ const { exec } = require('child_process');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
7
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
8
+ const { z } = require('zod');
9
+
10
+ console.error("Starting standalone CODESYS MCP server...");
11
+
12
+ // Create a simple MCP server
13
+ const server = new McpServer({
14
+ name: "Standalone CODESYS MCP Server",
15
+ version: "1.0.0",
16
+ capabilities: { resources: {}, tools: {} }
17
+ });
18
+
19
+ // Direct implementation of executeCodesysScript
20
+ async function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
21
+ // Validate inputs
22
+ if (!codesysExePath) throw new Error('CODESYS executable path was not provided');
23
+ if (!codesysProfileName) throw new Error('CODESYS profile name was not provided');
24
+
25
+ console.error(`DIRECT: Using CODESYS Path: ${codesysExePath}`);
26
+ console.error(`DIRECT: Using CODESYS Profile: ${codesysProfileName}`);
27
+
28
+ // Create temp script file
29
+ const tempDir = os.tmpdir();
30
+ const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).slice(2, 9)}.py`;
31
+ const tempFilePath = path.join(tempDir, tempFileName);
32
+
33
+ let output = '';
34
+ let success = false;
35
+
36
+ try {
37
+ // Write script to temp file
38
+ fs.writeFileSync(tempFilePath, scriptContent, 'latin1');
39
+ console.error(`DIRECT: Wrote script to: ${tempFilePath}`);
40
+
41
+ // Build command with proper quoting
42
+ const commandWithArgs = `"${codesysExePath}" --profile="${codesysProfileName}" --noUI --runscript="${tempFilePath}"`;
43
+ console.error(`DIRECT: Command: ${commandWithArgs}`);
44
+
45
+ // Execute using exec
46
+ success = await new Promise((resolve) => {
47
+ exec(commandWithArgs, (error, stdout, stderr) => {
48
+ if (stdout) output += stdout;
49
+ if (stderr) output += stderr;
50
+
51
+ console.error(`DIRECT: stdout: ${stdout.slice(0, 200)}${stdout.length > 200 ? '...' : ''}`);
52
+ if (stderr) console.error(`DIRECT: stderr: ${stderr}`);
53
+
54
+ if (error) {
55
+ console.error(`DIRECT: Error: ${error.message}`);
56
+ resolve(false);
57
+ } else {
58
+ resolve(true);
59
+ }
60
+ });
61
+ });
62
+
63
+ // Check for success/error markers in output
64
+ if (output.includes('SCRIPT_SUCCESS')) {
65
+ success = true;
66
+ } else if (output.includes('SCRIPT_ERROR')) {
67
+ success = false;
68
+ }
69
+
70
+ } catch (err) {
71
+ console.error(`DIRECT: Exception: ${err.message}`);
72
+ output += `\nError: ${err.message}`;
73
+ success = false;
74
+ } finally {
75
+ // Clean up temp file
76
+ try {
77
+ fs.unlinkSync(tempFilePath);
78
+ console.error(`DIRECT: Deleted temp file: ${tempFilePath}`);
79
+ } catch (err) {
80
+ console.error(`DIRECT: Failed to delete temp file: ${err.message}`);
81
+ }
82
+ }
83
+
84
+ return { success, output };
85
+ }
86
+
87
+ // Implement a basic check_status tool
88
+ server.tool("check_status", {}, async () => {
89
+ try {
90
+ const codesysPath = process.argv[2] || "C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\Common\\CODESYS.exe";
91
+ const profileName = process.argv[3] || "CODESYS V3.5 SP21";
92
+
93
+ console.error(`DIRECT: Check Status with ${codesysPath} and ${profileName}`);
94
+
95
+ const scriptContent = `
96
+ import sys, scriptengine as script_engine, os
97
+ try:
98
+ print("Python script running")
99
+ print("Checking CODESYS status...")
100
+ scripting_ok = True
101
+ primary_project = script_engine.projects.primary
102
+ print("Primary project: " + str(primary_project))
103
+ print("SCRIPT_SUCCESS: Status check completed")
104
+ sys.exit(0)
105
+ except Exception as e:
106
+ print("Error: " + str(e))
107
+ print("SCRIPT_ERROR: " + str(e))
108
+ sys.exit(1)
109
+ `;
110
+
111
+ const result = await executeCodesysScript(scriptContent, codesysPath, profileName);
112
+ return {
113
+ content: [{ type: "text", text: `Status check ${result.success ? 'succeeded' : 'failed'}.\nOutput:\n${result.output}` }],
114
+ isError: !result.success
115
+ };
116
+ } catch (err) {
117
+ console.error(`DIRECT: Tool error: ${err.message}`);
118
+ return { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true };
119
+ }
120
+ });
121
+
122
+ // Start the server
123
+ async function startServer() {
124
+ try {
125
+ const transport = new StdioServerTransport();
126
+ console.error("DIRECT: Connecting server via stdio...");
127
+ await server.connect(transport);
128
+ console.error("DIRECT: Server connected and listening");
129
+ } catch (error) {
130
+ console.error("DIRECT: Failed to start server:", error);
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ // Show args
136
+ console.error(`DIRECT: Args: ${JSON.stringify(process.argv)}`);
137
+
138
+ // Start the server
139
+ startServer();
140
+
141
+ // Handle shutdown
142
+ process.on('SIGINT', () => {
143
+ console.error('\nDIRECT: SIGINT received, shutting down...');
144
+ process.exit(0);
145
+ });
146
+
147
+ process.on('SIGTERM', () => {
148
+ console.error('\nDIRECT: SIGTERM received, shutting down...');
149
+ process.exit(0);
150
+ });
package/dist/bin.js CHANGED
@@ -18,13 +18,22 @@ commander_1.program
18
18
  .option('-p, --codesys-profile <profile>', 'CODESYS profile name', 'CODESYS V3.5 SP21')
19
19
  .parse(process.argv);
20
20
  const options = commander_1.program.opts();
21
- // Log startup message for debugging
21
+ // Enhanced logging for debugging purposes
22
+ console.error(`========================================`);
22
23
  console.error(`Starting CODESYS MCP Server v${version}`);
23
- console.error(`Using CODESYS path: ${options.codesysPath}`);
24
- console.error(`Using CODESYS profile: ${options.codesysProfile}`);
25
- // Pass command line arguments to the CODESYS executable path and profile
24
+ console.error(`CODESYS path: ${options.codesysPath}`);
25
+ console.error(`CODESYS profile: ${options.codesysProfile}`);
26
+ console.error(`Using Node.js ${process.version}`);
27
+ console.error(`Current directory: ${process.cwd()}`);
28
+ console.error(`========================================`);
29
+ // Pass properly sanitized command line arguments
30
+ const codesysPath = options.codesysPath.trim();
31
+ const codesysProfile = options.codesysProfile.trim();
26
32
  // The server.js file expects these as positional parameters
27
- process.argv[2] = options.codesysPath;
28
- process.argv[3] = options.codesysProfile;
33
+ process.argv[2] = codesysPath;
34
+ process.argv[3] = codesysProfile;
35
+ // Also set them as environment variables as a backup mechanism
36
+ process.env.CODESYS_PATH = codesysPath;
37
+ process.env.CODESYS_PROFILE = codesysProfile;
29
38
  // Import and start the server
30
39
  import('./server.js');
@@ -43,7 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.executeCodesysScript = executeCodesysScript;
46
- const child_process_1 = require("child_process");
46
+ const child_process_1 = require("child_process"); // Use spawn directly
47
47
  const promises_1 = require("fs/promises");
48
48
  const path = __importStar(require("path"));
49
49
  const os = __importStar(require("os"));
@@ -52,6 +52,7 @@ const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
52
52
  const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
53
53
  /**
54
54
  * Executes a CODESYS Python script using the command line interface.
55
+ * Uses spawn for better handling of paths with spaces.
55
56
  *
56
57
  * @param scriptContent The Python script code to execute.
57
58
  * @param codesysExePath The full path to the CODESYS.exe executable.
@@ -69,58 +70,95 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
69
70
  }
70
71
  // Create a temporary file for the script
71
72
  const tempDir = os.tmpdir();
72
- const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(7)}.py`;
73
+ // Ensure temp file name is safe for command line (though spawn handles args better)
74
+ const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(2, 9)}.py`;
73
75
  const tempFilePath = path.join(tempDir, tempFileName);
74
76
  let output = '';
75
77
  let success = false;
78
+ let exitCode = null;
76
79
  try {
77
80
  // Normalize line endings to LF (\n)
78
81
  const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
79
82
  // Write the script content to the temporary file using latin1 encoding
80
- yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1'); // Explicitly use latin1
81
- process.stderr.write(`Temporary script written to: ${tempFilePath} with latin1 encoding and LF line endings\n`); // Use process.stderr
82
- // Add --additionalfolder argument
83
- const additionalFolderPath = "C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\AdditionalFolders\\Default"; // Assuming Default instance
84
- // Fix: Use shell:true but ensure proper quoting for paths with spaces
85
- const commandWithArgs = `"${codesysExePath}" --profile="${codesysProfileName}" --noUI --runscript="${tempFilePath}"`;
86
- process.stderr.write(`Executing: ${commandWithArgs}\n`); // Log the command
87
- // Use exec instead of spawn - more reliable for command line processing
88
- const exitCode = yield new Promise((resolve, reject) => {
89
- const timeoutMs = 30000; // 30 seconds
90
- let timeoutHandle = null;
91
- // Start the command using exec
92
- const childProcess = (0, child_process_1.exec)(commandWithArgs, (error, stdout, stderr) => {
83
+ yield (0, promises_1.writeFile)(tempFilePath, normalizedScriptContent, 'latin1');
84
+ process.stderr.write(`Temporary script written to: ${tempFilePath} with latin1 encoding and LF line endings\n`);
85
+ // --- Use spawn ---
86
+ const command = codesysExePath; // The executable itself
87
+ const args = [
88
+ `--profile=${codesysProfileName}`, // Format expected by CODESYS.exe
89
+ '--noUI',
90
+ `--runscript=${tempFilePath}` // Format expected by CODESYS.exe
91
+ // If CODESYS requires quotes *around* the profile/script path *value*, adjust like:
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.
96
+ ];
97
+ process.stderr.write(`Spawning: ${command} with args: ${JSON.stringify(args)}\n`);
98
+ const spawnResult = yield new Promise((resolve, reject) => {
99
+ let stdoutData = '';
100
+ let stderrData = '';
101
+ // Spawn the process. Detached:false is default. Timeout via AbortController recommended for spawn.
102
+ const controller = new AbortController();
103
+ const timeoutSignal = controller.signal;
104
+ const timeoutDuration = 30000; // 30 seconds
105
+ const childProcess = (0, child_process_1.spawn)(command, args, {
106
+ windowsHide: true, // Hides the console window on Windows
107
+ signal: timeoutSignal // Use AbortSignal for timeout
108
+ });
109
+ const timeoutId = setTimeout(() => {
110
+ process.stderr.write(`TIMEOUT: CODESYS process timed out after ${timeoutDuration / 1000} seconds\n`);
111
+ controller.abort(); // Abort the process via the signal
112
+ }, timeoutDuration);
113
+ childProcess.stdout.on('data', (data) => {
114
+ const chunk = data.toString();
115
+ stdoutData += chunk;
116
+ process.stderr.write(`CODESYS stdout chunk: ${chunk.length > 100 ? chunk.substring(0, 100) + '...' : chunk}\n`);
117
+ });
118
+ childProcess.stderr.on('data', (data) => {
119
+ const chunk = data.toString();
120
+ stderrData += chunk;
121
+ process.stderr.write(`CODESYS stderr chunk: ${chunk}\n`); // Log stderr more verbosely
122
+ });
123
+ childProcess.on('error', (error) => {
93
124
  var _a;
94
- if (timeoutHandle)
95
- clearTimeout(timeoutHandle);
96
- // Save all output
97
- if (stdout)
98
- output += stdout;
99
- if (stderr) {
100
- process.stderr.write(`CODESYS stderr: ${stderr}\n`);
101
- output += stderr;
102
- }
103
- if (error) {
104
- // Command failed
105
- process.stderr.write(`CODESYS process error: ${error.message}\n`);
106
- resolve((_a = error.code) !== null && _a !== void 0 ? _a : 1); // Return error code
107
- }
108
- else {
109
- // Command succeeded
110
- resolve(0);
111
- }
125
+ clearTimeout(timeoutId); // Clear timeout on explicit error
126
+ // This usually means the process couldn't be spawned (e.g., EACCES, ENOENT)
127
+ process.stderr.write(`CODESYS spawn error: ${error.message}\n`);
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}` });
130
+ });
131
+ childProcess.on('close', (code) => {
132
+ clearTimeout(timeoutId); // Clear timeout on successful close
133
+ process.stderr.write(`CODESYS process closed with code: ${code}\n`);
134
+ const combinedOutput = stdoutData + stderrData;
135
+ resolve({ code: code, output: combinedOutput });
112
136
  });
113
- // Set a timeout
114
- timeoutHandle = setTimeout(() => {
115
- process.stderr.write(`TIMEOUT: CODESYS process did not complete in ${timeoutMs}ms\n`);
116
- childProcess.kill();
117
- resolve(0); // Continue processing with what we have
118
- }, timeoutMs);
137
+ // Handle the timeout signal specifically
138
+ timeoutSignal.addEventListener('abort', () => {
139
+ process.stderr.write('Abort signal received, killing process if still running.\n');
140
+ if (!childProcess.killed) {
141
+ childProcess.kill('SIGTERM'); // Attempt graceful kill first
142
+ // Optionally force kill after a short delay if SIGTERM doesn't work
143
+ setTimeout(() => {
144
+ if (!childProcess.killed) {
145
+ process.stderr.write('SIGTERM ineffective, sending SIGKILL.\n');
146
+ childProcess.kill('SIGKILL');
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
119
158
  });
120
- process.stderr.write(`CODESYS process exited with code: ${exitCode}\n`); // Use process.stderr
159
+ output = spawnResult.output;
160
+ exitCode = spawnResult.code; // Can be null if process killed before exit code retrieved
121
161
  // Determine success based on exit code and output markers
122
- // Note: CODESYS scripting might return exit code 0 even if the script had logical errors.
123
- // Relying on specific output markers is more robust.
124
162
  if (output.includes(SCRIPT_SUCCESS_MARKER)) {
125
163
  success = true;
126
164
  }
@@ -128,48 +166,50 @@ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName)
128
166
  success = false;
129
167
  }
130
168
  else {
131
- // If no markers found, fall back to exit code (less reliable)
132
- success = exitCode === 0;
133
- if (!success) {
134
- // Append the specific CODESYS error if available and no other marker found
169
+ // Fallback logic (handle null exit code)
170
+ if (exitCode === 0) {
171
+ console.warn("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found.");
172
+ success = !output.includes(SCRIPT_ERROR_MARKER); // Assume success if exit code 0 and no error marker
173
+ }
174
+ else {
175
+ success = false;
176
+ const exitMsg = exitCode !== null ? `exit code ${exitCode}` : 'signal or unknown reason';
135
177
  if (!output.includes(SCRIPT_ERROR_MARKER)) {
136
- output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}. Check CODESYS stderr output above for details.`;
178
+ output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}. Check CODESYS stderr output above for details.`;
137
179
  }
138
180
  else {
139
- output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}.`;
181
+ output += `\nSCRIPT_ERROR: Process exited with ${exitMsg}.`;
140
182
  }
141
- }
142
- else {
143
- // If exit code is 0 but no success marker, treat as potential issue or simple script
144
- console.error("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found."); // Log warning to stderr
145
- // Optionally, you could treat this as success or failure depending on requirements
146
- // For now, let's assume success if exit code is 0 and no error marker.
147
- if (!output.includes(SCRIPT_ERROR_MARKER)) {
148
- success = true;
183
+ // Add timeout message specifically if that was the cause
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.`;
149
190
  }
150
191
  }
151
192
  }
152
193
  }
153
194
  catch (error) {
154
- process.stderr.write(`Error executing CODESYS script: ${error}\n`); // Use process.stderr
155
- output += `\nSCRIPT_ERROR: Failed to execute script process. ${error.message}`;
195
+ process.stderr.write(`Error executing CODESYS script: ${error}\n`);
196
+ output += `\nSCRIPT_ERROR: Failed to set up script execution process. ${error.message}`;
156
197
  success = false;
157
198
  }
158
199
  finally {
159
200
  // Clean up the temporary file
160
201
  try {
161
202
  yield (0, promises_1.unlink)(tempFilePath);
162
- process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`); // Use process.stderr
203
+ process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`);
163
204
  }
164
205
  catch (cleanupError) {
165
- process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`); // Use process.stderr
166
- // Don't overwrite the original error if cleanup fails
167
- if (success) { // Only add cleanup error if the main process didn't already fail
206
+ process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`);
207
+ if (success) {
168
208
  output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
169
209
  }
170
210
  }
171
211
  }
172
- process.stderr.write(`Final script output received:\n---\n${output}\n---\n`); // Use process.stderr
173
- return { success, output }; // Revert return object
212
+ process.stderr.write(`Final script output received (Success: ${success}):\n---\n${output}\n---\n`);
213
+ return { success, output };
174
214
  });
175
215
  }