@codesys/mcp-toolkit 2.6.0 → 2.8.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +11 -8
  3. package/README.zh-CN.md +7 -6
  4. package/dist/codesys/compare.d.ts +17 -0
  5. package/dist/codesys/compare.js +105 -0
  6. package/dist/codesys/compare.js.map +1 -0
  7. package/dist/codesys/debug.d.ts +2 -0
  8. package/dist/codesys/debug.js +96 -2
  9. package/dist/codesys/debug.js.map +1 -1
  10. package/dist/codesys/graphical-read.d.ts +7 -4
  11. package/dist/codesys/graphical-read.js +103 -3
  12. package/dist/codesys/graphical-read.js.map +1 -1
  13. package/dist/codesys/graphical.d.ts +51 -4
  14. package/dist/codesys/graphical.js +247 -44
  15. package/dist/codesys/graphical.js.map +1 -1
  16. package/dist/codesys/recipes.d.ts +45 -0
  17. package/dist/codesys/recipes.js +130 -0
  18. package/dist/codesys/recipes.js.map +1 -0
  19. package/dist/hmi/native.d.ts +1 -1
  20. package/dist/hmi/native.js +20 -1
  21. package/dist/hmi/native.js.map +1 -1
  22. package/dist/hmi/render.d.ts +4 -0
  23. package/dist/hmi/render.js +13 -1
  24. package/dist/hmi/render.js.map +1 -1
  25. package/dist/hmi/service.d.ts +3 -0
  26. package/dist/hmi/service.js +29 -2
  27. package/dist/hmi/service.js.map +1 -1
  28. package/dist/hmi/spec.d.ts +18 -0
  29. package/dist/hmi/spec.js +45 -0
  30. package/dist/hmi/spec.js.map +1 -1
  31. package/dist/hmi/tools.js +50 -5
  32. package/dist/hmi/tools.js.map +1 -1
  33. package/dist/hmi/value.js +3 -0
  34. package/dist/hmi/value.js.map +1 -1
  35. package/dist/panel/hmi.js +66 -61
  36. package/dist/panel.js +1 -1
  37. package/dist/panel.js.map +1 -1
  38. package/dist/server.d.ts +1 -1
  39. package/dist/server.js +2 -2
  40. package/dist/tools.d.ts +2 -0
  41. package/dist/tools.js +100 -13
  42. package/dist/tools.js.map +1 -1
  43. package/package.json +1 -1
  44. package/watcher/mcp_alarms.py +42 -0
  45. package/watcher/mcp_caps.py +66 -0
  46. package/watcher/mcp_debug.py +54 -2
  47. package/watcher/mcp_lang.py +13 -1
  48. package/watcher/watcher.py +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesys/mcp-toolkit",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "MCP server for CODESYS V3.5: read, write, compile and simulate Structured Text through a persistent CODESYS session",
5
5
  "mcpName": "io.github.johannesPettersson80/codesys-mcp-toolkit",
6
6
  "type": "module",
@@ -151,6 +151,47 @@ def op_alarms_list(args):
151
151
  return {"groups": groups}
152
152
 
153
153
 
154
+ def op_textlists_translate(args):
155
+ """Translations into CODESYS's text lists (documented text-list scripting): the
156
+ GlobalTextList, where a visualization looks up its static texts by the text itself,
157
+ and the HMI's alarm group list (alarm messages, matched by their default text)."""
158
+ c = Ctx()
159
+ out = {"languages": [], "rows": 0, "added": 0, "errors": []}
160
+ lists = []
161
+ names = (["GlobalTextList"] if args.get("global", True) else []) + ([args["alarm_group"]] if args.get("alarm_group") else [])
162
+ for name in names:
163
+ for tl in c.proj.find(name, True):
164
+ if hasattr(tl, "rows"):
165
+ lists.append((name, tl))
166
+ break
167
+ for lang, texts in (args.get("translations") or {}).items():
168
+ out["languages"].append(lang)
169
+ for name, tl in lists:
170
+ try:
171
+ tl.addlanguage(lang)
172
+ except Exception:
173
+ pass # it has the language already
174
+ rows = {}
175
+ for r in tl.rows:
176
+ rows[to_text(r.defaulttext)] = r
177
+ for text, tr in texts.items():
178
+ try:
179
+ r = rows.get(text)
180
+ if r is None:
181
+ if name != "GlobalTextList":
182
+ continue
183
+ r = tl.rows.add(text, text)
184
+ rows[text] = r
185
+ out["added"] += 1
186
+ r.setlanguagetext(lang, tr)
187
+ out["rows"] += 1
188
+ except Exception as e:
189
+ if len(out["errors"]) < 8:
190
+ out["errors"].append("%s/%s: %s" % (name, text[:30], to_text(e)[:120]))
191
+ out["saved"] = c.save_if(args)
192
+ return out
193
+
194
+
154
195
  def op_visu_users(args):
