@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/.claude/settings.local.json +2 -5
- package/CHANGELOG.md +3 -5
- package/dist/bin.js +2 -41
- package/dist/server.js +319 -503
- package/package.json +3 -3
- package/codesys_scripts/listener_script.py +0 -1539
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
|
-
|
|
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.
|
|
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
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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
|
-
//
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
}
|
|
480
|
-
|
|
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: `
|
|
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
|
-
//
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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: `
|
|
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
|
-
//
|
|
580
|
-
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
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: `
|
|
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
|
-
//
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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: `
|
|
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("
|
|
661
|
-
|
|
662
|
-
|
|
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,
|
|
665
|
-
|
|
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:
|
|
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
|
-
//
|
|
676
|
-
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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: `
|
|
517
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
706
518
|
isError: true
|
|
707
519
|
};
|
|
708
520
|
}
|
|
709
521
|
}));
|
|
710
|
-
server.tool("
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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,
|
|
528
|
+
const { projectFilePath, parentPouPath, propertyName, propertyType } = args;
|
|
717
529
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
718
|
-
const
|
|
719
|
-
const
|
|
720
|
-
|
|
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
|
-
//
|
|
723
|
-
const
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
|
565
|
+
console.error(`Error create_property ${sanPropName} in ${absPath}: ${e}`);
|
|
750
566
|
return {
|
|
751
|
-
content: [{ type: "text", text: `
|
|
567
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
752
568
|
isError: true
|
|
753
569
|
};
|
|
754
570
|
}
|
|
755
571
|
}));
|
|
756
|
-
server.tool("
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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,
|
|
578
|
+
const { projectFilePath, parentPouPath, methodName, returnType } = args;
|
|
768
579
|
let absPath = path.normalize(path.isAbsolute(projectFilePath) ? projectFilePath : path.join(WORKSPACE_DIR, projectFilePath));
|
|
769
|
-
const
|
|
770
|
-
const
|
|
771
|
-
|
|
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
|
-
//
|
|
774
|
-
const
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
615
|
+
console.error(`Error create_method ${sanMethName} in ${absPath}: ${e}`);
|
|
806
616
|
return {
|
|
807
|
-
content: [{ type: "text", text: `
|
|
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
|
|
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:
|
|
627
|
+
console.error(`Tool call: compile_project: ${absPath}`);
|
|
818
628
|
try {
|
|
819
|
-
//
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
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: `
|
|
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
|
-
// ---
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
// ---
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|