From 7b0c5db125e9d44e58ba0fe7b9171236e166f31f Mon Sep 17 00:00:00 2001 From: Karstein Phobic Nyvold Kvistad Date: Tue, 12 May 2026 18:57:07 +0200 Subject: [PATCH] feat(launch): launch_codesys_with_project tool Detached spawn of an arbitrary CODESYS.exe with a .project as CLI arg and optional --Profile= override. Useful when you want an SP22-saved project opened in an SP21 IDE for SIM/inspection work, or when this MCP is bound to install A but you want install B to handle the open without registering a second server. The launched IDE is not managed by this MCP: no IPC, no watcher, no shutdown_codesys. Validates the exe + project paths up front and returns the new PID. --- package-lock.json | 4 ++-- package.json | 2 +- src/server.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b59f6bd..a1015dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codesys-mcp-sp21-plus", - "version": "0.9.11", + "version": "0.9.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codesys-mcp-sp21-plus", - "version": "0.9.11", + "version": "0.9.12", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", diff --git a/package.json b/package.json index 72677fa..00f6305 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codesys-mcp-sp21-plus", - "version": "0.9.11", + "version": "0.9.12", "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": { diff --git a/src/server.ts b/src/server.ts index 882711c..d87892e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -863,6 +863,48 @@ export async function startMcpServer(config: ServerConfig): Promise { } ); + s.tool( + 'launch_codesys_with_project', + "Launch a (potentially different) CODESYS install and open a project in it -- detached from this MCP. Useful for cross-version inspection (e.g. opening an SP22-saved project in an SP21 IDE for a SIM workflow), or for opening a project in an install this MCP isn't bound to. The launched IDE is NOT managed by this server: no IPC, no watcher, no shutdown_codesys. This is intentional -- the goal is just to hand the user a running IDE on a chosen install.", + { + projectFilePath: z.string().describe("Path to the .project file to open. Forward or back slashes both work."), + codesysPath: z.string().optional().describe("Optional override for the CODESYS.exe to launch. Defaults to this server's configured --codesys-path. Use to launch a DIFFERENT install (e.g. SP21 while this server runs SP22)."), + profileName: z.string().optional().describe("Optional --Profile= value for the launched IDE (e.g. 'CODESYS V3.5 SP21 Patch 5'). Omit to let CODESYS pick the project's saved profile or its install default. Mismatched profiles will pop the IDE's profile-conversion dialog -- which is sometimes exactly what you want."), + }, + async (args: { projectFilePath: string; codesysPath?: string; profileName?: string }) => { + const targetExe = (args.codesysPath ?? config.codesysPath).trim(); + const projectPath = resolvePath(args.projectFilePath, workspaceDir); + if (!fs.existsSync(targetExe)) { + return { content: [{ type: 'text' as const, text: `CODESYS executable not found: ${targetExe}` }], isError: true }; + } + // Strip the surrounding single-quotes that resolvePath() adds for use as + // a Python string literal. The shell spawn below quotes argv itself. + const cleanProjectPath = projectPath.replace(/^'|'$/g, ''); + if (!fs.existsSync(cleanProjectPath)) { + return { content: [{ type: 'text' as const, text: `Project file not found: ${cleanProjectPath}` }], isError: true }; + } + try { + const { spawn } = require('child_process') as typeof import('child_process'); + const argv: string[] = []; + if (args.profileName) argv.push(`--Profile=${args.profileName}`); + argv.push(cleanProjectPath); + const child = spawn(targetExe, argv, { detached: true, stdio: 'ignore' }); + child.unref(); + const profileHint = args.profileName ? ` --Profile="${args.profileName}"` : ''; + return { + content: [{ + type: 'text' as const, + text: `Launched ${targetExe}${profileHint} with project ${cleanProjectPath}. PID ${child.pid ?? 'unknown'}. The IDE is detached -- this MCP does not manage its lifecycle.`, + }], + isError: false, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { content: [{ type: 'text' as const, text: `Launch failed: ${msg}` }], isError: true }; + } + } + ); + s.tool( 'shutdown_codesys', 'Shut down the persistent CODESYS instance.',