0.3.0 package and tool migration: Install
industriagents-s7comm-mcpfrom npm (requires uv) or PyPI; the command remainss7comm-mcp. The advertised catalog now contains 9 tools. Older tool names in the examples below remain compatibility calls. See the architecture and canonical tool guide and release guide.
s7comm-python
Python implementation of the Siemens S7 MCP server built with uv, mcp[cli], and python-snap7. The CLI entry point is s7comm-mcp. See the repository README for an overview, the tool list and known limitations.
Prerequisites
- Python 3.11+
- uv
- Snap7 native library: bundled in the python-snap7 wheels for macOS, glibc Linux (x86_64, aarch64) and Windows x64, so there is nothing to install there. Elsewhere (e.g. musl/Alpine, 32-bit ARM) build or install libsnap7 yourself.
Setup
From the repository root, with the mock PLC running (uv run s7comm-mock-server in s7comm-mock-server/, port 1102):
cd s7comm-python
uv sync # installs dependencies into .venv
S7_HOST=127.0.0.1 S7_PORT=1102 uv run s7comm-mcp # MCP over stdio, against the mock PLC
The server speaks MCP over stdio, so on its own it just waits for a client; the repository README has the client configuration. It starts without a reachable PLC: the connection is opened by the first tool call that needs it, and ping reports whether the PLC answers.
Configuration
Settings come from the process environment (for example the env block of an MCP client configuration), from a .env file, or from a file passed with --env-file. Copy .env.example to start one.
- Implicit
.env. In a source checkout the server loadss7comm-python/.env: it walks up from thes7comm_mcppackage to the nearestpyproject.tomland uses the.envbeside it only if that is this project's ownpyproject.toml. It never reads the working directory, the repository root or anything above. An installed server (any package undersite-packagesordist-packages, even in a venv insides7comm-python/) has no implicit.env. --env-file PATH. Loads that file instead, e.g."args": [..., "run", "s7comm-mcp", "--env-file", "/abs/path/plant.env"]in a client configuration.- Precedence. Variables already set in the environment win over either file. The file that was loaded is logged on stderr.
- Safety switches.
S7_WRITES_ENABLEDandS7_SYSTEM_CMDS_ENABLEDare honoured only from the process environment or an--env-file. An implicit.envmay set connection settings, but a value in it that would enable a switch is ignored with a warning on stderr naming the variable. Names are compared case-insensitively, and anexportprefix or quotes make no difference, sos7_writes_enabled=trueis ignored too (Windows would upper-case it into the real switch). - Nothing else from the implicit
.env. It may set only this server's own settings,S7_*andTAG_MAP_FILE. Any other key (PATH,LD_PRELOAD,DYLD_*,PYTHON*, ...) is ignored with a warning, because it could change how the process itself behaves.--env-filemay set anything. A server started from an untrusted project directory therefore cannot be talked into writing, or into loading other code, by a file it happens to find.
S7_HOST=192.168.0.1
S7_PORT=102
S7_RACK=0
S7_SLOT=2
S7_CONNECTION_TYPE=PG
S7_TIMEOUT=5
S7_MAX_RETRIES=3
S7_RETRY_BACKOFF_BASE=0.5
TAG_MAP_FILE=/absolute/path/to/tags.json
| Variable | Default | Meaning |
|---|---|---|
S7_HOST |
127.0.0.1 |
PLC address |
S7_PORT |
102 |
ISO-on-TCP port (the mock uses 1102) |
S7_RACK / S7_SLOT |
0 / 2 |
CPU rack and slot (see Connecting to a real PLC) |
S7_CONNECTION_TYPE |
PG |
Snap7 connection resource: PG, OP or S7BASIC |
S7_TIMEOUT |
5 |
Seconds. Bounds the TCP connect, and is Snap7's ping, send and receive timeout |
S7_REQUEST_DEADLINE |
45 |
Seconds. Overall deadline for one tool call, counted from its start and shared by every PLC request it makes (a STRING header read and the write, a batch's checks and writes, waiting for another call to finish, reconnects and retries). Shorter if S7_TIMEOUT x 6 x (S7_MAX_RETRIES + 1) is; longer by 2 x S7_TIMEOUT per extra PDU for large transfers. It stays below the 60 s at which MCP clients commonly give up, so the server decides the outcome. Must be larger than S7_TIMEOUT |
S7_MAX_RETRIES |
3 |
Reconnect-and-retry attempts after a link error (TCP/ISO/timeout). Reads are retried. Writes and RUN/STOP are never resent: only connecting and a short read that checks the link before the request are retried. Errors the CPU reports, such as CPU : Item not available, are returned at once |
S7_RETRY_BACKOFF_BASE |
0.5 |
Seconds; doubles on each retry |
S7_DEBUG |
false |
true logs Snap7 and server debug output to stderr |
S7_WRITES_ENABLED |
false |
true lets the write tools change PLC memory and outputs; otherwise they refuse. Not read from an implicit .env |
S7_SYSTEM_CMDS_ENABLED |
false |
true allows set_cpu_state (RUN/STOP), together with S7_WRITES_ENABLED=true. Not read from an implicit .env |
TAG_MAP_FILE |
unset | Path to a JSON tag map (below) |
Invalid values (a port that is not a number, S7_WRITES_ENABLED=enable, an unknown connection type) stop the server at startup with exit code 2 and a message on stderr. stdout carries only MCP JSON-RPC.
Data types
read_db_typed, write_db, write_output, write_marker and the batch and tag tools take data_type = BOOL, BYTE, WORD, DWORD, INT, DINT, REAL or STRING. Values are big-endian, as the PLC stores them, and range-checked before anything is sent. Types are strict: addresses, offsets, sizes, bit indexes, byte values and integer values must be JSON integers (never booleans, strings or 4.0), and REAL values must be numbers.
BOOLusesbit_index0-7 (reads default to bit 0; writes require it). A write changes only that bit, using the S7 BIT transport size, so the other seven bits of the byte are never rewritten. Values must be JSON booleans (true/false);1,"true"and the like are refused.STRINGis the S7STRING[N]: a max-length byte, a current-length byte, then the characters (Latin-1). Reads return only the text;string_length(theN) is optional and, if given, must match the header in the PLC. Writes requirestring_length, check it against the header, refuse text longer thanN, and write the header plus the text.- Without
data_type, writes take raw bytes: a hex string ("0a ff") or a list of values 0-255. bit_indexis accepted only withBOOLandstring_lengthonly withSTRING; asizemust match the type. Anything else is refused rather than ignored, so a straybit_indexcan never turn into a whole-byte write.- Process-image inputs (
input) can be read but never written.
What a write reports
Every write (write_db, write_output, write_marker, write_tag, each write_multiple_vars entry) and set_cpu_state returns meta.outcome and meta.request_sent:
outcome |
request_sent |
Meaning |
|---|---|---|
written |
true | The PLC confirmed it |
rejected |
true | The CPU explicitly refused a request that fits in one PDU (up to 205 bytes, or a bit, or RUN/STOP); nothing was applied |
unknown |
true | Anything else after the request went out: a lost or garbled reply, a longer write that failed part-way (it may be partly applied), or RUN/STOP accepted but its mode not confirmed (not reached in time, or the read-back failed). It may have been applied: check before retrying. It is never resent automatically |
not_sent |
false | Stopped before the request went out: writes disabled, invalid arguments, a failed check, a failure while building the request, an unreachable PLC, or the deadline expiring before the request was sent (it is then never sent later) |
When the deadline passes, the server stops waiting: a write already sent is unknown, one not sent yet is not_sent and is never sent afterwards (in a batch, so are all entries not yet sent), and a read fails with a clean error. A Snap7 call that is still busy at that point (for instance a reply trickling in) is interrupted by closing its connection; against a PLC that simply stays silent it ends at the latest after S7_TIMEOUT, which is why S7_TIMEOUT must be smaller than S7_REQUEST_DEADLINE (the server refuses to start otherwise).
If the MCP client cancels a call (notifications/cancelled) or disconnects (stdin closes), every write or RUN/STOP that has not gone out yet (still queued, connecting, retrying or checking the target) is cancelled and is never sent afterwards (not_sent). One already sent is not resent.
Tool annotations follow the same line: the write tools and set_cpu_state are marked destructiveHint: true, idempotentHint: false, and the read tools readOnlyHint: true, so a client is never invited to retry a write on its own.
Before a write is sent, the server reads its whole target range from the PLC, so a range the PLC does not have is refused as not_sent instead of being partly written. write_multiple_vars first checks every entry locally and then reads every target from the PLC (including STRING headers); if any check fails, nothing is written. The entries are then written in order, and the batch stops at the first rejected or unknown entry. Its meta.outcome is that entry's outcome.
Tag map
TAG_MAP_FILE points at a JSON object mapping tag names to addresses. Use an absolute path in MCP client configurations: a relative path is resolved against the directory the server is started in (s7comm-python/ when started with uv --directory s7comm-python run s7comm-mcp).
{
"MotorSpeed": { "area": "db", "db_number": 1, "offset": 0, "data_type": "REAL", "description": "Motor speed" },
"AlarmActive": { "area": "db", "db_number": 2, "offset": 0, "data_type": "BOOL", "bit": 0 },
"Status": { "area": "db", "db_number": 2, "offset": 2, "data_type": "STRING", "string_length": 30 },
"PumpCommand": { "area": "output", "byte": 0, "data_type": "BOOL", "bit": 0 }
}
Tag and batch entries use the same keys: area (db, input, output or marker; default db), db_number (data blocks only), the byte offset as offset, start_offset, byte or start_byte (required), bit or bit_index (BOOL only), data_type, string_length (STRING only), and size for raw reads. Numbers must be JSON integers. An entry without an offset is rejected rather than read at offset 0, and input entries can be read but not written.
The file is re-read when it changes. If TAG_MAP_FILE is set but the file is missing or is not valid JSON, list_tags, read_tag and write_tag return success: false with the reason instead of an empty list.
Connecting to a real PLC
Try everything against the mock first. Then, for a real CPU:
- Rack and slot. S7-300: rack 0, slot 2 (the defaults). S7-1200/1500: rack 0, slot 1, or 0 in some configurations (
S7_SLOT=1). S7-400: the rack and slot of the CPU in the hardware configuration. - PUT/GET (S7-1200/1500). In TIA Portal, open the CPU properties, go to Protection & Security > Connection mechanisms and enable Permit access with PUT/GET communication from remote partner. The CPU's protection level must also allow what you do: writes need write access.
- Non-optimized data blocks (S7-1200/1500). Absolute offsets only work on DBs without optimized block access: in each DB's properties under Attributes, clear Optimized block access, then compile and download.
- Writes. Keep
S7_WRITES_ENABLED=falseuntil the session needs to write, and read SECURITY.md first.
With the wrong slot, or PUT/GET disabled, the connection or the first request fails and the tool returns success: false with the Snap7 error.
Tests
uv sync --extra dev
uv run pytest # unit tests, mock PLC tests, MCP over stdio
uv run ruff check . ../s7comm-mock-server
uv run ruff format --check . ../s7comm-mock-server
The integration tests start the mock PLC from ../s7comm-mock-server/s7_mock_server.py on a free loopback port and stop it again; no PLC or second virtualenv is needed.
Project Layout
s7comm-python/
├── README.md
├── pyproject.toml
├── uv.lock
├── .env.example
├── .python-version
├── .gitignore
├── src/s7comm_mcp
│ ├── __init__.py
│ ├── cli.py # entry point: .env, logging to stderr, config errors
│ ├── config.py # environment parsing and validation
│ ├── server.py # FastMCP wiring, argument-error envelopes
│ ├── tools.py # the MCP tools, encoding and validation
│ └── s7_client.py # python-snap7 wrapper: connect, retry, bit writes
└── tests/ # pytest suite (see Tests)
Status
All tools return the { success, data, error, meta } envelope and work against the bundled mock. The known limitations list what is not supported; the server has not yet been validated across a range of real CPUs.
Metadata
Release files for industriagents-s7comm-mcp 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| industriagents_s7comm_mcp-0.3.0.tar.gz | 128.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| industriagents_s7comm_mcp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 184.7 kB
Release files / industriagents_s7comm_mcp-0.3.0.tar.gz
| Download URL | industriagents_s7comm_mcp-0.3.0.tar.gz |
|---|---|
| Size | 128.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3ef4b626e0aab1899e22fe9d0913bd5fa703dd65d59cbbba3429d4842673e8bf
|
|
BLAKE2b-256 checksum How to use checksums |
fa266b2c0aa89212196259715fa5efb7ed1ec85a46a3ad6ad85fd2ac93b303cb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / industriagents_s7comm_mcp-0.3.0-py3-none-any.whl
| Download URL | industriagents_s7comm_mcp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 56.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b7fe1665de054501a60e4cb8f58ad8dc87ee80292888aae89d450f5d9272533e
|
|
BLAKE2b-256 checksum How to use checksums |
ea4e1de24bf0f32a69f7a1225f1ceb7721f616e2c61113bf6aa8c93fff1fb582
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency log