0
0
Fork 0

fix(device-tools): surface JSON output in tool responses

scan_network_devices, verify_device_reachable, and
rebind_device_to_scan_result all emit JSON between marker pairs in their
script stdout. The tool handlers wrapped that output with
formatToolResponse which returns only the success line on success --
hiding the actual data so the agent had nothing to act on.

Adds extractMarkerJson() helper that pulls the block between the start
and end markers and pretty-prints it, then returns the JSON in the tool
response. Now the agent sees the full scan results / reachability
candidates / rebind outcome.
This commit is contained in:
Karstein Phobic Nyvold Kvistad 2026-05-11 17:45:30 +02:00
parent 1feb76d009
commit 4200b15b40
3 changed files with 38 additions and 6 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "codesys-mcp-sp21-plus",
"version": "0.9.5",
"version": "0.9.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codesys-mcp-sp21-plus",
"version": "0.9.5",
"version": "0.9.6",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0",

View file

@ -1,6 +1,6 @@
{
"name": "codesys-mcp-sp21-plus",
"version": "0.9.5",
"version": "0.9.6",
"description": "Codesys-MCP-SP21+ -- fork of luke-harriman/Codesys-MCP carrying CODESYS V3.5 SP22 Patch 1 fixes (and forward-compat with later SPs): script-engine API drift, online/runtime tool auto-login, dual-SHA release classifier, set_pou_code omitted-decl wipe fix, add_library managed-overload, etc. MCP server for CODESYS with persistent UI instance and file-based IPC.",
"main": "dist/server.js",
"bin": {

View file

@ -1845,6 +1845,23 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
}
);
// Extract a JSON block between marker lines and pretty-print it; if no
// markers found, return the raw output. Used by the device tools so the
// agent actually sees the scan results / reachability candidates.
const extractMarkerJson = (output: string, startMarker: string, endMarker: string): string => {
const startIdx = output.indexOf(startMarker);
const endIdx = output.indexOf(endMarker);
if (startIdx === -1 || endIdx === -1 || startIdx >= endIdx) {
return output.trim();
}
const raw = output.substring(startIdx + startMarker.length, endIdx).trim();
try {
return JSON.stringify(JSON.parse(raw), null, 2);
} catch {
return raw;
}
};
s.tool(
'scan_network_devices',
"Drive the gateway's Scan Network on the project's configured device. Returns the list of physical CODESYS targets currently visible to the gateway (device_name, type_name, vendor_name, address, device_id). Useful when the cached device address is stale and you need to find where the PLC actually is now. Set useCache=true to return the gateway's last scan result without re-scanning (cheap polling).",
@ -1860,7 +1877,12 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
['ensure_project_open', 'find_target_device']
);
const result = await executor.executeScript(script, 60_000);
return formatToolResponse(result, `Network scan completed for ${args.projectFilePath}.`);
const success = result.success && result.output.includes('SCRIPT_SUCCESS');
if (!success) {
return formatToolResponse(result, '');
}
const json = extractMarkerJson(result.output, '### NETWORK_SCAN_START ###', '### NETWORK_SCAN_END ###');
return { content: [{ type: 'text' as const, text: `Network scan for ${args.projectFilePath}:\n${json}` }], isError: false };
}
);
@ -1878,7 +1900,12 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
['ensure_project_open', 'find_target_device']
);
const result = await executor.executeScript(script, 60_000);
return formatToolResponse(result, `Device reachability checked for ${args.projectFilePath}.`);
const success = result.success && result.output.includes('SCRIPT_SUCCESS');
if (!success) {
return formatToolResponse(result, '');
}
const json = extractMarkerJson(result.output, '### DEVICE_REACHABILITY_START ###', '### DEVICE_REACHABILITY_END ###');
return { content: [{ type: 'text' as const, text: `Reachability for ${args.projectFilePath}:\n${json}` }], isError: false };
}
);
@ -1904,7 +1931,12 @@ export async function startMcpServer(config: ServerConfig): Promise<void> {
['ensure_project_open', 'find_target_device']
);
const result = await executor.executeScript(script, 60_000);
return formatToolResponse(result, `Device rebind attempted for ${args.projectFilePath}.`);
const success = result.success && result.output.includes('SCRIPT_SUCCESS');
if (!success) {
return formatToolResponse(result, '');
}
const json = extractMarkerJson(result.output, '### REBIND_RESULT_START ###', '### REBIND_RESULT_END ###');
return { content: [{ type: 'text' as const, text: `Rebind for ${args.projectFilePath}:\n${json}` }], isError: false };
}
);