0.3.0 package and tool migration: Install
industriagents-profinet-mcpfrom npm (requires uv) or PyPI; the command remainsprofinet-mcp. The advertised catalog now contains 11 tools. Older tool names in the examples below remain compatibility calls. See the architecture and canonical tool guide and release guide.
profinet-python
Python MCP server for PROFINET IO networks. Built with uv, mcp[cli],
scapy and lxml, it exposes the profinet-mcp CLI for AI agents and MCP
clients. Which tools work depends on the backend; see
Status in the repository README.
Requirements
- Python 3.11+
- uv
- For
PROFINET_BACKEND=mock: the mock IO device - For
PROFINET_BACKEND=network: an Ethernet interface on the PROFINET network and raw-socket privileges on it (root orCAP_NET_RAWon Linux,/dev/bpf*access on macOS)
Quick start
From the repository root:
uv sync --directory profinet-python
PROFINET_BACKEND=mock uv run --directory profinet-python profinet-mcp # against the mock
uv run --directory profinet-python profinet-mcp --help # options and settings
The server speaks MCP over stdio, so it waits for a client on stdin; see the repository README for an MCP client configuration. Only JSON-RPC goes to stdout; logs go to stderr.
Backends
PROFINET_BACKEND |
Behaviour |
|---|---|
none (default) |
Contacts nothing. Device tools return success: false explaining how to choose a backend. |
mock |
Sends the mock's JSON-lines requests to PROFINET_MOCK_HOST:PROFINET_MOCK_PORT. One TCP connection per call; answers are checked (byte counts, confirmations) before a tool reports success. |
network |
DCP Identify All on PROFINET_INTERFACE for discover_devices, get_device_info and test_device_communication, at most once every 2 s. I/O and DCP Set tools return success: false ("Not implemented ...") and send nothing. |
meta.backend in each envelope names the backend that answered.
Tools that change a device (write_io_data, write_device_by_alias,
set_device_name, set_device_ip, identify_device) also report
meta.outcome and meta.request_sent:
outcome |
request_sent |
Meaning |
|---|---|---|
written |
true |
The device confirmed the change |
rejected |
true |
The device received the request and refused it |
unknown |
true |
Sent, but no usable reply: it may have been applied; check before retrying. It is never re-sent automatically. |
not_sent |
false |
Refused by a switch, invalid arguments, no backend, backend unreachable, or not implemented |
Connecting to the mock is retried (PROFINET_MAX_RETRIES); a read whose reply
is lost is sent again, but a write or DCP command never is. "rejected" is used
only when the mock marks a refusal "rejected": true (refused before anything
changed); any other failure after a send is "unknown".
Every tool call has one absolute deadline, fixed when the call starts
(all connect and reply timeouts, retries and backoff, plus slack: 45.5 s with
the defaults) and shared by every request the call makes; it is never
restarted per request. Waiting to reach a busy device counts against it, and
the deadline is checked again right before each send, so a change is never
sent after it: a request that never left is not_sent, a change that did is
unknown. There is no lock or queue in the server itself (each mock request
uses its own connection, and the DCP rate limit refuses rather than queues).
When the MCP client cancels a call (notifications/cancelled), a change that
has not been sent yet is never sent: the server captures the request's cancel
scope when the call starts and checks it right before each send, because the
SDK only sets a flag and anyio may let the call run one more step first.
When the MCP client disconnects (stdin closes), pending requests are stopped:
one still connecting or waiting to retry is never sent (not_sent), one
already sent is not repeated, no new request starts, and the server exits with
status 0.
Configuration
Set these as environment variables, for example in your MCP client's env
block, or in a file passed with --env-file PATH. Values already in the
environment win over the file. Settings are read when the server starts, and
an unusable value (for example PROFINET_WRITES_ENABLED=maybe) stops it with
a message on stderr. .env.example lists them all.
Without --env-file, the server also reads profinet-python/.env of a
source checkout, found relative to the package source. It never reads a .env
from the working directory or its parents (when an MCP client starts the
server, that is the client's project, which may not be trusted; settings such
as the mock address or the device map decide where a write goes), and an
installed package reads no implicit .env at all. That file is applied
defensively:
- safety switches (
PROFINET_*_ENABLED) in it are ignored - they come only from the real environment or--env-file; - it may set only
PROFINET_*settings andDEVICE_MAP_FILE; anything else (PATH,LD_PRELOAD,PYTHONPATH, ...) is ignored; - keys are compared case-insensitively (as Windows does), with
exportand quotes handled; - each ignored key is named in a warning on stderr.
| Variable | Default | Meaning |
|---|---|---|
PROFINET_BACKEND |
none |
none, mock or network, see above |
PROFINET_WRITES_ENABLED |
false |
true allows tools that change a device (write_io_data, write_device_by_alias; also required by the DCP commands) |
PROFINET_CONFIG_CMDS_ENABLED |
false |
true (together with PROFINET_WRITES_ENABLED) allows set_device_name, set_device_ip and identify_device |
PROFINET_MOCK_HOST |
127.0.0.1 |
Address of the mock IO device |
PROFINET_MOCK_PORT |
5600 |
Port of the mock IO device |
PROFINET_INTERFACE |
eth0 |
Ethernet interface for DCP discovery (network backend) |
PROFINET_DCP_TIMEOUT |
5 |
Default discovery time, in seconds (0.5-60) |
PROFINET_TIMEOUT |
5 |
Timeout for each connect and each reply from the mock, in seconds |
PROFINET_MAX_RETRIES |
3 |
Extra connection attempts to the mock, and re-sends of a read whose reply was lost (0-10); changes are never re-sent |
PROFINET_RETRY_BACKOFF_BASE |
0.5 |
First retry delay in seconds; doubles each attempt |
PROFINET_CONTROLLER_IP |
192.168.1.1 |
Reported by ping only |
PROFINET_NETWORK |
192.168.1.0/24 |
Reported by ping only |
PROFINET_DCP_PORT |
34964 |
Reported by ping only (DCP itself is layer 2 and has no port) |
DEVICE_MAP_FILE |
unset | JSON file of device aliases, see below |
PROFINET_GSD_PATH |
unset | Directory searched by load_gsd_file for relative paths |
Booleans accept true/false, 1/0, yes/no, y/n, on/off and
t/f.
DEVICE_MAP_FILE maps aliases to a device and slot, used by list_devices,
read_device_by_alias and write_device_by_alias:
{
"line1_inputs": {
"device_name": "pn-device-01",
"ip_address": "192.168.1.100",
"description": "Line 1 digital inputs",
"slot": 1,
"subslot": 1,
"data_length": 8
}
}
slot is required (an alias without one is reported as an error rather than
guessed); subslot defaults to 1 and data_length to 8. The file is
re-read when it changes; if it cannot be parsed, list_devices and ping
report the error.
Structure
profinet-python/
├── pyproject.toml
├── uv.lock
├── .env.example # every setting; safety switches only via the real env or --env-file
├── .python-version
├── src/profinet_mcp
│ ├── __init__.py
│ ├── cli.py # argument parsing, .env loading, startup
│ ├── config.py # ProfinetConfig.from_env()
│ ├── server.py # FastMCP wiring; bad arguments -> envelopes
│ ├── tools.py # the 14 MCP tools
│ ├── validation.py # MAC, station name, IP suite, slot checks
│ ├── pn_client.py # safety switches, timing, routing to the backend
│ ├── backends.py # none / mock (JSON over TCP) / network (DCP)
│ ├── dcp.py # DCP Identify codec and Scapy layer-2 transport
│ └── gsd_parser.py # GSDML parsing and cache
└── tests/ # pytest: unit tests and mock-backed integration tests
Tests
cd profinet-python
uv sync --locked
uv run --no-sync ruff check . && uv run --no-sync ruff format --check .
uv run --no-sync pytest
The integration tests start ../profinet-mock-server on an OS-assigned port
and drive the server over stdio with the MCP client SDK. No test opens a raw
socket: DCP is tested against fake layer-2 transports. The safety tests are
mutation-checked: each one fails when the protection it covers is removed.
Dependency floors in pyproject.toml are the lowest versions the suite passes
on; CI's lowest-direct job checks that with --resolution lowest-direct.
Metadata
Release files for industriagents-profinet-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_profinet_mcp-0.3.0.tar.gz | 142.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| industriagents_profinet_mcp-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 203.9 kB
Release files / industriagents_profinet_mcp-0.3.0.tar.gz
| Download URL | industriagents_profinet_mcp-0.3.0.tar.gz |
|---|---|
| Size | 142.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dee869c75896daf1a590918c590f29f035e1c686e4a288bcca7d171356a29b82
|
|
BLAKE2b-256 checksum How to use checksums |
9e248d473d398f380fdfba6b688bf5edc7d66c886d8336c147e42ba8bebbafe1
|
| 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_profinet_mcp-0.3.0-py3-none-any.whl
| Download URL | industriagents_profinet_mcp-0.3.0-py3-none-any.whl |
|---|---|
| Size | 61.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d134594289a5bf46f7d892977f756b7399639012273116d2058eec00fb2b3595
|
|
BLAKE2b-256 checksum How to use checksums |
67a0dc9df621febed9bafd48b3c4ca86c2de8cffecf383c5ab210451e0087d27
|
| 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