mcp-snap7 0.1.0__tar.gz

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.
@@ -0,0 +1,25 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .eggs/
7
+ *.egg
8
+ .env
9
+ .venv
10
+ venv/
11
+ env/
12
+ *.pyc
13
+ *.pyo
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+ .mypy_cache/
17
+ htmlcov/
18
+ .coverage
19
+ .coverage.*
20
+ *.log
21
+ .DS_Store
22
+ .idea/
23
+ .vscode/
24
+ *.swp
25
+ *.swo
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dario Clavijo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcp-snap7
3
+ Version: 0.1.0
4
+ Summary: MCP server for python-snap7, enabling MCP clients to interact with Siemens PLCs
5
+ Project-URL: Homepage, https://github.com/daedalus/mcp-snap7
6
+ Project-URL: Repository, https://github.com/daedalus/mcp-snap7
7
+ Project-URL: Issues, https://github.com/daedalus/mcp-snap7/issues
8
+ Author-email: Dario Clavijo <clavijodario@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: fastmcp
13
+ Requires-Dist: python-snap7
14
+ Provides-Extra: all
15
+ Requires-Dist: hatch; extra == 'all'
16
+ Requires-Dist: hypothesis; extra == 'all'
17
+ Requires-Dist: mypy; extra == 'all'
18
+ Requires-Dist: pytest; extra == 'all'
19
+ Requires-Dist: pytest-asyncio; extra == 'all'
20
+ Requires-Dist: pytest-cov; extra == 'all'
21
+ Requires-Dist: pytest-mock; extra == 'all'
22
+ Requires-Dist: ruff; extra == 'all'
23
+ Provides-Extra: dev
24
+ Requires-Dist: hatch; extra == 'dev'
25
+ Requires-Dist: mypy; extra == 'dev'
26
+ Requires-Dist: ruff; extra == 'dev'
27
+ Provides-Extra: lint
28
+ Requires-Dist: mypy; extra == 'lint'
29
+ Requires-Dist: ruff; extra == 'lint'
30
+ Provides-Extra: test
31
+ Requires-Dist: hypothesis; extra == 'test'
32
+ Requires-Dist: pytest; extra == 'test'
33
+ Requires-Dist: pytest-asyncio; extra == 'test'
34
+ Requires-Dist: pytest-cov; extra == 'test'
35
+ Requires-Dist: pytest-mock; extra == 'test'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # mcp-snap7
39
+
40
+ > MCP server for python-snap7, enabling MCP clients to interact with Siemens PLCs
41
+
42
+ [![PyPI](https://img.shields.io/pypi/v/mcp-snap7.svg)](https://pypi.org/project/mcp-snap7/)
43
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-snap7.svg)](https://pypi.org/project/mcp-snap7/)
44
+ [![Coverage](https://codecov.io/gh/daedalus/mcp-snap7/branch/main/graph/badge.svg)](https://codecov.io/gh/daedalus/mcp-snap7)
45
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pip install mcp-snap7
51
+ ```
52
+
53
+ ## Requirements
54
+
55
+ - python-snap7 requires the libsnap7 library to be installed on your system.
56
+ See [python-snap7 documentation](https://python-snap7.readthedocs.io/) for installation instructions.
57
+
58
+ ## Usage
59
+
60
+ ### As MCP Server
61
+
62
+ Configure your MCP client to use `mcp-snap7` as a stdio server:
63
+
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "mcp-snap7": {
68
+ "command": "mcp-snap7"
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Available Tools
75
+
76
+ - `connect_plc` - Connect to a Siemens PLC
77
+ - `disconnect_plc` - Disconnect from PLC
78
+ - `get_connected` - Check connection status
79
+ - `db_read` / `db_write` - Read/write data blocks
80
+ - `mb_read` / `mb_write` - Read/write memory bytes
81
+ - `tm_read` / `tm_write` - Read/write timers
82
+ - `ct_read` / `ct_write` - Read/write counters
83
+ - `eb_read` / `eb_write` - Read/write edge inputs
84
+ - `ab_read` / `ab_write` - Read/write absolute bytes
85
+ - `get_cpu_info` - Get CPU information
86
+ - `get_cpu_state` - Get CPU state
87
+ - `get_protection` - Get PLC protection level
88
+ - `plc_cold_start` - Trigger cold start
89
+ - `plc_hot_start` - Trigger hot start
90
+ - `plc_stop` - Stop PLC
91
+ - `get_error_text` - Get error description
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ git clone https://github.com/daedalus/mcp-snap7.git
97
+ cd mcp-snap7
98
+ pip install -e ".[test]"
99
+
100
+ # run tests
101
+ pytest
102
+
103
+ # format
104
+ ruff format src/ tests/
105
+
106
+ # lint
107
+ ruff check src/ tests/
108
+
109
+ # type check
110
+ mypy src/
111
+ ```
@@ -0,0 +1,74 @@
1
+ # mcp-snap7
2
+
3
+ > MCP server for python-snap7, enabling MCP clients to interact with Siemens PLCs
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/mcp-snap7.svg)](https://pypi.org/project/mcp-snap7/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/mcp-snap7.svg)](https://pypi.org/project/mcp-snap7/)
7
+ [![Coverage](https://codecov.io/gh/daedalus/mcp-snap7/branch/main/graph/badge.svg)](https://codecov.io/gh/daedalus/mcp-snap7)
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install mcp-snap7
14
+ ```
15
+
16
+ ## Requirements
17
+
18
+ - python-snap7 requires the libsnap7 library to be installed on your system.
19
+ See [python-snap7 documentation](https://python-snap7.readthedocs.io/) for installation instructions.
20
+
21
+ ## Usage
22
+
23
+ ### As MCP Server
24
+
25
+ Configure your MCP client to use `mcp-snap7` as a stdio server:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "mcp-snap7": {
31
+ "command": "mcp-snap7"
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ ### Available Tools
38
+
39
+ - `connect_plc` - Connect to a Siemens PLC
40
+ - `disconnect_plc` - Disconnect from PLC
41
+ - `get_connected` - Check connection status
42
+ - `db_read` / `db_write` - Read/write data blocks
43
+ - `mb_read` / `mb_write` - Read/write memory bytes
44
+ - `tm_read` / `tm_write` - Read/write timers
45
+ - `ct_read` / `ct_write` - Read/write counters
46
+ - `eb_read` / `eb_write` - Read/write edge inputs
47
+ - `ab_read` / `ab_write` - Read/write absolute bytes
48
+ - `get_cpu_info` - Get CPU information
49
+ - `get_cpu_state` - Get CPU state
50
+ - `get_protection` - Get PLC protection level
51
+ - `plc_cold_start` - Trigger cold start
52
+ - `plc_hot_start` - Trigger hot start
53
+ - `plc_stop` - Stop PLC
54
+ - `get_error_text` - Get error description
55
+
56
+ ## Development
57
+
58
+ ```bash
59
+ git clone https://github.com/daedalus/mcp-snap7.git
60
+ cd mcp-snap7
61
+ pip install -e ".[test]"
62
+
63
+ # run tests
64
+ pytest
65
+
66
+ # format
67
+ ruff format src/ tests/
68
+
69
+ # lint
70
+ ruff check src/ tests/
71
+
72
+ # type check
73
+ mypy src/
74
+ ```
@@ -0,0 +1,89 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-snap7"
7
+ version = "0.1.0"
8
+ description = "MCP server for python-snap7, enabling MCP clients to interact with Siemens PLCs"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Dario Clavijo", email = "clavijodario@gmail.com"}
14
+ ]
15
+ dependencies = ["fastmcp", "python-snap7"]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "ruff",
20
+ "mypy",
21
+ "hatch",
22
+ ]
23
+ test = [
24
+ "pytest",
25
+ "pytest-cov",
26
+ "pytest-mock",
27
+ "pytest-asyncio",
28
+ "hypothesis",
29
+ ]
30
+ lint = [
31
+ "ruff",
32
+ "mypy",
33
+ ]
34
+ all = ["mcp-snap7[dev,test,lint]"]
35
+
36
+ [project.scripts]
37
+ mcp-snap7 = "mcp_snap7.__main__:main"
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/daedalus/mcp-snap7"
41
+ Repository = "https://github.com/daedalus/mcp-snap7"
42
+ Issues = "https://github.com/daedalus/mcp-snap7/issues"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["src/mcp_snap7"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = ["src/mcp_snap7"]
49
+
50
+ [tool.ruff]
51
+ line-length = 88
52
+ target-version = "py311"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "W", "I", "UP", "ANN", "TCH", "N", "C4", "ARG"]
56
+ ignore = ["E501"]
57
+
58
+ [tool.ruff.lint.per-file-ignores]
59
+ "__init__.py" = ["F401"]
60
+ "tests/*" = ["ANN", "ARG", "F401"]
61
+
62
+ [tool.ruff.lint.pydocstyle]
63
+ convention = "google"
64
+
65
+ [tool.mypy]
66
+ python_version = "3.11"
67
+ strict = true
68
+ warn_return_any = true
69
+ warn_unused_ignores = true
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ addopts = "-v --tb=short --cov=src --cov-fail-under=80"
74
+ filterwarnings = ["ignore::DeprecationWarning"]
75
+
76
+ [tool.coverage.run]
77
+ source = ["src"]
78
+ branch = true
79
+
80
+ [tool.coverage.report]
81
+ exclude = ["tests/*", "*/__init__.py"]
82
+ exclude_lines = [
83
+ "pragma: no cover",
84
+ "def __repr__",
85
+ "raise AssertionError",
86
+ "raise NotImplementedError",
87
+ "if __name__ == .__main__.:",
88
+ "if TYPE_CHECKING:",
89
+ ]
@@ -0,0 +1,9 @@
1
+ __version__ = "0.1.0"
2
+ __all__ = ["mcp", "client"]
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ from ._mcp import mcp
7
+
8
+ if TYPE_CHECKING:
9
+ from ._client import Snap7Client
@@ -0,0 +1,10 @@
1
+ from . import mcp
2
+
3
+
4
+ def main() -> int:
5
+ mcp.run()
6
+ return 0
7
+
8
+
9
+ if __name__ == "__main__":
10
+ raise SystemExit(main())
@@ -0,0 +1,190 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ import snap7
5
+
6
+
7
+ class Snap7Client:
8
+ def __init__(self) -> None:
9
+ self._client = snap7.client.Client()
10
+ self._connected = False
11
+
12
+ def connect(self, address: str, rack: int, slot: int, tcp_port: int) -> str:
13
+ try:
14
+ self._client.connect(address, rack, slot, tcp_port)
15
+ self._connected = True
16
+ return f"Connected to {address} (rack={rack}, slot={slot}, port={tcp_port})"
17
+ except Exception as e:
18
+ return f"Connection failed: {str(e)}"
19
+
20
+ def disconnect(self) -> str:
21
+ try:
22
+ if self._connected:
23
+ self._client.disconnect()
24
+ self._connected = False
25
+ return "Disconnected"
26
+ except Exception as e:
27
+ return f"Disconnect failed: {str(e)}"
28
+
29
+ def get_connected(self) -> bool:
30
+ try:
31
+ return self._client.get_connected()
32
+ except Exception:
33
+ return False
34
+
35
+ def _hex_to_bytearray(self, hex_string: str) -> bytearray:
36
+ if hex_string.startswith("0x"):
37
+ hex_string = hex_string[2:]
38
+ return bytearray(bytes.fromhex(hex_string))
39
+
40
+ def _data_to_hex(self, data: bytearray | bytes) -> str:
41
+ return bytes(data).hex()
42
+
43
+ def db_read(self, db_number: int, start: int, size: int) -> str:
44
+ try:
45
+ data = self._client.db_read(db_number, start, size)
46
+ return self._data_to_hex(data)
47
+ except Exception as e:
48
+ raise RuntimeError(f"DB read failed: {str(e)}")
49
+
50
+ def db_write(self, db_number: int, start: int, data: str) -> str:
51
+ try:
52
+ data_bytes = self._hex_to_bytearray(data)
53
+ self._client.db_write(db_number, start, data_bytes)
54
+ return f"Written {len(data_bytes)} bytes to DB{db_number}"
55
+ except Exception as e:
56
+ raise RuntimeError(f"DB write failed: {str(e)}")
57
+
58
+ def mb_read(self, start: int, size: int) -> str:
59
+ try:
60
+ data = self._client.mb_read(start, size)
61
+ return self._data_to_hex(data)
62
+ except Exception as e:
63
+ raise RuntimeError(f"MB read failed: {str(e)}")
64
+
65
+ def mb_write(self, start: int, data: str) -> str:
66
+ try:
67
+ data_bytes = self._hex_to_bytearray(data)
68
+ self._client.mb_write(start, len(data_bytes), data_bytes)
69
+ return f"Written {len(data_bytes)} bytes to MB"
70
+ except Exception as e:
71
+ raise RuntimeError(f"MB write failed: {str(e)}")
72
+
73
+ def tm_read(self, start: int, size: int) -> str:
74
+ try:
75
+ data = self._client.tm_read(start, size)
76
+ return self._data_to_hex(data)
77
+ except Exception as e:
78
+ raise RuntimeError(f"TM read failed: {str(e)}")
79
+
80
+ def tm_write(self, start: int, data: str) -> str:
81
+ try:
82
+ data_bytes = self._hex_to_bytearray(data)
83
+ self._client.tm_write(start, len(data_bytes), data_bytes)
84
+ return f"Written {len(data_bytes)} bytes to TM"
85
+ except Exception as e:
86
+ raise RuntimeError(f"TM write failed: {str(e)}")
87
+
88
+ def ct_read(self, start: int, size: int) -> str:
89
+ try:
90
+ data = self._client.ct_read(start, size)
91
+ return self._data_to_hex(data)
92
+ except Exception as e:
93
+ raise RuntimeError(f"CT read failed: {str(e)}")
94
+
95
+ def ct_write(self, start: int, data: str) -> str:
96
+ try:
97
+ data_bytes = self._hex_to_bytearray(data)
98
+ self._client.ct_write(start, len(data_bytes), data_bytes)
99
+ return f"Written {len(data_bytes)} bytes to CT"
100
+ except Exception as e:
101
+ raise RuntimeError(f"CT write failed: {str(e)}")
102
+
103
+ def eb_read(self, start: int, size: int) -> str:
104
+ try:
105
+ data = self._client.eb_read(start, size)
106
+ return self._data_to_hex(data)
107
+ except Exception as e:
108
+ raise RuntimeError(f"EB read failed: {str(e)}")
109
+
110
+ def eb_write(self, start: int, data: str) -> str:
111
+ try:
112
+ data_bytes = self._hex_to_bytearray(data)
113
+ self._client.eb_write(start, len(data_bytes), data_bytes)
114
+ return f"Written {len(data_bytes)} bytes to EB"
115
+ except Exception as e:
116
+ raise RuntimeError(f"EB write failed: {str(e)}")
117
+
118
+ def ab_read(self, start: int, size: int) -> str:
119
+ try:
120
+ data = self._client.ab_read(start, size)
121
+ return self._data_to_hex(data)
122
+ except Exception as e:
123
+ raise RuntimeError(f"AB read failed: {str(e)}")
124
+
125
+ def ab_write(self, start: int, data: str) -> str:
126
+ try:
127
+ data_bytes = self._hex_to_bytearray(data)
128
+ self._client.ab_write(start, data_bytes)
129
+ return f"Written {len(data_bytes)} bytes to AB"
130
+ except Exception as e:
131
+ raise RuntimeError(f"AB write failed: {str(e)}")
132
+
133
+ def get_cpu_info(self) -> dict[str, Any]:
134
+ try:
135
+ info = self._client.get_cpu_info()
136
+ return {
137
+ "module_type": info.ModuleType,
138
+ "serial_number": info.SerialNumber,
139
+ "as_name": info.ASName,
140
+ "module_name": info.ModuleName,
141
+ "copyright": info.Copyright,
142
+ }
143
+ except Exception as e:
144
+ raise RuntimeError(f"Get CPU info failed: {str(e)}")
145
+
146
+ def get_cpu_state(self) -> str:
147
+ try:
148
+ return self._client.get_cpu_state()
149
+ except Exception as e:
150
+ raise RuntimeError(f"Get CPU state failed: {str(e)}")
151
+
152
+ def get_protection(self) -> dict[str, Any]:
153
+ try:
154
+ protection = self._client.get_protection()
155
+ return {
156
+ "protection_level": protection.szl_protection.SZLProtection,
157
+ }
158
+ except Exception as e:
159
+ raise RuntimeError(f"Get protection failed: {str(e)}")
160
+
161
+ def plc_cold_start(self) -> str:
162
+ try:
163
+ self._client.plc_cold_start()
164
+ return "Cold start command sent"
165
+ except Exception as e:
166
+ raise RuntimeError(f"Cold start failed: {str(e)}")
167
+
168
+ def plc_hot_start(self) -> str:
169
+ try:
170
+ self._client.plc_hot_start()
171
+ return "Hot start command sent"
172
+ except Exception as e:
173
+ raise RuntimeError(f"Hot start failed: {str(e)}")
174
+
175
+ def plc_stop(self) -> str:
176
+ try:
177
+ self._client.plc_stop()
178
+ return "Stop command sent"
179
+ except Exception as e:
180
+ raise RuntimeError(f"Stop failed: {str(e)}")
181
+
182
+ def get_error_text(self, error_code: int) -> str:
183
+ return self._client.error_text(error_code)
184
+
185
+ def get_status(self) -> str:
186
+ return json.dumps(
187
+ {
188
+ "connected": self.get_connected(),
189
+ }
190
+ )
@@ -0,0 +1,328 @@
1
+ from typing import Any
2
+
3
+ import fastmcp
4
+
5
+ from ._client import Snap7Client
6
+
7
+ mcp = fastmcp.FastMCP("mcp-snap7")
8
+
9
+ _client: Snap7Client | None = None
10
+
11
+
12
+ def _get_client() -> Snap7Client:
13
+ global _client
14
+ if _client is None:
15
+ _client = Snap7Client()
16
+ return _client
17
+
18
+
19
+ @mcp.tool()
20
+ def connect_plc(address: str, rack: int = 0, slot: int = 1, tcp_port: int = 102) -> str:
21
+ """Connect to a Siemens PLC.
22
+
23
+ Args:
24
+ address: PLC IP address (e.g., "192.168.1.1")
25
+ rack: Rack number (default 0)
26
+ slot: Slot number (default 1)
27
+ tcp_port: TCP port (default 102)
28
+
29
+ Returns:
30
+ Connection status message
31
+ """
32
+ client = _get_client()
33
+ return client.connect(address, rack, slot, tcp_port)
34
+
35
+
36
+ @mcp.tool()
37
+ def disconnect_plc() -> str:
38
+ """Disconnect from the connected PLC.
39
+
40
+ Returns:
41
+ Disconnection status message
42
+ """
43
+ client = _get_client()
44
+ return client.disconnect()
45
+
46
+
47
+ @mcp.tool()
48
+ def get_connected() -> bool:
49
+ """Check if PLC is connected.
50
+
51
+ Returns:
52
+ True if connected, False otherwise
53
+ """
54
+ client = _get_client()
55
+ return client.get_connected()
56
+
57
+
58
+ @mcp.tool()
59
+ def db_read(db_number: int, start: int, size: int) -> str:
60
+ """Read data block from PLC.
61
+
62
+ Args:
63
+ db_number: Data block number
64
+ start: Start byte offset
65
+ size: Number of bytes to read
66
+
67
+ Returns:
68
+ Hex string of read data
69
+ """
70
+ client = _get_client()
71
+ return client.db_read(db_number, start, size)
72
+
73
+
74
+ @mcp.tool()
75
+ def db_write(db_number: int, start: int, data: str) -> str:
76
+ """Write data block to PLC.
77
+
78
+ Args:
79
+ db_number: Data block number
80
+ start: Start byte offset
81
+ data: Hex string of data to write
82
+
83
+ Returns:
84
+ Write status message
85
+ """
86
+ client = _get_client()
87
+ return client.db_write(db_number, start, data)
88
+
89
+
90
+ @mcp.tool()
91
+ def mb_read(start: int, size: int) -> str:
92
+ """Read memory bytes from PLC.
93
+
94
+ Args:
95
+ start: Start byte offset
96
+ size: Number of bytes to read
97
+
98
+ Returns:
99
+ Hex string of read data
100
+ """
101
+ client = _get_client()
102
+ return client.mb_read(start, size)
103
+
104
+
105
+ @mcp.tool()
106
+ def mb_write(start: int, data: str) -> str:
107
+ """Write memory bytes to PLC.
108
+
109
+ Args:
110
+ start: Start byte offset
111
+ data: Hex string of data to write
112
+
113
+ Returns:
114
+ Write status message
115
+ """
116
+ client = _get_client()
117
+ return client.mb_write(start, data)
118
+
119
+
120
+ @mcp.tool()
121
+ def tm_read(start: int, size: int) -> str:
122
+ """Read timers from PLC.
123
+
124
+ Args:
125
+ start: Start byte offset
126
+ size: Number of bytes to read
127
+
128
+ Returns:
129
+ Hex string of read data
130
+ """
131
+ client = _get_client()
132
+ return client.tm_read(start, size)
133
+
134
+
135
+ @mcp.tool()
136
+ def tm_write(start: int, data: str) -> str:
137
+ """Write timers to PLC.
138
+
139
+ Args:
140
+ start: Start byte offset
141
+ data: Hex string of data to write
142
+
143
+ Returns:
144
+ Write status message
145
+ """
146
+ client = _get_client()
147
+ return client.tm_write(start, data)
148
+
149
+
150
+ @mcp.tool()
151
+ def ct_read(start: int, size: int) -> str:
152
+ """Read counters from PLC.
153
+
154
+ Args:
155
+ start: Start byte offset
156
+ size: Number of bytes to read
157
+
158
+ Returns:
159
+ Hex string of read data
160
+ """
161
+ client = _get_client()
162
+ return client.ct_read(start, size)
163
+
164
+
165
+ @mcp.tool()
166
+ def ct_write(start: int, data: str) -> str:
167
+ """Write counters to PLC.
168
+
169
+ Args:
170
+ start: Start byte offset
171
+ data: Hex string of data to write
172
+
173
+ Returns:
174
+ Write status message
175
+ """
176
+ client = _get_client()
177
+ return client.ct_write(start, data)
178
+
179
+
180
+ @mcp.tool()
181
+ def eb_read(start: int, size: int) -> str:
182
+ """Read edge inputs from PLC.
183
+
184
+ Args:
185
+ start: Start byte offset
186
+ size: Number of bytes to read
187
+
188
+ Returns:
189
+ Hex string of read data
190
+ """
191
+ client = _get_client()
192
+ return client.eb_read(start, size)
193
+
194
+
195
+ @mcp.tool()
196
+ def eb_write(start: int, data: str) -> str:
197
+ """Write edge inputs to PLC.
198
+
199
+ Args:
200
+ start: Start byte offset
201
+ data: Hex string of data to write
202
+
203
+ Returns:
204
+ Write status message
205
+ """
206
+ client = _get_client()
207
+ return client.eb_write(start, data)
208
+
209
+
210
+ @mcp.tool()
211
+ def ab_read(start: int, size: int) -> str:
212
+ """Read absolute bytes from PLC.
213
+
214
+ Args:
215
+ start: Start byte offset
216
+ size: Number of bytes to read
217
+
218
+ Returns:
219
+ Hex string of read data
220
+ """
221
+ client = _get_client()
222
+ return client.ab_read(start, size)
223
+
224
+
225
+ @mcp.tool()
226
+ def ab_write(start: int, data: str) -> str:
227
+ """Write absolute bytes to PLC.
228
+
229
+ Args:
230
+ start: Start byte offset
231
+ data: Hex string of data to write
232
+
233
+ Returns:
234
+ Write status message
235
+ """
236
+ client = _get_client()
237
+ return client.ab_write(start, data)
238
+
239
+
240
+ @mcp.tool()
241
+ def get_cpu_info() -> dict[str, Any]:
242
+ """Get CPU information from PLC.
243
+
244
+ Returns:
245
+ Dictionary containing CPU information
246
+ """
247
+ client = _get_client()
248
+ return client.get_cpu_info()
249
+
250
+
251
+ @mcp.tool()
252
+ def get_cpu_state() -> str:
253
+ """Get CPU state from PLC.
254
+
255
+ Returns:
256
+ CPU state string
257
+ """
258
+ client = _get_client()
259
+ return client.get_cpu_state()
260
+
261
+
262
+ @mcp.tool()
263
+ def get_protection() -> dict[str, Any]:
264
+ """Get PLC protection level.
265
+
266
+ Returns:
267
+ Dictionary containing protection level info
268
+ """
269
+ client = _get_client()
270
+ return client.get_protection()
271
+
272
+
273
+ @mcp.tool()
274
+ def plc_cold_start() -> str:
275
+ """Trigger PLC cold start.
276
+
277
+ Returns:
278
+ Status message
279
+ """
280
+ client = _get_client()
281
+ return client.plc_cold_start()
282
+
283
+
284
+ @mcp.tool()
285
+ def plc_hot_start() -> str:
286
+ """Trigger PLC hot start.
287
+
288
+ Returns:
289
+ Status message
290
+ """
291
+ client = _get_client()
292
+ return client.plc_hot_start()
293
+
294
+
295
+ @mcp.tool()
296
+ def plc_stop() -> str:
297
+ """Stop PLC.
298
+
299
+ Returns:
300
+ Status message
301
+ """
302
+ client = _get_client()
303
+ return client.plc_stop()
304
+
305
+
306
+ @mcp.tool()
307
+ def get_error_text(error_code: int) -> str:
308
+ """Get error description from error code.
309
+
310
+ Args:
311
+ error_code: Numeric error code
312
+
313
+ Returns:
314
+ Error description string
315
+ """
316
+ client = _get_client()
317
+ return client.get_error_text(error_code)
318
+
319
+
320
+ @mcp.resource("connection://status")
321
+ def connection_status() -> str:
322
+ """Get current connection status.
323
+
324
+ Returns:
325
+ Connection status as JSON string
326
+ """
327
+ client = _get_client()
328
+ return client.get_status()
File without changes