@codesys/mcp-toolkit 1.5.8 → 2.0.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 (110) hide show
  1. package/CHANGELOG.md +153 -189
  2. package/LICENSE +21 -21
  3. package/README.md +269 -152
  4. package/dist/actions.d.ts +28 -0
  5. package/dist/actions.js +98 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/bridge/bridge.d.ts +105 -0
  8. package/dist/bridge/bridge.js +400 -0
  9. package/dist/bridge/bridge.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +196 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/codesys/cache.d.ts +48 -0
  14. package/dist/codesys/cache.js +82 -0
  15. package/dist/codesys/cache.js.map +1 -0
  16. package/dist/codesys/changes.d.ts +57 -0
  17. package/dist/codesys/changes.js +133 -0
  18. package/dist/codesys/changes.js.map +1 -0
  19. package/dist/codesys/decl.d.ts +45 -0
  20. package/dist/codesys/decl.js +251 -0
  21. package/dist/codesys/decl.js.map +1 -0
  22. package/dist/codesys/devices.d.ts +34 -0
  23. package/dist/codesys/devices.js +82 -0
  24. package/dist/codesys/devices.js.map +1 -0
  25. package/dist/codesys/diff.d.ts +13 -0
  26. package/dist/codesys/diff.js +61 -0
  27. package/dist/codesys/diff.js.map +1 -0
  28. package/dist/codesys/install.d.ts +40 -0
  29. package/dist/codesys/install.js +144 -0
  30. package/dist/codesys/install.js.map +1 -0
  31. package/dist/codesys/library-browse.d.ts +19 -0
  32. package/dist/codesys/library-browse.js +57 -0
  33. package/dist/codesys/library-browse.js.map +1 -0
  34. package/dist/codesys/outline.d.ts +32 -0
  35. package/dist/codesys/outline.js +219 -0
  36. package/dist/codesys/outline.js.map +1 -0
  37. package/dist/codesys/simulation.d.ts +34 -0
  38. package/dist/codesys/simulation.js +57 -0
  39. package/dist/codesys/simulation.js.map +1 -0
  40. package/dist/codesys/st.d.ts +36 -0
  41. package/dist/codesys/st.js +69 -0
  42. package/dist/codesys/st.js.map +1 -0
  43. package/dist/daemon.d.ts +46 -0
  44. package/dist/daemon.js +173 -0
  45. package/dist/daemon.js.map +1 -0
  46. package/dist/driver.d.ts +25 -0
  47. package/dist/driver.js +33 -0
  48. package/dist/driver.js.map +1 -0
  49. package/dist/panel/sim-panel.html +357 -0
  50. package/dist/panel/viewer.html +126 -0
  51. package/dist/panel-types.d.ts +20 -0
  52. package/dist/panel-types.js +3 -0
  53. package/dist/panel-types.js.map +1 -0
  54. package/dist/panel.d.ts +27 -0
  55. package/dist/panel.js +208 -0
  56. package/dist/panel.js.map +1 -0
  57. package/dist/server.d.ts +5 -0
  58. package/dist/server.js +33 -974
  59. package/dist/server.js.map +1 -0
  60. package/dist/tools.d.ts +103 -0
  61. package/dist/tools.js +793 -0
  62. package/dist/tools.js.map +1 -0
  63. package/dist/viewer.d.ts +14 -0
  64. package/dist/viewer.js +125 -0
  65. package/dist/viewer.js.map +1 -0
  66. package/package.json +66 -51
  67. package/watcher/mcp_compat.py +147 -0
  68. package/watcher/mcp_ops.py +1178 -0
  69. package/watcher/mcp_sim.py +246 -0
  70. package/watcher/watcher.py +255 -0
  71. package/.claude/settings.local.json +0 -29
  72. package/CLAUDE.md +0 -26
  73. package/CONTRIBUTING.md +0 -72
  74. package/README.md.enhanced +0 -155
  75. package/codesys-mcp-toolkit-1.2.0.tgz +0 -0
  76. package/codesys-mcp-toolkit-1.2.1.tgz +0 -0
  77. package/codesys-mcp-toolkit-1.5.7.tgz +0 -0
  78. package/codesys-mcp-toolkit-1.5.8.tgz +0 -0
  79. package/dist/bin.js +0 -75
  80. package/dist/check_session_paths.js +0 -149
  81. package/dist/codesys_interop.js +0 -435
  82. package/dist/direct_session_command.js +0 -293
  83. package/dist/scriptLoader.js +0 -165
  84. package/dist/scripts/check_status.py +0 -21
  85. package/dist/scripts/compile_project.py +0 -65
  86. package/dist/scripts/create_method.py +0 -80
  87. package/dist/scripts/create_pou.py +0 -102
  88. package/dist/scripts/create_project.py +0 -116
  89. package/dist/scripts/create_property.py +0 -78
  90. package/dist/scripts/ensure_project_open.py +0 -154
  91. package/dist/scripts/find_object_by_path.py +0 -111
  92. package/dist/scripts/get_pou_code.py +0 -91
  93. package/dist/scripts/get_project_structure.py +0 -72
  94. package/dist/scripts/open_project.py +0 -25
  95. package/dist/scripts/save_project.py +0 -29
  96. package/dist/scripts/session_status.py +0 -281
  97. package/dist/scripts/set_pou_code.py +0 -109
  98. package/dist/scripts/start_session.py +0 -809
  99. package/dist/scripts/stop_session.py +0 -324
  100. package/dist/sessionManager.js +0 -798
  101. package/docs/configuration.md +0 -141
  102. package/docs/index.md +0 -26
  103. package/docs/installation.md +0 -84
  104. package/docs/session_management.md +0 -110
  105. package/examples/README.md +0 -78
  106. package/examples/sample_config.json +0 -32
  107. package/replacements_session_status.txt +0 -16
  108. package/scriptDoc.txt +0 -8679
  109. package/scripts/build.js +0 -25
  110. package/scripts/fix_fstrings.py +0 -76
