sylo-codesys 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yeti-Trix <131923258+Yeti-Trix@users.noreply.github.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # sylo-codesys
2
+
3
+ Drive a **live, open CODESYS V3.5 project** from Sylo chat — create POUs, write
4
+ Structured Text, compile, read/write PLC variables — with changes appearing in
5
+ the CODESYS IDE in real time.
6
+
7
+ This package does **not** speak MCP and does **not** spawn CODESYS. It is the
8
+ skill + a setup/diagnostic tool. The actual CODESYS tools come from the external
9
+ [`codesys-mcp-sp21-plus`](https://github.com/phobicdotno/Codesys-MCP-SP21-plus)
10
+ stdio MCP server, surfaced into Pi by the **`pi-mcp-adapter`** package once you
11
+ add a `codesys` server to a workspace `.mcp.json`.
12
+
13
+ ## Requirements
14
+
15
+ - **CODESYS V3.5 SP19 / SP21 / SP22** installed (Windows).
16
+ - **Node.js 18+** (for the MCP server).
17
+ - The **`pi-mcp-adapter`** package enabled in Sylo → Capability manager.
18
+ - The global npm package `codesys-mcp-sp21-plus`:
19
+ ```bash
20
+ npm install -g codesys-mcp-sp21-plus
21
+ ```
22
+
23
+ ## Setup
24
+
25
+ 1. Enable **sylo-codesys** in Sylo → Capability manager → Sylo optional packages.
26
+ 2. Enable **pi-mcp-adapter** (separate package) if it isn't already.
27
+ 3. In Sylo chat, ask the agent to call **`codesys_setup_check`**. It detects your
28
+ CODESYS install, verifies the npm package, checks for a `codesys` server in
29
+ your `.mcp.json`, confirms the `mcp` adapter is live, and prints a
30
+ ready-to-paste, augmented `.mcp.json` snippet.
31
+ 4. Paste that snippet into your workspace `.mcp.json` (merge into the existing
32
+ `mcpServers` object). The recommended entry looks like:
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "codesys": {
38
+ "command": "codesys-mcp-sp21-plus",
39
+ "args": [
40
+ "--codesys-path", "C:\\Program Files\\CODESYS 3.5.22.30\\CODESYS\\Common\\CODESYS.exe",
41
+ "--codesys-profile", "CODESYS V3.5 SP22 Patch 3",
42
+ "--mode", "persistent",
43
+ "--no-auto-launch",
44
+ "--auto-mirror"
45
+ ],
46
+ "lifecycle": "lazy-keep-alive",
47
+ "requestTimeoutMs": 180000,
48
+ "directTools": ["get_codesys_status", "get_project_info", "get_all_pou_code", "create_pou", "set_pou_code", "create_method", "create_property", "create_dut", "create_gvl", "create_folder", "delete_object", "rename_object", "save_project", "compile_project", "get_compile_messages", "add_library", "list_project_libraries", "connect_to_device", "read_variable", "write_variable", "start_stop_application", "get_application_state", "mirror_export", "shutdown_codesys"],
49
+ "approveTools": ["delete_object", "write_variable", "force_variables", "unforce_variables", "download_to_device", "reset_application", "shutdown_codesys", "remove_library"]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+ Get the exact `--codesys-path` / `--codesys-profile` strings with
55
+ `codesys-mcp-sp21-plus --print-config` (the setup tool does this for you).
56
+
57
+ 5. Restart the broker so `pi-mcp-adapter` re-reads `.mcp.json`.
58
+ 6. In chat, ask the agent to drive CODESYS (e.g. "create a POU `FB_Motor` in
59
+ Application and compile"). The first tool call launches CODESYS (persistent,
60
+ UI visible); with `lazy-keep-alive` it stays resident across turns.
61
+
62
+ ## How it works
63
+
64
+ ```
65
+ Sylo agent (LLM)
66
+ │ calls codesys_create_pou, codesys_compile_project, … (directTools)
67
+ │ or mcp({ tool: "create_pou", args }) (proxy, other tools)
68
+
69
+ pi-mcp-adapter (Pi extension; spawns + owns the MCP server stdio process)
70
+
71
+ codesys-mcp-sp21-plus (Node, stdio MCP server)
72
+ │ file-based IPC: writes command .py → watcher polls → result
73
+
74
+ CODESYS.exe (persistent, --runscript=watcher.py, UI visible)
75
+
76
+ open .project (live; operator watches changes in the IDE)
77
+ ```
78
+
79
+ ## Notes
80
+
81
+ - Modifying tools (`set_pou_code`, `create_pou`, …) **auto-save** the project.
82
+ **Ctrl+Z in the IDE does not recover `set_pou_code` edits.** With
83
+ `--auto-mirror`, each change re-runs `mirror_export` to a `.st` mirror on disk
84
+ — commit that mirror to git for real undo.
85
+ - `connect_to_device` may pop a "Device User Login" modal in the IDE. Pre-set
86
+ `CODESYS_DEVICE_USER` / `CODESYS_DEVICE_PASSWORD` in the server `env` block to
87
+ suppress it.
88
+ - MCP-server compatibility & docs: see the
89
+ [codesys-mcp-sp21-plus README](https://github.com/phobicdotno/Codesys-MCP-SP21-plus#readme)
90
+ and [ARCHITECTURE.md](https://github.com/phobicdotno/Codesys-MCP-SP21-plus/blob/main/ARCHITECTURE.md).
@@ -0,0 +1,387 @@
1
+ /**
2
+ * sylo-codesys — setup + diagnostics for the CODESYS MCP bridge.
3
+ *
4
+ * IMPORTANT: this package does NOT speak MCP itself and does NOT spawn CODESYS.
5
+ * The actual CODESYS tools come from the `codesys-mcp-sp21-plus` stdio MCP
6
+ * server, surfaced into Pi through the **pi-mcp-adapter** package (enabled
7
+ * separately in the Capability manager) once the operator adds a `codesys`
8
+ * server entry to a workspace `.mcp.json`.
9
+ *
10
+ * The single tool registered here, `codesys_setup_check`, validates that wiring:
11
+ * - detects the installed CODESYS V3.5.x under C:\Program Files,
12
+ * - checks the global `codesys-mcp-sp21-plus` npm package (and runs
13
+ * `--print-config` to read the exact codesys-path / codesys-profile),
14
+ * - inspects the workspace `.mcp.json` (+ ~/.pi/agent/mcp.json) for a codesys
15
+ * server entry and reports its key fields,
16
+ * - confirms the `mcp` adapter tool (pi-mcp-adapter) is live, and
17
+ * - emits a ready-to-paste, *augmented* `.mcp.json` snippet: a curated
18
+ * ~24-tool directTools surface, destructive-op approval gating, and a
19
+ * lazy-keep-alive lifecycle that keeps the CODESYS instance resident across
20
+ * turns while launching it lazily on first use.
21
+ *
22
+ * It is read-only with respect to operator config — it never writes the
23
+ * `.mcp.json` (operator-owned config; the host does not silently enable MCP
24
+ * servers). The operator (or the agent via `write`) pastes the snippet.
25
+ */
26
+ import { execFile } from 'node:child_process'
27
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
28
+ import { homedir } from 'node:os'
29
+ import { join } from 'node:path'
30
+ import { promisify } from 'node:util'
31
+
32
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
33
+ import { Type } from 'typebox'
34
+
35
+ const execFileAsync = promisify(execFile)
36
+
37
+ type ToolContentBlock = { type: 'text'; text: string }
38
+
39
+ function toolText(text: string): { content: ToolContentBlock[]; details: unknown } {
40
+ return { content: [{ type: 'text', text }], details: {} }
41
+ }
42
+
43
+ /** Strip // line + block comments and trailing commas so a JSONC .mcp.json parses. */
44
+ function parseJsoncLoose(raw: string): unknown {
45
+ const attempt = (s: string): unknown => {
46
+ try {
47
+ return JSON.parse(s)
48
+ } catch {
49
+ return null
50
+ }
51
+ }
52
+ const direct = attempt(raw)
53
+ if (direct !== null) return direct
54
+ const stripped = raw
55
+ .replace(/\/\*[\s\S]*?\*\//g, '')
56
+ .split('\n')
57
+ .map((line) => {
58
+ const i = line.indexOf('//')
59
+ return i === -1 ? line : line.slice(0, i)
60
+ })
61
+ .join('\n')
62
+ .replace(/,(\s*[}\]])/g, '$1')
63
+ return attempt(stripped)
64
+ }
65
+
66
+ interface CodesysInstall {
67
+ path: string
68
+ folder: string
69
+ }
70
+
71
+ function detectCodesysInstalls(): CodesysInstall[] {
72
+ const out: CodesysInstall[] = []
73
+ const progFiles = 'C:\\Program Files'
74
+ try {
75
+ if (!existsSync(progFiles)) return out
76
+ for (const folder of readdirSync(progFiles)) {
77
+ if (!/^CODESYS 3\.5\./i.test(folder)) continue
78
+ const exe = join(progFiles, folder, 'CODESYS', 'Common', 'CODESYS.exe')
79
+ if (existsSync(exe)) out.push({ path: exe, folder })
80
+ }
81
+ } catch {
82
+ /* ignore FS errors — this is best-effort detection */
83
+ }
84
+ return out
85
+ }
86
+
87
+ interface ServerEntry {
88
+ command?: string
89
+ args?: string[]
90
+ lifecycle?: string
91
+ directTools?: unknown
92
+ approveTools?: unknown
93
+ [k: string]: unknown
94
+ }
95
+
96
+ function readMcpJson(path: string): Record<string, unknown> | null {
97
+ if (!existsSync(path)) return null
98
+ try {
99
+ const raw = readFileSync(path, 'utf8')
100
+ const v = parseJsoncLoose(raw)
101
+ return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null
102
+ } catch {
103
+ return null
104
+ }
105
+ }
106
+
107
+ function findCodesysServers(obj: Record<string, unknown>): Record<string, ServerEntry> | null {
108
+ const servers = obj.mcpServers
109
+ if (!servers || typeof servers !== 'object' || Array.isArray(servers)) return null
110
+ const map = servers as Record<string, ServerEntry>
111
+ const keys = Object.keys(map).filter((k) => /codesys/i.test(k))
112
+ if (keys.length === 0) return null
113
+ const out: Record<string, ServerEntry> = {}
114
+ for (const k of keys) out[k] = map[k]!
115
+ return out
116
+ }
117
+
118
+ function argAfter(args: string[] | undefined, flag: string): string | undefined {
119
+ if (!args) return undefined
120
+ const i = args.indexOf(flag)
121
+ return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined
122
+ }
123
+
124
+ /** Curated first-class tool surface (original MCP tool names). */
125
+ const CURATED_DIRECT_TOOLS = [
126
+ 'get_codesys_status',
127
+ 'get_project_info',
128
+ 'get_all_pou_code',
129
+ 'create_pou',
130
+ 'set_pou_code',
131
+ 'create_method',
132
+ 'create_property',
133
+ 'create_dut',
134
+ 'create_gvl',
135
+ 'create_folder',
136
+ 'delete_object',
137
+ 'rename_object',
138
+ 'save_project',
139
+ 'compile_project',
140
+ 'get_compile_messages',
141
+ 'add_library',
142
+ 'list_project_libraries',
143
+ 'connect_to_device',
144
+ 'read_variable',
145
+ 'write_variable',
146
+ 'start_stop_application',
147
+ 'get_application_state',
148
+ 'mirror_export',
149
+ 'shutdown_codesys',
150
+ ]
151
+
152
+ /** Tools that must be confirmed before running (destructive / online forces). */
153
+ const APPROVE_TOOLS = [
154
+ 'delete_object',
155
+ 'write_variable',
156
+ 'force_variables',
157
+ 'unforce_variables',
158
+ 'download_to_device',
159
+ 'reset_application',
160
+ 'shutdown_codesys',
161
+ 'remove_library',
162
+ ]
163
+
164
+ function buildAugmentedEntry(codesysPath: string, profile: string): Record<string, unknown> {
165
+ return {
166
+ command: 'codesys-mcp-sp21-plus',
167
+ args: [
168
+ '--codesys-path',
169
+ codesysPath,
170
+ '--codesys-profile',
171
+ profile,
172
+ '--mode',
173
+ 'persistent',
174
+ '--no-auto-launch',
175
+ '--auto-mirror',
176
+ ],
177
+ lifecycle: 'lazy-keep-alive',
178
+ requestTimeoutMs: 180000,
179
+ directTools: CURATED_DIRECT_TOOLS,
180
+ approveTools: APPROVE_TOOLS,
181
+ }
182
+ }
183
+
184
+ function extractPathProfileFromPrintConfig(raw: string): {
185
+ path?: string
186
+ profile?: string
187
+ } {
188
+ const v = parseJsoncLoose(raw) as { mcpServers?: Record<string, ServerEntry> } | null
189
+ if (!v?.mcpServers) return {}
190
+ const keys = Object.keys(v.mcpServers)
191
+ // Prefer an SP22 entry; fall back to the first.
192
+ const pick = keys.find((k) => /sp22/i.test(k)) ?? keys[0]
193
+ if (!pick) return {}
194
+ const entry = v.mcpServers[pick]!
195
+ return {
196
+ path: argAfter(entry.args, '--codesys-path'),
197
+ profile: argAfter(entry.args, '--codesys-profile'),
198
+ }
199
+ }
200
+
201
+ async function runPrintConfig(): Promise<{ ok: boolean; raw?: string; error?: string }> {
202
+ try {
203
+ const { stdout } = await execFileAsync('codesys-mcp-sp21-plus', ['--print-config'], {
204
+ windowsHide: true,
205
+ timeout: 15_000,
206
+ })
207
+ return { ok: true, raw: stdout }
208
+ } catch (err) {
209
+ const e = err as NodeJS.ErrnoException
210
+ if (e.code === 'ENOENT') return { ok: false, error: 'not-installed' }
211
+ return { ok: false, error: e.message ?? String(err) }
212
+ }
213
+ }
214
+
215
+ export default function piSyloCodesysExtension(pi: ExtensionAPI): void {
216
+ pi.registerTool({
217
+ name: 'codesys_setup_check',
218
+ label: 'CODESYS MCP setup check',
219
+ description:
220
+ 'Validate the CODESYS MCP bridge wiring for this Sylo: detect the CODESYS V3.5 install, ' +
221
+ 'check the global `codesys-mcp-sp21-plus` npm package, inspect the workspace `.mcp.json` for a ' +
222
+ 'codesys server entry, confirm the `mcp` adapter tool (pi-mcp-adapter) is live, and emit a ' +
223
+ 'ready-to-paste augmented `.mcp.json` snippet (curated directTools + destructive-op approval ' +
224
+ 'gating + lazy-keep-alive lifecycle). The CODESYS tools themselves come from the MCP server via ' +
225
+ 'pi-mcp-adapter (directTools + the `mcp` proxy), NOT from this tool. Run once after enabling the ' +
226
+ 'sylo-codesys package and whenever the wiring changes.',
227
+ parameters: Type.Object({
228
+ mcp_json_path: Type.Optional(
229
+ Type.String({
230
+ description:
231
+ 'Path to the `.mcp.json` to inspect (defaults to <cwd>/.mcp.json). ' +
232
+ 'Also checks ~/.pi/agent/mcp.json.',
233
+ }),
234
+ ),
235
+ print_config: Type.Optional(
236
+ Type.Boolean({
237
+ description:
238
+ 'Run `codesys-mcp-sp21-plus --print-config` to read the exact CODESYS path/profile ' +
239
+ '(default true; needs the package installed).',
240
+ }),
241
+ ),
242
+ }),
243
+ async execute(_toolCallId, params) {
244
+ const lines: string[] = []
245
+ const wantPrint = params.print_config !== false
246
+
247
+ // 1. CODESYS install detection
248
+ lines.push('=== CODESYS install ===')
249
+ const installs = detectCodesysInstalls()
250
+ if (installs.length === 0) {
251
+ lines.push(' No CODESYS 3.5.x install found under C:\\Program Files.')
252
+ } else {
253
+ for (const i of installs) lines.push(` ${i.folder} -> ${i.path}`)
254
+ }
255
+
256
+ // 2. codesys-mcp-sp21-plus package + --print-config
257
+ lines.push('')
258
+ lines.push('=== codesys-mcp-sp21-plus (npm) ===')
259
+ let detectedPath: string | undefined
260
+ let detectedProfile: string | undefined
261
+ let packageInstalled = false
262
+ if (wantPrint) {
263
+ const pc = await runPrintConfig()
264
+ if (pc.ok && pc.raw) {
265
+ packageInstalled = true
266
+ const ex = extractPathProfileFromPrintConfig(pc.raw)
267
+ detectedPath = ex.path
268
+ detectedProfile = ex.profile
269
+ lines.push(' Installed. `--print-config` OK.')
270
+ if (detectedPath) lines.push(` path: ${detectedPath}`)
271
+ if (detectedProfile) lines.push(` profile: ${detectedProfile}`)
272
+ } else if (pc.error === 'not-installed') {
273
+ lines.push(' NOT installed / not on PATH. Install with:')
274
+ lines.push(' npm install -g codesys-mcp-sp21-plus')
275
+ } else {
276
+ lines.push(` \`--print-config\` failed: ${pc.error}`)
277
+ }
278
+ } else {
279
+ lines.push(' (print_config=false — skipped)')
280
+ }
281
+ if (!detectedPath && installs.length > 0) {
282
+ detectedPath = installs[0]!.path
283
+ lines.push(` (using filesystem-scanned path for the snippet: ${detectedPath})`)
284
+ }
285
+ if (!detectedProfile) {
286
+ lines.push(
287
+ ' profile: UNKNOWN — run `codesys-mcp-sp21-plus --print-config` to get the exact string.',
288
+ )
289
+ }
290
+
291
+ // 3. .mcp.json inspection
292
+ lines.push('')
293
+ lines.push('=== .mcp.json ===')
294
+ const cwd = process.cwd()
295
+ const candidates = [
296
+ {
297
+ label: 'workspace',
298
+ path: (params.mcp_json_path ?? '').trim() || join(cwd, '.mcp.json'),
299
+ },
300
+ { label: 'user-global', path: join(homedir(), '.pi', 'agent', 'mcp.json') },
301
+ ]
302
+ let foundCodesysEntry = false
303
+ for (const c of candidates) {
304
+ const obj = readMcpJson(c.path)
305
+ if (!obj) {
306
+ lines.push(` ${c.label}: (none) ${c.path}`)
307
+ continue
308
+ }
309
+ lines.push(` ${c.label}: ${c.path}`)
310
+ const codesys = findCodesysServers(obj)
311
+ if (codesys) {
312
+ foundCodesysEntry = true
313
+ for (const [k, e] of Object.entries(codesys)) {
314
+ const cp = argAfter(e.args, '--codesys-path')
315
+ const cf = argAfter(e.args, '--codesys-profile')
316
+ const mode = argAfter(e.args, '--mode')
317
+ const lifecycle = typeof e.lifecycle === 'string' ? e.lifecycle : '(unset)'
318
+ const dt = Array.isArray(e.directTools) ? e.directTools.length : 0
319
+ lines.push(` server "${k}":`)
320
+ lines.push(` command: ${e.command ?? '?'}`)
321
+ if (cp) lines.push(` codesys-path: ${cp}`)
322
+ if (cf) lines.push(` codesys-profile: ${cf}`)
323
+ if (mode) lines.push(` mode: ${mode}`)
324
+ lines.push(` lifecycle: ${lifecycle}, directTools: ${dt}`)
325
+ }
326
+ } else {
327
+ lines.push(' (no codesys* server entry — add one using the snippet below)')
328
+ }
329
+ }
330
+
331
+ // 4. pi-mcp-adapter live?
332
+ lines.push('')
333
+ lines.push('=== pi-mcp-adapter ===')
334
+ let mcpTool = false
335
+ try {
336
+ mcpTool = pi.getAllTools().some((t) => t.name === 'mcp')
337
+ } catch {
338
+ /* getAllTools not available yet — treat as unknown */
339
+ }
340
+ if (mcpTool) {
341
+ lines.push(' `mcp` tool is registered — pi-mcp-adapter is enabled.')
342
+ } else {
343
+ lines.push(
344
+ ' `mcp` tool NOT registered — enable the pi-mcp-adapter package in the Capability manager, then restart the broker.',
345
+ )
346
+ }
347
+ if (mcpTool && !foundCodesysEntry) {
348
+ lines.push(
349
+ ' (adapter is live but no codesys server is configured — paste the snippet below into a .mcp.json.)',
350
+ )
351
+ }
352
+
353
+ // 5. Ready-to-paste snippet
354
+ if (detectedPath && detectedProfile) {
355
+ lines.push('')
356
+ lines.push('=== Augmented .mcp.json snippet (merge into your .mcp.json) ===')
357
+ const entry = buildAugmentedEntry(detectedPath, detectedProfile)
358
+ lines.push(JSON.stringify({ mcpServers: { codesys: entry } }, null, 2))
359
+ lines.push('')
360
+ lines.push('Notes:')
361
+ lines.push(
362
+ ' - directTools promote a ~24-tool curated surface as first-class Pi tools named codesys_<tool> ' +
363
+ '(codesys_create_pou, codesys_set_pou_code, codesys_compile_project, ...).',
364
+ )
365
+ lines.push(
366
+ ' - approveTools require confirmation before destructive/online ops (delete_object, write_variable, force_variables, download_to_device, ...).',
367
+ )
368
+ lines.push(
369
+ ' - lazy-keep-alive: CODESYS spawns on the first tool call and stays resident across turns; ' +
370
+ '--no-auto-launch keeps it from starting at broker boot.',
371
+ )
372
+ lines.push(
373
+ ' - --auto-mirror re-runs mirror_export after each change so the .st mirror stays in sync for undo ' +
374
+ '(Ctrl+Z in the IDE does NOT recover set_pou_code edits).',
375
+ )
376
+ lines.push(' - After editing .mcp.json, restart the broker so pi-mcp-adapter re-reads it.')
377
+ } else if (!packageInstalled) {
378
+ lines.push('')
379
+ lines.push(
380
+ 'Next: `npm install -g codesys-mcp-sp21-plus`, then re-run codesys_setup_check to get the exact .mcp.json snippet.',
381
+ )
382
+ }
383
+
384
+ return toolText(lines.join('\n'))
385
+ },
386
+ })
387
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "sylo-codesys",
3
+ "version": "0.1.0",
4
+ "description": "Drive a live, open CODESYS V3.5 project from chat via the codesys-mcp-sp21-plus MCP server, bridged through pi-mcp-adapter. Skill + setup/diagnostic tool. No host MCP client.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package"
8
+ ],
9
+ "files": [
10
+ "extensions",
11
+ "skills",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "pi": {
16
+ "extensions": [
17
+ "./extensions/index.ts"
18
+ ],
19
+ "skills": [
20
+ "./skills/codesys"
21
+ ]
22
+ },
23
+ "peerDependencies": {
24
+ "@earendil-works/pi-coding-agent": "^0.84.2"
25
+ },
26
+ "dependencies": {
27
+ "typebox": "^1.1.24"
28
+ },
29
+ "license": "MIT"
30
+ }
@@ -0,0 +1,247 @@
1
+ ---
2
+ name: codesys
3
+ description: Drive a live, open CODESYS V3.5 project from chat — create POUs, write Structured Text, compile, read/write PLC variables — via the codesys-mcp-sp21-plus MCP server bridged through pi-mcp-adapter.
4
+ metadata:
5
+ sylo:
6
+ category: automation
7
+ icon: cpu
8
+ ---
9
+
10
+ # CODESYS — live project via MCP
11
+
12
+ You drive a **live, open CODESYS V3.5 project** from chat. Every change is
13
+ applied to the project the operator has open and appears in the CODESYS IDE in
14
+ real time. It is the standalone CODESYS chat experience for live projects.
15
+
16
+ ## How this is wired (the part you cannot see)
17
+
18
+ These CODESYS tools are **not** built into Sylo. They come from the external
19
+ `codesys-mcp-sp21-plus` stdio MCP server, surfaced into Pi by the
20
+ **pi-mcp-adapter** package once a `codesys` server is configured in a
21
+ workspace `.mcp.json`. If the wiring is missing or broken, your tool calls will
22
+ fail — fix the wiring, don't keep retrying blindly.
23
+
24
+ **Run `codesys_setup_check` first** whenever CODESYS is the task or a call
25
+ fails. It reports: is CODESYS installed? is `codesys-mcp-sp21-plus` on PATH? is
26
+ there a `codesys` server in `.mcp.json`? is the `mcp` adapter tool live? It also
27
+ emits a ready-to-paste `.mcp.json` snippet. Tell the operator the gaps and the
28
+ exact fix (install command / enable pi-mcp-adapter / paste snippet / restart
29
+ broker).
30
+
31
+ ## Two ways to call the tools
32
+
33
+ 1. **Direct tools** (if `directTools` is configured in `.mcp.json`): a curated
34
+ ~24-tool surface is registered as first-class Pi tools named `codesys_<mcp_name>`:
35
+ `codesys_get_codesys_status`, `codesys_get_project_info`, `codesys_get_all_pou_code`,
36
+ `codesys_create_pou`, `codesys_set_pou_code`, `codesys_create_method`,
37
+ `codesys_create_property`, `codesys_create_dut`, `codesys_create_gvl`,
38
+ `codesys_create_folder`, `codesys_delete_object`, `codesys_rename_object`,
39
+ `codesys_save_project`, `codesys_compile_project`, `codesys_get_compile_messages`,
40
+ `codesys_add_library`, `codesys_list_project_libraries`, `codesys_connect_to_device`,
41
+ `codesys_read_variable`, `codesys_write_variable`, `codesys_start_stop_application`,
42
+ `codesys_get_application_state`, `codesys_mirror_export`, `codesys_shutdown_codesys`.
43
+ Prefer these when they fit — no search round-trip.
44
+
45
+ 2. **The `mcp` proxy** for everything else (the other ~78 tools, plus resources).
46
+ Discover → describe → call:
47
+ ```
48
+ mcp({ search: "symbol config" }) // find candidate tools
49
+ mcp({ describe: "create_symbol_config" }) // read the schema
50
+ mcp({ tool: "create_symbol_config", args: { projectFilePath: "C:/p.project", ... } })
51
+ ```
52
+ **Project structure is an MCP resource, not a tool**: read it via the proxy
53
+ with the resource URI `codesys://project/{+project_path}/structure` (the
54
+ adapter exposes resources as tools when `exposeResources` is on). For a plain
55
+ project-info read, use the `codesys_get_project_info` direct tool.
56
+
57
+ ## `projectFilePath` is required on every tool
58
+
59
+ Every CODESYS tool takes `projectFilePath` (absolute path to the open `.project`
60
+ file, forward or back slashes both work). **Determine it before calling:**
61
+ - Ask the operator which `.project` they have open, or
62
+ - Look in the workspace for `*.project`, or
63
+ - Run `codesys_setup_check` (it lists the detected CODESYS install but not the
64
+ project — the project path still comes from the operator/workspace).
65
+
66
+ The server's `ensure_project_open` helper opens the project if it isn't already
67
+ open, so passing the correct path is sufficient.
68
+
69
+ ## Discipline
70
+
71
+ - **Inspect before editing.** Call `codesys_get_project_info` + `codesys_get_all_pou_code`
72
+ (or the structure resource) to understand the project. Don't edit blind.
73
+ - **Always compile after authoring.** After `create_pou` / `set_pou_code` /
74
+ `create_method` / `create_dut` / `create_gvl` / `add_library` / `rename_object`,
75
+ call `codesys_compile_project` and **report errors/warnings to the operator
76
+ before declaring success.** Use `codesys_get_compile_messages` for detail.
77
+ These modifying tools **auto-save** the project on success.
78
+ - **Confirm destructive ops.** `delete_object`, `write_variable` (forces),
79
+ `download_to_device`, `reset_application`, `shutdown_codesys`, `remove_library`
80
+ are approval-gated (the operator clicks to confirm). Still announce intent in
81
+ chat first — e.g. "I'm about to delete `Application/OldPou` — confirm?"
82
+ - **Undo caveat.** `set_pou_code` writes the live binary; **Ctrl+Z in the IDE
83
+ does NOT recover it.** With `--auto-mirror` configured, each change re-runs
84
+ `mirror_export` to a `.st` mirror on disk — commit that mirror to git for
85
+ real undo. Call `codesys_mirror_export` to refresh it on demand.
86
+
87
+ ## Online / runtime
88
+
89
+ - `codesys_connect_to_device` may pop a **"Device User Login" modal in the
90
+ CODESYS IDE** that the agent cannot see or dismiss. **Announce this to the
91
+ operator before calling** and tell them to be ready to click. To suppress it,
92
+ set `CODESYS_DEVICE_USER` / `CODESYS_DEVICE_PASSWORD` in the `codesys` server
93
+ `env` block of `.mcp.json`.
94
+ - Read/write variables require an online connection first (`codesys_connect_to_device`).
95
+ - `codesys_get_application_state` returns run / stop / exception + logged-in status.
96
+
97
+ ## Lifecycle
98
+
99
+ The first tool call launches CODESYS **persistent (UI visible)**; with the
100
+ recommended `lazy-keep-alive` lifecycle the instance stays resident across turns
101
+ so subsequent calls are fast (cold launch + watcher ready can take 60s+). To stop
102
+ CODESYS from chat, call `codesys_shutdown_codesys` (it relaunches on the next
103
+ tool call). If the operator already has CODESYS open with the project, the
104
+ bridge uses that instance.
105
+
106
+ ## ⚠️ UI lock — CODESYS scripting engine is single-threaded
107
+
108
+ **While the MCP bridge is active, the operator has limited ability to interact
109
+ with the CODESYS IDE.** The bridge runs a watcher script inside CODESYS's
110
+ scripting engine, which is single-threaded. The watcher loop (`system.delay()`
111
+ polling) blocks new commands — direct double-click navigation and menu actions
112
+ are locked.
113
+
114
+ **Workaround:** right-clicking still works (it's a raw Win32 event that
115
+ bypasses CODESYS's `StartLengthyOperation` gate). After right-clicking on an
116
+ object, double-clicking to open it works. This is not a bug — it's a side
117
+ effect of `system.delay()` pumping the Win32 message loop while
118
+ `StartLengthyOperation()` blocks CODESYS-level commands. See the [CODESYS
119
+ scripting docs on `system.delay`](https://product-help.se.com/docs/Machine+Expert/V1.1/en/ScriptEngine/topics/system.htm).
120
+
121
+ **What works vs. what's blocked in the agent's instance:**
122
+ - ✅ Right-click (raw Win32 event bypasses the gate)
123
+ - ✅ Open a POU via right-click → double-click
124
+ - ❌ Scroll the editor (CODESYS editor command — blocked)
125
+ - ❌ Menu bar, typing, editing (blocked)
126
+
127
+ You can open a POU and see the top of the code, but **you cannot scroll to
128
+ read further down.** For reading long routines, use the second read-only
129
+ instance (below) or ask the agent to dump the code to chat.
130
+
131
+ This is a fundamental limitation of the CODESYS scripting engine, not a bug in
132
+ the MCP server.
133
+
134
+ ### Recommended workflow: second read-only CODESYS instance
135
+
136
+ The cleanest workflow uses **two CODESYS instances** — one for the agent
137
+ (MCP bridge), one for the operator (read-only inspection):
138
+
139
+ 1. **Agent's instance** — launched by the MCP server, runs the watcher script,
140
+ UI is locked. This is where tool calls create POUs, write ST, compile, etc.
141
+ 2. **Operator's instance** — a second CODESYS window opened separately (e.g.
142
+ launch CODESYS manually, File → Open Project, pick the same `.project` file).
143
+ This instance is fully interactive — the operator can navigate, inspect code,
144
+ double-click POUs, etc. without interfering with the bridge.
145
+ 3. **Seeing live updates** — CODESYS does NOT auto-detect external file changes
146
+ and has no "reload from disk" button. But the operator can **reopen the
147
+ project without closing CODESYS first** (File → Open Project → select the
148
+ same `.project`). This reloads the project from disk with all the agent's
149
+ changes, without killing the agent's MCP-connected instance.
150
+ 4. **When the agent is done with a batch** — the operator reopens the project
151
+ in the read-only instance to inspect the results. No kill, no relaunch, no
152
+ Sylo restart needed.
153
+
154
+ This avoids the kill/relaunch cycle entirely for routine inspection. Only use
155
+ the recovery procedure below if the watcher actually dies (cancel, close, or
156
+ crash).
157
+
158
+ ### Recovery when the watcher dies (cancel, close, or crash)
159
+
160
+ No Sylo restart is needed. The MCP server child process is still alive
161
+ (lazy-keep-alive keeps it resident), but the watcher inside CODESYS is dead —
162
+ tool calls will time out (60s). To recover:
163
+
164
+ 1. The agent kills the dead CODESYS process from bash:
165
+ ```bash
166
+ taskkill /F /IM CODESYS.exe
167
+ ```
168
+ 2. The next tool call relaunches CODESYS with a fresh watcher and reopens the
169
+ project automatically.
170
+
171
+ Alternatively, call `codesys_launch_codesys_with_project` with
172
+ `killExisting=true` — it kills the orphan and relaunches in one step.
173
+
174
+ ### Orphaned CODESYS after Sylo broker restart
175
+
176
+ If the Sylo broker restarts (or the machine reboots), the MCP server child
177
+ process dies but the CODESYS window it launched stays open as an orphan. The
178
+ new MCP server instance refuses to share IPC with the orphan:
179
+
180
+ > `Refusing to launch: 1 CODESYS.exe instance(s) of the same install already
181
+ > running... This MCP server cannot share IPC with an instance it didn't spawn.`
182
+
183
+ Fix: kill the orphan (`taskkill /F /IM CODESYS.exe`) or call
184
+ `launch_codesys_with_project` with `killExisting=true`, then the next tool call
185
+ works normally.
186
+
187
+ ## Argument cheat-sheet (most-used tools)
188
+
189
+ ```js
190
+ // Create a POU (type: Program | FunctionBlock | Function; language: ST | LD | FBD | SFC | IL | CFC)
191
+ codesys_create_pou({
192
+ projectFilePath: "C:/Projects/MyPLC.project",
193
+ name: "FB_Motor",
194
+ type: "FunctionBlock",
195
+ language: "ST",
196
+ parentPath: "Application", // relative path under the project root
197
+ declarationCode: "FUNCTION_BLOCK FB_Motor\nVAR\n bRun : BOOL;\nEND_VAR",
198
+ implementationCode: "IF bRun THEN\n ;\nEND_IF" // optional
199
+ })
200
+
201
+ // Write declaration and/or implementation of an existing POU / method / property
202
+ codesys_set_pou_code({
203
+ projectFilePath: "C:/Projects/MyPLC.project",
204
+ pouPath: "Application/FB_Motor", // slash path to the object
205
+ declarationCode: "...", // omit a field to leave it unchanged
206
+ implementationCode: "..."
207
+ })
208
+
209
+ // Create a method on an FB (returnType optional; pass code or leave empty + set_pou_code later)
210
+ codesys_create_method({
211
+ projectFilePath: "...",
212
+ parentPouPath: "Application/FB_Motor",
213
+ methodName: "Reset",
214
+ returnType: "BOOL",
215
+ implementationCode: "Reset := TRUE;"
216
+ })
217
+
218
+ // DUT (dutType: Structure | Enumeration | Union | Alias) / GVL / folder
219
+ codesys_create_dut({ projectFilePath: "...", name: "ST_Axis", dutType: "Structure", parentPath: "Application" })
220
+ codesys_create_gvl({ projectFilePath: "...", name: "GVL_IO", parentPath: "Application", declarationCode: "VAR_GLOBAL\n bReady : BOOL;\nEND_VAR" })
221
+
222
+ // Build + messages
223
+ codesys_compile_project({ projectFilePath: "..." })
224
+ codesys_get_compile_messages({ projectFilePath: "..." })
225
+
226
+ // Libraries (name must match the installed Library Repository; refuses unknown libs)
227
+ codesys_add_library({ projectFilePath: "...", libraryName: "Standard" })
228
+ codesys_list_project_libraries({ projectFilePath: "..." })
229
+
230
+ // Online
231
+ codesys_connect_to_device({ projectFilePath: "..." }) // may pop a login modal
232
+ codesys_read_variable({ projectFilePath: "...", variablePath: "PLC_PRG.bMotorRunning" })
233
+ codesys_write_variable({ projectFilePath: "...", variablePath: "PLC_PRG.bMotorRunning", value: "TRUE" })
234
+ codesys_start_stop_application({ projectFilePath: "...", action: "start" }) // action: start | stop
235
+ codesys_get_application_state({ projectFilePath: "..." })
236
+ ```
237
+
238
+ **Searching across POUs:** there is no `search_code` tool. Call
239
+ `codesys_get_all_pou_code` (returns all declarations + implementations) and grep
240
+ the returned text. The project tree is the `codesys://project/{+path}/structure`
241
+ resource, read via the `mcp` proxy.
242
+
243
+ ## Scope
244
+
245
+ This skill is only for **live CODESYS V3.5 projects** driven through the MCP
246
+ server. It is not for projects edited as plain files on disk with no CODESYS
247
+ running.