@codesys/mcp-toolkit 1.1.4 → 1.1.5
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/.claude/settings.local.json +13 -0
- package/CHANGELOG.md +38 -26
- package/CONTRIBUTING.md +72 -72
- package/LICENSE +21 -21
- package/README.md +145 -145
- package/README.md.enhanced +155 -155
- package/codesys_scripts/listener_script.py +1539 -0
- package/dist/bin.js +41 -2
- package/dist/server.js +503 -319
- package/docs/index.md +26 -26
- package/docs/installation.md +84 -84
- package/examples/README.md +78 -78
- package/examples/sample_config.json +32 -32
- package/package.json +48 -48
- package/scriptDoc.txt +8679 -8679
package/dist/server.js
CHANGED
|
@@ -63,10 +63,23 @@ console.error(`>>> Initial CWD: ${process.cwd()}`);
|
|
|
63
63
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
64
64
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
65
65
|
const zod_1 = require("zod");
|
|
66
|
-
|
|
66
|
+
// import { executeCodesysScript } from "./codesys_interop"; // We will replace this
|
|
67
67
|
const path = __importStar(require("path"));
|
|
68
68
|
const promises_1 = require("fs/promises");
|
|
69
69
|
const scriptLoader_1 = require("./scriptLoader");
|
|
70
|
+
// Add imports for networking and child processes
|
|
71
|
+
const net = __importStar(require("net"));
|
|
72
|
+
const child_process_1 = require("child_process");
|
|
73
|
+
// --- End Define an interface for configuration ---
|
|
74
|
+
// --- Global variables for persistent CODESYS process and socket client ---
|
|
75
|
+
let codesysProcess = null;
|
|
76
|
+
let codesysClient = null;
|
|
77
|
+
let commandQueue = [];
|
|
78
|
+
let isProcessingCommand = false;
|
|
79
|
+
let socketConnected = false;
|
|
80
|
+
let socketConnecting = false;
|
|
81
|
+
let socketQueue = ''; // Buffer for incoming socket data
|
|
82
|
+
// --- End Global variables ---
|
|
70
83
|
// --- Wrap server logic in an exported function ---
|
|
71
84
|
function startMcpServer(config) {
|
|
72
85
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -76,9 +89,11 @@ function startMcpServer(config) {
|
|
|
76
89
|
const WORKSPACE_DIR = config.workspaceDir;
|
|
77
90
|
const codesysExePath = config.codesysPath;
|
|
78
91
|
const codesysProfileName = config.profileName;
|
|
92
|
+
const codesysScriptsDir = config.codesysScriptsDir; // Get the scripts directory
|
|
79
93
|
console.error(`SERVER.TS: Using Workspace Directory: ${WORKSPACE_DIR}`);
|
|
80
94
|
console.error(`SERVER.TS: Using CODESYS Path: ${codesysExePath}`);
|
|
81
95
|
console.error(`SERVER.TS: Using CODESYS Profile: ${codesysProfileName}`);
|
|
96
|
+
console.error(`SERVER.TS: Using CODESYS Scripts Dir: ${codesysScriptsDir}`);
|
|
82
97
|
// --- Sanity check - confirm the path exists if possible ---
|
|
83
98
|
console.error(`SERVER.TS: Checking existence of CODESYS executable: ${codesysExePath}`);
|
|
84
99
|
try {
|
|
@@ -113,6 +128,195 @@ function startMcpServer(config) {
|
|
|
113
128
|
});
|
|
114
129
|
}
|
|
115
130
|
// --- End Helper Function ---
|
|
131
|
+
// --- Persistent CODESYS Instance Management ---
|
|
132
|
+
/**
|
|
133
|
+
* Launches the persistent CODESYS process with the listener script.
|
|
134
|
+
*/
|
|
135
|
+
function launchCodesysPersistent() {
|
|
136
|
+
if (codesysProcess) {
|
|
137
|
+
console.error("SERVER.TS: CODESYS process already running.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const listenerScriptPath = path.join(codesysScriptsDir, 'listener_script.py');
|
|
141
|
+
console.error(`SERVER.TS: Launching CODESYS with listener script: ${listenerScriptPath}`);
|
|
142
|
+
// Command to launch CODESYS with the listener script and no UI
|
|
143
|
+
const command = `"${codesysExePath}" --profile="${codesysProfileName}" --runscript="${listenerScriptPath}" --noUI`;
|
|
144
|
+
console.error(`SERVER.TS: Executing command: ${command}`);
|
|
145
|
+
// Use spawn to run the command in the background
|
|
146
|
+
codesysProcess = (0, child_process_1.spawn)(command, [], {
|
|
147
|
+
shell: true, // Use shell to handle quotes and spaces in path/profile
|
|
148
|
+
detached: true, // Allow the child process to run independently of the parent
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe'] // Pipe stdout and stderr
|
|
150
|
+
});
|
|
151
|
+
console.error(`SERVER.TS: CODESYS process spawned with PID: ${codesysProcess.pid}`);
|
|
152
|
+
codesysProcess.stdout.on('data', (data) => {
|
|
153
|
+
console.log(`CODESYS STDOUT: ${data}`);
|
|
154
|
+
});
|
|
155
|
+
codesysProcess.stderr.on('data', (data) => {
|
|
156
|
+
console.error(`CODESYS STDERR: ${data}`);
|
|
157
|
+
});
|
|
158
|
+
codesysProcess.on('error', (err) => {
|
|
159
|
+
console.error(`SERVER.TS ERROR: Failed to start CODESYS process: ${err.message}`);
|
|
160
|
+
codesysProcess = null;
|
|
161
|
+
socketConnected = false;
|
|
162
|
+
socketConnecting = false;
|
|
163
|
+
// Attempt to reconnect or handle error appropriately
|
|
164
|
+
});
|
|
165
|
+
codesysProcess.on('close', (code) => {
|
|
166
|
+
console.error(`SERVER.TS: CODESYS process exited with code ${code}`);
|
|
167
|
+
codesysProcess = null;
|
|
168
|
+
socketConnected = false;
|
|
169
|
+
socketConnecting = false;
|
|
170
|
+
// Handle process exit - maybe attempt to relaunch?
|
|
171
|
+
});
|
|
172
|
+
// Unref the process to allow the Node.js event loop to exit if the server is done
|
|
173
|
+
codesysProcess.unref();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Connects the socket client to the running listener script.
|
|
177
|
+
*/
|
|
178
|
+
function connectToCodesysListener() {
|
|
179
|
+
if (codesysClient && socketConnected) {
|
|
180
|
+
console.error("SERVER.TS: Socket client already connected.");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (socketConnecting) {
|
|
184
|
+
console.error("SERVER.TS: Socket client already attempting to connect.");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
socketConnecting = true;
|
|
188
|
+
console.error(`SERVER.TS: Attempting to connect to CODESYS listener on ${net.isIPv6(HOST) ? `[${HOST}]` : HOST}:${PORT}`);
|
|
189
|
+
codesysClient = new net.Socket();
|
|
190
|
+
codesysClient.connect(PORT, HOST, () => {
|
|
191
|
+
console.error("SERVER.TS: Socket connected to CODESYS listener.");
|
|
192
|
+
socketConnected = true;
|
|
193
|
+
socketConnecting = false;
|
|
194
|
+
// Process any queued commands now that the socket is connected
|
|
195
|
+
processCommandQueue();
|
|
196
|
+
});
|
|
197
|
+
codesysClient.on('data', (data) => {
|
|
198
|
+
socketQueue += data.toString();
|
|
199
|
+
// Process incoming data, assuming newline-delimited JSON messages
|
|
200
|
+
let newlineIndex;
|
|
201
|
+
while ((newlineIndex = socketQueue.indexOf('\n')) !== -1) {
|
|
202
|
+
const message = socketQueue.substring(0, newlineIndex);
|
|
203
|
+
socketQueue = socketQueue.substring(newlineIndex + 1);
|
|
204
|
+
try {
|
|
205
|
+
const response = JSON.parse(message);
|
|
206
|
+
console.log("SERVER.TS: Received response from listener:", response);
|
|
207
|
+
// Find and resolve the corresponding command in the queue
|
|
208
|
+
if (commandQueue.length > 0) {
|
|
209
|
+
const { resolve } = commandQueue.shift(); // Get the oldest command
|
|
210
|
+
resolve(response);
|
|
211
|
+
// Process the next command in the queue
|
|
212
|
+
processCommandQueue();
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
console.warn("SERVER.TS: Received unexpected response (command queue empty):", response);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
console.error("SERVER.TS ERROR: Failed to parse JSON response from listener:", e, "Raw data:", message);
|
|
220
|
+
// Handle parsing error - potentially reject the current command
|
|
221
|
+
if (commandQueue.length > 0) {
|
|
222
|
+
const { reject } = commandQueue.shift();
|
|
223
|
+
reject(new Error(`Failed to parse listener response: ${message}`));
|
|
224
|
+
processCommandQueue();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
codesysClient.on('close', () => {
|
|
230
|
+
console.error("SERVER.TS: Socket connection to CODESYS listener closed.");
|
|
231
|
+
socketConnected = false;
|
|
232
|
+
socketConnecting = false;
|
|
233
|
+
// Handle socket close - attempt to reconnect? Reject pending commands?
|
|
234
|
+
// For now, reject all pending commands
|
|
235
|
+
while (commandQueue.length > 0) {
|
|
236
|
+
const { reject } = commandQueue.shift();
|
|
237
|
+
reject(new Error("Socket connection to CODESYS listener closed."));
|
|
238
|
+
}
|
|
239
|
+
// Attempt to reconnect after a delay
|
|
240
|
+
setTimeout(connectToCodesysListener, 5000); // Retry every 5 seconds
|
|
241
|
+
});
|
|
242
|
+
codesysClient.on('error', (err) => {
|
|
243
|
+
console.error(`SERVER.TS ERROR: Socket error: ${err.message}`);
|
|
244
|
+
// Error event is often followed by 'close', so cleanup is in 'close' handler
|
|
245
|
+
socketConnected = false;
|
|
246
|
+
socketConnecting = false;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Sends a command to the CODESYS listener script and waits for a response.
|
|
251
|
+
* This function replaces executeCodesysScript.
|
|
252
|
+
*/
|
|
253
|
+
function sendCodesysCommand(command) {
|
|
254
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
commandQueue.push({ command, resolve, reject });
|
|
257
|
+
processCommandQueue();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Processes the command queue. Sends the next command if the socket is connected and not busy.
|
|
263
|
+
*/
|
|
264
|
+
function processCommandQueue() {
|
|
265
|
+
if (commandQueue.length > 0 && socketConnected && !isProcessingCommand) {
|
|
266
|
+
isProcessingCommand = true;
|
|
267
|
+
const { command } = commandQueue[0]; // Get the oldest command without removing it yet
|
|
268
|
+
const commandString = JSON.stringify(command) + '\n';
|
|
269
|
+
console.log("SERVER.TS: Sending command to listener:", command);
|
|
270
|
+
codesysClient.write(commandString, (err) => {
|
|
271
|
+
if (err) {
|
|
272
|
+
console.error("SERVER.TS ERROR: Failed to send command:", err);
|
|
273
|
+
// If sending fails, reject the current command and process the next
|
|
274
|
+
const { reject } = commandQueue.shift();
|
|
275
|
+
reject(err);
|
|
276
|
+
isProcessingCommand = false;
|
|
277
|
+
processCommandQueue();
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// Command sent successfully, waiting for response in 'data' handler
|
|
281
|
+
// isProcessingCommand remains true until response is received
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else if (commandQueue.length > 0 && !socketConnected && !socketConnecting) {
|
|
286
|
+
console.warn("SERVER.TS: Commands in queue, but socket not connected. Attempting to reconnect.");
|
|
287
|
+
connectToCodesysListener();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Gracefully shuts down the CODESYS process and socket connection.
|
|
292
|
+
*/
|
|
293
|
+
function shutdownCodesysPersistent() {
|
|
294
|
+
console.error("SERVER.TS: Shutting down persistent CODESYS instance.");
|
|
295
|
+
if (codesysClient && socketConnected) {
|
|
296
|
+
// Send exit command and close socket
|
|
297
|
+
sendCodesysCommand({ command: "exit" }).catch(err => console.error("SERVER.TS ERROR sending exit command:", err));
|
|
298
|
+
// Give some time for the exit command to be processed before closing socket
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
if (codesysClient) {
|
|
301
|
+
codesysClient.end();
|
|
302
|
+
codesysClient = null;
|
|
303
|
+
}
|
|
304
|
+
}, 1000); // Wait 1 second
|
|
305
|
+
}
|
|
306
|
+
else if (codesysClient && !socketConnected) {
|
|
307
|
+
// Socket not connected, just destroy it
|
|
308
|
+
codesysClient.destroy();
|
|
309
|
+
codesysClient = null;
|
|
310
|
+
}
|
|
311
|
+
if (codesysProcess) {
|
|
312
|
+
console.error(`SERVER.TS: Terminating CODESYS process (PID: ${codesysProcess.pid}).`);
|
|
313
|
+
// Use kill with a signal to allow graceful shutdown if CODESYS handles it
|
|
314
|
+
// 'SIGTERM' (15) is a common signal for graceful termination
|
|
315
|
+
codesysProcess.kill('SIGTERM');
|
|
316
|
+
codesysProcess = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// --- End Persistent CODESYS Instance Management ---
|
|
116
320
|
// --- Preload script templates ---
|
|
117
321
|
try {
|
|
118
322
|
yield (0, scriptLoader_1.preloadAllScriptTemplates)();
|
|
@@ -125,7 +329,7 @@ function startMcpServer(config) {
|
|
|
125
329
|
console.error("SERVER.TS: Initializing McpServer...");
|
|
126
330
|
const server = new mcp_js_1.McpServer({
|
|
127
331
|
name: "CODESYS Control MCP Server",
|
|
128
|
-
version: "1.
|
|
332
|
+
version: "1.9.0", // Updated version for persistent instance
|
|
129
333
|
capabilities: {
|
|
130
334
|
resources: { listChanged: true },
|
|
131
335
|
tools: { listChanged: true }
|
|
@@ -144,23 +348,33 @@ function startMcpServer(config) {
|
|
|
144
348
|
var _a, _b, _c, _d, _e;
|
|
145
349
|
console.error(`SERVER.TS Resource request: ${uri.href}`);
|
|
146
350
|
try {
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
outputLines.forEach(line => {
|
|
152
|
-
const match = line.match(/^([^:]+):\s*(.*)$/);
|
|
153
|
-
if (match) {
|
|
154
|
-
statusData[match[1].trim()] = match[2].trim();
|
|
155
|
-
}
|
|
351
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
352
|
+
const response = yield sendCodesysCommand({
|
|
353
|
+
command: "execute_script",
|
|
354
|
+
args: { script_name: "check_status.py" } // Assuming listener script has a generic script execution command
|
|
156
355
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
356
|
+
if (response.status === "success") {
|
|
357
|
+
const result = response.result; // Assuming the listener returns the script output in 'result'
|
|
358
|
+
const outputLines = result.output.split(/[\r\n]+/).filter(line => line.trim());
|
|
359
|
+
const statusData = {};
|
|
360
|
+
outputLines.forEach(line => {
|
|
361
|
+
const match = line.match(/^([^:]+):\s*(.*)$/);
|
|
362
|
+
if (match) {
|
|
363
|
+
statusData[match[1].trim()] = match[2].trim();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
const statusText = `CODESYS Status:\n - Scripting OK: ${(_a = statusData['Scripting OK']) !== null && _a !== void 0 ? _a : 'Unknown'}\n - Project Open: ${(_b = statusData['Project Open']) !== null && _b !== void 0 ? _b : 'Unknown'}\n - Project Name: ${(_c = statusData['Project Name']) !== null && _c !== void 0 ? _c : 'Unknown'}\n - Project Path: ${(_d = statusData['Project Path']) !== null && _d !== void 0 ? _d : 'N/A'}`;
|
|
367
|
+
const isError = ((_e = statusData['Scripting OK']) === null || _e === void 0 ? void 0 : _e.toLowerCase()) !== 'true'; // Check only scripting status for error
|
|
368
|
+
return { contents: [{ uri: uri.href, text: statusText, contentType: "text/plain" }], isError: isError };
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
// Handle error response from listener
|
|
372
|
+
return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
|
|
373
|
+
}
|
|
160
374
|
}
|
|
161
375
|
catch (error) {
|
|
162
376
|
console.error(`Error resource ${uri.href}:`, error);
|
|
163
|
-
return { contents: [{ uri: uri.href, text: `Failed
|
|
377
|
+
return { contents: [{ uri: uri.href, text: `Failed to communicate with listener: ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
164
378
|
}
|
|
165
379
|
}));
|
|
166
380
|
// Templates
|
|
@@ -184,38 +398,23 @@ function startMcpServer(config) {
|
|
|
184
398
|
console.error(`Resource request: project structure for ${projectPath}`);
|
|
185
399
|
try {
|
|
186
400
|
const absoluteProjPath = path.normalize(path.isAbsolute(projectPath) ? projectPath : path.join(WORKSPACE_DIR, projectPath));
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'PROJECT_FILE_PATH': absoluteProjPath.replace(/\\/g, '\\\\')
|
|
401
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
402
|
+
// The listener script will handle opening the project and getting the structure
|
|
403
|
+
const response = yield sendCodesysCommand({
|
|
404
|
+
command: "get_project_structure", // New command for listener
|
|
405
|
+
args: { project_path: absoluteProjPath }
|
|
193
406
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (result.success && result.output.includes("SCRIPT_SUCCESS")) {
|
|
198
|
-
const startMarker = "--- PROJECT STRUCTURE START ---";
|
|
199
|
-
const endMarker = "--- PROJECT STRUCTURE END ---";
|
|
200
|
-
const startIndex = result.output.indexOf(startMarker);
|
|
201
|
-
const endIndex = result.output.indexOf(endMarker);
|
|
202
|
-
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex) {
|
|
203
|
-
structureText = result.output.substring(startIndex + startMarker.length, endIndex).replace(/\\n/g, '\n').trim();
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
console.error("Error: Could not find structure markers in script output.");
|
|
207
|
-
structureText = `Could not parse structure markers in output for ${absoluteProjPath}.\n\nOutput:\n${result.output}`;
|
|
208
|
-
isError = true;
|
|
209
|
-
}
|
|
407
|
+
if (response.status === "success") {
|
|
408
|
+
const structureText = response.structure; // Assuming listener returns structure in 'structure' field
|
|
409
|
+
return { contents: [{ uri: uri.href, text: structureText, contentType: "text/plain" }], isError: false };
|
|
210
410
|
}
|
|
211
411
|
else {
|
|
212
|
-
isError
|
|
412
|
+
return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
|
|
213
413
|
}
|
|
214
|
-
return { contents: [{ uri: uri.href, text: structureText, contentType: "text/plain" }], isError: isError };
|
|
215
414
|
}
|
|
216
415
|
catch (error) {
|
|
217
416
|
console.error(`Error getting structure ${uri.href}:`, error);
|
|
218
|
-
return { contents: [{ uri: uri.href, text: `Failed
|
|
417
|
+
return { contents: [{ uri: uri.href, text: `Failed to communicate with listener for '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
219
418
|
}
|
|
220
419
|
}));
|
|
221
420
|
server.resource("pou-code", pouCodeTemplate, (uri, params) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -239,51 +438,23 @@ function startMcpServer(config) {
|
|
|
239
438
|
try {
|
|
240
439
|
const absoluteProjPath = path.normalize(path.isAbsolute(projectPath) ? projectPath : path.join(WORKSPACE_DIR, projectPath));
|
|
241
440
|
const sanitizedPouPath = String(pouPath).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
|
|
248
|
-
'PROJECT_FILE_PATH': absoluteProjPath.replace(/\\/g, '\\\\'),
|
|
249
|
-
'POU_FULL_PATH': sanitizedPouPath
|
|
441
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
442
|
+
// The listener script will handle opening the project and getting the POU code
|
|
443
|
+
const response = yield sendCodesysCommand({
|
|
444
|
+
command: "get_pou_code", // New command for listener
|
|
445
|
+
args: { project_path: absoluteProjPath, pou_path: sanitizedPouPath }
|
|
250
446
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (result.success && result.output.includes("SCRIPT_SUCCESS")) {
|
|
255
|
-
const declStartMarker = "### POU DECLARATION START ###";
|
|
256
|
-
const declEndMarker = "### POU DECLARATION END ###";
|
|
257
|
-
const implStartMarker = "### POU IMPLEMENTATION START ###";
|
|
258
|
-
const implEndMarker = "### POU IMPLEMENTATION END ###";
|
|
259
|
-
const declStartIdx = result.output.indexOf(declStartMarker);
|
|
260
|
-
const declEndIdx = result.output.indexOf(declEndMarker);
|
|
261
|
-
const implStartIdx = result.output.indexOf(implStartMarker);
|
|
262
|
-
const implEndIdx = result.output.indexOf(implEndMarker);
|
|
263
|
-
let declaration = "/* Declaration not found in output */";
|
|
264
|
-
let implementation = "/* Implementation not found in output */";
|
|
265
|
-
if (declStartIdx !== -1 && declEndIdx !== -1 && declStartIdx < declEndIdx) {
|
|
266
|
-
declaration = result.output.substring(declStartIdx + declStartMarker.length, declEndIdx).replace(/\\n/g, '\n').trim();
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
console.error(`WARN: Declaration markers not found correctly for ${sanitizedPouPath}`);
|
|
270
|
-
}
|
|
271
|
-
if (implStartIdx !== -1 && implEndIdx !== -1 && implStartIdx < implEndIdx) {
|
|
272
|
-
implementation = result.output.substring(implStartIdx + implStartMarker.length, implEndIdx).replace(/\\n/g, '\n').trim();
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
console.error(`WARN: Implementation markers not found correctly for ${sanitizedPouPath}`);
|
|
276
|
-
}
|
|
277
|
-
codeText = `// ----- Declaration -----\n${declaration}\n\n// ----- Implementation -----\n${implementation}`;
|
|
447
|
+
if (response.status === "success") {
|
|
448
|
+
const codeText = response.code; // Assuming listener returns code in 'code' field
|
|
449
|
+
return { contents: [{ uri: uri.href, text: codeText, contentType: "text/plain" }], isError: false };
|
|
278
450
|
}
|
|
279
451
|
else {
|
|
280
|
-
isError
|
|
452
|
+
return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
|
|
281
453
|
}
|
|
282
|
-
return { contents: [{ uri: uri.href, text: codeText, contentType: "text/plain" }], isError: isError };
|
|
283
454
|
}
|
|
284
455
|
catch (error) {
|
|
285
456
|
console.error(`Error getting POU code ${uri.href}:`, error);
|
|
286
|
-
return { contents: [{ uri: uri.href, text: `Failed
|
|
457
|
+
return { contents: [{ uri: uri.href, text: `Failed to communicate with listener for '${pouPath}' in '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
|
|
287
458
|
}
|
|
288
459
|
}));
|
|
289
460
|
// --- End Resources ---
|
|
@@ -295,28 +466,31 @@ function startMcpServer(config) {
|
|
|
295
466
|
let absPath = path.normalize(path.isAbsolute(filePath) ? filePath : path.join(WORKSPACE_DIR, filePath));
|
|
296
467
|
console.error(`Tool call: open_project: ${absPath}`);
|
|
297
468
|
try {
|
|
298
|
-
//
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const combinedScript = ensureProjectOpenScript + '\n\n' + scriptTemplate;
|
|
303
|
-
const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
|
|
304
|
-
'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\')
|
|
469
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
470
|
+
const response = yield sendCodesysCommand({
|
|
471
|
+
command: "open_project", // Command matches tool name
|
|
472
|
+
args: { file_path: absPath }
|
|
305
473
|
});
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
474
|
+
if (response.status === "success") {
|
|
475
|
+
return {
|
|
476
|
+
content: [{
|
|
477
|
+
type: "text",
|
|
478
|
+
text: `Project opened: ${absPath}`
|
|
479
|
+
}],
|
|
480
|
+
isError: false
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
return {
|
|
485
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
486
|
+
isError: true
|
|
487
|
+
};
|
|
488
|
+
}
|
|
315
489
|
}
|
|
316
490
|
catch (e) {
|
|
317
491
|
console.error(`Error open_project ${absPath}: ${e}`);
|
|
318
492
|
return {
|
|
319
|
-
content: [{ type: "text", text: `
|
|
493
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
320
494
|
isError: true
|
|
321
495
|
};
|
|
322
496
|
}
|
|
@@ -327,9 +501,9 @@ function startMcpServer(config) {
|
|
|
327
501
|
const { filePath } = args;
|
|
328
502
|
let absPath = path.normalize(path.isAbsolute(filePath) ? filePath : path.join(WORKSPACE_DIR, filePath));
|
|
329
503
|
console.error(`Tool call: create_project (copy template): ${absPath}`);
|
|
504
|
+
// Template finding logic remains the same as it doesn't require CODESYS interaction
|
|
330
505
|
let templatePath = "";
|
|
331
506
|
try {
|
|
332
|
-
// Template finding logic
|
|
333
507
|
const baseDir = path.dirname(path.dirname(codesysExePath));
|
|
334
508
|
templatePath = path.normalize(path.join(baseDir, 'Templates', 'Standard.project'));
|
|
335
509
|
if (!(yield fileExists(templatePath))) {
|
|
@@ -365,28 +539,32 @@ function startMcpServer(config) {
|
|
|
365
539
|
};
|
|
366
540
|
}
|
|
367
541
|
try {
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
542
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
543
|
+
// The listener script will handle creating the project from template
|
|
544
|
+
const response = yield sendCodesysCommand({
|
|
545
|
+
command: "create_project", // Command matches tool name
|
|
546
|
+
args: { file_path: absPath, template_path: templatePath }
|
|
373
547
|
});
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
548
|
+
if (response.status === "success") {
|
|
549
|
+
return {
|
|
550
|
+
content: [{
|
|
551
|
+
type: "text",
|
|
552
|
+
text: `Project created from template: ${absPath}`
|
|
553
|
+
}],
|
|
554
|
+
isError: false
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
return {
|
|
559
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
560
|
+
isError: true
|
|
561
|
+
};
|
|
562
|
+
}
|
|
385
563
|
}
|
|
386
564
|
catch (e) {
|
|
387
565
|
console.error(`Error create_project ${absPath}: ${e}`);
|
|
388
566
|
return {
|
|
389
|
-
content: [{ type: "text", text: `
|
|
567
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
390
568
|
isError: true
|
|
391
569
|
};
|
|
392
570
|
}
|
|
@@ -398,27 +576,32 @@ function startMcpServer(config) {
|
|
|
398
576
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
399
577
|
console.error(`Tool call: save_project: ${absPath}`);
|
|
400
578
|
try {
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\')
|
|
579
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
580
|
+
// The listener script will handle ensuring the project is open and saving it
|
|
581
|
+
const response = yield sendCodesysCommand({
|
|
582
|
+
command: "save_project", // Command matches tool name
|
|
583
|
+
args: { project_file_path: absPath }
|
|
407
584
|
});
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
585
|
+
if (response.status === "success") {
|
|
586
|
+
return {
|
|
587
|
+
content: [{
|
|
588
|
+
type: "text",
|
|
589
|
+
text: `Project saved: ${absPath}`
|
|
590
|
+
}],
|
|
591
|
+
isError: false
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
return {
|
|
596
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
597
|
+
isError: true
|
|
598
|
+
};
|
|
599
|
+
}
|
|
417
600
|
}
|
|
418
601
|
catch (e) {
|
|
419
602
|
console.error(`Error save_project ${absPath}: ${e}`);
|
|
420
603
|
return {
|
|
421
|
-
content: [{ type: "text", text: `
|
|
604
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
422
605
|
isError: true
|
|
423
606
|
};
|
|
424
607
|
}
|
|
@@ -436,260 +619,261 @@ function startMcpServer(config) {
|
|
|
436
619
|
const sanName = name.trim();
|
|
437
620
|
console.error(`Tool call: create_pou: Name='${sanName}', Type='${type}', Lang='${language}', Parent='${sanParentPath}', Project='${absPath}'`);
|
|
438
621
|
try {
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
622
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
623
|
+
// The listener script will handle opening the project and creating the POU
|
|
624
|
+
const response = yield sendCodesysCommand({
|
|
625
|
+
command: "create_pou", // Command matches tool name
|
|
626
|
+
args: {
|
|
627
|
+
project_file_path: absPath,
|
|
628
|
+
name: sanName,
|
|
629
|
+
type: type,
|
|
630
|
+
language: language,
|
|
631
|
+
parent_path: sanParentPath
|
|
632
|
+
}
|
|
450
633
|
});
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
634
|
+
if (response.status === "success") {
|
|
635
|
+
return {
|
|
636
|
+
content: [{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: `POU '${sanName}' created in '${sanParentPath}' of ${absPath}. Project saved.`
|
|
639
|
+
}],
|
|
640
|
+
isError: false
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
return {
|
|
645
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
646
|
+
isError: true
|
|
647
|
+
};
|
|
648
|
+
}
|
|
462
649
|
}
|
|
463
650
|
catch (e) {
|
|
464
651
|
console.error(`Error create_pou ${sanName} in ${absPath}: ${e}`);
|
|
465
652
|
return {
|
|
466
|
-
content: [{ type: "text", text: `
|
|
653
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
467
654
|
isError: true
|
|
468
655
|
};
|
|
469
656
|
}
|
|
470
657
|
}));
|
|
471
658
|
server.tool("set_pou_code", "Sets the declaration and/or implementation code for a specific POU, Method, or Property.", {
|
|
472
659
|
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
|
|
473
|
-
pouPath: zod_1.z.string().describe("
|
|
474
|
-
|
|
475
|
-
|
|
660
|
+
pouPath: zod_1.z.string().describe("Relative path to the POU, Method, or Property (e.g., 'Application.PLC_PRG' or 'Application.MyFB.MyMethod')."),
|
|
661
|
+
declaration: zod_1.z.string().optional().describe("The new declaration code for the POU."),
|
|
662
|
+
implementation: zod_1.z.string().optional().describe("The new implementation code for the POU.")
|
|
476
663
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
477
|
-
const { projectFilePath, pouPath,
|
|
478
|
-
|
|
664
|
+
const { projectFilePath, pouPath, declaration, implementation } = args;
|
|
665
|
+
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
666
|
+
const sanPouPath = pouPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
667
|
+
console.error(`Tool call: set_pou_code: POU='${sanPouPath}', Project='${absPath}'`);
|
|
668
|
+
if (declaration === undefined && implementation === undefined) {
|
|
479
669
|
return {
|
|
480
|
-
content: [{ type: "text", text: "Error:
|
|
670
|
+
content: [{ type: "text", text: "Error: Either 'declaration' or 'implementation' must be provided." }],
|
|
481
671
|
isError: true
|
|
482
672
|
};
|
|
483
673
|
}
|
|
484
|
-
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
485
|
-
const sanPouPath = pouPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
486
|
-
console.error(`Tool call: set_pou_code: Target='${sanPouPath}', Project='${absPath}'`);
|
|
487
674
|
try {
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
'POU_FULL_PATH': sanPouPath,
|
|
499
|
-
'DECLARATION_CONTENT': sanDeclCode,
|
|
500
|
-
'IMPLEMENTATION_CONTENT': sanImplCode
|
|
675
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
676
|
+
// The listener script will handle opening the project and setting the POU code
|
|
677
|
+
const response = yield sendCodesysCommand({
|
|
678
|
+
command: "set_pou_code", // Command matches tool name
|
|
679
|
+
args: {
|
|
680
|
+
project_file_path: absPath,
|
|
681
|
+
pou_path: sanPouPath,
|
|
682
|
+
declaration: declaration,
|
|
683
|
+
implementation: implementation
|
|
684
|
+
}
|
|
501
685
|
});
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
686
|
+
if (response.status === "success") {
|
|
687
|
+
return {
|
|
688
|
+
content: [{
|
|
689
|
+
type: "text",
|
|
690
|
+
text: `Code set for POU '${sanPouPath}' in ${absPath}. Project saved.`
|
|
691
|
+
}],
|
|
692
|
+
isError: false
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
return {
|
|
697
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
698
|
+
isError: true
|
|
699
|
+
};
|
|
700
|
+
}
|
|
513
701
|
}
|
|
514
702
|
catch (e) {
|
|
515
703
|
console.error(`Error set_pou_code ${sanPouPath} in ${absPath}: ${e}`);
|
|
516
704
|
return {
|
|
517
|
-
content: [{ type: "text", text: `
|
|
705
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
518
706
|
isError: true
|
|
519
707
|
};
|
|
520
708
|
}
|
|
521
709
|
}));
|
|
522
|
-
server.tool("
|
|
710
|
+
server.tool("create_method", "Creates a new Method within a Function Block.", {
|
|
523
711
|
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
712
|
+
functionBlockPath: zod_1.z.string().describe("Relative path to the parent Function Block (e.g., 'Application.MyFB')."),
|
|
713
|
+
name: zod_1.z.string().describe("Name for the new Method (must be a valid IEC identifier)."),
|
|
714
|
+
language: ImplementationLanguageEnum.describe("Implementation language (ST, LD, FBD, etc.). CODESYS default will be used if specific language is not set."),
|
|
527
715
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
528
|
-
const { projectFilePath,
|
|
716
|
+
const { projectFilePath, functionBlockPath, name, language } = args;
|
|
529
717
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
console.error(`Tool call: create_property: Name='${sanPropName}', Type='${sanPropType}', ParentPOU='${sanParentPath}', Project='${absPath}'`);
|
|
534
|
-
if (!sanPropName || !sanPropType) {
|
|
535
|
-
return {
|
|
536
|
-
content: [{ type: "text", text: `Error: Property name and type cannot be empty.` }],
|
|
537
|
-
isError: true
|
|
538
|
-
};
|
|
539
|
-
}
|
|
718
|
+
const sanFBPath = functionBlockPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
719
|
+
const sanName = name.trim();
|
|
720
|
+
console.error(`Tool call: create_method: Name='${sanName}', Lang='${language}', Parent='${sanFBPath}', Project='${absPath}'`);
|
|
540
721
|
try {
|
|
541
|
-
//
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
'PROPERTY_TYPE': sanPropType
|
|
722
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
723
|
+
const response = yield sendCodesysCommand({
|
|
724
|
+
command: "create_method", // Command matches tool name
|
|
725
|
+
args: {
|
|
726
|
+
project_file_path: absPath,
|
|
727
|
+
function_block_path: sanFBPath,
|
|
728
|
+
name: sanName,
|
|
729
|
+
language: language
|
|
730
|
+
}
|
|
551
731
|
});
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
732
|
+
if (response.status === "success") {
|
|
733
|
+
return {
|
|
734
|
+
content: [{
|
|
735
|
+
type: "text",
|
|
736
|
+
text: `Method '${sanName}' created in '${sanFBPath}' of ${absPath}. Project saved.`
|
|
737
|
+
}],
|
|
738
|
+
isError: false
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
return {
|
|
743
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
744
|
+
isError: true
|
|
745
|
+
};
|
|
746
|
+
}
|
|
563
747
|
}
|
|
564
748
|
catch (e) {
|
|
565
|
-
console.error(`Error
|
|
749
|
+
console.error(`Error create_method ${sanName} in ${absPath}: ${e}`);
|
|
566
750
|
return {
|
|
567
|
-
content: [{ type: "text", text: `
|
|
751
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
568
752
|
isError: true
|
|
569
753
|
};
|
|
570
754
|
}
|
|
571
755
|
}));
|
|
572
|
-
server.tool("
|
|
756
|
+
server.tool("create_property", "Creates a new Property within a Function Block.", {
|
|
573
757
|
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
758
|
+
functionBlockPath: zod_1.z.string().describe("Relative path to the parent Function Block (e.g., 'Application.MyFB')."),
|
|
759
|
+
name: zod_1.z.string().describe("Name for the new Property (must be a valid IEC identifier)."),
|
|
760
|
+
dataType: zod_1.z.string().describe("Data type of the Property (e.g., 'INT', 'BOOL')."),
|
|
761
|
+
isRetain: zod_1.z.boolean().optional().default(false).describe("Whether the Property should be RETAIN."),
|
|
762
|
+
isPersistent: zod_1.z.boolean().optional().default(false).describe("Whether the Property should be PERSISTENT."),
|
|
763
|
+
isConstant: zod_1.z.boolean().optional().default(false).describe("Whether the Property should be CONSTANT."),
|
|
764
|
+
initialValue: zod_1.z.string().optional().describe("Initial value for the Property."),
|
|
765
|
+
comment: zod_1.z.string().optional().describe("Comment for the Property.")
|
|
577
766
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
578
|
-
const { projectFilePath,
|
|
767
|
+
const { projectFilePath, functionBlockPath, name, dataType, isRetain, isPersistent, isConstant, initialValue, comment } = args;
|
|
579
768
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
console.error(`Tool call: create_method: Name='${sanMethName}', Return='${sanReturnType}', ParentPOU='${sanParentPath}', Project='${absPath}'`);
|
|
584
|
-
if (!sanMethName) {
|
|
585
|
-
return {
|
|
586
|
-
content: [{ type: "text", text: `Error: Method name cannot be empty.` }],
|
|
587
|
-
isError: true
|
|
588
|
-
};
|
|
589
|
-
}
|
|
769
|
+
const sanFBPath = functionBlockPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
770
|
+
const sanName = name.trim();
|
|
771
|
+
console.error(`Tool call: create_property: Name='${sanName}', Type='${dataType}', Parent='${sanFBPath}', Project='${absPath}'`);
|
|
590
772
|
try {
|
|
591
|
-
//
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
773
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
774
|
+
const response = yield sendCodesysCommand({
|
|
775
|
+
command: "create_property", // Command matches tool name
|
|
776
|
+
args: {
|
|
777
|
+
project_file_path: absPath,
|
|
778
|
+
function_block_path: sanFBPath,
|
|
779
|
+
name: sanName,
|
|
780
|
+
data_type: dataType,
|
|
781
|
+
is_retain: isRetain,
|
|
782
|
+
is_persistent: isPersistent,
|
|
783
|
+
is_constant: isConstant,
|
|
784
|
+
initial_value: initialValue,
|
|
785
|
+
comment: comment
|
|
786
|
+
}
|
|
601
787
|
});
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
788
|
+
if (response.status === "success") {
|
|
789
|
+
return {
|
|
790
|
+
content: [{
|
|
791
|
+
type: "text",
|
|
792
|
+
text: `Property '${sanName}' created in '${sanFBPath}' of ${absPath}. Project saved.`
|
|
793
|
+
}],
|
|
794
|
+
isError: false
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
else {
|
|
798
|
+
return {
|
|
799
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}` }],
|
|
800
|
+
isError: true
|
|
801
|
+
};
|
|
802
|
+
}
|
|
613
803
|
}
|
|
614
804
|
catch (e) {
|
|
615
|
-
console.error(`Error
|
|
805
|
+
console.error(`Error create_property ${sanName} in ${absPath}: ${e}`);
|
|
616
806
|
return {
|
|
617
|
-
content: [{ type: "text", text: `
|
|
807
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
618
808
|
isError: true
|
|
619
809
|
};
|
|
620
810
|
}
|
|
621
811
|
}));
|
|
622
|
-
server.tool("compile_project", "Compiles
|
|
623
|
-
projectFilePath: zod_1.z.string().describe("Path to the project file
|
|
812
|
+
server.tool("compile_project", "Compiles the specified CODESYS project.", {
|
|
813
|
+
projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project').")
|
|
624
814
|
}, (args) => __awaiter(this, void 0, void 0, function* () {
|
|
625
815
|
const { projectFilePath } = args;
|
|
626
816
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
627
|
-
console.error(`Tool call: compile_project: ${absPath}`);
|
|
817
|
+
console.error(`Tool call: compile_project: Project='${absPath}'`);
|
|
628
818
|
try {
|
|
629
|
-
//
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
|
|
634
|
-
'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\')
|
|
819
|
+
// Replace executeCodesysScript with sendCodesysCommand
|
|
820
|
+
const response = yield sendCodesysCommand({
|
|
821
|
+
command: "compile_project", // Command matches tool name
|
|
822
|
+
args: { project_file_path: absPath }
|
|
635
823
|
});
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
824
|
+
if (response.status === "success") {
|
|
825
|
+
return {
|
|
826
|
+
content: [{
|
|
827
|
+
type: "text",
|
|
828
|
+
text: `Project compiled: ${absPath}. Output:\n${response.output}` // Assuming listener returns compile output
|
|
829
|
+
}],
|
|
830
|
+
isError: false
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
return {
|
|
835
|
+
content: [{ type: "text", text: `Listener Error: ${response.message}\nOutput:\n${response.output}` }],
|
|
836
|
+
isError: true
|
|
837
|
+
};
|
|
647
838
|
}
|
|
648
|
-
return {
|
|
649
|
-
content: [{ type: "text", text: message }],
|
|
650
|
-
isError: isError
|
|
651
|
-
};
|
|
652
839
|
}
|
|
653
840
|
catch (e) {
|
|
654
841
|
console.error(`Error compile_project ${absPath}: ${e}`);
|
|
655
842
|
return {
|
|
656
|
-
content: [{ type: "text", text: `
|
|
843
|
+
content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
|
|
657
844
|
isError: true
|
|
658
845
|
};
|
|
659
846
|
}
|
|
660
847
|
}));
|
|
661
848
|
// --- End Tools ---
|
|
662
849
|
console.error("SERVER.TS: Resources and Tools defined.");
|
|
663
|
-
// ---
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
850
|
+
// --- Start the persistent CODESYS instance and connect the socket client ---
|
|
851
|
+
launchCodesysPersistent();
|
|
852
|
+
// Give CODESYS a moment to start before attempting to connect
|
|
853
|
+
setTimeout(connectToCodesysListener, 2000); // Wait 2 seconds
|
|
854
|
+
// --- End Startup of Persistent Instance ---
|
|
855
|
+
// --- Start the MCP Server Transport ---
|
|
856
|
+
console.error("SERVER.TS: Starting StdioServerTransport...");
|
|
857
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
858
|
+
server.connect(transport);
|
|
859
|
+
console.error("SERVER.TS: StdioServerTransport started.");
|
|
860
|
+
// --- End Start the MCP Server Transport ---
|
|
861
|
+
// --- Handle process signals for graceful shutdown ---
|
|
862
|
+
process.on('SIGINT', () => {
|
|
863
|
+
console.error("SERVER.TS: Received SIGINT. Shutting down.");
|
|
864
|
+
shutdownCodesysPersistent();
|
|
865
|
+
process.exit(0);
|
|
866
|
+
});
|
|
867
|
+
process.on('SIGTERM', () => {
|
|
868
|
+
console.error("SERVER.TS: Received SIGTERM. Shutting down.");
|
|
869
|
+
shutdownCodesysPersistent();
|
|
870
|
+
process.exit(0);
|
|
871
|
+
});
|
|
872
|
+
// --- End Signal Handling ---
|
|
873
|
+
console.error(">>> SERVER.TS startMcpServer() FINISHED <<<");
|
|
679
874
|
});
|
|
680
|
-
}
|
|
681
|
-
// ---
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
process.exit(0);
|
|
686
|
-
});
|
|
687
|
-
process.on('SIGTERM', () => {
|
|
688
|
-
console.error('\nSERVER.TS: SIGTERM received, shutting down...');
|
|
689
|
-
process.exit(0);
|
|
690
|
-
});
|
|
691
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
692
|
-
console.error('SERVER.TS: Unhandled Rejection at:', promise, 'reason:', reason);
|
|
693
|
-
});
|
|
694
|
-
// --- End Graceful Shutdown / Unhandled Rejection ---
|
|
695
|
-
console.error(">>> SERVER.TS Module Parsed <<<"); // Log end of script parsing
|
|
875
|
+
}
|
|
876
|
+
// --- Define the socket listener host and port ---
|
|
877
|
+
const HOST = '127.0.0.1';
|
|
878
|
+
const PORT = 65432;
|
|
879
|
+
// --- End Define host and port ---
|