@@ -0,0 +1,246 @@
1
+ # -*- coding: utf-8 -*-
2
+ # CODESYS's own simulation: runs the active application inside CODESYS, with no
3
+ # controller involved. Runs inside CODESYS (IronPython 2.7) and under CPython 3
4
+ # with a fake scriptengine in tests. Keep this file ASCII-only.
5
+ #
6
+ # Every online call first checks that the device is in simulation mode, so these
7
+ # operations can't reach a controller. While a simulation runs the project is
8
+ # locked, so the simulation flag on the device is never saved into it.
9
+ from __future__ import print_function
10
+
11
+ import scriptengine as se
12
+
13
+ import mcp_ops
14
+ from mcp_compat import to_text
15
+ from mcp_ops import Ctx, OpError, _safe
16
+
17
+ SIM = {"online": None, "device": None, "project": None, "was_dirty": False, "forced": set(), "cycles": 0}
18
+
19
+ # Operations that change the project, or could save the simulation flag into it.
20
+ LOCKED = (
21
+ "project.open", "project.open_new", "project.create_empty", "project.save", "project.close",
22
+ "object.write", "object.create", "object.delete", "object.rename", "object.import_xml",
23
+ "libraries.add", "libraries.remove", "task.configure", "build",
24
+ )
25
+
26
+ # Between single cycles: the command returns before the cycle has run.
27
+ CYCLE_PAUSE_MS = 100
28
+ MAX_CYCLES = 1000
29
+ # Long enough for CODESYS to refresh a value it monitors (see _read).
30
+ READ_SETTLE_MS = 150
31
+
32
+
33
+ def _require_simulation():
34
+ device = SIM["device"]
35
+ if device is None or not device.get_simulation_mode():
36
+ raise OpError("not_simulating", "The device is not in simulation mode, so the toolkit won't go online.")
37
+
38
+
39
+ def _online():
40
+ if SIM["online"] is None:
41
+ raise OpError("not_running", "The CODESYS simulation isn't running. Call codesys_sim_start first.")
42
+ _require_simulation()
43
+ return SIM["online"]
44
+
45
+
46
+ def _pause(ms):
47
+ if not hasattr(se, "FAKE"):
48
+ se.system.delay(ms)
49
+
50
+
51
+ def _device_of(app):
52
+ """The outermost device above the application."""
53
+ found = None
54
+ obj = app
55
+ while obj is not None:
56
+ if _safe(lambda: obj.is_device, False):
57
+ found = obj
58
+ obj = _safe(lambda: obj.parent)
59
+ if found is None:
60
+ raise OpError("no_device", "No device found above the application.")
61
+ return found
62
+
63
+
64
+ def _status():
65
+ online = SIM["online"]
66
+ if online is None:
67
+ return {"running": False}
68
+ return {
69
+ "running": True,
70
+ "state": to_text(online.application_state),
71
+ "operation": to_text(online.operation_state),
72
+ "forced": sorted(SIM["forced"]),
73
+ "cycles": SIM["cycles"],
74
+ }
75
+
76
+
77
+ def _release():
78
+ """Take the device out of simulation mode and leave the project as it was."""
79
+ device, proj, was_dirty = SIM["device"], SIM["project"], SIM["was_dirty"]
80
+ SIM.update({"online": None, "device": None, "project": None, "was_dirty": False, "forced": set(), "cycles": 0})
81
+ if device is not None:
82
+ device.set_simulation_mode(False)
83
+ if proj is not None and not was_dirty:
84
+ proj.save() # same content as before; leaves no unsaved change behind
85
+
86
+
87
+ def _stop():
88
+ online = SIM["online"]
89
+ try:
90
+ _safe(lambda: online.unforce_all_values())
91
+ online.logout()
92
+ finally:
93
+ _release()
94
+
95
+
96
+ def op_sim_start(args):
97
+ c = Ctx()
98
+ if c.app is None:
99
+ raise OpError("no_application", "The project has no active application to simulate.")
100
+ if SIM["online"] is not None:
101
+ _stop()
102
+ SIM["was_dirty"] = bool(_safe(lambda: c.proj.dirty, False))
103
+ SIM["project"] = c.proj
104
+ SIM["device"] = _device_of(c.app)
105
+ try:
106
+ SIM["device"].set_simulation_mode(True)
107
+ _require_simulation()
108
+ online = se.online.create_online_application(c.app)
109
+ online.login(se.OnlineChangeOption.Never, True)
110
+ except BaseException:
111
+ _release()
112
+ raise
113
+ SIM["online"] = online
114
+ if args.get("run"):
115
+ online.start()
116
+ return _status()
117
+
118
+
119
+ def op_sim_stop(args):
120
+ if SIM["online"] is not None:
121
+ _stop()
122
+ return _status()
123
+
124
+
125
+ def op_sim_status(args):
126
+ return _status()
127
+
128
+
129
+ def op_sim_control(args):
130
+ online = _online()
131
+ action = args["action"]
132
+ if action == "run":
133
+ online.start()
134
+ elif action == "stop":
135
+ online.stop()
136
+ elif action in ("reset_warm", "reset_cold"):
137
+ online.reset(se.ResetOption.Warm if action == "reset_warm" else se.ResetOption.Cold, True)
138
+ SIM["forced"].clear() # a reset releases forced values
139
+ else:
140
+ raise OpError("bad_request", "Unknown action: %s" % action)
141
+ return _status()
142
+
143
+
144
+ def op_sim_cycle(args):
145
+ online = _online()
146
+ count = int(args.get("count") or 1)
147
+ if count < 1 or count > MAX_CYCLES:
148
+ raise OpError("bad_request", "count must be between 1 and %d." % MAX_CYCLES)
149
+ if to_text(online.application_state).lower() == "run":
150
+ online.stop()
151
+ command = se.system.commands["online", "singlecycleactiveapp"]
152
+ for _ in range(count):
153
+ command.execute()
154
+ SIM["cycles"] += 1
155
+ _pause(CYCLE_PAUSE_MS)
156
+ return _status()
157
+
158
+
159
+ def _read(online, names):
160
+ """Current values and errors by name. CODESYS answers from its monitoring cache, so
161
+ the first read after a change can return the old value: read once, let it refresh, read again."""
162
+ _read_once(online, names)
163
+ _pause(READ_SETTLE_MS)
164
+ return _read_once(online, names)
165
+
166
+
167
+ def _read_once(online, names):
168
+ """Values and errors by name. One bad name must not hide the good ones."""
169
+ try:
170
+ return dict(zip(names, [to_text(v) for v in online.read_values(names)])), {}
171
+ except Exception:
172
+ values, errors = {}, {}
173
+ for name in names:
174
+ try:
175
+ values[name] = to_text(online.read_value(name))
176
+ except Exception as e:
177
+ errors[name] = to_text(e)
178
+ return values, errors
179
+
180
+
181
+ def op_sim_read(args):
182
+ values, errors = _read(_online(), args["names"])
183
+ return {"values": values, "errors": errors}
184
+
185
+
186
+ def _prepare(online, values):
187
+ """Queue values, after checking every name, so a bad name leaves nothing queued."""
188
+ names = sorted(values)
189
+ errors = _read_once(online, names)[1]
190
+ if errors:
191
+ raise OpError("unknown_variable", "; ".join("%s: %s" % (n, errors[n]) for n in sorted(errors)))
192
+ for name in names:
193
+ online.set_prepared_value(name, to_text(values[name]))
194
+
195
+
196
+ def op_sim_write(args):
197
+ online = _online()
198
+ _prepare(online, args["values"])
199
+ online.write_prepared_values()
200
+ return _status()
201
+
202
+
203
+ def op_sim_force(args):
204
+ online = _online()
205
+ _prepare(online, args["values"])
206
+ online.force_prepared_values()
207
+ SIM["forced"].update(args["values"].keys())
208
+ return _status()
209
+
210
+
211
+ def op_sim_unforce(args):
212
+ online = _online()
213
+ names = args.get("names")
214
+ if names:
215
+ for name in names:
216
+ online.set_prepared_value(name, se.online.UNFORCE)
217
+ online.force_prepared_values()
218
+ SIM["forced"].difference_update(names)
219
+ else:
220
+ online.unforce_all_values()
221
+ SIM["forced"].clear()
222
+ return _status()
223
+
224
+
225
+ def _locked(fn):
226
+ def run(args):
227
+ if SIM["online"] is not None:
228
+ raise OpError("simulation_running", "The CODESYS simulation is running, so the project is locked. Call codesys_sim_stop first.")
229
+ return fn(args)
230
+ return run
231
+
232
+
233
+ for _name in LOCKED:
234
+ mcp_ops.OPS[_name] = _locked(mcp_ops.OPS[_name])
235
+
236
+ mcp_ops.OPS.update({
237
+ "sim.start": op_sim_start,
238
+ "sim.stop": op_sim_stop,
239
+ "sim.status": op_sim_status,
240
+ "sim.control": op_sim_control,
241
+ "sim.cycle": op_sim_cycle,
242
+ "sim.read": op_sim_read,
243
+ "sim.write": op_sim_write,
244
+ "sim.force": op_sim_force,
245
+ "sim.unforce": op_sim_unforce,
246
+ })
@@ -0,0 +1,255 @@
1
+ # -*- coding: utf-8 -*-
2
+ # codesys-mcp-toolkit v2 watcher.
3
+ #
4
+ # Started by the MCP server as: CODESYS.exe --profile="..." --noUI --runscript="<dir>/watcher.py"
5
+ # Environment (inherited from the server):
6
+ # CODESYS_MCP_DIR session folder (requests/, results/, heartbeat.json, ready.json, watcher.log)
7
+ # CODESYS_MCP_SCRIPTS folder holding this file and its modules
8
+ # CODESYS_MCP_PARENT_PID server PID; the watcher exits when it is gone
9
+ #
10
+ # Protocol: the server writes requests/<id>.json ({"id","op","args","deadline_ms"}) via tmp+rename.
11
+ # The watcher handles one request at a time and writes results/<id>.json
12
+ # ({"id","ok","result"} or {"id","ok":false,"error":{"code","message","detail"}}).
13
+ # Requests are JSON op names + arguments; no code is ever generated or exec'd.
14
+ #
15
+ # Keep this file ASCII-only (IronPython 2.7 inside CODESYS).
16
+ from __future__ import print_function
17
+
18
+ import os
19
+ import re
20
+ import sys
21
+ import time
22
+ import traceback
23
+
24
+ SCRIPTS = os.environ.get("CODESYS_MCP_SCRIPTS")
25
+ if SCRIPTS and SCRIPTS not in sys.path:
26
+ sys.path.insert(0, SCRIPTS)
27
+
28
+ from mcp_compat import current_pid, now, pid_alive, read_json, remove_file, retry_fs, to_text, write_json_atomic # noqa: E402
29
+
30
+ SESSION = os.environ["CODESYS_MCP_DIR"]
31
+ PARENT_PID = int(os.environ.get("CODESYS_MCP_PARENT_PID") or "0")
32
+ REQ_DIR = os.path.join(SESSION, "requests")
33
+ RES_DIR = os.path.join(SESSION, "results")
34
+ HEARTBEAT = os.path.join(SESSION, "heartbeat.json")
35
+ READY = os.path.join(SESSION, "ready.json")
36
+ LOG = os.path.join(SESSION, "watcher.log")
37
+
38
+ DIALOG = os.path.join(SESSION, "dialog.json")
39
+
40
+ PID = current_pid()
41
+ STATE = {"count": 0, "last_beat": 0.0, "busy_since": None, "op": None}
42
+
43
+ # A dialog must stay up this long before the server is told, so brief windows don't count.
44
+ DIALOG_GRACE_S = 10.0
45
+
46
+
47
+ def log(msg):
48
+ try:
49
+ f = open(LOG, "a")
50
+ try:
51
+ f.write("%s %s\n" % (time.strftime("%H:%M:%S"), to_text(msg).encode("ascii", "replace").decode("ascii")))
52
+ finally:
53
+ f.close()
54
+ except Exception:
55
+ pass
56
+
57
+
58
+ def beat(state, op=None, rid=None, deadline_ms=None):
59
+ # A heartbeat that cannot be written (file held open past the retries) must not
60
+ # kill the watcher: log it and go on. If it keeps failing, the server sees a
61
+ # stale heartbeat and reports the watcher as hung, which is the right signal.
62
+ t = now()
63
+ try:
64
+ write_json_atomic(HEARTBEAT, {
65
+ "pid": PID, "ts": t, "state": state, "op": op, "id": rid,
66
+ "deadline": (t + deadline_ms / 1000.0) if deadline_ms else None,
67
+ "count": STATE["count"],
68
+ })
69
+ except Exception as e:
70
+ log("heartbeat write failed (%s): %s" % (state, to_text(e)))
71
+ STATE["last_beat"] = t
72
+
73
+
74
+ def setup_prompts(se):
75
+ # Never block on a modal dialog: suppress with defaults and log them.
76
+ # SP21 only has the old prompt_handling; newer versions have script_prompt_handling.
77
+ try:
78
+ se.system.script_prompt_handling = se.ScriptPromptHandling.LogPrompts
79
+ return "script_prompt_handling=LogPrompts"
80
+ except Exception:
81
+ pass
82
+ try:
83
+ se.system.prompt_handling = se.PromptHandling.LogSimplePrompts
84
+ return "prompt_handling=LogSimplePrompts"
85
+ except Exception as e:
86
+ return "prompt handling not set: %s" % e
87
+
88
+
89
+ def scripting_version():
90
+ # In CODESYS, sys.version ends in "ScriptEngine.plugin 4.2.0.0".
91
+ m = re.search(r"ScriptEngine\.plugin\s+([0-9.]+)", sys.version)
92
+ return m.group(1) if m else None
93
+
94
+
95
+ def find_dialog():
96
+ # A session without a UI has no open forms and no main window (checked on SP21 and
97
+ # SP22), so a modal form or a main window means CODESYS is waiting for an answer.
98
+ from System.Windows.Forms import Application
99
+ from System.Diagnostics import Process
100
+ for f in Application.OpenForms:
101
+ if f.Modal:
102
+ return to_text(f.Text) or "(untitled)"
103
+ return to_text(Process.GetCurrentProcess().MainWindowTitle) or None
104
+
105
+
106
+ def watch_dialogs():
107
+ # Runs beside the request loop. A dialog blocks that loop for good and nobody can
108
+ # answer it in a session without a window, so tell the server (dialog.json).
109
+ import clr
110
+ clr.AddReference("System.Windows.Forms")
111
+ title, since, reported, failed = None, 0.0, False, False
112
+ while True:
113
+ time.sleep(1.0)
114
+ busy = STATE["busy_since"]
115
+ current = None
116
+ if busy is not None and now() - busy > 2.0:
117
+ try:
118
+ current = find_dialog()
119
+ except Exception as e: # e.g. the form list changed while it was read
120
+ if not failed:
121
+ log("dialog check failed: %s" % to_text(e))
122
+ failed = True
123
+ if current != title:
124
+ title, since = current, now()
125
+ if current:
126
+ log("dialog up during %s: %s" % (STATE["op"], current))
127
+ elif reported:
128
+ remove_file(DIALOG)
129
+ reported = False
130
+ elif current and not reported and now() - since >= DIALOG_GRACE_S:
131
+ write_json_atomic(DIALOG, {"title": current, "op": STATE["op"]})
132
+ reported = True
133
+
134
+
135
+ def start_dialog_watch():
136
+ import threading
137
+
138
+ def run():
139
+ try:
140
+ watch_dialogs()
141
+ except BaseException as e:
142
+ log("dialog watch stopped: %s" % to_text(e))
143
+ t = threading.Thread(target=run)
144
+ t.daemon = True
145
+ t.start()
146
+
147
+
148
+ def sleep_ms(se, ms):
149
+ delay = getattr(getattr(se, "system", None), "delay", None)
150
+ if delay is not None and not hasattr(se, "FAKE"):
151
+ try:
152
+ delay(ms)
153
+ return
154
+ except Exception:
155
+ pass
156
+ time.sleep(ms / 1000.0)
157
+
158
+
159
+ def next_request():
160
+ try:
161
+ names = sorted(n for n in os.listdir(REQ_DIR) if n.endswith(".json"))
162
+ except OSError:
163
+ return None
164
+ return names[0] if names else None
165
+
166
+
167
+ def handle(ops_mod, fname):
168
+ path = os.path.join(REQ_DIR, fname)
169
+ rid = fname[:-5]
170
+ try:
171
+ # Another process (an antivirus scan, the indexer) can hold a new file for a moment.
172
+ req = retry_fs(read_json, path)
173
+ except Exception as e:
174
+ remove_file(path)
175
+ write_json_atomic(os.path.join(RES_DIR, rid + ".json"), {
176
+ "id": rid, "ok": False, "error": {"code": "bad_request", "message": "Unreadable request: %s" % e}})
177
+ return True
178
+ remove_file(path)
179
+ rid = req.get("id") or rid
180
+ op = req.get("op")
181
+ args = req.get("args") or {}
182
+ STATE["count"] += 1
183
+ beat("busy", op, rid, req.get("deadline_ms"))
184
+ started = now()
185
+ STATE["op"] = op
186
+ STATE["busy_since"] = started
187
+ stop = False
188
+ if op == "shutdown":
189
+ res = {"id": rid, "ok": True, "result": {"bye": True}}
190
+ stop = True
191
+ else:
192
+ fn = ops_mod.OPS.get(op)
193
+ if fn is None:
194
+ res = {"id": rid, "ok": False, "error": {"code": "unknown_op", "message": "Unknown op: %s" % op}}
195
+ else:
196
+ try:
197
+ res = {"id": rid, "ok": True, "result": fn(args)}
198
+ except ops_mod.OpError as e:
199
+ res = {"id": rid, "ok": False, "error": {"code": e.code, "message": to_text(e.message)}}
200
+ except BaseException as e: # IronPython: KeyboardInterrupt is not an Exception
201
+ res = {"id": rid, "ok": False, "error": {
202
+ "code": "codesys_error", "message": to_text(e) or e.__class__.__name__,
203
+ "detail": to_text(traceback.format_exc())}}
204
+ STATE["busy_since"] = None
205
+ res["ms"] = int((now() - started) * 1000)
206
+ # Go idle before publishing the result: once the server sees the result it may
207
+ # read the heartbeat right away, and it must not find this op still "busy".
208
+ beat("idle")
209
+ write_json_atomic(os.path.join(RES_DIR, rid + ".json"), res)
210
+ log("%s %s %s %dms" % (rid, op, "ok" if res.get("ok") else res["error"]["code"], res["ms"]))
211
+ return not stop
212
+
213
+
214
+ def main():
215
+ for d in (REQ_DIR, RES_DIR):
216
+ if not os.path.isdir(d):
217
+ os.makedirs(d)
218
+ log("watcher starting pid=%s parent=%s python=%s" % (PID, PARENT_PID, sys.version.split()[0]))
219
+ import scriptengine as se
220
+ import mcp_ops
221
+ import mcp_sim # noqa: F401 (adds its operations to mcp_ops.OPS)
222
+ prompts = setup_prompts(se)
223
+ log(prompts)
224
+ if not hasattr(se, "FAKE"):
225
+ start_dialog_watch()
226
+ beat("idle")
227
+ write_json_atomic(READY, {"pid": PID, "ts": now(), "python": sys.version.split()[0], "prompts": prompts, "scripting": scripting_version()})
228
+ last_parent_check = 0.0
229
+ while True:
230
+ t = now()
231
+ if PARENT_PID and t - last_parent_check > 2.0:
232
+ last_parent_check = t
233
+ if not pid_alive(PARENT_PID):
234
+ log("parent %s gone; exiting" % PARENT_PID)
235
+ break
236
+ fname = next_request()
237
+ if fname:
238
+ if not handle(mcp_ops, fname):
239
+ log("shutdown requested")
240
+ break
241
+ continue
242
+ if t - STATE["last_beat"] > 1.0:
243
+ beat("idle")
244
+ sleep_ms(se, 40)
245
+ beat("stopped")
246
+
247
+
248
+ try:
249
+ main()
250
+ except BaseException:
251
+ log("watcher crashed: " + traceback.format_exc())
252
+ try:
253
+ beat("crashed")
254
+ except Exception:
255
+ pass
@@ -1,29 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(grep:*)",
5
- "Bash(grep:*)",
6
- "Bash(npm version:*)",
7
- "Bash(npm version:*)",
8
- "Bash(cat:*)",
9
- "Bash(cat:*)",
10
- "Bash(npm run build:*)",
11
- "Bash(npm run build:*)",
12
- "Bash(npm install:*)",
13
- "Bash(npm install:*)",
14
- "Bash(npx tsc:*)",
15
- "Bash(npx tsc:*)",
16
- "Bash(npm pack:*)",
17
- "Bash(npm pack:*)",
18
- "Bash(npm publish:*)",
19
- "Bash(npm publish:*)",
20
- "Bash(ls:*)",
21
- "Bash(ls:*)",
22
- "Bash(mkdir:*)",
23
- "Bash(mkdir:*)",
24
- "Bash(node:*)",
25
- "Bash(node:*)"
26
- ],
27
- "deny": []
28
- }
29
- }
package/CLAUDE.md DELETED
@@ -1,26 +0,0 @@
1
- # CLAUDE.md
2
-
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
-
5
- ## Build Commands
6
- - `npm run build` - Compile TypeScript to JavaScript
7
- - `npm start` - Start the MCP server
8
- - `tsc` - Run TypeScript compiler manually
9
-
10
- ## Coding Style Guidelines
11
- - **Imports**: Import 'os' first if needed, then group related imports
12
- - **Naming**: Use camelCase for variables/functions, PascalCase for classes/interfaces
13
- - **Types**: TypeScript with interfaces for structured data, avoid implicit 'any'
14
- - **Error Handling**: Check conditions early, throw meaningful errors with context
15
- - **Comments**: Add JSDoc comments for functions, especially exported ones
16
- - **Formatting**: Follow existing indentation (2 spaces) and code patterns
17
- - **Path Handling**: Use path.join() for cross-platform compatibility
18
- - **Promises**: Prefer async/await over raw promises when possible
19
- - **Logging**: Use console.error for diagnostic/debug messages
20
-
21
- ## Project Structure
22
- - `/src` - TypeScript source files
23
- - `/scripts` - Python scripts for CODESYS integration
24
- - `/dist` - Compiled JavaScript (generated)
25
- - `/docs` - Documentation files
26
- - `/examples` - Example configurations and usage
package/CONTRIBUTING.md DELETED
@@ -1,72 +0,0 @@
1
- # Contributing to @codesys/mcp-toolkit
2
-
3
- Thank you for considering contributing to the CODESYS MCP Toolkit! This document provides guidelines and steps for contributing.
4
-
5
- ## Code of Conduct
6
-
7
- Please be respectful and considerate of others when contributing to this project. We aim to foster an inclusive and welcoming community.
8
-
9
- ## How Can I Contribute?
10
-
11
- ### Reporting Bugs
12
-
13
- Bug reports help improve the project! When reporting bugs:
14
-
15
- 1. Check if the bug has already been reported
16
- 2. Use the bug report template
17
- 3. Include detailed steps to reproduce the issue
18
- 4. Describe the expected vs. actual behavior
19
- 5. Include screenshots if applicable
20
- 6. Provide environment details (OS, Node.js version, etc.)
21
-
22
- ### Suggesting Features
23
-
24
- We welcome feature suggestions! When suggesting features:
25
-
26
- 1. Describe the problem your feature would solve
27
- 2. Explain how your feature would work
28
- 3. Consider potential alternatives
29
- 4. Provide examples of similar features in other projects (if applicable)
30
-
31
- ### Pull Requests
32
-
33
- 1. Fork the repository
34
- 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
35
- 3. Make your changes
36
- 4. Run tests and ensure they pass
37
- 5. Commit your changes (`git commit -m 'Add amazing feature'`)
38
- 6. Push to your branch (`git push origin feature/amazing-feature`)
39
- 7. Open a Pull Request
40
-
41
- ## Development Setup
42
-
43
- 1. Clone the repository
44
- ```bash
45
- git clone https://github.com/yourusername/codesys-mcp-toolkit.git
46
- cd codesys-mcp-toolkit
47
- ```
48
-
49
- 2. Install dependencies
50
- ```bash
51
- npm install
52
- ```
53
-
54
- 3. Build the project
55
- ```bash
56
- npm run build
57
- ```
58
-
59
- ## Style Guidelines
60
-
61
- - Follow existing code formatting patterns
62
- - Use meaningful variable and function names
63
- - Add comments where necessary to explain complex logic
64
- - Update documentation when changing functionality
65
-
66
- ## Testing
67
-
68
- - Add tests for new features
69
- - Ensure all tests pass before submitting a PR
70
- - Consider edge cases in your tests
71
-
72
- Thank you for contributing!