155
196
  """Visualization user groups (no users: their passwords are the engineer's to set)."""
156
197
  import System
@@ -190,3 +231,4 @@ def op_visu_users(args):
190
231
  mcp_ops.OPS["alarms.configure"] = mcp_sim._locked(op_alarms_configure)
191
232
  mcp_ops.OPS["visu.users"] = mcp_sim._locked(op_visu_users)
192
233
  mcp_ops.OPS["alarms.list"] = op_alarms_list
234
+ mcp_ops.OPS["textlists.translate"] = mcp_sim._locked(op_textlists_translate)
@@ -0,0 +1,66 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Which optional features this CODESYS offers. Several actions rely on CODESYS's
3
+ # services beyond its documented scripting API, or on add-ons (visualization, alarm
4
+ # management). Each check reaches the service the same way its module does, so a
5
+ # feature reported available is one that action can use.
6
+ #
7
+ # Keep this file ASCII-only (IronPython 2.7 inside CODESYS).
8
+ import os
9
+
10
+ import mcp_ops
11
+ from mcp_ops import _safe
12
+
13
+ import scriptengine as se
14
+
15
+
16
+ def _type(name):
17
+ from System import AppDomain
18
+ for a in AppDomain.CurrentDomain.GetAssemblies():
19
+ try:
20
+ t = a.GetType(name)
21
+ except Exception:
22
+ t = None
23
+ if t is not None:
24
+ return t
25
+ return None
26
+
27
+
28
+ def _assembly(name):
29
+ from System import AppDomain
30
+ for a in AppDomain.CurrentDomain.GetAssemblies():
31
+ if a.GetName().Name == name:
32
+ return True
33
+ return False
34
+
35
+
36
+ def _check(fn):
37
+ try:
38
+ return bool(fn())
39
+ except Exception:
40
+ return False
41
+
42
+
43
+ def op_capabilities(args):
44
+ import mcp_lang
45
+ import mcp_debug
46
+ caps = {
47
+ "visualization": _check(lambda: hasattr(se, "VisualElementType")),
48
+ "signatures": _check(lambda: mcp_lang._service("LanguageModelMgr") is not None),
49
+ "cross_references": _check(lambda: mcp_lang._service("CrossReferenceService") is not None),
50
+ "debugging": _check(lambda: mcp_debug._bpm() is not None),
51
+ "trace": _check(lambda: hasattr(se, "trace")),
52
+ "alarms": _check(lambda: _assembly("AlarmConfigurationObjects.plugin")),
53
+ "visu_users": _check(lambda: _type("_3S.CoDeSys.VisualObject.VisuUserMgmtLibraryInserter") is not None),
54
+ "device_repository": _check(lambda: _safe(lambda: se.device_repository) is not None),
55
+ "library_manager": _check(lambda: getattr(se, "librarymanager", None) is not None),
56
+ "editor_window": os.environ.get("CODESYS_MCP_UI") == "1",
57
+ }
58
+ fpu = None
59
+ glm = getattr(se, "librarymanager", None)
60
+ if glm is not None:
61
+ fpu = _check(lambda: any((l.title or "").lower() == "floatingpointutils" for l in glm.get_all_libraries()))
62
+ caps["floating_point_utils"] = fpu
63
+ return {"capabilities": caps}
64
+
65
+
66
+ mcp_ops.OPS["capabilities"] = op_capabilities
@@ -83,6 +83,55 @@ def op_debug_set(args):
83
83
  return {"id": key}
84
84
 
85
85
 
