@aryacode/dsh-engineering 0.0.0-stage → 0.4.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/CHANGELOG.md +128 -0
- package/LICENSE +21 -0
- package/README.md +230 -2
- package/cordis.patch.yml +123 -0
- package/lib/client.js +3448 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +9 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +94 -3
- package/vendor/ftview/README.md +55 -0
- package/vendor/ftview/SKILL.md +74 -0
- package/vendor/ftview/lib/checks.d.ts +27 -0
- package/vendor/ftview/lib/checks.js +39 -0
- package/vendor/ftview/lib/core.d.ts +208 -0
- package/vendor/ftview/lib/core.js +160 -0
- package/vendor/ftview/lib/edit/drafts.d.ts +49 -0
- package/vendor/ftview/lib/edit/drafts.js +84 -0
- package/vendor/ftview/lib/edit/operations.d.ts +62 -0
- package/vendor/ftview/lib/edit/operations.js +126 -0
- package/vendor/ftview/lib/edit/prepare.d.ts +38 -0
- package/vendor/ftview/lib/edit/prepare.js +133 -0
- package/vendor/ftview/lib/edit/tools.d.ts +25 -0
- package/vendor/ftview/lib/edit/tools.js +198 -0
- package/vendor/ftview/lib/edit.d.ts +30 -0
- package/vendor/ftview/lib/edit.js +38 -0
- package/vendor/ftview/lib/index.d.ts +31 -0
- package/vendor/ftview/lib/index.js +36 -0
- package/vendor/ftview/lib/model.d.ts +80 -0
- package/vendor/ftview/lib/model.js +267 -0
- package/vendor/ftview/lib/session.d.ts +48 -0
- package/vendor/ftview/lib/session.js +98 -0
- package/vendor/ftview/lib/skill.d.ts +29 -0
- package/vendor/ftview/lib/skill.js +43 -0
- package/vendor/ftview/lib/studio/discover.d.ts +36 -0
- package/vendor/ftview/lib/studio/discover.js +87 -0
- package/vendor/ftview/lib/studio/preflight.d.ts +51 -0
- package/vendor/ftview/lib/studio/preflight.js +64 -0
- package/vendor/ftview/lib/studio/tools.d.ts +15 -0
- package/vendor/ftview/lib/studio/tools.js +83 -0
- package/vendor/ftview/lib/studio.d.ts +30 -0
- package/vendor/ftview/lib/studio.js +37 -0
- package/vendor/ftview/lib/tools.d.ts +14 -0
- package/vendor/ftview/lib/tools.js +202 -0
- package/vendor/l5x/README.md +62 -0
- package/vendor/l5x/SKILL.md +81 -0
- package/vendor/l5x/lib/approvals.d.ts +50 -0
- package/vendor/l5x/lib/approvals.js +93 -0
- package/vendor/l5x/lib/client.js +412 -0
- package/vendor/l5x/lib/client.js.map +1 -0
- package/vendor/l5x/lib/core/errors.d.ts +38 -0
- package/vendor/l5x/lib/core/errors.js +48 -0
- package/vendor/l5x/lib/core/params.d.ts +49 -0
- package/vendor/l5x/lib/core/params.js +39 -0
- package/vendor/l5x/lib/core/result.d.ts +46 -0
- package/vendor/l5x/lib/core/result.js +21 -0
- package/vendor/l5x/lib/core/tool.d.ts +102 -0
- package/vendor/l5x/lib/core/tool.js +19 -0
- package/vendor/l5x/lib/drafts.d.ts +66 -0
- package/vendor/l5x/lib/drafts.js +98 -0
- package/vendor/l5x/lib/editing.d.ts +30 -0
- package/vendor/l5x/lib/editing.js +42 -0
- package/vendor/l5x/lib/engine/acd-edit.d.ts +13 -0
- package/vendor/l5x/lib/engine/acd-edit.js +115 -0
- package/vendor/l5x/lib/engine/analysis.d.ts +22 -0
- package/vendor/l5x/lib/engine/analysis.js +258 -0
- package/vendor/l5x/lib/engine/capabilities.d.ts +9 -0
- package/vendor/l5x/lib/engine/capabilities.js +40 -0
- package/vendor/l5x/lib/engine/change-set.d.ts +122 -0
- package/vendor/l5x/lib/engine/change-set.js +446 -0
- package/vendor/l5x/lib/engine/configuration.d.ts +21 -0
- package/vendor/l5x/lib/engine/configuration.js +42 -0
- package/vendor/l5x/lib/engine/details.d.ts +47 -0
- package/vendor/l5x/lib/engine/details.js +173 -0
- package/vendor/l5x/lib/engine/direct-polling.d.ts +91 -0
- package/vendor/l5x/lib/engine/direct-polling.js +193 -0
- package/vendor/l5x/lib/engine/download-review.d.ts +15 -0
- package/vendor/l5x/lib/engine/download-review.js +63 -0
- package/vendor/l5x/lib/engine/logic-syntax.d.ts +60 -0
- package/vendor/l5x/lib/engine/logic-syntax.js +237 -0
- package/vendor/l5x/lib/engine/observations.d.ts +41 -0
- package/vendor/l5x/lib/engine/observations.js +67 -0
- package/vendor/l5x/lib/engine/project-analysis.d.ts +131 -0
- package/vendor/l5x/lib/engine/project-analysis.js +314 -0
- package/vendor/l5x/lib/engine/review-evidence.d.ts +8 -0
- package/vendor/l5x/lib/engine/review-evidence.js +49 -0
- package/vendor/l5x/lib/engine/review.d.ts +18 -0
- package/vendor/l5x/lib/engine/review.js +123 -0
- package/vendor/l5x/lib/engine/studio-sdk.d.ts +101 -0
- package/vendor/l5x/lib/engine/studio-sdk.js +571 -0
- package/vendor/l5x/lib/engine/tag-resolution.d.ts +20 -0
- package/vendor/l5x/lib/engine/tag-resolution.js +169 -0
- package/vendor/l5x/lib/engine/usage.d.ts +16 -0
- package/vendor/l5x/lib/engine/usage.js +92 -0
- package/vendor/l5x/lib/engine/xml-regions.d.ts +23 -0
- package/vendor/l5x/lib/engine/xml-regions.js +77 -0
- package/vendor/l5x/lib/fs.d.ts +17 -0
- package/vendor/l5x/lib/fs.js +36 -0
- package/vendor/l5x/lib/index.d.ts +36 -0
- package/vendor/l5x/lib/index.js +42 -0
- package/vendor/l5x/lib/live.d.ts +48 -0
- package/vendor/l5x/lib/live.js +48 -0
- package/vendor/l5x/lib/model/aois.d.ts +39 -0
- package/vendor/l5x/lib/model/aois.js +112 -0
- package/vendor/l5x/lib/model/errors.d.ts +15 -0
- package/vendor/l5x/lib/model/errors.js +18 -0
- package/vendor/l5x/lib/model/index.d.ts +20 -0
- package/vendor/l5x/lib/model/index.js +19 -0
- package/vendor/l5x/lib/model/module.d.ts +37 -0
- package/vendor/l5x/lib/model/module.js +54 -0
- package/vendor/l5x/lib/model/modules.d.ts +24 -0
- package/vendor/l5x/lib/model/modules.js +55 -0
- package/vendor/l5x/lib/model/parse.d.ts +32 -0
- package/vendor/l5x/lib/model/parse.js +116 -0
- package/vendor/l5x/lib/model/project.d.ts +15 -0
- package/vendor/l5x/lib/model/project.js +31 -0
- package/vendor/l5x/lib/model/routines.d.ts +71 -0
- package/vendor/l5x/lib/model/routines.js +183 -0
- package/vendor/l5x/lib/model/tags.d.ts +51 -0
- package/vendor/l5x/lib/model/tags.js +119 -0
- package/vendor/l5x/lib/model/types.d.ts +304 -0
- package/vendor/l5x/lib/model/types.js +12 -0
- package/vendor/l5x/lib/model/udts.d.ts +34 -0
- package/vendor/l5x/lib/model/udts.js +81 -0
- package/vendor/l5x/lib/model/xml.d.ts +139 -0
- package/vendor/l5x/lib/model/xml.js +291 -0
- package/vendor/l5x/lib/model/xref.d.ts +28 -0
- package/vendor/l5x/lib/model/xref.js +154 -0
- package/vendor/l5x/lib/session.d.ts +119 -0
- package/vendor/l5x/lib/session.js +236 -0
- package/vendor/l5x/lib/skill.d.ts +30 -0
- package/vendor/l5x/lib/skill.js +44 -0
- package/vendor/l5x/lib/studio-state.d.ts +77 -0
- package/vendor/l5x/lib/studio-state.js +79 -0
- package/vendor/l5x/lib/studio.d.ts +29 -0
- package/vendor/l5x/lib/studio.js +40 -0
- package/vendor/l5x/lib/tools/checks.d.ts +19 -0
- package/vendor/l5x/lib/tools/checks.js +140 -0
- package/vendor/l5x/lib/tools/common.d.ts +41 -0
- package/vendor/l5x/lib/tools/common.js +84 -0
- package/vendor/l5x/lib/tools/controller.d.ts +21 -0
- package/vendor/l5x/lib/tools/controller.js +170 -0
- package/vendor/l5x/lib/tools/drafts.d.ts +23 -0
- package/vendor/l5x/lib/tools/drafts.js +270 -0
- package/vendor/l5x/lib/tools/live.d.ts +21 -0
- package/vendor/l5x/lib/tools/live.js +175 -0
- package/vendor/l5x/lib/tools/read.d.ts +19 -0
- package/vendor/l5x/lib/tools/read.js +245 -0
- package/vendor/l5x/lib/tools/references.d.ts +20 -0
- package/vendor/l5x/lib/tools/references.js +269 -0
- package/vendor/l5x/lib/tools/studio.d.ts +21 -0
- package/vendor/l5x/lib/tools/studio.js +193 -0
- package/vendor/l5x/poll-worker.mjs +77 -0
- package/vendor/l5x/studio-bridge/NuGet.Config +1 -0
- package/vendor/l5x/studio-bridge/Program.cs +304 -0
- package/vendor/l5x/studio-bridge/StudioBridge.csproj +11 -0
- package/vendor/mwi/README.md +60 -0
- package/vendor/mwi/SKILL.md +87 -0
- package/vendor/mwi/bridge/README.md +8 -0
- package/vendor/mwi/bridge/mw_bridge.ps1 +2763 -0
- package/vendor/mwi/bridge/sdk-interop-importer.cs +30 -0
- package/vendor/mwi/bridge/sdk-variable-reader.cs +22 -0
- package/vendor/mwi/bridge/start_bridge.cmd +21 -0
- package/vendor/mwi/bridge/watch_dialogs.cmd +13 -0
- package/vendor/mwi/bridge/watch_dialogs.ps1 +153 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__init__.py +14 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/__init__.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/block_interfaces.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/cfb.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/datatypes.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/declarations.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/errors.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/graphical_listing.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/grid.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/knowledge.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/mwt_bind.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/navigation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/program_checks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/project.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/staging.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/structure.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/task_bindings.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tasks.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/tree.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/validation.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variable_edit.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/variables.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/__pycache__/workflow.cpython-312.pyc +0 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/block_interfaces.py +275 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/build.py +339 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/capture.py +448 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/cfb.py +765 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/com.py +268 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/datatypes.py +204 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/declarations.py +399 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/descriptions.py +30 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/eip.py +292 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/errors.py +36 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/graphical_listing.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/grid.py +227 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/ide.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/iec.py +317 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/installed_help.py +151 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/knowledge.py +218 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/libraries.py +250 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/manuals.py +520 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/mwt_bind.py +133 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/navigation.py +44 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/pou_writer.py +1547 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/program_checks.py +360 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/project.py +704 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/catalog.json +672 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/reference_data/patterns.json +86 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/restore.py +208 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/server.py +1913 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/snapshot.py +138 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/st_comments.py +25 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/staging.py +179 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/stlint.py +676 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/structure.py +98 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/task_bindings.py +68 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tasks.py +162 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/transaction.py +157 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree.py +563 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/tree_writer.py +790 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/uia.py +465 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/validation.py +41 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variable_edit.py +183 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/variables.py +472 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/workflow.py +130 -0
- package/vendor/mwi/code/engine/motionworks_iec_mcp/writer.py +666 -0
- package/vendor/mwi/code/mw_code.py +1258 -0
- package/vendor/mwi/legacy/edit-session.js +50 -0
- package/vendor/mwi/legacy/fb-insertion.js +74 -0
- package/vendor/mwi/legacy/native-code.js +83 -0
- package/vendor/mwi/legacy/native-groups.js +75 -0
- package/vendor/mwi/legacy/native-structure.js +143 -0
- package/vendor/mwi/legacy/native-variables.js +77 -0
- package/vendor/mwi/legacy/pou-conversion.js +47 -0
- package/vendor/mwi/legacy/pou-package.js +66 -0
- package/vendor/mwi/lib/core/bridge.d.ts +71 -0
- package/vendor/mwi/lib/core/bridge.js +53 -0
- package/vendor/mwi/lib/core/code-engine.d.ts +47 -0
- package/vendor/mwi/lib/core/code-engine.js +109 -0
- package/vendor/mwi/lib/core/errors.d.ts +37 -0
- package/vendor/mwi/lib/core/errors.js +47 -0
- package/vendor/mwi/lib/core/params.d.ts +52 -0
- package/vendor/mwi/lib/core/params.js +46 -0
- package/vendor/mwi/lib/core/ps-bridge.d.ts +62 -0
- package/vendor/mwi/lib/core/ps-bridge.js +127 -0
- package/vendor/mwi/lib/core/result.d.ts +45 -0
- package/vendor/mwi/lib/core/result.js +20 -0
- package/vendor/mwi/lib/core/tool.d.ts +80 -0
- package/vendor/mwi/lib/core/tool.js +55 -0
- package/vendor/mwi/lib/edit/compare.d.ts +33 -0
- package/vendor/mwi/lib/edit/compare.js +70 -0
- package/vendor/mwi/lib/edit.d.ts +20 -0
- package/vendor/mwi/lib/edit.js +32 -0
- package/vendor/mwi/lib/index.d.ts +21 -0
- package/vendor/mwi/lib/index.js +33 -0
- package/vendor/mwi/lib/read/service.d.ts +117 -0
- package/vendor/mwi/lib/read/service.js +229 -0
- package/vendor/mwi/lib/read.d.ts +21 -0
- package/vendor/mwi/lib/read.js +33 -0
- package/vendor/mwi/lib/session/project.d.ts +34 -0
- package/vendor/mwi/lib/session/project.js +54 -0
- package/vendor/mwi/lib/session/service.d.ts +167 -0
- package/vendor/mwi/lib/session/service.js +566 -0
- package/vendor/mwi/lib/session/stage.d.ts +84 -0
- package/vendor/mwi/lib/session/stage.js +187 -0
- package/vendor/mwi/lib/session/workspace.d.ts +52 -0
- package/vendor/mwi/lib/session/workspace.js +90 -0
- package/vendor/mwi/lib/skill.d.ts +36 -0
- package/vendor/mwi/lib/skill.js +56 -0
- package/vendor/mwi/lib/tools/edit.d.ts +20 -0
- package/vendor/mwi/lib/tools/edit.js +286 -0
- package/vendor/mwi/lib/tools/read.d.ts +19 -0
- package/vendor/mwi/lib/tools/read.js +161 -0
- package/vendor/mwi/lib/tools/session.d.ts +20 -0
- package/vendor/mwi/lib/tools/session.js +166 -0
- package/vendor/mwi/lib/tools/verify.d.ts +19 -0
- package/vendor/mwi/lib/tools/verify.js +85 -0
- package/vendor/mwi/lib/verify.d.ts +17 -0
- package/vendor/mwi/lib/verify.js +29 -0
|
@@ -0,0 +1,1913 @@
|
|
|
1
|
+
"""FastMCP tool surface for offline Yaskawa MotionWorks IEC 3 Pro projects.
|
|
2
|
+
|
|
3
|
+
Run over stdio::
|
|
4
|
+
|
|
5
|
+
motionworks-iec-mcp
|
|
6
|
+
|
|
7
|
+
Tier 1 tools (``mw_*``) are read-only and safe to call while MotionWorks IEC is
|
|
8
|
+
open. They are the majority of this module on purpose: an agent that can read
|
|
9
|
+
the project accurately needs far less help writing to it.
|
|
10
|
+
|
|
11
|
+
Design rules that all tools follow:
|
|
12
|
+
|
|
13
|
+
* Return readable text, not raw JSON dumps. The caller is a language model;
|
|
14
|
+
a compact labelled report is more useful than a serialised object.
|
|
15
|
+
* Never guess. When something is unsupported (MotionWorks' compressed stream
|
|
16
|
+
container), say so explicitly and name the affected POUs.
|
|
17
|
+
* Resolve ``project`` leniently: full path, ``.mwt`` file, or a bare name.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from mcp.server.fastmcp import FastMCP
|
|
27
|
+
|
|
28
|
+
from . import project as P
|
|
29
|
+
from .errors import MotionWorksError, NotFound, ProjectNotFound, UnsupportedFormat
|
|
30
|
+
from .variables import Variable
|
|
31
|
+
|
|
32
|
+
INSTRUCTIONS = """\
|
|
33
|
+
Read and (in later tiers) safely edit offline Yaskawa MotionWorks IEC 3 Pro
|
|
34
|
+
projects on disk.
|
|
35
|
+
|
|
36
|
+
The project on disk is an expanded directory (containing POE/, C/, src.st1)
|
|
37
|
+
beside a small .mwt wrapper file. POU sources are OLE/CFB compound files: each
|
|
38
|
+
POU stores its variables as text in <POU>V.VB and its body in <POU>.STB
|
|
39
|
+
(Structured Text) or <POU>.GB (graphical ladder/FBD).
|
|
40
|
+
|
|
41
|
+
Critical workflow rules:
|
|
42
|
+
1. These tools never build. After any edit you MUST open MotionWorks IEC and
|
|
43
|
+
run Build -> Rebuild Project, then Make. Rebuild is required because tmp.sto
|
|
44
|
+
caches are stale after external edits.
|
|
45
|
+
2. Never trust tmp.sto. It is build output, not source.
|
|
46
|
+
3. Do not download to a controller or command motion.
|
|
47
|
+
4. Read mw_get_symbol before referencing any symbol, so you do not invent names.
|
|
48
|
+
5. Graphical LD/FBD bodies cannot be authored byte-by-byte. Copy a working
|
|
49
|
+
body from a donor POU of the same language instead.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
#: log_level is raised to WARNING so the SDK does not emit an INFO line to
|
|
53
|
+
#: stderr for every request. MCP uses stdout for the protocol, but noisy stderr
|
|
54
|
+
#: still clutters agent logs and can be mistaken for a problem.
|
|
55
|
+
mcp = FastMCP("motionworks-iec", instructions=INSTRUCTIONS, log_level="WARNING")
|
|
56
|
+
|
|
57
|
+
#: Search root used when a tool is given a bare project name. Overridable so a
|
|
58
|
+
#: user with projects elsewhere does not need to pass full paths everywhere.
|
|
59
|
+
SEARCH_ROOT_ENV = "MOTIONWORKS_MCP_ROOT"
|
|
60
|
+
DEFAULT_SEARCH_ROOT = "Yaskawa"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _search_root() -> Path:
|
|
64
|
+
configured = os.environ.get(SEARCH_ROOT_ENV)
|
|
65
|
+
if configured:
|
|
66
|
+
return Path(configured)
|
|
67
|
+
return Path(DEFAULT_SEARCH_ROOT)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _project(project: str | None) -> P.Project:
|
|
71
|
+
"""Resolve a project argument, falling back to a sensible default.
|
|
72
|
+
|
|
73
|
+
Resolution order: explicit argument, then an ancestor of the current
|
|
74
|
+
working directory that looks like a project, then the only discovered
|
|
75
|
+
project. This keeps the common case (one project, agent opened in it) free
|
|
76
|
+
of ceremony while still refusing to guess when several are present.
|
|
77
|
+
"""
|
|
78
|
+
if project:
|
|
79
|
+
return P.resolve(project, _search_root())
|
|
80
|
+
|
|
81
|
+
# Walk up from cwd looking for a project root.
|
|
82
|
+
cwd = Path.cwd().resolve()
|
|
83
|
+
for candidate in (cwd, *cwd.parents):
|
|
84
|
+
if P.looks_like_project(candidate):
|
|
85
|
+
return P.Project(root=candidate)
|
|
86
|
+
|
|
87
|
+
found = P.discover(_search_root())
|
|
88
|
+
if len(found) == 1:
|
|
89
|
+
return P.Project(root=found[0])
|
|
90
|
+
if not found:
|
|
91
|
+
raise ProjectNotFound(
|
|
92
|
+
f"No project given and none discovered under {_search_root()}. "
|
|
93
|
+
f"Pass the project path, or set {SEARCH_ROOT_ENV}."
|
|
94
|
+
)
|
|
95
|
+
listing = "\n ".join(str(p) for p in found)
|
|
96
|
+
raise ProjectNotFound(
|
|
97
|
+
f"No project given and {len(found)} were discovered; pass one of:\n {listing}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _fail(exc: Exception) -> str:
|
|
102
|
+
"""Render an error as an actionable message instead of a stack trace."""
|
|
103
|
+
if isinstance(exc, MotionWorksError):
|
|
104
|
+
return f"ERROR: {exc}"
|
|
105
|
+
return f"ERROR ({type(exc).__name__}): {exc}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _format_variables(variables: list[Variable], indent: str = " ") -> list[str]:
|
|
109
|
+
"""Render declarations grouped by block, with a classification per line."""
|
|
110
|
+
lines: list[str] = []
|
|
111
|
+
section = None
|
|
112
|
+
for variable in variables:
|
|
113
|
+
if variable.section != section:
|
|
114
|
+
section = variable.section
|
|
115
|
+
lines.append(f"{indent}[{section}]")
|
|
116
|
+
flags = []
|
|
117
|
+
if variable.is_external:
|
|
118
|
+
flags.append("external")
|
|
119
|
+
elif variable.is_fb_instance:
|
|
120
|
+
flags.append("fb-instance")
|
|
121
|
+
suffix = f" ({', '.join(flags)})" if flags else ""
|
|
122
|
+
lines.append(f"{indent}{variable.format_line()}{suffix}")
|
|
123
|
+
return lines
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _format_library(entry: str) -> str:
|
|
127
|
+
"""Render one ``@LIBRARY.LST`` line as ``[KIND] name (path)``.
|
|
128
|
+
|
|
129
|
+
The list is semicolon-separated as ``kind;path;name;version``. For ``USER``
|
|
130
|
+
entries MotionWorks writes the literal ``LIST`` as the name, so the real
|
|
131
|
+
library name has to come from the final path component.
|
|
132
|
+
"""
|
|
133
|
+
parts = entry.split(";")
|
|
134
|
+
kind = parts[0].strip() if parts else "?"
|
|
135
|
+
path = parts[1].strip() if len(parts) > 1 else ""
|
|
136
|
+
name = parts[2].strip() if len(parts) > 2 else ""
|
|
137
|
+
if not name or name.upper() == "LIST":
|
|
138
|
+
# Derive from the directory name, which is the library's real identity.
|
|
139
|
+
name = Path(path).name or name or entry
|
|
140
|
+
return f"[{kind}] {name}" + (f" ({path})" if path else "")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Tier 1 - discovery
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@mcp.tool()
|
|
149
|
+
def mw_list_projects(search_root: str | None = None) -> str:
|
|
150
|
+
"""List every expanded MotionWorks IEC project found on disk.
|
|
151
|
+
|
|
152
|
+
Call this first when you do not know the project path. Returns each
|
|
153
|
+
project's name, full path, POU count by language, and last-change stamp.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
search_root: Directory to search. Defaults to the configured root.
|
|
157
|
+
"""
|
|
158
|
+
root = Path(search_root) if search_root else _search_root()
|
|
159
|
+
try:
|
|
160
|
+
projects = P.describe_all(root)
|
|
161
|
+
except Exception as exc: # noqa: BLE001
|
|
162
|
+
return _fail(exc)
|
|
163
|
+
if not projects:
|
|
164
|
+
return (
|
|
165
|
+
f"No expanded MotionWorks projects found under {root}.\n"
|
|
166
|
+
f"A project directory must contain a POE subdirectory. "
|
|
167
|
+
f"Set {SEARCH_ROOT_ENV} if your projects live elsewhere."
|
|
168
|
+
)
|
|
169
|
+
lines = [f"{len(projects)} project(s) under {root}:"]
|
|
170
|
+
for entry in projects:
|
|
171
|
+
langs = ", ".join(f"{k}:{v}" for k, v in sorted(entry["languages"].items()))
|
|
172
|
+
lines.append(f"\n{entry['name']}")
|
|
173
|
+
lines.append(f" path : {entry['root']}")
|
|
174
|
+
lines.append(f" POUs : {entry['pou_count']} ({langs})")
|
|
175
|
+
lines.append(f" last change : {entry['last_change']}")
|
|
176
|
+
return "\n".join(lines)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@mcp.tool()
|
|
180
|
+
def mw_project_summary(project: str | None = None) -> str:
|
|
181
|
+
"""Report structure for one project: POUs, tasks, libraries, and files.
|
|
182
|
+
|
|
183
|
+
Use this to orient yourself before reading or editing: it shows the task
|
|
184
|
+
assignments and library references that constrain what code can legally do.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
project: Project path, .mwt file, or bare project name.
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
proj = _project(project)
|
|
191
|
+
info = proj.inventory()
|
|
192
|
+
except Exception as exc: # noqa: BLE001
|
|
193
|
+
return _fail(exc)
|
|
194
|
+
|
|
195
|
+
lines = [
|
|
196
|
+
f"Project : {info['name']}",
|
|
197
|
+
f"Root : {info['root']}",
|
|
198
|
+
f"MWT wrapper : {info['mwt']}",
|
|
199
|
+
f"Last change : {info['last_change']}",
|
|
200
|
+
f"Resource src : {info['resource_source']}",
|
|
201
|
+
f"POUs : {info['pou_count']} "
|
|
202
|
+
f"({', '.join(f'{k}:{v}' for k, v in sorted(info['languages'].items()))})",
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
tasks: dict[str, list[str]] = info["tasks"] # type: ignore[assignment]
|
|
206
|
+
lines.append(f"\nTasks ({len(tasks)}):")
|
|
207
|
+
if tasks:
|
|
208
|
+
for task, programs in tasks.items():
|
|
209
|
+
assigned = ", ".join(programs) if programs else "<none>"
|
|
210
|
+
lines.append(f" {task:12} <- {assigned}")
|
|
211
|
+
else:
|
|
212
|
+
lines.append(" <none recorded in NODES.LST>")
|
|
213
|
+
|
|
214
|
+
libraries: list[str] = info["libraries"] # type: ignore[assignment]
|
|
215
|
+
lines.append(f"\nLibraries ({len(libraries)}):")
|
|
216
|
+
for entry in libraries:
|
|
217
|
+
lines.append(f" {_format_library(entry)}")
|
|
218
|
+
|
|
219
|
+
streams: dict[str, int] = info["tree_streams"] # type: ignore[assignment]
|
|
220
|
+
if streams:
|
|
221
|
+
lines.append(f"\nProject src.st1 streams: {streams}")
|
|
222
|
+
|
|
223
|
+
lines.append("\nPOUs:")
|
|
224
|
+
for pou in proj.pous():
|
|
225
|
+
streams_n = len(pou.stream_names())
|
|
226
|
+
lines.append(
|
|
227
|
+
f" {pou.name:24} {pou.language() or '?':3} "
|
|
228
|
+
f"{streams_n} streams {pou.directory}"
|
|
229
|
+
)
|
|
230
|
+
return "\n".join(lines)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@mcp.tool()
|
|
234
|
+
def mw_list_pous(project: str | None = None) -> str:
|
|
235
|
+
"""List the POUs in a project with language and body stream information.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
project: Project path, .mwt file, or bare project name.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
proj = _project(project)
|
|
242
|
+
except Exception as exc: # noqa: BLE001
|
|
243
|
+
return _fail(exc)
|
|
244
|
+
|
|
245
|
+
assignments = proj.task_assignments()
|
|
246
|
+
owner: dict[str, list[str]] = {}
|
|
247
|
+
for task, programs in assignments.items():
|
|
248
|
+
for program in programs:
|
|
249
|
+
owner.setdefault(program.casefold(), []).append(task)
|
|
250
|
+
|
|
251
|
+
lines = [f"POUs in {proj.name} ({len(proj.pous())}):"]
|
|
252
|
+
for pou in proj.pous():
|
|
253
|
+
body = pou.body_stream()
|
|
254
|
+
tasks = owner.get(pou.name.casefold(), [])
|
|
255
|
+
lines.append(f"\n{pou.name}")
|
|
256
|
+
lines.append(f" language : {body[1] if body else '?'}")
|
|
257
|
+
lines.append(f" body : {body[0] if body else '<none found>'}")
|
|
258
|
+
lines.append(f" tasks : {', '.join(tasks) if tasks else '<unassigned>'}")
|
|
259
|
+
try:
|
|
260
|
+
table = pou.declarations()
|
|
261
|
+
lines.append(f" variables: {len(table)}")
|
|
262
|
+
except UnsupportedFormat as exc:
|
|
263
|
+
lines.append(f" variables: UNREADABLE - {exc}")
|
|
264
|
+
return "\n".join(lines)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
# Tier 1 - variables
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@mcp.tool()
|
|
273
|
+
def mw_get_variables(
|
|
274
|
+
pou: str | None = None,
|
|
275
|
+
project: str | None = None,
|
|
276
|
+
) -> str:
|
|
277
|
+
"""Read variable declarations for one POU, or the project's globals.
|
|
278
|
+
|
|
279
|
+
Omit ``pou`` to read ``VAR_GLOBAL`` declarations from the resource. Each
|
|
280
|
+
variable is reported with its block (VAR / VAR_EXTERNAL), address, type,
|
|
281
|
+
default value, description, and a classification:
|
|
282
|
+
|
|
283
|
+
* ``external`` - declared VAR_EXTERNAL: owned by another POU or globally
|
|
284
|
+
* ``fb-instance`` - a function-block instance (stateful, needs .CLK etc.)
|
|
285
|
+
* otherwise - a local value owned by this POU
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
pou: POU name. Omit for global variables.
|
|
289
|
+
project: Project path, .mwt file, or bare project name.
|
|
290
|
+
"""
|
|
291
|
+
try:
|
|
292
|
+
proj = _project(project)
|
|
293
|
+
if pou:
|
|
294
|
+
table = proj.pou(pou).declarations()
|
|
295
|
+
title = f"Variables of POU {proj.pou(pou).name} in {proj.name}"
|
|
296
|
+
else:
|
|
297
|
+
table = proj.global_variables()
|
|
298
|
+
title = f"Global variables of {proj.name}"
|
|
299
|
+
except Exception as exc: # noqa: BLE001
|
|
300
|
+
return _fail(exc)
|
|
301
|
+
|
|
302
|
+
lines = [f"{title} ({len(table)} variables)"]
|
|
303
|
+
if table.source_stream:
|
|
304
|
+
lines.append(f"Source stream: {table.source_stream}")
|
|
305
|
+
for note in table.warnings:
|
|
306
|
+
lines.append(f"WARNING: {note}")
|
|
307
|
+
lines.append("")
|
|
308
|
+
lines.extend(_format_variables(table.variables))
|
|
309
|
+
if not table.variables:
|
|
310
|
+
lines.append(" <no variables>")
|
|
311
|
+
return "\n".join(lines)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
def mw_find_symbol(symbol: str, project: str | None = None) -> str:
|
|
316
|
+
"""Find every declaration of a symbol across the whole project.
|
|
317
|
+
|
|
318
|
+
Use this before referencing any symbol so you do not invent a name that does
|
|
319
|
+
not exist, and to discover which POU owns a symbol you need.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
symbol: Exact symbol name (case-insensitive).
|
|
323
|
+
project: Project path, .mwt file, or bare project name.
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
proj = _project(project)
|
|
327
|
+
found = P.find_symbol(proj, symbol)
|
|
328
|
+
except Exception as exc: # noqa: BLE001
|
|
329
|
+
return _fail(exc)
|
|
330
|
+
|
|
331
|
+
if not found:
|
|
332
|
+
index = P.symbol_index(proj)
|
|
333
|
+
close = [n for n in index if symbol.casefold() in n.casefold()][:15]
|
|
334
|
+
lines = [f"Symbol {symbol!r} is NOT declared anywhere in {proj.name}."]
|
|
335
|
+
if close:
|
|
336
|
+
lines.append("Names containing that text:")
|
|
337
|
+
lines.extend(f" {n}" for n in close)
|
|
338
|
+
else:
|
|
339
|
+
lines.append(
|
|
340
|
+
"It may come from a library (see mw_project_summary for the "
|
|
341
|
+
"library list) or from device I/O configuration."
|
|
342
|
+
)
|
|
343
|
+
return "\n".join(lines)
|
|
344
|
+
|
|
345
|
+
lines = [f"{symbol!r} declared {len(found)} time(s) in {proj.name}:"]
|
|
346
|
+
for variable in found:
|
|
347
|
+
lines.append(f" [{variable.section}] {variable.format_line()}")
|
|
348
|
+
if len(found) > 1:
|
|
349
|
+
lines.append(
|
|
350
|
+
"\nNOTE: multiple declarations exist. MotionWorks requires the "
|
|
351
|
+
"owner to hold a real VAR and every other POU to use VAR_EXTERNAL."
|
|
352
|
+
)
|
|
353
|
+
return "\n".join(lines)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
@mcp.tool()
|
|
357
|
+
def mw_symbol_index(project: str | None = None, limit: int = 400) -> str:
|
|
358
|
+
"""List every declared symbol and where it is declared.
|
|
359
|
+
|
|
360
|
+
This is a compact cross-reference. It does not include which lines of code
|
|
361
|
+
use a symbol, only where each symbol is declared.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
project: Project path, .mwt file, or bare project name.
|
|
365
|
+
limit: Maximum symbols to list.
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
proj = _project(project)
|
|
369
|
+
index = P.symbol_index(proj)
|
|
370
|
+
except Exception as exc: # noqa: BLE001
|
|
371
|
+
return _fail(exc)
|
|
372
|
+
|
|
373
|
+
lines = [f"{len(index)} declared symbols in {proj.name}:"]
|
|
374
|
+
for name in sorted(index, key=str.casefold):
|
|
375
|
+
where = ", ".join(index[name])
|
|
376
|
+
lines.append(f" {name:34} {where}")
|
|
377
|
+
if len(lines) > limit:
|
|
378
|
+
lines.append(f" ... truncated at {limit} lines")
|
|
379
|
+
break
|
|
380
|
+
return "\n".join(lines)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@mcp.tool()
|
|
384
|
+
def mw_library_usage(project: str | None = None, limit: int = 60) -> str:
|
|
385
|
+
"""Show which function-block and data types the project actually uses.
|
|
386
|
+
|
|
387
|
+
Use this to find which library a block comes from, and to pick a donor POU
|
|
388
|
+
that already instantiates a block correctly.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
project: Project path, .mwt file, or bare project name.
|
|
392
|
+
limit: Maximum type entries to list.
|
|
393
|
+
"""
|
|
394
|
+
try:
|
|
395
|
+
proj = _project(project)
|
|
396
|
+
usage = P.library_usage(proj)
|
|
397
|
+
except Exception as exc: # noqa: BLE001
|
|
398
|
+
return _fail(exc)
|
|
399
|
+
|
|
400
|
+
lines = [f"Declared types in {proj.name} ({len(usage)} distinct):"]
|
|
401
|
+
for name, count in list(usage.items())[:limit]:
|
|
402
|
+
lines.append(f" {count:3} x {name}")
|
|
403
|
+
if len(usage) > limit:
|
|
404
|
+
lines.append(f" ... {len(usage) - limit} more")
|
|
405
|
+
return "\n".join(lines)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Tier 1 - code bodies
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@mcp.tool()
|
|
414
|
+
def mw_get_st_body(pou: str, project: str | None = None) -> str:
|
|
415
|
+
"""Read the Structured Text body of a POU.
|
|
416
|
+
|
|
417
|
+
Returns an error for graphical (LD/FBD) POUs, whose bodies are proprietary
|
|
418
|
+
binary and are not text; use mw_pou_streams to inspect those instead.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
pou: POU name.
|
|
422
|
+
project: Project path, .mwt file, or bare project name.
|
|
423
|
+
"""
|
|
424
|
+
try:
|
|
425
|
+
proj = _project(project)
|
|
426
|
+
info = proj.pou(pou)
|
|
427
|
+
language = info.language()
|
|
428
|
+
if language is None:
|
|
429
|
+
return f"ERROR: POU {pou!r} has no recognisable body stream."
|
|
430
|
+
if language != "ST":
|
|
431
|
+
return (
|
|
432
|
+
f"POU {info.name!r} is {language} (graphical), not Structured Text.\n"
|
|
433
|
+
f"Its body is the binary stream {info.body_stream()[0]!r} and cannot "
|
|
434
|
+
f"be read or edited as text. Use mw_pou_streams to inspect its "
|
|
435
|
+
f"structure, and copy a known-good body from a donor POU rather "
|
|
436
|
+
f"than authoring LD bytes."
|
|
437
|
+
)
|
|
438
|
+
body = info.st_body()
|
|
439
|
+
except Exception as exc: # noqa: BLE001
|
|
440
|
+
return _fail(exc)
|
|
441
|
+
|
|
442
|
+
header = (
|
|
443
|
+
f"--- ST body of {info.name} in {proj.name} "
|
|
444
|
+
f"({len(body or '')} bytes) ---"
|
|
445
|
+
)
|
|
446
|
+
return f"{header}\n{body or ''}"
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
@mcp.tool()
|
|
450
|
+
def mw_pou_streams(pou: str, project: str | None = None) -> str:
|
|
451
|
+
"""List the CFB streams inside a POU's src.st1 and their byte sizes.
|
|
452
|
+
|
|
453
|
+
Useful for graphical POUs, where the .GB body stream size tells you how much
|
|
454
|
+
logic is in there, and for confirming which streams an edit will touch.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
pou: POU name.
|
|
458
|
+
project: Project path, .mwt file, or bare project name.
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
proj = _project(project)
|
|
462
|
+
info = proj.pou(pou)
|
|
463
|
+
source = info.source()
|
|
464
|
+
summary = source.summary()
|
|
465
|
+
except Exception as exc: # noqa: BLE001
|
|
466
|
+
return _fail(exc)
|
|
467
|
+
|
|
468
|
+
body = info.body_stream()
|
|
469
|
+
lines = [
|
|
470
|
+
f"Streams in {info.name}/src.st1 ({info.source_path}):",
|
|
471
|
+
]
|
|
472
|
+
for name, size in summary.items():
|
|
473
|
+
role = ""
|
|
474
|
+
upper = name.upper()
|
|
475
|
+
if upper.endswith("T.TXT"):
|
|
476
|
+
role = "worksheet comments"
|
|
477
|
+
elif upper.endswith("V.VGR"):
|
|
478
|
+
role = "binary variable grid"
|
|
479
|
+
elif upper.endswith("V.VB"):
|
|
480
|
+
role = "text variable declarations"
|
|
481
|
+
elif upper.endswith(".STB"):
|
|
482
|
+
role = "Structured Text body"
|
|
483
|
+
elif upper.endswith(".GB"):
|
|
484
|
+
role = "graphical LD/FBD body"
|
|
485
|
+
elif upper.endswith(".SN"):
|
|
486
|
+
role = "compression digest sidecar"
|
|
487
|
+
lines.append(f" {name:34} {size:>8} bytes {role}")
|
|
488
|
+
lines.append(f"\nLanguage: {body[1] if body else '?'}")
|
|
489
|
+
return "\n".join(lines)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@mcp.tool()
|
|
493
|
+
def mw_get_pou_comments(pou: str, project: str | None = None) -> str:
|
|
494
|
+
"""Read the worksheet comment stream (``<POU>T.TXT``) of a POU.
|
|
495
|
+
|
|
496
|
+
MotionWorks stores free-text worksheet notes here, separate from the body.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
pou: POU name.
|
|
500
|
+
project: Project path, .mwt file, or bare project name.
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
proj = _project(project)
|
|
504
|
+
info = proj.pou(pou)
|
|
505
|
+
name = info.comment_stream_name()
|
|
506
|
+
if name is None:
|
|
507
|
+
return f"POU {info.name!r} has no T.TXT comment stream."
|
|
508
|
+
raw = info.source().read_stream(name)
|
|
509
|
+
except Exception as exc: # noqa: BLE001
|
|
510
|
+
return _fail(exc)
|
|
511
|
+
return f"--- comments of {info.name} ({len(raw)} bytes) ---\n{raw.decode('latin1')}"
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
# Tier 1 - integrity
|
|
516
|
+
# ---------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@mcp.tool()
|
|
520
|
+
def mw_snapshot(
|
|
521
|
+
output: str | None = None,
|
|
522
|
+
project: str | None = None,
|
|
523
|
+
include_generated: bool = True,
|
|
524
|
+
) -> str:
|
|
525
|
+
"""Hash every file in a project and write a JSON manifest.
|
|
526
|
+
|
|
527
|
+
Take a snapshot BEFORE any change so you can prove exactly what was touched
|
|
528
|
+
afterwards with mw_diff_snapshot. This matters because MotionWorks rewrites
|
|
529
|
+
unrelated state on save.
|
|
530
|
+
|
|
531
|
+
Args:
|
|
532
|
+
output: Manifest path. Defaults to <project>/.motionworks-snapshot.json.
|
|
533
|
+
project: Project path, .mwt file, or bare project name.
|
|
534
|
+
include_generated: Include tmp.sto build caches and .sn sidecars.
|
|
535
|
+
"""
|
|
536
|
+
try:
|
|
537
|
+
from .snapshot import make_manifest, write_manifest
|
|
538
|
+
|
|
539
|
+
proj = _project(project)
|
|
540
|
+
target = Path(output) if output else proj.root / ".motionworks-snapshot.json"
|
|
541
|
+
manifest = make_manifest(
|
|
542
|
+
proj.root, exclude=target, include_generated=include_generated
|
|
543
|
+
)
|
|
544
|
+
write_manifest(manifest, target)
|
|
545
|
+
except Exception as exc: # noqa: BLE001
|
|
546
|
+
return _fail(exc)
|
|
547
|
+
|
|
548
|
+
files: dict[str, str] = manifest["files"] # type: ignore[assignment]
|
|
549
|
+
return (
|
|
550
|
+
f"Snapshot of {proj.name} written.\n"
|
|
551
|
+
f" manifest : {target}\n"
|
|
552
|
+
f" files : {len(files)}\n"
|
|
553
|
+
f" created : {manifest['created']}\n"
|
|
554
|
+
f"Use mw_diff_snapshot with this manifest to see what changed."
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
@mcp.tool()
|
|
559
|
+
def mw_diff_snapshot(manifest: str, project: str | None = None) -> str:
|
|
560
|
+
"""Report files added, changed, or removed since a snapshot was taken.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
manifest: Path to a manifest written by mw_snapshot.
|
|
564
|
+
project: Optional project root override; defaults to the manifest's base.
|
|
565
|
+
"""
|
|
566
|
+
try:
|
|
567
|
+
from .snapshot import diff_manifest, load_manifest
|
|
568
|
+
|
|
569
|
+
data = load_manifest(Path(manifest))
|
|
570
|
+
base = Path(project) if project else None
|
|
571
|
+
result = diff_manifest(data, base)
|
|
572
|
+
except Exception as exc: # noqa: BLE001
|
|
573
|
+
return _fail(exc)
|
|
574
|
+
|
|
575
|
+
total = sum(len(v) for v in result.values())
|
|
576
|
+
if not total:
|
|
577
|
+
return (
|
|
578
|
+
f"No differences: the tree matches the snapshot exactly "
|
|
579
|
+
f"({len(data['files'])} files, taken {data['created']})."
|
|
580
|
+
)
|
|
581
|
+
lines = [f"{total} difference(s) since snapshot taken {data['created']}:"]
|
|
582
|
+
for kind in ("changed", "added", "missing"):
|
|
583
|
+
entries = result[kind]
|
|
584
|
+
if entries:
|
|
585
|
+
lines.append(f"\n{kind.upper()} ({len(entries)}):")
|
|
586
|
+
lines.extend(f" {name}" for name in entries[:60])
|
|
587
|
+
if len(entries) > 60:
|
|
588
|
+
lines.append(f" ... and {len(entries) - 60} more")
|
|
589
|
+
return "\n".join(lines)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
@mcp.tool()
|
|
593
|
+
def mw_check_project_open(project: str | None = None) -> str:
|
|
594
|
+
"""Check whether MotionWorks IEC is running, which blocks safe editing.
|
|
595
|
+
|
|
596
|
+
Read-only tools work either way. Any Tier 2 write will refuse while the IDE
|
|
597
|
+
is open, because the IDE caches project state and would overwrite or
|
|
598
|
+
contradict an external edit.
|
|
599
|
+
|
|
600
|
+
If it is running, call `mw_close_motionworks` to close it and proceed, or pass
|
|
601
|
+
`close_ide=True` to a write tool to have it close the IDE itself.
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
project: Unused; accepted so callers can always pass the project.
|
|
605
|
+
"""
|
|
606
|
+
try:
|
|
607
|
+
from . import ide
|
|
608
|
+
|
|
609
|
+
procs = ide.motionworks_running()
|
|
610
|
+
except Exception as exc: # noqa: BLE001
|
|
611
|
+
return _fail(exc)
|
|
612
|
+
if procs:
|
|
613
|
+
listing = "\n".join(
|
|
614
|
+
f" pid {pid} {name}"
|
|
615
|
+
+ (" (main)" if name.casefold() in ide.MAIN_PROCESSES else " (helper)")
|
|
616
|
+
for pid, name in procs
|
|
617
|
+
)
|
|
618
|
+
return (
|
|
619
|
+
f"MotionWorks IEC IS RUNNING ({len(procs)} process(es)):\n{listing}\n\n"
|
|
620
|
+
f"Close the IDE before any edit, or call mw_close_motionworks. "
|
|
621
|
+
f"Read-only tools are still safe."
|
|
622
|
+
)
|
|
623
|
+
return "MotionWorks IEC is not running. Editing is safe from the IDE's point of view."
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
@mcp.tool()
|
|
627
|
+
def mw_unsupported_streams(project: str | None = None) -> str:
|
|
628
|
+
"""List streams this server cannot read, and why.
|
|
629
|
+
|
|
630
|
+
MotionWorks compresses some large POU sources into a proprietary container
|
|
631
|
+
(magic ``cadac758``) that is not a standard codec. Affected POUs cannot be
|
|
632
|
+
read or edited safely by this server. Check this before promising changes.
|
|
633
|
+
|
|
634
|
+
Args:
|
|
635
|
+
project: Project path, .mwt file, or bare project name.
|
|
636
|
+
"""
|
|
637
|
+
try:
|
|
638
|
+
proj = _project(project)
|
|
639
|
+
except Exception as exc: # noqa: BLE001
|
|
640
|
+
return _fail(exc)
|
|
641
|
+
|
|
642
|
+
blocked: list[tuple[str, str]] = []
|
|
643
|
+
for pou in proj.pous():
|
|
644
|
+
name = pou.declaration_stream_name()
|
|
645
|
+
if name is None:
|
|
646
|
+
continue
|
|
647
|
+
try:
|
|
648
|
+
pou.declarations()
|
|
649
|
+
except UnsupportedFormat as exc:
|
|
650
|
+
blocked.append((pou.name, str(exc)))
|
|
651
|
+
except Exception as exc: # noqa: BLE001
|
|
652
|
+
blocked.append((pou.name, f"{type(exc).__name__}: {exc}"))
|
|
653
|
+
|
|
654
|
+
try:
|
|
655
|
+
proj.global_variables()
|
|
656
|
+
except UnsupportedFormat as exc:
|
|
657
|
+
blocked.append(("<global variables>", str(exc)))
|
|
658
|
+
|
|
659
|
+
if not blocked:
|
|
660
|
+
return f"All readable: no unsupported streams found in {proj.name}."
|
|
661
|
+
|
|
662
|
+
lines = [
|
|
663
|
+
f"{len(blocked)} unreadable stream group(s) in {proj.name}:",
|
|
664
|
+
"",
|
|
665
|
+
]
|
|
666
|
+
for name, why in blocked:
|
|
667
|
+
lines.append(f"{name}")
|
|
668
|
+
lines.append(f" {why}")
|
|
669
|
+
lines.append("")
|
|
670
|
+
lines.append(
|
|
671
|
+
"Do not attempt to edit variables for these POUs. Their bodies can still "
|
|
672
|
+
"be inspected with mw_pou_streams."
|
|
673
|
+
)
|
|
674
|
+
return "\n".join(lines)
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# ---------------------------------------------------------------------------
|
|
678
|
+
# Tier 1 - static analysis
|
|
679
|
+
# ---------------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _lint_report(result: object, title: str) -> str:
|
|
683
|
+
"""Render a LintResult as a readable report."""
|
|
684
|
+
errors = result.errors # type: ignore[attr-defined]
|
|
685
|
+
warnings = result.warnings # type: ignore[attr-defined]
|
|
686
|
+
findings = result.findings # type: ignore[attr-defined]
|
|
687
|
+
|
|
688
|
+
if not findings:
|
|
689
|
+
return f"{title}: clean (no findings)."
|
|
690
|
+
|
|
691
|
+
lines = [f"{title}: {len(errors)} error(s), {len(warnings)} warning(s)"]
|
|
692
|
+
for finding in findings:
|
|
693
|
+
lines.append("")
|
|
694
|
+
lines.append(finding.format())
|
|
695
|
+
return "\n".join(lines)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
@mcp.tool()
|
|
699
|
+
def mw_lint_st(pou: str, project: str | None = None) -> str:
|
|
700
|
+
"""Statically check the Structured Text body of one POU.
|
|
701
|
+
|
|
702
|
+
Catches the mistakes that matter when editing ST offline, while staying
|
|
703
|
+
quiet about anything it cannot resolve (library blocks, struct members,
|
|
704
|
+
function-block pin names). Checks:
|
|
705
|
+
|
|
706
|
+
* ``undefined-assignment-target`` (error) - assigning to a name that is
|
|
707
|
+
declared nowhere. This is the "invented variable" mistake.
|
|
708
|
+
* ``unused-local`` (warning) - a ``VAR`` local never used in the body.
|
|
709
|
+
* ``unassigned-fb-output`` (warning) - a function block is called but none
|
|
710
|
+
of its outputs is read, so the result is discarded.
|
|
711
|
+
* ``unterminated-statement`` (warning) - a statement missing its ``;``.
|
|
712
|
+
|
|
713
|
+
Run this after any edit and before handing the project back to rebuild.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
pou: POU name.
|
|
717
|
+
project: Project path, .mwt file, or bare project name.
|
|
718
|
+
"""
|
|
719
|
+
try:
|
|
720
|
+
from .stlint import lint_pou
|
|
721
|
+
|
|
722
|
+
proj = _project(project)
|
|
723
|
+
info = proj.pou(pou)
|
|
724
|
+
known = P.known_symbol_names(proj)
|
|
725
|
+
result = lint_pou(info, known_symbols=known)
|
|
726
|
+
except Exception as exc: # noqa: BLE001
|
|
727
|
+
return _fail(exc)
|
|
728
|
+
return _lint_report(result, f"Lint of {info.name} in {proj.name}")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
@mcp.tool()
|
|
732
|
+
def mw_lint_project(project: str | None = None, errors_only: bool = False) -> str:
|
|
733
|
+
"""Lint every Structured Text POU in a project.
|
|
734
|
+
|
|
735
|
+
Use this to find problems introduced by a set of edits, or to assess an
|
|
736
|
+
unfamiliar project. Graphical (LD/FBD) POUs are skipped because their logic
|
|
737
|
+
is not text.
|
|
738
|
+
|
|
739
|
+
Args:
|
|
740
|
+
project: Project path, .mwt file, or bare project name.
|
|
741
|
+
errors_only: Show only errors, hiding warnings.
|
|
742
|
+
"""
|
|
743
|
+
try:
|
|
744
|
+
from .stlint import lint_pou
|
|
745
|
+
|
|
746
|
+
proj = _project(project)
|
|
747
|
+
known = P.known_symbol_names(proj)
|
|
748
|
+
blocks: list[str] = []
|
|
749
|
+
total_e = total_w = linted = skipped = 0
|
|
750
|
+
for pou in proj.pous():
|
|
751
|
+
if pou.language() != "ST":
|
|
752
|
+
skipped += 1
|
|
753
|
+
continue
|
|
754
|
+
linted += 1
|
|
755
|
+
result = lint_pou(pou, known_symbols=known)
|
|
756
|
+
total_e += len(result.errors)
|
|
757
|
+
total_w += len(result.warnings)
|
|
758
|
+
if errors_only and not result.errors:
|
|
759
|
+
continue
|
|
760
|
+
if result.findings:
|
|
761
|
+
blocks.append(_lint_report(result, pou.name))
|
|
762
|
+
except Exception as exc: # noqa: BLE001
|
|
763
|
+
return _fail(exc)
|
|
764
|
+
|
|
765
|
+
header = (
|
|
766
|
+
f"Lint of {proj.name}: {linted} ST POU(s) checked, "
|
|
767
|
+
f"{skipped} graphical skipped\n"
|
|
768
|
+
f"TOTAL: {total_e} error(s), {total_w} warning(s)"
|
|
769
|
+
)
|
|
770
|
+
if not blocks:
|
|
771
|
+
return header + (
|
|
772
|
+
"\n\nAll linted POUs are clean."
|
|
773
|
+
if not errors_only or total_e == 0
|
|
774
|
+
else "\n\nNo errors (warnings hidden)."
|
|
775
|
+
)
|
|
776
|
+
return header + "\n\n" + "\n\n".join(blocks)
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
@mcp.tool()
|
|
780
|
+
def mw_set_variable_description(
|
|
781
|
+
variable: str,
|
|
782
|
+
description: str = "",
|
|
783
|
+
pou: str | None = None,
|
|
784
|
+
project: str | None = None,
|
|
785
|
+
dry_run: bool = True,
|
|
786
|
+
) -> str:
|
|
787
|
+
"""Set or clear one variable's description comment.
|
|
788
|
+
|
|
789
|
+
This is the only write the server currently offers, chosen because it needs
|
|
790
|
+
**no binary edit at all**: MotionWorks stores descriptions only in the
|
|
791
|
+
textual ``.VB`` declaration stream and never in the ``.VGR`` binary grid.
|
|
792
|
+
(Verified: description text from real projects appears in ``.VB`` and is
|
|
793
|
+
absent from ``.VGR`` in both latin-1 and UTF-16.) That makes it the safest
|
|
794
|
+
possible mutation, and the ``.VGR`` stream is confirmed byte-identical
|
|
795
|
+
afterwards.
|
|
796
|
+
|
|
797
|
+
Safety behaviour, always enforced:
|
|
798
|
+
|
|
799
|
+
* refused while MotionWorks IEC is running -- the IDE holds project state in
|
|
800
|
+
memory and would overwrite or contradict the change;
|
|
801
|
+
* every file is backed up outside the project tree first;
|
|
802
|
+
* the result is read back and every sibling stream is verified unchanged.
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
variable: Exact variable name.
|
|
806
|
+
description: New description text. Empty string removes the description.
|
|
807
|
+
pou: POU name. Omit to edit the project's global variables.
|
|
808
|
+
project: Project path, .mwt file, or bare project name.
|
|
809
|
+
dry_run: Default True, which only reports what would change. Set to
|
|
810
|
+
False to actually write.
|
|
811
|
+
"""
|
|
812
|
+
try:
|
|
813
|
+
from .writer import WriteRefused, apply_plan, plan_variable_description
|
|
814
|
+
|
|
815
|
+
proj = _project(project)
|
|
816
|
+
plan = plan_variable_description(
|
|
817
|
+
proj.root, pou, variable, description or None
|
|
818
|
+
)
|
|
819
|
+
except Exception as exc: # noqa: BLE001
|
|
820
|
+
return _fail(exc)
|
|
821
|
+
|
|
822
|
+
where = f"POU {pou}" if pou else "global variables"
|
|
823
|
+
if not plan.changed:
|
|
824
|
+
return (
|
|
825
|
+
f"No change needed: {variable!r} in {where} of {proj.name} already "
|
|
826
|
+
f"has that description state."
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
if dry_run:
|
|
830
|
+
return "\n".join(
|
|
831
|
+
[
|
|
832
|
+
f"DRY RUN - nothing written.",
|
|
833
|
+
f" project : {proj.name}",
|
|
834
|
+
f" target : {where}",
|
|
835
|
+
f" stream : {plan.stream}",
|
|
836
|
+
f" size : {len(plan.before)} -> {len(plan.after)} bytes",
|
|
837
|
+
f" variable: {variable}",
|
|
838
|
+
f" new text: {description!r}" if description else " new text: <removed>",
|
|
839
|
+
*[f" note : {n}" for n in plan.notes],
|
|
840
|
+
"",
|
|
841
|
+
"Call again with dry_run=False to apply.",
|
|
842
|
+
]
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
try:
|
|
846
|
+
result = apply_plan(plan, proj.root)
|
|
847
|
+
except Exception as exc: # noqa: BLE001
|
|
848
|
+
return _fail(exc)
|
|
849
|
+
|
|
850
|
+
lines = [
|
|
851
|
+
f"WROTE description for {variable!r} in {where} of {proj.name}.",
|
|
852
|
+
f" stream : {result['stream']}",
|
|
853
|
+
f" size : {result['before_bytes']} -> {result['after_bytes']} bytes",
|
|
854
|
+
f" sibling streams : {result['siblings_verified']} verified unchanged",
|
|
855
|
+
]
|
|
856
|
+
for backup in result.get("backups", []):
|
|
857
|
+
lines.append(f" backup : {backup}")
|
|
858
|
+
for note in result.get("notes", []):
|
|
859
|
+
lines.append(f" note : {note}")
|
|
860
|
+
lines.append("")
|
|
861
|
+
lines.append(
|
|
862
|
+
"NEXT: open MotionWorks IEC and run Build -> Rebuild Project, then Make. "
|
|
863
|
+
"The .VGR grid was not modified and does not need to be."
|
|
864
|
+
)
|
|
865
|
+
return "\n".join(lines)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
@mcp.tool()
|
|
869
|
+
def mw_set_st_body(
|
|
870
|
+
pou: str,
|
|
871
|
+
body: str = "",
|
|
872
|
+
body_file: str = "",
|
|
873
|
+
project: str | None = None,
|
|
874
|
+
run_lint: bool = True,
|
|
875
|
+
dry_run: bool = True,
|
|
876
|
+
) -> str:
|
|
877
|
+
"""Replace a POU's Structured Text body.
|
|
878
|
+
|
|
879
|
+
Only the ``.STB`` stream changes. A body does not reference the variable
|
|
880
|
+
grid, so this needs no binary grid edit and no project-tree edit -- that is
|
|
881
|
+
why it is one of the two writes the server offers. The ``.VGR`` grid and
|
|
882
|
+
``.VB`` declarations are verified byte-identical afterwards.
|
|
883
|
+
|
|
884
|
+
Provide the new body either inline with ``body`` or, for anything sizeable,
|
|
885
|
+
with ``body_file`` pointing at a file (real bodies reach tens of kilobytes,
|
|
886
|
+
which is awkward to move through a JSON argument).
|
|
887
|
+
|
|
888
|
+
Refused, always:
|
|
889
|
+
|
|
890
|
+
* while MotionWorks IEC is running;
|
|
891
|
+
* for graphical (LD/FBD) POUs, whose bodies are proprietary binary --
|
|
892
|
+
transplant a proven body instead;
|
|
893
|
+
* when the new body fails lint, unless ``run_lint=False`` overrides it.
|
|
894
|
+
|
|
895
|
+
Args:
|
|
896
|
+
pou: POU name.
|
|
897
|
+
body: New Structured Text, inline. Ignored if body_file is given.
|
|
898
|
+
body_file: Path to a file holding the new body.
|
|
899
|
+
project: Project path, .mwt file, or bare project name.
|
|
900
|
+
run_lint: Refuse the write if the new body has lint errors.
|
|
901
|
+
dry_run: Default True, which only reports what would change.
|
|
902
|
+
"""
|
|
903
|
+
try:
|
|
904
|
+
from .writer import apply_st_body, plan_st_body, plan_st_body_from_file
|
|
905
|
+
|
|
906
|
+
proj = _project(project)
|
|
907
|
+
if body_file:
|
|
908
|
+
plan = plan_st_body_from_file(proj.root, pou, Path(body_file), run_lint)
|
|
909
|
+
else:
|
|
910
|
+
if not body.strip():
|
|
911
|
+
return (
|
|
912
|
+
"ERROR: no body supplied. Pass `body` inline, or `body_file` "
|
|
913
|
+
"with a path to the new body."
|
|
914
|
+
)
|
|
915
|
+
plan = plan_st_body(proj.root, pou, body, run_lint)
|
|
916
|
+
except Exception as exc: # noqa: BLE001
|
|
917
|
+
return _fail(exc)
|
|
918
|
+
|
|
919
|
+
if not plan.changed:
|
|
920
|
+
return f"No change needed: {pou} in {proj.name} already has that body."
|
|
921
|
+
|
|
922
|
+
if dry_run:
|
|
923
|
+
return "\n".join(
|
|
924
|
+
[
|
|
925
|
+
"DRY RUN - nothing written.",
|
|
926
|
+
f" project : {proj.name}",
|
|
927
|
+
f" POU : {pou}",
|
|
928
|
+
f" stream : {plan.stream}",
|
|
929
|
+
f" size : {len(plan.before)} -> {len(plan.after)} bytes",
|
|
930
|
+
*[f" note : {n}" for n in plan.notes],
|
|
931
|
+
"",
|
|
932
|
+
"Call again with dry_run=False to apply.",
|
|
933
|
+
]
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
try:
|
|
937
|
+
result = apply_st_body(plan, proj.root)
|
|
938
|
+
except Exception as exc: # noqa: BLE001
|
|
939
|
+
return _fail(exc)
|
|
940
|
+
|
|
941
|
+
lines = [
|
|
942
|
+
f"WROTE Structured Text body of {pou} in {proj.name}.",
|
|
943
|
+
f" stream : {result['stream']}",
|
|
944
|
+
f" size : {result['before_bytes']} -> {result['after_bytes']} bytes",
|
|
945
|
+
f" sibling streams : {result['siblings_verified']} verified unchanged",
|
|
946
|
+
]
|
|
947
|
+
for backup in result.get("backups", []):
|
|
948
|
+
lines.append(f" backup : {backup}")
|
|
949
|
+
for note in result.get("notes", []):
|
|
950
|
+
lines.append(f" note : {note}")
|
|
951
|
+
lines.append("")
|
|
952
|
+
lines.append(
|
|
953
|
+
"NEXT: open MotionWorks IEC and run Build -> Rebuild Project, then Make. "
|
|
954
|
+
"The .VGR grid and .VB declarations were not modified."
|
|
955
|
+
)
|
|
956
|
+
return "\n".join(lines)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
@mcp.tool()
|
|
960
|
+
def mw_add_variable(
|
|
961
|
+
name: str,
|
|
962
|
+
type_name: str,
|
|
963
|
+
section: str = "VAR",
|
|
964
|
+
address: str = "",
|
|
965
|
+
initial_value: str = "",
|
|
966
|
+
description: str = "",
|
|
967
|
+
pou: str = "",
|
|
968
|
+
project: str | None = None,
|
|
969
|
+
verify: bool = False,
|
|
970
|
+
dry_run: bool = True,
|
|
971
|
+
) -> str:
|
|
972
|
+
"""Add a variable declaration to a POU, or to the project's globals.
|
|
973
|
+
|
|
974
|
+
Touches only the textual ``.VB`` declaration stream. Verified by compiling:
|
|
975
|
+
the `.VGR` binary grid does not gate compilation, so a declaration added this
|
|
976
|
+
way builds successfully with the grid left byte-identical.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
name: New variable name.
|
|
980
|
+
type_name: IEC type, e.g. BOOL, INT, LREAL, MC_Power.
|
|
981
|
+
section: VAR (default), VAR_GLOBAL, VAR_EXTERNAL, VAR_INPUT, ...
|
|
982
|
+
address: Optional AT address, e.g. %MW1.100.
|
|
983
|
+
initial_value: Optional initial value, e.g. 0 or TRUE.
|
|
984
|
+
description: Optional description comment.
|
|
985
|
+
pou: POU name. Omit to edit the project's global variables.
|
|
986
|
+
project: Project path, .mwt file, or bare project name.
|
|
987
|
+
verify: Drive a real build afterwards and revert automatically if the
|
|
988
|
+
project stops compiling. Slower, needs MotionWorks.
|
|
989
|
+
dry_run: Default True, which only reports what would change.
|
|
990
|
+
"""
|
|
991
|
+
return _declaration_write(
|
|
992
|
+
"add", project=project, pou=pou or None, dry_run=dry_run, verify=verify,
|
|
993
|
+
kwargs={
|
|
994
|
+
"name": name, "type_name": type_name, "section": section,
|
|
995
|
+
"address": address or None, "initial_value": initial_value or None,
|
|
996
|
+
"description": description or None,
|
|
997
|
+
},
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
@mcp.tool()
|
|
1002
|
+
def mw_edit_variable(
|
|
1003
|
+
name: str,
|
|
1004
|
+
new_name: str = "",
|
|
1005
|
+
type_name: str = "",
|
|
1006
|
+
description: str = "",
|
|
1007
|
+
address: str = "",
|
|
1008
|
+
initial_value: str = "",
|
|
1009
|
+
pou: str = "",
|
|
1010
|
+
project: str | None = None,
|
|
1011
|
+
force: bool = False,
|
|
1012
|
+
verify: bool = False,
|
|
1013
|
+
dry_run: bool = True,
|
|
1014
|
+
) -> str:
|
|
1015
|
+
"""Change a variable's type, name, address, initial value or description.
|
|
1016
|
+
|
|
1017
|
+
Only the fields you supply are changed. Verified by compiling for an
|
|
1018
|
+
unaddressed variable; a type change **is refused** when it contradicts an
|
|
1019
|
+
``AT`` bit address, because that cannot compile (measured: retyping an
|
|
1020
|
+
I/O-mapped ``BOOL AT %IX...`` as ``DINT`` failed the build).
|
|
1021
|
+
|
|
1022
|
+
Args:
|
|
1023
|
+
name: Existing variable name.
|
|
1024
|
+
new_name: Rename to this. Any body still using the old name is checked.
|
|
1025
|
+
type_name: New type.
|
|
1026
|
+
description: New description. Pass "-" to clear it.
|
|
1027
|
+
address: New AT address.
|
|
1028
|
+
initial_value: New initial value.
|
|
1029
|
+
pou: POU name. Omit for the project's globals.
|
|
1030
|
+
project: Project path, .mwt file, or bare project name.
|
|
1031
|
+
force: Write even if a safety check objects.
|
|
1032
|
+
verify: Drive a real build afterwards and revert automatically if the
|
|
1033
|
+
project stops compiling. Slower, needs MotionWorks.
|
|
1034
|
+
dry_run: Default True, which only reports what would change.
|
|
1035
|
+
"""
|
|
1036
|
+
kwargs: dict[str, object] = {"name": name}
|
|
1037
|
+
if new_name:
|
|
1038
|
+
kwargs["new_name"] = new_name
|
|
1039
|
+
if type_name:
|
|
1040
|
+
kwargs["type_name"] = type_name
|
|
1041
|
+
if address:
|
|
1042
|
+
kwargs["address"] = address
|
|
1043
|
+
if initial_value:
|
|
1044
|
+
kwargs["initial_value"] = initial_value
|
|
1045
|
+
if description == "-":
|
|
1046
|
+
kwargs["description"] = ""
|
|
1047
|
+
elif description:
|
|
1048
|
+
kwargs["description"] = description
|
|
1049
|
+
return _declaration_write(
|
|
1050
|
+
"edit", project=project, pou=pou or None, dry_run=dry_run,
|
|
1051
|
+
kwargs=kwargs, force=force, verify=verify,
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
|
|
1055
|
+
@mcp.tool()
|
|
1056
|
+
def mw_delete_variable(
|
|
1057
|
+
name: str,
|
|
1058
|
+
pou: str = "",
|
|
1059
|
+
project: str | None = None,
|
|
1060
|
+
force: bool = False,
|
|
1061
|
+
verify: bool = False,
|
|
1062
|
+
dry_run: bool = True,
|
|
1063
|
+
) -> str:
|
|
1064
|
+
"""Delete a variable declaration.
|
|
1065
|
+
|
|
1066
|
+
CAUTION - this is the least reliable write the server offers. Measured
|
|
1067
|
+
results are mixed: deleting an unreferenced global variable compiled, but
|
|
1068
|
+
deleting a POU-local variable that was verified *not* referenced by its body
|
|
1069
|
+
still failed the build. The cause is not understood, so a delete cannot be
|
|
1070
|
+
predicted to succeed.
|
|
1071
|
+
|
|
1072
|
+
It is refused while any POU body still references the variable, but that
|
|
1073
|
+
check does not catch every failure. **Pass verify=True** to have the build
|
|
1074
|
+
driven and the change reverted automatically if it does not compile -- that
|
|
1075
|
+
is the recommended way to use this tool.
|
|
1076
|
+
|
|
1077
|
+
Args:
|
|
1078
|
+
name: Variable to delete.
|
|
1079
|
+
pou: POU name. Omit for the project's globals.
|
|
1080
|
+
project: Project path, .mwt file, or bare project name.
|
|
1081
|
+
force: Write even if the reference check objects.
|
|
1082
|
+
verify: Drive a real build afterwards and revert automatically if the
|
|
1083
|
+
project stops compiling. Strongly recommended for delete.
|
|
1084
|
+
dry_run: Default True, which only reports what would change.
|
|
1085
|
+
"""
|
|
1086
|
+
return _declaration_write(
|
|
1087
|
+
"delete", project=project, pou=pou or None, dry_run=dry_run,
|
|
1088
|
+
kwargs={"name": name}, force=force, verify=verify,
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _declaration_write(
|
|
1093
|
+
operation: str,
|
|
1094
|
+
project: str | None,
|
|
1095
|
+
pou: str | None,
|
|
1096
|
+
dry_run: bool,
|
|
1097
|
+
kwargs: dict[str, object],
|
|
1098
|
+
force: bool = False,
|
|
1099
|
+
verify: bool = False,
|
|
1100
|
+
) -> str:
|
|
1101
|
+
"""Shared body for the variable add/edit/delete tools."""
|
|
1102
|
+
try:
|
|
1103
|
+
from . import writer
|
|
1104
|
+
|
|
1105
|
+
proj = _project(project)
|
|
1106
|
+
planners = {
|
|
1107
|
+
"add": writer.plan_variable_add,
|
|
1108
|
+
"edit": writer.plan_variable_edit,
|
|
1109
|
+
"delete": writer.plan_variable_delete,
|
|
1110
|
+
}
|
|
1111
|
+
planner = planners[operation]
|
|
1112
|
+
if force:
|
|
1113
|
+
kwargs = {**kwargs, "force": True}
|
|
1114
|
+
plan = planner(proj.root, pou, **kwargs)
|
|
1115
|
+
except Exception as exc: # noqa: BLE001
|
|
1116
|
+
return _fail(exc)
|
|
1117
|
+
|
|
1118
|
+
where = f"POU {pou}" if pou else "global variables"
|
|
1119
|
+
if not plan.changed:
|
|
1120
|
+
return f"No change: {operation} on {where} of {proj.name} would do nothing."
|
|
1121
|
+
|
|
1122
|
+
if dry_run:
|
|
1123
|
+
return "\n".join(
|
|
1124
|
+
[
|
|
1125
|
+
"DRY RUN - nothing written.",
|
|
1126
|
+
f" project : {proj.name}",
|
|
1127
|
+
f" target : {where}",
|
|
1128
|
+
f" stream : {plan.stream}",
|
|
1129
|
+
f" size : {len(plan.before)} -> {len(plan.after)} bytes",
|
|
1130
|
+
*[f" note : {n}" for n in plan.notes],
|
|
1131
|
+
"",
|
|
1132
|
+
"Call again with dry_run=False to apply.",
|
|
1133
|
+
]
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
# With verify=True, use compile-then-revert: apply, build, and restore the
|
|
1137
|
+
# backup automatically if the project no longer compiles.
|
|
1138
|
+
if verify:
|
|
1139
|
+
try:
|
|
1140
|
+
from . import build as build_module
|
|
1141
|
+
|
|
1142
|
+
outcome = build_module.apply_and_verify(plan, proj.root)
|
|
1143
|
+
except Exception as exc: # noqa: BLE001
|
|
1144
|
+
return _fail(exc)
|
|
1145
|
+
lines = [f"WROTE {operation} on {where} in {proj.name} (with verification)."]
|
|
1146
|
+
written = outcome.written
|
|
1147
|
+
lines.append(f" stream : {written.get('stream')}")
|
|
1148
|
+
lines.append(
|
|
1149
|
+
f" size : {written.get('before_bytes')} -> {written.get('after_bytes')} bytes"
|
|
1150
|
+
)
|
|
1151
|
+
for backup in written.get("backups", []): # type: ignore[union-attr]
|
|
1152
|
+
lines.append(f" backup : {backup}")
|
|
1153
|
+
lines.append("")
|
|
1154
|
+
if outcome.build is None:
|
|
1155
|
+
lines.append("VERIFICATION NOT RUN:")
|
|
1156
|
+
elif outcome.build.ok:
|
|
1157
|
+
lines.append(
|
|
1158
|
+
f"VERIFIED: the project still compiles "
|
|
1159
|
+
f"({outcome.build.dlls} POU DLLs produced)."
|
|
1160
|
+
)
|
|
1161
|
+
else:
|
|
1162
|
+
lines.append("VERIFICATION FAILED:")
|
|
1163
|
+
lines.append(f" build : {outcome.build.summary() if outcome.build else 'n/a'}")
|
|
1164
|
+
if outcome.reverted:
|
|
1165
|
+
lines.append(" REVERTED: the backup was restored, so the project is")
|
|
1166
|
+
lines.append(" unchanged. Nothing needs rebuilding.")
|
|
1167
|
+
elif outcome.build is not None and outcome.build.ide_would_not_load:
|
|
1168
|
+
lines.append(
|
|
1169
|
+
" The IDE did not load the project, so no compile was attempted."
|
|
1170
|
+
)
|
|
1171
|
+
lines.append(
|
|
1172
|
+
" That is an environment problem, not a compile failure, so the"
|
|
1173
|
+
)
|
|
1174
|
+
lines.append(" write was LEFT IN PLACE. Rebuild in MotionWorks to confirm.")
|
|
1175
|
+
for note in outcome.notes:
|
|
1176
|
+
lines.append(f" note : {note}")
|
|
1177
|
+
return "\n".join(lines)
|
|
1178
|
+
|
|
1179
|
+
try:
|
|
1180
|
+
result = writer.apply_declaration(plan, proj.root)
|
|
1181
|
+
except Exception as exc: # noqa: BLE001
|
|
1182
|
+
return _fail(exc)
|
|
1183
|
+
|
|
1184
|
+
lines = [
|
|
1185
|
+
f"WROTE {operation} on {where} in {proj.name}.",
|
|
1186
|
+
f" stream : {result['stream']}",
|
|
1187
|
+
f" size : {result['before_bytes']} -> {result['after_bytes']} bytes",
|
|
1188
|
+
f" sibling streams : {result['siblings_verified']} verified unchanged",
|
|
1189
|
+
]
|
|
1190
|
+
for backup in result.get("backups", []):
|
|
1191
|
+
lines.append(f" backup : {backup}")
|
|
1192
|
+
for note in result.get("notes", []):
|
|
1193
|
+
lines.append(f" note : {note}")
|
|
1194
|
+
lines.append("")
|
|
1195
|
+
lines.append(
|
|
1196
|
+
"NEXT: open MotionWorks IEC and run Build -> Rebuild Project, then Make. "
|
|
1197
|
+
"If IsCompiled is false afterwards, restore the backup above. "
|
|
1198
|
+
"Pass verify=True instead to have the build driven automatically."
|
|
1199
|
+
)
|
|
1200
|
+
return "\n".join(lines)
|
|
1201
|
+
|
|
1202
|
+
|
|
1203
|
+
@mcp.tool()
|
|
1204
|
+
def mw_build_project(project: str | None = None, timeout: int = 600) -> str:
|
|
1205
|
+
"""Build and rebuild a project through MotionWorks' own compiler.
|
|
1206
|
+
|
|
1207
|
+
Drives the IDE over COM so a change can be checked without doing it by hand.
|
|
1208
|
+
Requires MotionWorks to be installed and able to open the project.
|
|
1209
|
+
|
|
1210
|
+
Note it operates on the project where it lies; use it on a copy unless you
|
|
1211
|
+
intend to change the real project's build output.
|
|
1212
|
+
|
|
1213
|
+
Args:
|
|
1214
|
+
project: Project path, .mwt file, or bare project name.
|
|
1215
|
+
timeout: Seconds to allow for the build.
|
|
1216
|
+
"""
|
|
1217
|
+
try:
|
|
1218
|
+
from . import build as build_module
|
|
1219
|
+
|
|
1220
|
+
available, reason = build_module.build_available()
|
|
1221
|
+
if not available:
|
|
1222
|
+
return (
|
|
1223
|
+
f"BUILD UNAVAILABLE: {reason}\n"
|
|
1224
|
+
f"Read-only tools all still work; only build verification needs "
|
|
1225
|
+
f"MotionWorks."
|
|
1226
|
+
)
|
|
1227
|
+
proj = _project(project)
|
|
1228
|
+
mwt = proj.mwt_path
|
|
1229
|
+
if mwt is None:
|
|
1230
|
+
return (
|
|
1231
|
+
f"ERROR: no .mwt wrapper beside {proj.root}. MotionWorks needs it "
|
|
1232
|
+
f"to open the project."
|
|
1233
|
+
)
|
|
1234
|
+
result = build_module.run_build(mwt, timeout=timeout)
|
|
1235
|
+
except Exception as exc: # noqa: BLE001
|
|
1236
|
+
return _fail(exc)
|
|
1237
|
+
|
|
1238
|
+
lines = [f"Build of {proj.name}: {'OK' if result.ok else 'NOT OK'}"]
|
|
1239
|
+
lines.append(f" {result.summary()}")
|
|
1240
|
+
if result.ide_would_not_load:
|
|
1241
|
+
lines.append("")
|
|
1242
|
+
lines.append(
|
|
1243
|
+
"The IDE never loaded the project, so no compile happened. This is an "
|
|
1244
|
+
"environment problem, not a code problem -- typically a startup or "
|
|
1245
|
+
"licensing prompt in the MotionWorks window. Open the project by hand "
|
|
1246
|
+
"once to clear it."
|
|
1247
|
+
)
|
|
1248
|
+
return "\n".join(lines)
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
@mcp.tool()
|
|
1252
|
+
def mw_build_status() -> str:
|
|
1253
|
+
"""Report whether the automatic build can be used, and what it needs.
|
|
1254
|
+
|
|
1255
|
+
Call this before relying on `verify=True` or `mw_build_project`, so a missing
|
|
1256
|
+
or blocked IDE is diagnosed up front instead of surfacing as a confusing
|
|
1257
|
+
failure.
|
|
1258
|
+
"""
|
|
1259
|
+
try:
|
|
1260
|
+
from . import build as build_module
|
|
1261
|
+
|
|
1262
|
+
available, reason = build_module.build_available()
|
|
1263
|
+
except Exception as exc: # noqa: BLE001
|
|
1264
|
+
return _fail(exc)
|
|
1265
|
+
|
|
1266
|
+
lines = [
|
|
1267
|
+
f"Automatic build available : {'YES' if available else 'NO'}",
|
|
1268
|
+
f" reason : {reason}",
|
|
1269
|
+
f" 32-bit PowerShell : {build_module.POWERSHELL_32}",
|
|
1270
|
+
f" build helper : {build_module._HELPER}",
|
|
1271
|
+
]
|
|
1272
|
+
if available:
|
|
1273
|
+
lines.append("")
|
|
1274
|
+
lines.append(
|
|
1275
|
+
"This means the tooling is present. It does NOT mean a build will "
|
|
1276
|
+
"succeed: the build drives the IDE over COM, so MotionWorks must be "
|
|
1277
|
+
"able to open the project. If a build reports 'project did not load', "
|
|
1278
|
+
"look for a startup or licensing prompt in the MotionWorks window -- "
|
|
1279
|
+
"that condition makes every automation call fail, including on "
|
|
1280
|
+
"unmodified projects."
|
|
1281
|
+
)
|
|
1282
|
+
else:
|
|
1283
|
+
lines.append("")
|
|
1284
|
+
lines.append(
|
|
1285
|
+
"Writes still work; they simply will not be verified by compiling, so "
|
|
1286
|
+
"rebuild in MotionWorks by hand afterwards."
|
|
1287
|
+
)
|
|
1288
|
+
return "\n".join(lines)
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
@mcp.tool()
|
|
1292
|
+
def mw_list_tasks(project: str | None = None) -> str:
|
|
1293
|
+
"""List the project's tasks with cycle time, priority, watchdog and programs.
|
|
1294
|
+
|
|
1295
|
+
This is context that motion code cannot be judged without: a block that needs
|
|
1296
|
+
longer than its task's cycle time will trip the watchdog, and the priority
|
|
1297
|
+
decides what it competes with. Cycle times come from the task `.SET` files and
|
|
1298
|
+
program assignments from the node list.
|
|
1299
|
+
|
|
1300
|
+
Args:
|
|
1301
|
+
project: Project path, .mwt file, or bare project name.
|
|
1302
|
+
"""
|
|
1303
|
+
try:
|
|
1304
|
+
from .tasks import read_tasks
|
|
1305
|
+
|
|
1306
|
+
proj = _project(project)
|
|
1307
|
+
settings = read_tasks(proj.root)
|
|
1308
|
+
assignments = proj.task_assignments()
|
|
1309
|
+
except Exception as exc: # noqa: BLE001
|
|
1310
|
+
return _fail(exc)
|
|
1311
|
+
|
|
1312
|
+
if not settings:
|
|
1313
|
+
return (
|
|
1314
|
+
f"No task configuration found for {proj.name}. Expected <Task>.SET "
|
|
1315
|
+
f"files beside the resource src.st1."
|
|
1316
|
+
)
|
|
1317
|
+
|
|
1318
|
+
lines = [f"Tasks in {proj.name} ({len(settings)}), fastest first:"]
|
|
1319
|
+
for task in settings:
|
|
1320
|
+
programs = assignments.get(task.name, [])
|
|
1321
|
+
lines.append("")
|
|
1322
|
+
lines.append(f"{task.name}")
|
|
1323
|
+
lines.append(f" {task.summary()}")
|
|
1324
|
+
if programs:
|
|
1325
|
+
for program in programs:
|
|
1326
|
+
lines.append(f" runs: {program}")
|
|
1327
|
+
else:
|
|
1328
|
+
lines.append(" runs: <no program instances assigned>")
|
|
1329
|
+
for warning in task.warnings:
|
|
1330
|
+
lines.append(f" WARNING: {warning}")
|
|
1331
|
+
|
|
1332
|
+
unassigned = sorted(set(assignments) - {t.name for t in settings})
|
|
1333
|
+
if unassigned:
|
|
1334
|
+
lines.append("")
|
|
1335
|
+
lines.append(
|
|
1336
|
+
f"Note: node list mentions task(s) with no .SET file: "
|
|
1337
|
+
f"{', '.join(unassigned)}"
|
|
1338
|
+
)
|
|
1339
|
+
return "\n".join(lines)
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
@mcp.tool()
|
|
1343
|
+
def mw_assign_task(
|
|
1344
|
+
program: str,
|
|
1345
|
+
task: str,
|
|
1346
|
+
project: str | None = None,
|
|
1347
|
+
unassign: bool = False,
|
|
1348
|
+
dry_run: bool = True,
|
|
1349
|
+
) -> str:
|
|
1350
|
+
"""Assign or unassign a POU's program instance to/from a task.
|
|
1351
|
+
|
|
1352
|
+
This is a structural edit to the project tree, so it rewrites `PROJECT.TRE`
|
|
1353
|
+
inside the root `src.st1` and records the instance in the project's
|
|
1354
|
+
`NODES.LST` files. MotionWorks must be closed; the write is refused otherwise.
|
|
1355
|
+
|
|
1356
|
+
Every edit is verified before it is written: the edited tree is re-parsed and
|
|
1357
|
+
checked (node counts, the header total, the instance's presence or absence),
|
|
1358
|
+
every file is backed up first, and the result is read back and re-checked.
|
|
1359
|
+
|
|
1360
|
+
Unlike the variable writes, this edit is **not compile-verified** -- the build
|
|
1361
|
+
path needs a working IDE, which is currently unavailable in this environment.
|
|
1362
|
+
Correctness here rests on the structural checks and on the byte-exact
|
|
1363
|
+
round-trip of the parser.
|
|
1364
|
+
|
|
1365
|
+
Args:
|
|
1366
|
+
program: Program instance name, normally the POU name (e.g. "StraightCut").
|
|
1367
|
+
task: Task name, e.g. "SlowTsk" or "FastTsk".
|
|
1368
|
+
project: Project path, .mwt file, or bare project name.
|
|
1369
|
+
unassign: Remove the assignment instead of adding it.
|
|
1370
|
+
dry_run: Report the plan without writing. Defaults to True.
|
|
1371
|
+
"""
|
|
1372
|
+
try:
|
|
1373
|
+
from . import tree as tree_module
|
|
1374
|
+
from . import tree_writer as writer
|
|
1375
|
+
from .cfb import CompoundFile
|
|
1376
|
+
|
|
1377
|
+
proj = _project(project)
|
|
1378
|
+
src = proj.root / "src.st1"
|
|
1379
|
+
text = CompoundFile(src).read_stream("PROJECT.TRE").decode("latin1")
|
|
1380
|
+
document = tree_module.parse_document(text)
|
|
1381
|
+
planned = (
|
|
1382
|
+
writer.plan_unassign(document, task, program)
|
|
1383
|
+
if unassign
|
|
1384
|
+
else writer.plan_assign(document, task, program)
|
|
1385
|
+
)
|
|
1386
|
+
except Exception as exc: # noqa: BLE001
|
|
1387
|
+
return _fail(exc)
|
|
1388
|
+
|
|
1389
|
+
header = [f"Project: {proj.name}", planned.summary(), ""]
|
|
1390
|
+
if dry_run:
|
|
1391
|
+
header.append("DRY RUN - nothing written.")
|
|
1392
|
+
header.append("Call again with dry_run=False to apply.")
|
|
1393
|
+
return "\n".join(header)
|
|
1394
|
+
|
|
1395
|
+
try:
|
|
1396
|
+
result = writer.apply_assignment(
|
|
1397
|
+
src,
|
|
1398
|
+
proj.root,
|
|
1399
|
+
planned,
|
|
1400
|
+
backup_dir=writer.default_backup_dir(proj.root),
|
|
1401
|
+
)
|
|
1402
|
+
except Exception as exc: # noqa: BLE001
|
|
1403
|
+
return _fail(exc)
|
|
1404
|
+
|
|
1405
|
+
header.append(f"WRITTEN. Node total is now {result['node_total']}.")
|
|
1406
|
+
header.append("Files: " + ", ".join(Path(f).name for f in result["files"]))
|
|
1407
|
+
header.append("Backups: " + ", ".join(result["backups"]))
|
|
1408
|
+
header.append(
|
|
1409
|
+
"Open MotionWorks IEC and rebuild to confirm; this edit was structurally "
|
|
1410
|
+
"verified but not compile-verified."
|
|
1411
|
+
)
|
|
1412
|
+
return "\n".join(header)
|
|
1413
|
+
|
|
1414
|
+
|
|
1415
|
+
@mcp.tool()
|
|
1416
|
+
def mw_plan_pou_creation(
|
|
1417
|
+
pou_name: str,
|
|
1418
|
+
template_pou: str,
|
|
1419
|
+
project: str | None = None,
|
|
1420
|
+
) -> str:
|
|
1421
|
+
"""Plan creating an ST POU: show the patch, and what is still unknown.
|
|
1422
|
+
|
|
1423
|
+
**This writes nothing.** It is safe to run against a real project.
|
|
1424
|
+
|
|
1425
|
+
Creating a POU adds four nodes to the project tree (the POU plus its comments,
|
|
1426
|
+
variables and body worksheets), clones a template POU directory, and updates
|
|
1427
|
+
the view list, `LIST.POU` and `PROJECT.INF`. The record layout, node ids and
|
|
1428
|
+
GUIDs are derived; several worksheet fields are not, because their values in
|
|
1429
|
+
real projects are opaque handles that differ between POUs for reasons the file
|
|
1430
|
+
format does not reveal.
|
|
1431
|
+
|
|
1432
|
+
Rather than guess those and risk producing a project MotionWorks cannot open,
|
|
1433
|
+
this reports them precisely so they can be supplied. Nothing is written until
|
|
1434
|
+
they are.
|
|
1435
|
+
|
|
1436
|
+
Args:
|
|
1437
|
+
pou_name: Name of the POU to create.
|
|
1438
|
+
template_pou: An existing POU in the same project to clone. The template
|
|
1439
|
+
decides the body kind, so clone an ST POU for an ST POU.
|
|
1440
|
+
project: Project path, .mwt file, or bare project name.
|
|
1441
|
+
"""
|
|
1442
|
+
try:
|
|
1443
|
+
from .pou_writer import plan_pou_creation
|
|
1444
|
+
|
|
1445
|
+
proj = _project(project)
|
|
1446
|
+
plan = plan_pou_creation(proj.root, pou_name, template_pou)
|
|
1447
|
+
except Exception as exc: # noqa: BLE001
|
|
1448
|
+
return _fail(exc)
|
|
1449
|
+
|
|
1450
|
+
lines = [f"Project: {proj.name}", plan.summary()]
|
|
1451
|
+
unresolved = plan.unresolved()
|
|
1452
|
+
lines.append("")
|
|
1453
|
+
if unresolved:
|
|
1454
|
+
lines.append(
|
|
1455
|
+
f"NOT WRITABLE YET: {len(unresolved)} field(s) need values that cannot "
|
|
1456
|
+
f"be derived from the files. Nothing was written."
|
|
1457
|
+
)
|
|
1458
|
+
else:
|
|
1459
|
+
lines.append(
|
|
1460
|
+
"All fields resolved. Note that POU creation is not yet wired to a "
|
|
1461
|
+
"write path; this tool only plans."
|
|
1462
|
+
)
|
|
1463
|
+
return "\n".join(lines)
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
@mcp.tool()
|
|
1467
|
+
def mw_create_pou(
|
|
1468
|
+
pou_name: str,
|
|
1469
|
+
template_pou: str,
|
|
1470
|
+
project: str | None = None,
|
|
1471
|
+
dry_run: bool = True,
|
|
1472
|
+
) -> str:
|
|
1473
|
+
"""Create an ST POU by cloning an existing one.
|
|
1474
|
+
|
|
1475
|
+
This is the largest structural edit the server performs. It clones the template
|
|
1476
|
+
POU's directory, renames the files and streams that carry the template's name,
|
|
1477
|
+
gives the new POU fresh worksheet GUIDs, inserts four nodes into the project
|
|
1478
|
+
tree, and records the POU in `LIST.POU` and `PROJECT.INF`. MotionWorks must be
|
|
1479
|
+
closed; the write is refused otherwise.
|
|
1480
|
+
|
|
1481
|
+
Every part of the tree edit is verified in a scratch container *before* anything
|
|
1482
|
+
is written, the whole POU directory is removed if any step fails, and the result
|
|
1483
|
+
is read back and re-checked.
|
|
1484
|
+
|
|
1485
|
+
The template decides the body kind: clone an ST POU for an ST POU. Worksheet
|
|
1486
|
+
state lines are copied from the template, because their values are opaque
|
|
1487
|
+
project-local handles that cannot be derived from the files.
|
|
1488
|
+
|
|
1489
|
+
Args:
|
|
1490
|
+
pou_name: Name for the new POU.
|
|
1491
|
+
template_pou: An existing POU in the same project to clone.
|
|
1492
|
+
project: Project path, .mwt file, or bare project name.
|
|
1493
|
+
dry_run: Report the plan without writing. Defaults to True.
|
|
1494
|
+
"""
|
|
1495
|
+
try:
|
|
1496
|
+
from . import pou_writer as writer
|
|
1497
|
+
|
|
1498
|
+
proj = _project(project)
|
|
1499
|
+
plan = writer.plan_pou_creation(proj.root, pou_name, template_pou)
|
|
1500
|
+
except Exception as exc: # noqa: BLE001
|
|
1501
|
+
return _fail(exc)
|
|
1502
|
+
|
|
1503
|
+
lines = [f"Project: {proj.name}", plan.summary(), ""]
|
|
1504
|
+
if dry_run:
|
|
1505
|
+
lines.append("DRY RUN - nothing written.")
|
|
1506
|
+
lines.append("Call again with dry_run=False to create the POU.")
|
|
1507
|
+
return "\n".join(lines)
|
|
1508
|
+
|
|
1509
|
+
try:
|
|
1510
|
+
result = writer.apply_pou_creation(
|
|
1511
|
+
plan, backup_dir=writer._default_backup_dir(proj.root)
|
|
1512
|
+
)
|
|
1513
|
+
except Exception as exc: # noqa: BLE001
|
|
1514
|
+
return _fail(exc)
|
|
1515
|
+
|
|
1516
|
+
lines.append(
|
|
1517
|
+
f"CREATED {result['pou']!r} from {result['template']!r}; node total is now "
|
|
1518
|
+
f"{result['node_total']}."
|
|
1519
|
+
)
|
|
1520
|
+
lines.append(f"Files created: {len(result['files_created'])}")
|
|
1521
|
+
lines.append("Backups: " + ", ".join(result["backups"]))
|
|
1522
|
+
lines.append(
|
|
1523
|
+
"Open MotionWorks IEC and rebuild to confirm; this edit was structurally "
|
|
1524
|
+
"verified but not compile-verified."
|
|
1525
|
+
)
|
|
1526
|
+
return "\n".join(lines)
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
@mcp.tool()
|
|
1530
|
+
def mw_delete_pou(
|
|
1531
|
+
pou_name: str,
|
|
1532
|
+
project: str | None = None,
|
|
1533
|
+
force: bool = False,
|
|
1534
|
+
dry_run: bool = True,
|
|
1535
|
+
) -> str:
|
|
1536
|
+
"""Delete a POU from the project.
|
|
1537
|
+
|
|
1538
|
+
Removes the POU's four tree nodes, its task assignment if it has one, and its
|
|
1539
|
+
entries in `LIST.POU`, the node lists and `PROJECT.INF`. The POU's directory is
|
|
1540
|
+
**moved to an archive** rather than deleted, so the operation is recoverable
|
|
1541
|
+
independently of the file backups. MotionWorks must be closed; the write is
|
|
1542
|
+
refused otherwise.
|
|
1543
|
+
|
|
1544
|
+
**The reference guard matters here.** If another POU's body calls the one being
|
|
1545
|
+
deleted, this is refused: the tree edit would succeed, but the project would no
|
|
1546
|
+
longer compile, and a dangling call is worse than a refusal. Pass `force=True`
|
|
1547
|
+
only if you have already removed the calls, or accept a broken build.
|
|
1548
|
+
|
|
1549
|
+
Everything is verified in a scratch container before anything is written, and if
|
|
1550
|
+
a later step fails the POU directory is moved back.
|
|
1551
|
+
|
|
1552
|
+
Args:
|
|
1553
|
+
pou_name: The POU to delete.
|
|
1554
|
+
project: Project path, .mwt file, or bare project name.
|
|
1555
|
+
force: Delete even when other POUs call it. Defaults to False.
|
|
1556
|
+
dry_run: Report the plan without writing. Defaults to True.
|
|
1557
|
+
"""
|
|
1558
|
+
try:
|
|
1559
|
+
from . import pou_writer as writer
|
|
1560
|
+
|
|
1561
|
+
proj = _project(project)
|
|
1562
|
+
plan = writer.plan_pou_deletion(proj.root, pou_name, force=force)
|
|
1563
|
+
except Exception as exc: # noqa: BLE001
|
|
1564
|
+
return _fail(exc)
|
|
1565
|
+
|
|
1566
|
+
lines = [f"Project: {proj.name}", plan.summary(), ""]
|
|
1567
|
+
if dry_run:
|
|
1568
|
+
lines.append("DRY RUN - nothing written.")
|
|
1569
|
+
lines.append("Call again with dry_run=False to delete the POU.")
|
|
1570
|
+
return "\n".join(lines)
|
|
1571
|
+
|
|
1572
|
+
try:
|
|
1573
|
+
result = writer.apply_pou_deletion(
|
|
1574
|
+
plan, backup_dir=writer._default_backup_dir(proj.root)
|
|
1575
|
+
)
|
|
1576
|
+
except Exception as exc: # noqa: BLE001
|
|
1577
|
+
return _fail(exc)
|
|
1578
|
+
|
|
1579
|
+
lines.append(
|
|
1580
|
+
f"DELETED {pou_name!r}; node total is now {result['node_total']}."
|
|
1581
|
+
)
|
|
1582
|
+
if result.get("archived_to"):
|
|
1583
|
+
lines.append(f"Archived to: {result['archived_to']}")
|
|
1584
|
+
lines.append("Backups: " + ", ".join(result["backups"]))
|
|
1585
|
+
lines.append(
|
|
1586
|
+
"Open MotionWorks IEC and rebuild to confirm; this edit was structurally "
|
|
1587
|
+
"verified but not compile-verified."
|
|
1588
|
+
)
|
|
1589
|
+
return "\n".join(lines)
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
@mcp.tool()
|
|
1593
|
+
def mw_transplant_ld_body(
|
|
1594
|
+
donor_pou: str,
|
|
1595
|
+
target_pou: str,
|
|
1596
|
+
project: str | None = None,
|
|
1597
|
+
dry_run: bool = True,
|
|
1598
|
+
) -> str:
|
|
1599
|
+
"""Copy a graphical (LD) body from a donor POU into another POU.
|
|
1600
|
+
|
|
1601
|
+
Authoring ladder logic from scratch is out of scope -- the body is proprietary
|
|
1602
|
+
binary -- so the supported route is a **proven donor**. This copies the donor's
|
|
1603
|
+
graphical body, its declarations and its variable grid into the target, which
|
|
1604
|
+
must already be a graphical POU (create one by cloning an LD POU first).
|
|
1605
|
+
|
|
1606
|
+
The donor's external declarations are **localized** on the way: `VAR_EXTERNAL`
|
|
1607
|
+
becomes `VAR` in the declarations, and the matching grid records change usage
|
|
1608
|
+
from external to local. That is necessary because the donor's external
|
|
1609
|
+
references belong to the donor's project, not the target's.
|
|
1610
|
+
|
|
1611
|
+
MotionWorks must be closed. Every stream is verified in a scratch container
|
|
1612
|
+
before anything is written, and read back afterwards.
|
|
1613
|
+
|
|
1614
|
+
Args:
|
|
1615
|
+
donor_pou: The POU whose body is copied. Must be graphical (LD).
|
|
1616
|
+
target_pou: The POU to write into. Must already be graphical.
|
|
1617
|
+
project: Project path, .mwt file, or bare project name.
|
|
1618
|
+
dry_run: Report the plan without writing. Defaults to True.
|
|
1619
|
+
"""
|
|
1620
|
+
try:
|
|
1621
|
+
from . import pou_writer as writer
|
|
1622
|
+
|
|
1623
|
+
proj = _project(project)
|
|
1624
|
+
plan = writer.plan_transplant(proj.root, donor_pou, target_pou)
|
|
1625
|
+
except Exception as exc: # noqa: BLE001
|
|
1626
|
+
return _fail(exc)
|
|
1627
|
+
|
|
1628
|
+
lines = [f"Project: {proj.name}", plan.summary(), ""]
|
|
1629
|
+
if dry_run:
|
|
1630
|
+
lines.append("DRY RUN - nothing written.")
|
|
1631
|
+
lines.append("Call again with dry_run=False to transplant the body.")
|
|
1632
|
+
return "\n".join(lines)
|
|
1633
|
+
|
|
1634
|
+
try:
|
|
1635
|
+
result = writer.apply_transplant(
|
|
1636
|
+
plan, backup_dir=writer._default_backup_dir(proj.root)
|
|
1637
|
+
)
|
|
1638
|
+
except Exception as exc: # noqa: BLE001
|
|
1639
|
+
return _fail(exc)
|
|
1640
|
+
|
|
1641
|
+
lines.append(
|
|
1642
|
+
f"TRANSPLANTED {result['donor']!r} into {result['target']!r}: "
|
|
1643
|
+
f"{len(result['streams'])} stream(s), {result['localized']} external "
|
|
1644
|
+
f"declaration(s) localized."
|
|
1645
|
+
)
|
|
1646
|
+
lines.append("Backups: " + ", ".join(result["backups"]))
|
|
1647
|
+
lines.append(
|
|
1648
|
+
"Open MotionWorks IEC and rebuild to confirm; this edit was structurally "
|
|
1649
|
+
"verified but not compile-verified."
|
|
1650
|
+
)
|
|
1651
|
+
return "\n".join(lines)
|
|
1652
|
+
|
|
1653
|
+
|
|
1654
|
+
@mcp.tool()
|
|
1655
|
+
def mw_close_motionworks(timeout_seconds: float = 15.0) -> str:
|
|
1656
|
+
"""Close every running MotionWorks IEC instance, then verify it is gone.
|
|
1657
|
+
|
|
1658
|
+
The IDE is asked to quit through COM first so it can flush its state, and a
|
|
1659
|
+
process that survives is terminated. The result is **re-checked** rather than
|
|
1660
|
+
assumed, so "STILL RUNNING" in the reply means do not write.
|
|
1661
|
+
|
|
1662
|
+
Any helper processes (cam editor, plot tool, simulator) are closed too, since
|
|
1663
|
+
they can hold the same project files open.
|
|
1664
|
+
|
|
1665
|
+
Args:
|
|
1666
|
+
timeout_seconds: How long to wait for processes to disappear.
|
|
1667
|
+
"""
|
|
1668
|
+
try:
|
|
1669
|
+
from . import ide
|
|
1670
|
+
|
|
1671
|
+
result = ide.close_motionworks(timeout=timeout_seconds)
|
|
1672
|
+
except Exception as exc: # noqa: BLE001
|
|
1673
|
+
return _fail(exc)
|
|
1674
|
+
|
|
1675
|
+
lines = []
|
|
1676
|
+
before = result.get("before") or []
|
|
1677
|
+
if not before:
|
|
1678
|
+
lines.append("MotionWorks IEC was not running; nothing to close.")
|
|
1679
|
+
else:
|
|
1680
|
+
for pid, name in result.get("closed") or []:
|
|
1681
|
+
lines.append(f" closed pid {pid} ({name})")
|
|
1682
|
+
for pid, name in before:
|
|
1683
|
+
if (pid, name) not in (result.get("closed") or []):
|
|
1684
|
+
lines.append(f" was not running by the time it was closed: "
|
|
1685
|
+
f"pid {pid} ({name})")
|
|
1686
|
+
remaining = result.get("remaining") or []
|
|
1687
|
+
if remaining:
|
|
1688
|
+
lines.append("")
|
|
1689
|
+
lines.append("STILL RUNNING - do not write:")
|
|
1690
|
+
for pid, name in remaining:
|
|
1691
|
+
lines.append(f" pid {pid} {name}")
|
|
1692
|
+
else:
|
|
1693
|
+
lines.append("")
|
|
1694
|
+
lines.append("MotionWorks IEC is closed. Writes are safe.")
|
|
1695
|
+
return "\n".join(lines)
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
@mcp.tool()
|
|
1699
|
+
def mw_com_status() -> str:
|
|
1700
|
+
"""Report whether the pywin32 COM automation client can drive MotionWorks.
|
|
1701
|
+
|
|
1702
|
+
This is the Tier 3 path in Python: the `Ade.Application.550` automation server,
|
|
1703
|
+
which must be spoken to from a **32-bit** interpreter because `mwt.exe` is a
|
|
1704
|
+
32-bit local server. Run this server with `.venv32\\Scripts\\python.exe` for COM
|
|
1705
|
+
work; the 64-bit interpreter is fine for everything else.
|
|
1706
|
+
|
|
1707
|
+
If the client is usable it also reports the active project's state. Note the
|
|
1708
|
+
distinction the reply makes: if the IDE is running but its project services are
|
|
1709
|
+
not answering, **no compile was attempted**, so that must not be read as "the
|
|
1710
|
+
project does not compile".
|
|
1711
|
+
"""
|
|
1712
|
+
try:
|
|
1713
|
+
from . import com
|
|
1714
|
+
|
|
1715
|
+
result = com.query_via_32bit()
|
|
1716
|
+
except Exception as exc: # noqa: BLE001
|
|
1717
|
+
return _fail(exc)
|
|
1718
|
+
|
|
1719
|
+
if not result.get("ok"):
|
|
1720
|
+
lines = ["MotionWorks IEC automation: not available."]
|
|
1721
|
+
for key in ("reason", "error"):
|
|
1722
|
+
if result.get(key):
|
|
1723
|
+
lines.append(f" {key}: {result[key]}")
|
|
1724
|
+
lines.append("")
|
|
1725
|
+
lines.append(
|
|
1726
|
+
"The PowerShell build path (mw_build_project) works with either "
|
|
1727
|
+
"interpreter, because it invokes 32-bit Windows PowerShell itself."
|
|
1728
|
+
)
|
|
1729
|
+
return "\n".join(lines)
|
|
1730
|
+
|
|
1731
|
+
lines = [f"Automation client ({com.PROG_ID}): connected"]
|
|
1732
|
+
state = result.get("state") or {}
|
|
1733
|
+
for key in ("name", "project_open", "compiled", "modified"):
|
|
1734
|
+
lines.append(f" {key:13}: {state.get(key)}")
|
|
1735
|
+
if result.get("services_ready"):
|
|
1736
|
+
lines.append(" services : ready")
|
|
1737
|
+
else:
|
|
1738
|
+
lines.append(f" services : {result.get('services_note')}")
|
|
1739
|
+
lines.append("")
|
|
1740
|
+
lines.append(
|
|
1741
|
+
"The IDE is up but its project services are not answering, so NO compile "
|
|
1742
|
+
"was attempted. This is not a build failure."
|
|
1743
|
+
)
|
|
1744
|
+
if result.get("compile"):
|
|
1745
|
+
entry = result["compile"]
|
|
1746
|
+
lines.append("")
|
|
1747
|
+
lines.append(f" compile : accepted={entry.get('accepted')}")
|
|
1748
|
+
if entry.get("note"):
|
|
1749
|
+
lines.append(f" note : {entry['note']}")
|
|
1750
|
+
return "\n".join(lines)
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
@mcp.tool()
|
|
1754
|
+
def mw_list_windows(limit: int = 25) -> str:
|
|
1755
|
+
"""List visible windows, largest first, with their handles and positions.
|
|
1756
|
+
|
|
1757
|
+
Use this to find a window before capturing it. Handles are what the other
|
|
1758
|
+
screen tools take, so they stay unambiguous when two windows share a title.
|
|
1759
|
+
"""
|
|
1760
|
+
try:
|
|
1761
|
+
from . import capture
|
|
1762
|
+
|
|
1763
|
+
return capture.describe_windows(limit=limit)
|
|
1764
|
+
except Exception as exc: # noqa: BLE001
|
|
1765
|
+
return _fail(exc)
|
|
1766
|
+
|
|
1767
|
+
|
|
1768
|
+
@mcp.tool()
|
|
1769
|
+
def mw_screenshot(
|
|
1770
|
+
window: str | None = None,
|
|
1771
|
+
handle: int | None = None,
|
|
1772
|
+
screen: bool = False,
|
|
1773
|
+
) -> str:
|
|
1774
|
+
"""Capture the screen, or one window, and return the PNG path.
|
|
1775
|
+
|
|
1776
|
+
This is the agent's eyes on MotionWorks. Use it to see what the IDE is actually
|
|
1777
|
+
showing -- a modal dialog, a compile error, a project tree -- rather than
|
|
1778
|
+
inferring from files. The returned path can be attached directly to see the image.
|
|
1779
|
+
|
|
1780
|
+
Args:
|
|
1781
|
+
window: Case-insensitive substring of the window title, e.g. "Motionworks".
|
|
1782
|
+
handle: A window handle from mw_list_windows; more precise than a title.
|
|
1783
|
+
screen: Capture every monitor instead of one window.
|
|
1784
|
+
"""
|
|
1785
|
+
try:
|
|
1786
|
+
from . import capture
|
|
1787
|
+
|
|
1788
|
+
if screen or (window is None and handle is None):
|
|
1789
|
+
path = capture.capture_screen("screen")
|
|
1790
|
+
return f"Captured the full desktop to:\n {path}"
|
|
1791
|
+
path, info = capture.capture_window(
|
|
1792
|
+
title_contains=window, handle=handle, name="window"
|
|
1793
|
+
)
|
|
1794
|
+
return (
|
|
1795
|
+
f"Captured window {info['title']!r} to:\n {path}\n"
|
|
1796
|
+
f" size: {info['width']}x{info['height']} at "
|
|
1797
|
+
f"({info['left']},{info['top']}), hwnd={info['handle']}"
|
|
1798
|
+
)
|
|
1799
|
+
except Exception as exc: # noqa: BLE001
|
|
1800
|
+
return _fail(exc)
|
|
1801
|
+
|
|
1802
|
+
|
|
1803
|
+
@mcp.tool()
|
|
1804
|
+
def mw_read_screen_text(
|
|
1805
|
+
window: str | None = None,
|
|
1806
|
+
handle: int | None = None,
|
|
1807
|
+
image: str | None = None,
|
|
1808
|
+
) -> str:
|
|
1809
|
+
"""OCR text out of a window, the screen, or an existing image.
|
|
1810
|
+
|
|
1811
|
+
UI Automation is tried first and OCR is the fallback, because accessibility is not
|
|
1812
|
+
always available: MotionWorks' trial dialog reports its buttons as generic panes,
|
|
1813
|
+
and anything an application draws itself is invisible to automation entirely. When
|
|
1814
|
+
that happens, pixels are the only source of truth.
|
|
1815
|
+
|
|
1816
|
+
Args:
|
|
1817
|
+
window: Window title substring to capture and read.
|
|
1818
|
+
handle: A window handle from mw_list_windows.
|
|
1819
|
+
image: Read an already-captured PNG instead of taking a new one.
|
|
1820
|
+
"""
|
|
1821
|
+
try:
|
|
1822
|
+
from pathlib import Path as _Path
|
|
1823
|
+
|
|
1824
|
+
from . import capture
|
|
1825
|
+
|
|
1826
|
+
if image:
|
|
1827
|
+
target = _Path(image)
|
|
1828
|
+
if not target.is_file():
|
|
1829
|
+
return f"ERROR: no such image: {image}"
|
|
1830
|
+
elif window or handle:
|
|
1831
|
+
target, _info = capture.capture_window(
|
|
1832
|
+
title_contains=window, handle=handle, name="ocr"
|
|
1833
|
+
)
|
|
1834
|
+
else:
|
|
1835
|
+
target = capture.capture_screen("ocr")
|
|
1836
|
+
|
|
1837
|
+
text = capture.read_text(target)
|
|
1838
|
+
header = f"Text read from {target}:"
|
|
1839
|
+
return f"{header}\n\n{text}" if text else f"{header}\n\n(no text found)"
|
|
1840
|
+
except Exception as exc: # noqa: BLE001
|
|
1841
|
+
return _fail(exc)
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
@mcp.tool()
|
|
1845
|
+
def mw_ui_elements(
|
|
1846
|
+
window: str | None = None,
|
|
1847
|
+
handle: int | None = None,
|
|
1848
|
+
control_type: str = "",
|
|
1849
|
+
) -> str:
|
|
1850
|
+
"""List the UI Automation elements of a window (or the desktop).
|
|
1851
|
+
|
|
1852
|
+
Shows what is actually interactive, with each element's type, name and rectangle.
|
|
1853
|
+
Note that MotionWorks reports its dialog buttons as ``Pane`` rather than
|
|
1854
|
+
``Button``, so filtering by type can hide them -- listing everything is often the
|
|
1855
|
+
better first move.
|
|
1856
|
+
|
|
1857
|
+
Args:
|
|
1858
|
+
window: Window title substring; omit to inspect the whole desktop.
|
|
1859
|
+
handle: A window handle from mw_list_windows.
|
|
1860
|
+
control_type: Filter to one type, e.g. "Button" or "Edit".
|
|
1861
|
+
"""
|
|
1862
|
+
try:
|
|
1863
|
+
from . import uia
|
|
1864
|
+
|
|
1865
|
+
found = uia.elements(
|
|
1866
|
+
title_contains=window, handle=handle, control_type=control_type or ""
|
|
1867
|
+
)
|
|
1868
|
+
return uia.describe(found)
|
|
1869
|
+
except Exception as exc: # noqa: BLE001
|
|
1870
|
+
return _fail(exc)
|
|
1871
|
+
|
|
1872
|
+
|
|
1873
|
+
@mcp.tool()
|
|
1874
|
+
def mw_dismiss_trial(timeout_seconds: float = 45.0) -> str:
|
|
1875
|
+
"""Click **Use Trial** on the MotionWorks trial dialog, if it is showing.
|
|
1876
|
+
|
|
1877
|
+
While that dialog is up the IDE never loads a project, so every COM call -- even
|
|
1878
|
+
on an unmodified project -- returns `Internal error`. That looks like a broken
|
|
1879
|
+
install but is only an unanswered prompt, so this is worth trying before
|
|
1880
|
+
concluding anything is wrong.
|
|
1881
|
+
|
|
1882
|
+
If this reports that it could not click, the dialog needs one manual click: this
|
|
1883
|
+
process has been unable to inject mouse input (synthetic clicks are accepted but
|
|
1884
|
+
the cursor never moves), which is recorded in docs/computer-use.md.
|
|
1885
|
+
|
|
1886
|
+
Args:
|
|
1887
|
+
timeout_seconds: How long to wait for the dialog to appear.
|
|
1888
|
+
"""
|
|
1889
|
+
try:
|
|
1890
|
+
from . import uia
|
|
1891
|
+
|
|
1892
|
+
dismissed, detail = uia.dismiss_trial(timeout=timeout_seconds)
|
|
1893
|
+
if dismissed:
|
|
1894
|
+
return f"Trial dialog handled: {detail}"
|
|
1895
|
+
return (
|
|
1896
|
+
f"Trial dialog not dismissed: {detail}\n\n"
|
|
1897
|
+
"If the dialog is on screen, click **Use Trial** once by hand. Synthetic "
|
|
1898
|
+
"input does not reach the desktop from this process."
|
|
1899
|
+
)
|
|
1900
|
+
except Exception as exc: # noqa: BLE001
|
|
1901
|
+
return _fail(exc)
|
|
1902
|
+
|
|
1903
|
+
|
|
1904
|
+
def main() -> None:
|
|
1905
|
+
"""Console entry point: run the server over stdio."""
|
|
1906
|
+
try:
|
|
1907
|
+
mcp.run("stdio")
|
|
1908
|
+
except KeyboardInterrupt:
|
|
1909
|
+
sys.exit(0)
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
if __name__ == "__main__":
|
|
1913
|
+
main()
|