@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/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
- const codesys_interop_1 = require("./codesys_interop");
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.8.0", // Updated version for refactored code
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
- const scriptContent = yield (0, scriptLoader_1.loadScriptTemplate)('check_status');
148
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
149
- const outputLines = result.output.split(/[\r\n]+/).filter(line => line.trim());
150
- const statusData = {};
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
- 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'}`;
158
- const isError = !result.success || ((_e = statusData['Scripting OK']) === null || _e === void 0 ? void 0 : _e.toLowerCase()) !== 'true';
159
- return { contents: [{ uri: uri.href, text: statusText, contentType: "text/plain" }], isError: isError };
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 status script: ${error.message}`, contentType: "text/plain" }], isError: true };
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
- // Load script template and prepare parameters
188
- const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('get_project_structure');
189
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
190
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + scriptTemplate;
191
- const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
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
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
195
- let structureText = `Error retrieving structure for ${absoluteProjPath}.\n\n${result.output}`;
196
- let isError = !result.success;
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 = true;
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 structure script for '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
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
- // Load script template and prepare parameters
243
- const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('get_pou_code');
244
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
245
- const findObjectByPathTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('find_object_by_path');
246
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + findObjectByPathTemplate + '\n\n' + scriptTemplate;
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
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
252
- let codeText = `Error retrieving code for object '${sanitizedPouPath}' in project '${absoluteProjPath}'.\n\n${result.output}`;
253
- let isError = !result.success;
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 = true;
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 POU code script for '${pouPath}' in '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
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
- // Load the script template and prepare with parameters
299
- const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('open_project');
300
- // Combine with required ensure_project_open script (will handle any imports)
301
- const ensureProjectOpenScript = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
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
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
307
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
308
- return {
309
- content: [{
310
- type: "text",
311
- text: success ? `Project opened: ${absPath}` : `Failed open project ${absPath}. Output:\n${result.output}`
312
- }],
313
- isError: !success
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: `Error: ${e.message}` }],
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
- // Load script template and interpolate parameters
369
- const scriptTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('create_project');
370
- const scriptContent = (0, scriptLoader_1.interpolateScript)(scriptTemplate, {
371
- 'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\'),
372
- 'TEMPLATE_PROJECT_PATH': templatePath.replace(/\\/g, '\\\\')
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
- console.error(">>> create_project (copy-then-open): PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
375
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
376
- console.error(">>> create_project (copy-then-open): EXECUTION RESULT:", JSON.stringify(result));
377
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
378
- return {
379
- content: [{
380
- type: "text",
381
- text: success ? `Project created from template: ${absPath}` : `Failed create project ${absPath} from template. Output:\n${result.output}`
382
- }],
383
- isError: !success
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: `Error: ${e.message}` }],
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
- // Load and combine script templates
402
- const saveProjectTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('save_project');
403
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
404
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + saveProjectTemplate;
405
- const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
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
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
409
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
410
- return {
411
- content: [{
412
- type: "text",
413
- text: success ? `Project saved: ${absPath}` : `Failed save project ${absPath}. Output:\n${result.output}`
414
- }],
415
- isError: !success
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: `Error: ${e.message}` }],
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
- // Load and combine script templates
440
- const createPouTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('create_pou');
441
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
442
- const findObjectByPathTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('find_object_by_path');
443
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + findObjectByPathTemplate + '\n\n' + createPouTemplate;
444
- const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
445
- 'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\'),
446
- 'POU_NAME': sanName,
447
- 'POU_TYPE_STR': type,
448
- 'IMPL_LANGUAGE_STR': language,
449
- 'PARENT_PATH': sanParentPath
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
- console.error(">>> create_pou: PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
452
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
453
- console.error(">>> create_pou: EXECUTION RESULT:", JSON.stringify(result));
454
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
455
- return {
456
- content: [{
457
- type: "text",
458
- text: success ? `POU '${sanName}' created in '${sanParentPath}' of ${absPath}. Project saved.` : `Failed create POU '${sanName}'. Output:\n${result.output}`
459
- }],
460
- isError: !success
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: `Error: ${e.message}` }],
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("Full relative path to the target object (e.g., 'Application/MyPOU', 'MyFolder/MyFB/MyMethod', 'MyFolder/MyFB/MyProperty')."),
474
- declarationCode: zod_1.z.string().describe("Code for the declaration part (VAR...END_VAR). If omitted, the declaration is not changed.").optional(),
475
- implementationCode: zod_1.z.string().describe("Code for the implementation logic part. If omitted, the implementation is not changed.").optional()
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, declarationCode, implementationCode } = args;
478
- if (declarationCode === undefined && implementationCode === undefined) {
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: At least one of declarationCode or implementationCode must be provided." }],
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
- // Load and combine script templates
489
- const setPouCodeTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('set_pou_code');
490
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
491
- const findObjectByPathTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('find_object_by_path');
492
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + findObjectByPathTemplate + '\n\n' + setPouCodeTemplate;
493
- // Escape content for Python triple-quoted strings
494
- const sanDeclCode = (declarationCode !== null && declarationCode !== void 0 ? declarationCode : "").replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
495
- const sanImplCode = (implementationCode !== null && implementationCode !== void 0 ? implementationCode : "").replace(/\\/g, '\\\\').replace(/"""/g, '\\"\\"\\"');
496
- const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
497
- 'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\'),
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
- console.error(">>> set_pou_code: PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
503
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
504
- console.error(">>> set_pou_code: EXECUTION RESULT:", JSON.stringify(result));
505
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
506
- return {
507
- content: [{
508
- type: "text",
509
- text: success ? `Code set for '${sanPouPath}' in ${absPath}. Project saved.` : `Failed set code for '${sanPouPath}'. Output:\n${result.output}`
510
- }],
511
- isError: !success
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: `Error: ${e.message}` }],
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("create_property", "Creates a new Property within a specific Function Block POU.", {
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
- parentPouPath: zod_1.z.string().describe("Relative path to the parent Function Block POU (e.g., 'Application/MyFB')."),
525
- propertyName: zod_1.z.string().describe("Name for the new property (must be a valid IEC identifier)."),
526
- propertyType: zod_1.z.string().describe("Data type of the property (e.g., 'BOOL', 'INT', 'MyDUT').")
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, parentPouPath, propertyName, propertyType } = args;
716
+ const { projectFilePath, functionBlockPath, name, language } = args;
529
717
  let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
530
- const sanParentPath = parentPouPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
531
- const sanPropName = propertyName.trim();
532
- const sanPropType = propertyType.trim();
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
- // Load and combine script templates
542
- const createPropertyTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('create_property');
543
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
544
- const findObjectByPathTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('find_object_by_path');
545
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + findObjectByPathTemplate + '\n\n' + createPropertyTemplate;
546
- const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
547
- 'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\'),
548
- 'PARENT_POU_FULL_PATH': sanParentPath,
549
- 'PROPERTY_NAME': sanPropName,
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
- console.error(">>> create_property: PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
553
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
554
- console.error(">>> create_property: EXECUTION RESULT:", JSON.stringify(result));
555
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
556
- return {
557
- content: [{
558
- type: "text",
559
- text: success ? `Property '${sanPropName}' created under '${sanParentPath}' in ${absPath}. Project saved.` : `Failed to create property '${sanPropName}'. Output:\n${result.output}`
560
- }],
561
- isError: !success
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 create_property ${sanPropName} in ${absPath}: ${e}`);
749
+ console.error(`Error create_method ${sanName} in ${absPath}: ${e}`);
566
750
  return {
567
- content: [{ type: "text", text: `Error: ${e.message}` }],
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("create_method", "Creates a new Method within a specific Function Block POU.", {
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
- parentPouPath: zod_1.z.string().describe("Relative path to the parent Function Block POU (e.g., 'Application/MyFB')."),
575
- methodName: zod_1.z.string().describe("Name of the new method (must be a valid IEC identifier)."),
576
- returnType: zod_1.z.string().optional().describe("Return type (e.g., 'BOOL', 'INT'). Leave empty or omit for no return value (PROCEDURE).")
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, parentPouPath, methodName, returnType } = args;
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 sanParentPath = parentPouPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
581
- const sanMethName = methodName.trim();
582
- const sanReturnType = (returnType !== null && returnType !== void 0 ? returnType : "").trim();
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
- // Load and combine script templates
592
- const createMethodTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('create_method');
593
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
594
- const findObjectByPathTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('find_object_by_path');
595
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + findObjectByPathTemplate + '\n\n' + createMethodTemplate;
596
- const scriptContent = (0, scriptLoader_1.interpolateScript)(combinedScript, {
597
- 'PROJECT_FILE_PATH': absPath.replace(/\\/g, '\\\\'),
598
- 'PARENT_POU_FULL_PATH': sanParentPath,
599
- 'METHOD_NAME': sanMethName,
600
- 'RETURN_TYPE': sanReturnType
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
- console.error(">>> create_method: PREPARED SCRIPT:", scriptContent.substring(0, 500) + "...");
603
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
604
- console.error(">>> create_method: EXECUTION RESULT:", JSON.stringify(result));
605
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
606
- return {
607
- content: [{
608
- type: "text",
609
- text: success ? `Method '${sanMethName}' created under '${sanParentPath}' in ${absPath}. Project saved.` : `Failed to create method '${sanMethName}'. Output:\n${result.output}`
610
- }],
611
- isError: !success
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 create_method ${sanMethName} in ${absPath}: ${e}`);
805
+ console.error(`Error create_property ${sanName} in ${absPath}: ${e}`);
616
806
  return {
617
- content: [{ type: "text", text: `Error: ${e.message}` }],
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 (Builds) the primary application within a CODESYS project.", {
623
- projectFilePath: zod_1.z.string().describe("Path to the project file containing the application to compile (e.g., 'C:/Projects/MyPLC.project').")
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
- // Load and combine script templates
630
- const compileProjectTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('compile_project');
631
- const ensureProjectOpenTemplate = yield (0, scriptLoader_1.loadScriptTemplate)('ensure_project_open');
632
- const combinedScript = ensureProjectOpenTemplate + '\n\n' + compileProjectTemplate;
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
- const result = yield (0, codesys_interop_1.executeCodesysScript)(scriptContent, codesysExePath, codesysProfileName);
637
- const success = result.success && result.output.includes("SCRIPT_SUCCESS");
638
- // Check for actual compile errors in the output log
639
- const hasCompileErrors = result.output.includes("Compile complete --") && !/ 0 error\(s\),/.test(result.output);
640
- let message = success ? `Compilation initiated for application in ${absPath}. Check CODESYS messages for results.` : `Failed initiating compilation for ${absPath}. Output:\n${result.output}`;
641
- let isError = !success; // Base error status on script success
642
- if (success && hasCompileErrors) {
643
- message += " WARNING: Build command reported errors in the output log.";
644
- console.warn("Compile project reported build errors in the output.");
645
- // Report as error if compile fails, even if script technically succeeded
646
- isError = true;
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: `Error: ${e.message}` }],
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
- // --- End MCP Resources / Tools Definitions ---
664
- // --- Server Connection ---
665
- console.error("SERVER.TS: startServer() internal logic executing.");
666
- try {
667
- const transport = new stdio_js_1.StdioServerTransport();
668
- console.error("SERVER.TS: Connecting MCP server via stdio...");
669
- // No need to await connect here if startMcpServer is called by bin.ts which awaits it
670
- server.connect(transport); // Connect but don't await here, let bin.ts handle waiting
671
- console.error("SERVER.TS: MCP Server connection initiated via stdio.");
672
- }
673
- catch (error) {
674
- console.error("FATAL: Failed to initiate MCP server connection:", error);
675
- // Re-throw error so bin.ts can catch it
676
- throw error;
677
- }
678
- // --- End Server Connection ---
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
- } // --- End of startMcpServer function ---
681
- // --- Graceful Shutdown / Unhandled Rejection ---
682
- // These should remain at the top level, outside startMcpServer
683
- process.on('SIGINT', () => {
684
- console.error('\nSERVER.TS: SIGINT received, shutting down...');
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 ---