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.
- mcp_snap7-0.1.0/.gitignore +25 -0
- mcp_snap7-0.1.0/LICENSE +21 -0
- mcp_snap7-0.1.0/PKG-INFO +111 -0
- mcp_snap7-0.1.0/README.md +74 -0
- mcp_snap7-0.1.0/pyproject.toml +89 -0
- mcp_snap7-0.1.0/src/mcp_snap7/__init__.py +9 -0
- mcp_snap7-0.1.0/src/mcp_snap7/__main__.py +10 -0
- mcp_snap7-0.1.0/src/mcp_snap7/_client.py +190 -0
- mcp_snap7-0.1.0/src/mcp_snap7/_mcp.py +328 -0
- mcp_snap7-0.1.0/src/mcp_snap7/py.typed +0 -0
|
@@ -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
|
mcp_snap7-0.1.0/LICENSE
ADDED
|
@@ -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.
|
mcp_snap7-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/mcp-snap7/)
|
|
43
|
+
[](https://pypi.org/project/mcp-snap7/)
|
|
44
|
+
[](https://codecov.io/gh/daedalus/mcp-snap7)
|
|
45
|
+
[](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
|
+
[](https://pypi.org/project/mcp-snap7/)
|
|
6
|
+
[](https://pypi.org/project/mcp-snap7/)
|
|
7
|
+
[](https://codecov.io/gh/daedalus/mcp-snap7)
|
|
8
|
+
[](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,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
|