@codesys/mcp-toolkit 1.1.5 → 1.1.6

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,23 +63,10 @@ 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
- // import { executeCodesysScript } from "./codesys_interop"; // We will replace this
66
+ const codesys_interop_1 = require("./codesys_interop");
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 ---
83
70
  // --- Wrap server logic in an exported function ---
84
71
  function startMcpServer(config) {
85
72
  return __awaiter(this, void 0, void 0, function* () {
@@ -89,11 +76,9 @@ function startMcpServer(config) {
89
76
  const WORKSPACE_DIR = config.workspaceDir;
90
77
  const codesysExePath = config.codesysPath;
91
78
  const codesysProfileName = config.profileName;
92
- const codesysScriptsDir = config.codesysScriptsDir; // Get the scripts directory
93
79
  console.error(`SERVER.TS: Using Workspace Directory: ${WORKSPACE_DIR}`);
94
80
  console.error(`SERVER.TS: Using CODESYS Path: ${codesysExePath}`);
95
81
  console.error(`SERVER.TS: Using CODESYS Profile: ${codesysProfileName}`);
96
- console.error(`SERVER.TS: Using CODESYS Scripts Dir: ${codesysScriptsDir}`);
97
82
  // --- Sanity check - confirm the path exists if possible ---
98
83
  console.error(`SERVER.TS: Checking existence of CODESYS executable: ${codesysExePath}`);
99
84
  try {
@@ -128,195 +113,6 @@ function startMcpServer(config) {
128
113
  });
129
114
  }
130
115
  // --- 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 ---
320
116
  // --- Preload script templates ---
321
117
  try {
322
118
  yield (0, scriptLoader_1.preloadAllScriptTemplates)();
@@ -329,7 +125,7 @@ function startMcpServer(config) {
329
125
  console.error("SERVER.TS: Initializing McpServer...");
330
126
  const server = new mcp_js_1.McpServer({
331
127
  name: "CODESYS Control MCP Server",
332
- version: "1.9.0", // Updated version for persistent instance
128
+ version: "1.8.0", // Updated version for refactored code
333
129
  capabilities: {
334
130
  resources: { listChanged: true },
335
131
  tools: { listChanged: true }
@@ -348,33 +144,23 @@ function startMcpServer(config) {
348
144
  var _a, _b, _c, _d, _e;
349
145
  console.error(`SERVER.TS Resource request: ${uri.href}`);
350
146
  try {
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
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
+ }
355
156
  });
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
- }
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 };
374
160
  }
375
161
  catch (error) {
376
162
  console.error(`Error resource ${uri.href}:`, error);
377
- return { contents: [{ uri: uri.href, text: `Failed to communicate with listener: ${error.message}`, contentType: "text/plain" }], isError: true };
163
+ return { contents: [{ uri: uri.href, text: `Failed status script: ${error.message}`, contentType: "text/plain" }], isError: true };
378
164
  }
379
165
  }));
380
166
  // Templates
@@ -398,23 +184,38 @@ function startMcpServer(config) {
398
184
  console.error(`Resource request: project structure for ${projectPath}`);
399
185
  try {
400
186
  const absoluteProjPath = path.normalize(path.isAbsolute(projectPath) ? projectPath : path.join(WORKSPACE_DIR, projectPath));
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 }
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, '\\\\')
406
193
  });
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 };
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
+ }
410
210
  }
411
211
  else {
412
- return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
212
+ isError = true;
413
213
  }
214
+ return { contents: [{ uri: uri.href, text: structureText, contentType: "text/plain" }], isError: isError };
414
215
  }
415
216
  catch (error) {
416
217
  console.error(`Error getting structure ${uri.href}:`, error);
417
- return { contents: [{ uri: uri.href, text: `Failed to communicate with listener for '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
218
+ return { contents: [{ uri: uri.href, text: `Failed structure script for '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
418
219
  }
419
220
  }));