86
+ def op_debug_set_data(args):
87
+ """A data breakpoint: halt when a variable is written (optionally only when a condition holds)."""
88
+ import System
89
+ c = Ctx()
90
+ mcp_sim._online()
91
+ bpm = _bpm()
92
+ expr = args["expression"]
93
+ tasks = System.Array[str](args.get("tasks") or [])
94
+ try:
95
+ bp = bpm.CreateDataBreakpoint(c.app.guid, c.proj.handle, expr, int(args.get("size") or 0), tasks, _hit_always(), 0, False, args.get("condition") or "", "", "")
96
+ except AttributeError:
97
+ raise OpError("unavailable", "This CODESYS has no data breakpoints for scripts.")
98
+ bpm.SetBreakpoint(bp)
99
+ bpm.EnableBreakpoint(bp)
100
+ key = "data:%s" % expr
101
+ BPS[key] = bp
102
+ # With size 0 CODESYS keeps the breakpoint disabled; the server passes the variable's size.
103
+ return {"id": key, "enabled": bool(bp.IsEnabled)}
104
+
105
+
106
+ def _call_stack(c):
107
+ """The call stack while halted: each entry's POU and instance, innermost first."""
108
+ out = []
109
+ try:
110
+ from System.Reflection import BindingFlags
111
+ om = None
112
+ for tname in ("_3S.CoDeSys.MemoryTools.APEnvironment", "_3S.CoDeSys.VisualEditor.APEnvironment", "_3S.CoDeSys.RecipeManObject.APEnvironment"):
113
+ t = _find_type(tname)
114
+ p = t.GetProperty("OnlineMgr", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) if t is not None else None
115
+ if p is not None:
116
+ om = p.GetValue(None, None)
117
+ if om is not None:
118
+ break
119
+ if om is None:
120
+ return []
121
+ app = om.GetApplication(c.app.guid)
122
+ for e in list(app.CallStack or []):
123
+ # LocationInformation.EditorInstancePath: 'PLCWinNT.Application.PLC_PRG.fbCount';
124
+ # the task's own frames ('CallTask__MainTask', '__cycle__code__MainTask') are left out.
125
+ info = e.LocationInformation
126
+ path = to_text(info.EditorInstancePath or "")
127
+ if not path or path.split(".")[-1].startswith("__") or path.split(".")[-1].startswith("CallTask__"):
128
+ continue
129
+ out.append({"instance": path})
130
+ except Exception as ex:
131
+ out.append({"error": to_text(ex)[:160]})
132
+ return out
133
+
134
+
86
135
  def op_debug_clear(args):
87
136
  mcp_sim._online()
88
137
  bpm = _bpm()
@@ -120,6 +169,7 @@ def op_debug_status(args):
120
169
  out["at"]["implementation"] = pos["implementation"]
121
170
  except Exception as e:
122
171
  out["at_error"] = to_text(e)
172
+ out["call_stack"] = _call_stack(c)
123
173
  return out
124
174
 
125
175
 
@@ -135,14 +185,16 @@ def op_debug_step(args):
135
185
 
136
186
  def op_debug_continue(args):
137
187
  online = mcp_sim._online()
138
- online.start()
139
- mcp_sim._pause(200)
188
+ if to_text(online.application_state).lower() != "run":
189
+ online.start()
190
+ mcp_sim._pause(200)
140
191
  return {"state": to_text(online.application_state)}
141
192
 
142
193
 
143
194
  mcp_ops.OPS.update({
144
195
  "debug.positions": op_debug_positions,
145
196
  "debug.set": op_debug_set,
197
+ "debug.set_data": op_debug_set_data,
146
198
  "debug.clear": op_debug_clear,
147
199
  "debug.status": op_debug_status,
148
200
  "debug.step": op_debug_step,
@@ -101,7 +101,19 @@ def _signature(lmm, c, sig):
101
101
  ins = set(to_text(_get(v, "OrgName")) for v in (_get(sig, "Inputs") or []))
102
102
  outs = set(to_text(_get(v, "OrgName")) for v in (_get(sig, "Outputs") or []))
103
103
  variables = []
104
- for v in lmm.GetAllVariables(sig, c.app.guid):
104
+ found = list(lmm.GetAllVariables(sig, c.app.guid) or [])
105
+ if not found:
106
+ # A method of a library FB: the language model keeps its variables on the signature.
107
+ for prop in ("All", "Variables", "AllVariables"):
108
+ vs = _get(sig, prop)
109
+ if vs:
110
+ try:
111
+ found = list(vs)
112
+ except Exception:
113
+ found = []
114
+ if found:
115
+ break
116
+ for v in found:
105
117
  n = to_text(_get(v, "OrgName"))
106
118
  variables.append(_var(v, "input" if n in ins else "output" if n in outs else "other"))
107
119
  subs = []
@@ -260,6 +260,7 @@ def main(ui=False):
260
260
  import mcp_debug # noqa: F401 (adds breakpoints and stepping)
261
261
  import mcp_devices # noqa: F401 (adds device search and add)
262
262
  import mcp_alarms # noqa: F401 (adds alarm management and visu user groups)
263
+ import mcp_caps # noqa: F401 (adds the capability check)
263
264
  prompts = setup_prompts(se)
264
265
  log(prompts)
265
266
  if not hasattr(se, "FAKE"):
@@ -311,6 +312,7 @@ def main_attach():
311
312
  import mcp_debug # noqa: F401 (adds breakpoints and stepping)
312
313
  import mcp_devices # noqa: F401 (adds device search and add)
313
314
  import mcp_alarms # noqa: F401 (adds alarm management and visu user groups)
315
+ import mcp_caps # noqa: F401 (adds the capability check)
314
316
  prompts = setup_prompts(se)
315
317
  log(prompts)
316
318
  old = getattr(__builtin__, "_mcp_attach_timer", None)