@codesys/mcp-toolkit 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 CODESYS MCP Server Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @codesys/mcp-toolkit
2
+
3
+ This project provides a Model Context Protocol (MCP) server that allows interaction with a local CODESYS V3 development environment (tested with 3.5 SP21) via its scripting interface. It enables MCP clients like Claude Desktop to perform actions such as opening/creating/saving projects, creating POUs, adding code, and compiling projects.
4
+
5
+ ## Features
6
+
7
+ Currently implemented features:
8
+
9
+ * **Resources:**
10
+ * `codesys://project/status`: Checks scripting status and if a project is open.
11
+ * `codesys://project/structure`: Retrieves the structure of the open project as text.
12
+ * `codesys://pou/{+pou_path}/code`: Reads the implementation or declaration code of a specific POU or DUT/GVL.
13
+ * **Tools:**
14
+ * `open_project`: Opens a specified CODESYS project file (`.project`).
15
+ * `create_project`: Creates a new CODESYS project file (`.project`), optionally from a template.
16
+ * `save_project`: Saves the currently open CODESYS project.
17
+ * `create_pou`: Creates a new POU (Program, Function Block, Function) with a specified language under a parent object in the project tree.
18
+ * `add_code`: Sets the implementation code for a specified POU.
19
+ * `compile_project`: Initiates the compilation of the open project.
20
+
21
+ ## Prerequisites
22
+
23
+ 1. **CODESYS V3 Installation:** A working CODESYS V3 installation (e.g., 3.5 SP21) with the **Scripting Engine** component enabled.
24
+ 2. **Node.js:** Node.js (version 18 or later recommended) installed on your system. You can download it from [nodejs.org](https://nodejs.org/).
25
+ 3. **MCP Client:** An MCP client application, such as **Claude Desktop**.
26
+
27
+ ## Installation
28
+
29
+ ### Option 1: Install from npm (Recommended)
30
+
31
+ ```bash
32
+ npm install -g @codesys/mcp-toolkit
33
+ ```
34
+
35
+ This will install the server globally, making the `codesys-mcp-toolkit` command available in your terminal.
36
+
37
+ ### Option 2: Install from Source
38
+
39
+ 1. **Clone Repository:**
40
+ ```bash
41
+ git clone https://github.com/yourusername/codesys-mcp-server.git
42
+ cd codesys-mcp-server
43
+ ```
44
+
45
+ 2. **Install Dependencies:**
46
+ ```bash
47
+ npm install
48
+ ```
49
+
50
+ 3. **Build the Project:**
51
+ ```bash
52
+ npm run build
53
+ ```
54
+
55
+ 4. **Install Globally:**
56
+ ```bash
57
+ npm install -g .
58
+ ```
59
+
60
+ ## Running the Server (via Claude Desktop)
61
+
62
+ This server is designed to be launched automatically by an MCP client like Claude Desktop using its stdio connection mechanism. You don't typically run it directly from the command line yourself.
63
+
64
+ ## Configuring Claude Desktop
65
+
66
+ 1. **Locate Configuration File:** Find Claude Desktop's MCP configuration file. On Windows, it's typically located at:
67
+ `C:\Users\<YourUsername>\AppData\Roaming\Claude\claude_desktop_config.json`
68
+ (Replace `<YourUsername>` with your actual Windows username).
69
+ 2. **Edit Configuration:** Open `claude_desktop_config.json` in a text editor.
70
+ 3. **Add Server Entry:** Add an entry for the CODESYS server within the `mcpServers` object. Make sure to replace the example paths and profile name with your actual values.
71
+
72
+ ```json
73
+ {
74
+ "mcpServers": {
75
+ // ... other servers like puppeteer, filesystem ...
76
+
77
+ "codesys_local": {
78
+ "command": "npx",
79
+ "args": [
80
+ "-y",
81
+ "@codesys/mcp-toolkit",
82
+ "--codesys-path", "C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\Common\\CODESYS.exe",
83
+ "--codesys-profile", "CODESYS V3.5 SP21"
84
+ ]
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ **Important:**
91
+ * Replace `C:/Users/ek6430/Desktop/codesys-mcp-server/dist/server.js` with the correct **absolute path** to the compiled `server.js` file on your system. Use forward slashes (`/`) for path separators within this JSON string.
92
+ * Replace `"C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\Common\\CODESYS.exe"` with the correct **absolute path** to your `CODESYS.exe`. Note the use of double backslashes (`\\`) because backslashes need to be escaped within JSON strings.
93
+ * Replace `"CODESYS V3.5 SP21"` with the **exact name** of your CODESYS profile (the one with scripting enabled). Find this name when starting CODESYS or in its title bar.
94
+ 4. **Save the File.**
95
+ 5. **Restart Claude Desktop:** Close Claude Desktop completely and restart it. It will now read the updated configuration and attempt to launch the CODESYS server with the specified arguments.
96
+ 6. **Verify Connection:** Claude Desktop should indicate that it has successfully connected to the "codesys_local" server. You might see the server listed in your available MCP integrations.
97
+ 7. **Use Tools/Resources:** You can now interact with the server from within Claude. For example:
98
+ * Ask Claude to "Use the `project-status` resource from the CODESYS Local server."
99
+ * Ask Claude to "Use the `open_project` tool from CODESYS Local with filePath set to 'C:/Path/To/Your/Project.project'." (Replace with a real project path).
100
+
101
+ ## Troubleshooting
102
+
103
+ * **Configuration Errors:** If Claude Desktop fails to connect, check its logs (often found in `C:\Users\<YourUsername>\AppData\Roaming\Claude\logs\`). Look for errors related to the `codesys_local` server.
104
+ * Verify the paths and profile name in `claude_desktop_config.json` are absolutely correct. Pay attention to path separators (`/` for the script path, `\\` for the CODESYS path within the JSON string).
105
+ * Ensure the profile name exactly matches the one in CODESYS.
106
+ * **Server Script Errors:** If the server starts but tools/resources fail, check the server's output in the Claude Desktop logs. The `executeCodesysScript` function in the server code includes `console.log` and `console.error` statements that might appear in the logs, providing clues about script execution issues.
107
+ * **Permissions:** The user account running Claude Desktop (and thus the Node.js server) needs permission to execute `CODESYS.exe` and write temporary files to the OS's temporary directory.
108
+ * **CODESYS Script Errors:** If a CODESYS script itself fails (e.g., invalid POU path, project not open), the error message from the script should be returned in the MCP tool/resource response. Check the `output` field in the response.
109
+ * **CODESYS Not Running/Responding:** The `executeCodesysScript` might time out or fail if the CODESYS scripting engine cannot be launched properly. Ensure CODESYS isn't already running in a conflicting way (though `--noUI` should help).
110
+
111
+ ## Future Enhancements (Based on Provided API Details)
112
+
113
+ The detailed API information provided (especially `ScriptOnline`) opens up possibilities for significant future enhancements, such as:
114
+
115
+ * Connecting to a running PLC (`create_online_device`, `connect`).
116
+ * Logging into the PLC (`login`).
117
+ * Starting/Stopping applications (`start`, `stop`).
118
+ * Reading/Writing live variable values (`read_value`, `set_prepared_value`, `write_prepared_values`).
119
+ * Forcing/Unforcing values (`force_prepared_values`, `unforce_all_values`).
120
+ * Performing downloads and online changes (`login` with options).
121
+ * Managing PLC files (`download_file`, `upload_file`, `delete_file`).
122
+ * Handling PLC user management (`create_live_user_management`).
123
+ * License management (`activate_license`).
package/dist/bin.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * CODESYS MCP Server CLI
5
+ * Command-line interface for starting the CODESYS MCP server
6
+ * with configurable CODESYS executable paths and profile names.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const commander_1 = require("commander");
10
+ // Define version from package.json
11
+ const packageJson = require('../package.json');
12
+ const version = packageJson.version;
13
+ commander_1.program
14
+ .name('codesys-mcp-toolkit')
15
+ .description('Model Context Protocol (MCP) server for CODESYS automation platform')
16
+ .version(version)
17
+ .option('-e, --codesys-path <path>', 'Path to CODESYS executable', 'C:\\Program Files\\CODESYS 3.5.21.0\\CODESYS\\Common\\CODESYS.exe')
18
+ .option('-p, --codesys-profile <profile>', 'CODESYS profile name', 'CODESYS V3.5 SP21')
19
+ .parse(process.argv);
20
+ const options = commander_1.program.opts();
21
+ // Log startup message for debugging
22
+ 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
26
+ // The server.js file expects these as positional parameters
27
+ process.argv[2] = options.codesysPath;
28
+ process.argv[3] = options.codesysProfile;
29
+ // Import and start the server
30
+ import('./server.js');
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.executeCodesysScript = executeCodesysScript;
46
+ const child_process_1 = require("child_process");
47
+ const promises_1 = require("fs/promises");
48
+ const path = __importStar(require("path"));
49
+ const os = __importStar(require("os"));
50
+ // Define expected success/error markers from the Python scripts
51
+ const SCRIPT_SUCCESS_MARKER = 'SCRIPT_SUCCESS';
52
+ const SCRIPT_ERROR_MARKER = 'SCRIPT_ERROR';
53
+ /**
54
+ * Executes a CODESYS Python script using the command line interface.
55
+ *
56
+ * @param scriptContent The Python script code to execute.
57
+ * @param codesysExePath The full path to the CODESYS.exe executable.
58
+ * @param codesysProfileName The name of the CODESYS profile to use for scripting.
59
+ * @returns A promise resolving to an object containing the success status and the script's output.
60
+ */
61
+ function executeCodesysScript(scriptContent, codesysExePath, codesysProfileName) {
62
+ return __awaiter(this, void 0, void 0, function* () {
63
+ // Validate arguments passed to the function
64
+ if (!codesysExePath) {
65
+ throw new Error('CODESYS executable path was not provided to executeCodesysScript.');
66
+ }
67
+ if (!codesysProfileName) {
68
+ throw new Error('CODESYS profile name was not provided to executeCodesysScript.');
69
+ }
70
+ // Create a temporary file for the script
71
+ const tempDir = os.tmpdir();
72
+ const tempFileName = `codesys_script_${Date.now()}_${Math.random().toString(36).substring(7)}.py`;
73
+ const tempFilePath = path.join(tempDir, tempFileName);
74
+ let output = '';
75
+ let success = false;
76
+ try {
77
+ // Normalize line endings to LF (\n)
78
+ const normalizedScriptContent = scriptContent.replace(/\r\n/g, '\n');
79
+ // 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 and pass the entire command as a single string without additionalfolder
85
+ const commandWithArgs = `"${codesysExePath}" --profile="${codesysProfileName}" --noUI --runscript="${tempFilePath}"`;
86
+ process.stderr.write(`Executing: ${commandWithArgs}\n`); // Log the command string
87
+ // Spawn the CODESYS process using shell:true for proper argument handling
88
+ const codesysProcess = (0, child_process_1.spawn)(commandWithArgs, {
89
+ stdio: ['ignore', 'pipe', 'pipe'], // Ignore stdin, pipe stdout and stderr
90
+ shell: true // Use shell to handle the command string properly
91
+ });
92
+ // Capture stdout
93
+ codesysProcess.stdout.on('data', (data) => {
94
+ const chunk = data.toString();
95
+ // console.error('CODESYS stdout:', chunk); // Optional: Original debug log
96
+ output += chunk;
97
+ });
98
+ // Capture stderr
99
+ codesysProcess.stderr.on('data', (data) => {
100
+ const chunk = data.toString();
101
+ process.stderr.write(`CODESYS stderr: ${chunk}\n`); // Use process.stderr
102
+ output += chunk; // Include stderr in the output for debugging
103
+ });
104
+ // Wait for the process to exit with a timeout
105
+ const exitCode = yield new Promise((resolve, reject) => {
106
+ // Add timeout handling
107
+ const timeoutMs = 30000; // 30 seconds
108
+ const timeout = setTimeout(() => {
109
+ process.stderr.write(`TIMEOUT: CODESYS process did not complete in ${timeoutMs}ms\n`);
110
+ // Force resolve with 0 to proceed with parsing output collected so far
111
+ codesysProcess.kill();
112
+ resolve(0);
113
+ }, timeoutMs);
114
+ codesysProcess.on('close', (code) => {
115
+ clearTimeout(timeout);
116
+ resolve(code !== null && code !== void 0 ? code : 0);
117
+ });
118
+ codesysProcess.on('error', (err) => {
119
+ clearTimeout(timeout);
120
+ // Error starting the process itself (e.g., command not found)
121
+ process.stderr.write(`Failed to start CODESYS process: ${err.message}\n`); // Use process.stderr
122
+ reject(err);
123
+ });
124
+ });
125
+ process.stderr.write(`CODESYS process exited with code: ${exitCode}\n`); // Use process.stderr
126
+ // Determine success based on exit code and output markers
127
+ // Note: CODESYS scripting might return exit code 0 even if the script had logical errors.
128
+ // Relying on specific output markers is more robust.
129
+ if (output.includes(SCRIPT_SUCCESS_MARKER)) {
130
+ success = true;
131
+ }
132
+ else if (output.includes(SCRIPT_ERROR_MARKER)) {
133
+ success = false;
134
+ }
135
+ else {
136
+ // If no markers found, fall back to exit code (less reliable)
137
+ success = exitCode === 0;
138
+ if (!success) {
139
+ // Append the specific CODESYS error if available and no other marker found
140
+ if (!output.includes(SCRIPT_ERROR_MARKER)) {
141
+ output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}. Check CODESYS stderr output above for details.`;
142
+ }
143
+ else {
144
+ output += `\nSCRIPT_ERROR: Process exited with non-zero code ${exitCode}.`;
145
+ }
146
+ }
147
+ else {
148
+ // If exit code is 0 but no success marker, treat as potential issue or simple script
149
+ console.error("Warning: CODESYS script finished with exit code 0 but no SCRIPT_SUCCESS marker found."); // Log warning to stderr
150
+ // Optionally, you could treat this as success or failure depending on requirements
151
+ // For now, let's assume success if exit code is 0 and no error marker.
152
+ if (!output.includes(SCRIPT_ERROR_MARKER)) {
153
+ success = true;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ catch (error) {
159
+ process.stderr.write(`Error executing CODESYS script: ${error}\n`); // Use process.stderr
160
+ output += `\nSCRIPT_ERROR: Failed to execute script process. ${error.message}`;
161
+ success = false;
162
+ }
163
+ finally {
164
+ // Clean up the temporary file
165
+ try {
166
+ yield (0, promises_1.unlink)(tempFilePath);
167
+ process.stderr.write(`Temporary script deleted: ${tempFilePath}\n`); // Use process.stderr
168
+ }
169
+ catch (cleanupError) {
170
+ process.stderr.write(`Failed to delete temporary script file ${tempFilePath}: ${cleanupError}\n`); // Use process.stderr
171
+ // Don't overwrite the original error if cleanup fails
172
+ if (success) { // Only add cleanup error if the main process didn't already fail
173
+ output += `\nWARNING: Failed to delete temporary script file ${tempFilePath}. ${cleanupError.message}`;
174
+ }
175
+ }
176
+ }
177
+ process.stderr.write(`Final script output received:\n---\n${output}\n---\n`); // Use process.stderr
178
+ return { success, output }; // Revert return object
179
+ });
180
+ }