420
221
  server.resource("pou-code", pouCodeTemplate, (uri, params) => __awaiter(this, void 0, void 0, function* () {
@@ -438,23 +239,51 @@ function startMcpServer(config) {
438
239
  try {
439
240
  const absoluteProjPath = path.normalize(path.isAbsolute(projectPath) ? projectPath : path.join(WORKSPACE_DIR, projectPath));
440
241
  const sanitizedPouPath = String(pouPath).replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
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 }
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
446
250
  });
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 };
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}`;
450
278
  }
451
279
  else {
452
- return { contents: [{ uri: uri.href, text: `Listener Error: ${response.message}`, contentType: "text/plain" }], isError: true };
280
+ isError = true;
453
281
  }
282
+ return { contents: [{ uri: uri.href, text: codeText, contentType: "text/plain" }], isError: isError };
454
283
  }
455
284
  catch (error) {
456
285
  console.error(`Error getting POU code ${uri.href}:`, error);
457
- return { contents: [{ uri: uri.href, text: `Failed to communicate with listener for '${pouPath}' in '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
286
+ return { contents: [{ uri: uri.href, text: `Failed POU code script for '${pouPath}' in '${projectPath}': ${error.message}`, contentType: "text/plain" }], isError: true };
458
287
  }
459
288
  }));
460
289
  // --- End Resources ---
@@ -466,31 +295,28 @@ function startMcpServer(config) {
466
295
  let absPath = path.normalize(path.isAbsolute(filePath) ? filePath : path.join(WORKSPACE_DIR, filePath));
467
296
  console.error(`Tool call: open_project: ${absPath}`);
468
297
  try {
469
- // Replace executeCodesysScript with sendCodesysCommand
470
- const response = yield sendCodesysCommand({
471
- command: "open_project", // Command matches tool name
472
- args: { file_path: absPath }
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, '\\\\')
473
305
  });
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
- }
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
+ };
489
315
  }
490
316
  catch (e) {
491
317
  console.error(`Error open_project ${absPath}: ${e}`);
492
318
  return {
493
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
319
+ content: [{ type: "text", text: `Error: ${e.message}` }],
494
320
  isError: true
495
321
  };
496
322
  }
@@ -501,9 +327,9 @@ function startMcpServer(config) {
501
327
  const { filePath } = args;
502
328
  let absPath = path.normalize(path.isAbsolute(filePath) ? filePath : path.join(WORKSPACE_DIR, filePath));
503
329
  console.error(`Tool call: create_project (copy template): ${absPath}`);
504
- // Template finding logic remains the same as it doesn't require CODESYS interaction
505
330
  let templatePath = "";
506
331
  try {
332
+ // Template finding logic
507
333
  const baseDir = path.dirname(path.dirname(codesysExePath));
508
334
  templatePath = path.normalize(path.join(baseDir, 'Templates', 'Standard.project'));
509
335
  if (!(yield fileExists(templatePath))) {
@@ -539,32 +365,28 @@ function startMcpServer(config) {
539
365
  };
540
366
  }
541
367
  try {
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 }
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, '\\\\')
547
373
  });
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
- }
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
+ };
563
385
  }
564
386
  catch (e) {
565
387
  console.error(`Error create_project ${absPath}: ${e}`);
566
388
  return {
567
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
389
+ content: [{ type: "text", text: `Error: ${e.message}` }],
568
390
  isError: true
569
391
  };
570
392
  }
@@ -576,32 +398,27 @@ function startMcpServer(config) {
576
398
  let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
577
399
  console.error(`Tool call: save_project: ${absPath}`);
578
400
  try {
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 }
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, '\\\\')
584
407
  });
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
- }
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
+ };
600
417
  }
601
418
  catch (e) {
602
419
  console.error(`Error save_project ${absPath}: ${e}`);
603
420
  return {
604
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
421
+ content: [{ type: "text", text: `Error: ${e.message}` }],
605
422
  isError: true
606
423
  };
607
424
  }
@@ -619,261 +436,260 @@ function startMcpServer(config) {
619
436
  const sanName = name.trim();
620
437
  console.error(`Tool call: create_pou: Name='${sanName}', Type='${type}', Lang='${language}', Parent='${sanParentPath}', Project='${absPath}'`);
621
438
  try {
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
- }
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
633
450
  });
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
- }
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
+ };
649
462
  }
650
463
  catch (e) {
651
464
  console.error(`Error create_pou ${sanName} in ${absPath}: ${e}`);
652
465
  return {
653
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
466
+ content: [{ type: "text", text: `Error: ${e.message}` }],
654
467
  isError: true
655
468
  };
656
469
  }
657
470
  }));
