Skip to main content

0.3.0 package and tool migration: Install industriagents-s7comm-mcp from npm (requires uv) or PyPI; the command remains s7comm-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 loads s7comm-python/.env: it walks up from the s7comm_mcp package to the nearest pyproject.toml and uses the .env beside it only if that is this project's own pyproject.toml. It never reads the working directory, the repository root or anything above. An installed server (any package under site-packages or dist-packages, even in a venv inside s7comm-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_ENABLED and S7_SYSTEM_CMDS_ENABLED are honoured only from the process environment or an --env-file. An implicit .env may 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 an export prefix or quotes make no difference, so s7_writes_enabled=true is 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_* and TAG_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-file may 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.

  • BOOL uses bit_index 0-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.
  • STRING is the S7 STRING[N]: a max-length byte, a current-length byte, then the characters (Latin-1). Reads return only the text; string_length (the N) is optional and, if given, must match the header in the PLC. Writes require string_length, check it against the header, refuse text longer than N, 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_index is accepted only with BOOL and string_length only with STRING; a size must match the type. Anything else is refused rather than ignored, so a stray bit_index can 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=false until 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)

Source distribution for industriagents-s7comm-mcp 0.3.0
File Size Uploaded
industriagents_s7comm_mcp-0.3.0.tar.gz 128.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for industriagents-s7comm-mcp 0.3.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page