658
471
  server.tool("set_pou_code", "Sets the declaration and/or implementation code for a specific POU, Method, or Property.", {
659
472
  projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
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.")
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()
663
476
  }, (args) => __awaiter(this, void 0, void 0, function* () {
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) {
477
+ const { projectFilePath, pouPath, declarationCode, implementationCode } = args;
478
+ if (declarationCode === undefined && implementationCode === undefined) {
669
479
  return {
670
- content: [{ type: "text", text: "Error: Either 'declaration' or 'implementation' must be provided." }],
480
+ content: [{ type: "text", text: "Error: At least one of declarationCode or implementationCode must be provided." }],
671
481
  isError: true
672
482
  };
673
483
  }
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}'`);
674
487
  try {
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
- }
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
685
501
  });
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
- }
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
+ };
701
513
  }
702
514
  catch (e) {
703
515
  console.error(`Error set_pou_code ${sanPouPath} in ${absPath}: ${e}`);
704
516
  return {
705
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
517
+ content: [{ type: "text", text: `Error: ${e.message}` }],
706
518
  isError: true
707
519
  };
708
520
  }
709
521
  }));
710
- server.tool("create_method", "Creates a new Method within a Function Block.", {
522
+ server.tool("create_property", "Creates a new Property within a specific Function Block POU.", {
711
523
  projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
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."),
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').")
715
527
  }, (args) => __awaiter(this, void 0, void 0, function* () {
716
- const { projectFilePath, functionBlockPath, name, language } = args;
528
+ const { projectFilePath, parentPouPath, propertyName, propertyType } = args;
717
529
  let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
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}'`);
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
+ }
721
540
  try {
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
- }
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
731
551
  });
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
- }
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
+ };
747
563
  }
748
564
  catch (e) {
749
- console.error(`Error create_method ${sanName} in ${absPath}: ${e}`);
565
+ console.error(`Error create_property ${sanPropName} in ${absPath}: ${e}`);
750
566
  return {
751
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
567
+ content: [{ type: "text", text: `Error: ${e.message}` }],
752
568
  isError: true
753
569
  };
754
570
  }
755
571
  }));
756
- server.tool("create_property", "Creates a new Property within a Function Block.", {
572
+ server.tool("create_method", "Creates a new Method within a specific Function Block POU.", {
757
573
  projectFilePath: zod_1.z.string().describe("Path to the project file (e.g., 'C:/Projects/MyPLC.project')."),
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.")
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).")
766
577
  }, (args) => __awaiter(this, void 0, void 0, function* () {
767
- const { projectFilePath, functionBlockPath, name, dataType, isRetain, isPersistent, isConstant, initialValue, comment } = args;
578
+ const { projectFilePath, parentPouPath, methodName, returnType } = args;
768
579
  let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
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}'`);
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
+ }
772
590
  try {
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
- }
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
787
601
  });
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
- }
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
+ };
803
613
  }
804
614
  catch (e) {
805
- console.error(`Error create_property ${sanName} in ${absPath}: ${e}`);
615
+ console.error(`Error create_method ${sanMethName} in ${absPath}: ${e}`);
806
616
  return {
807
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
617
+ content: [{ type: "text", text: `Error: ${e.message}` }],
808
618
  isError: true
809
619
  };
810
620
  }
811
621
  }));
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').")
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').")
814
624
  }, (args) => __awaiter(this, void 0, void 0, function* () {
815
625
  const { projectFilePath } = args;
816
626
  let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
817
- console.error(`Tool call: compile_project: Project='${absPath}'`);
627
+ console.error(`Tool call: compile_project: ${absPath}`);
818
628
  try {
819
- // Replace executeCodesysScript with sendCodesysCommand
820
- const response = yield sendCodesysCommand({
821
- command: "compile_project", // Command matches tool name
822
- args: { project_file_path: absPath }
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, '\\\\')
823
635
  });
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
- };
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;
838
647
  }
648
+ return {
649
+ content: [{ type: "text", text: message }],
650
+ isError: isError
651
+ };
839
652
  }
840
653
  catch (e) {
841
654
  console.error(`Error compile_project ${absPath}: ${e}`);
842
655
  return {
843
- content: [{ type: "text", text: `Failed to communicate with listener: ${e.message}` }],
656
+ content: [{ type: "text", text: `Error: ${e.message}` }],
844
657
  isError: true
845
658
  };
846
659
  }
847
660
  }));
848
661
  // --- End Tools ---
849
662
  console.error("SERVER.TS: Resources and Tools defined.");
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 <<<");
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 ---
874
679
  });
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 ---
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