MCP server¶
pyDEXPI ships a Model Context Protocol server that exposes loading, querying, discovering and exporting DEXPI P&ID and process models to LLM clients such as Claude Desktop, Claude Code and Cursor. It is a thin layer over the public pyDEXPI API, and a failure comes back as a structured error rather than an exception (see Errors).
Installation¶
The server requires the optional mcp extra, which installs the official MCP Python SDK:
This provides the pydexpi-mcp command.
Extensions¶
The server itself loads, queries, discovers, exports and builds graphs; an installed extension
can add tools, prompts and resources of its own. An extension is a function register(mcp, sessions, file_access) declared under the entry-point group
pydexpi.mcp_extensions; the server registers every installed one after its own tools, in the
order of their names, and skips one that fails to import with a logged warning. In code,
create_server(extensions=()) builds the server without extensions, and
pydexpi.mcp_server.extensions.register_item_fields is how an extension adds reply fields (with
the passage of the tool's description that documents them), and place_tools_after where its
tools are listed among the server's own. The CLI is extended the same way, through
pydexpi.cli_plugins and pydexpi.patch_writers.
Running the server¶
By default the server speaks MCP over stdio, which is what local clients use: the client starts the process and talks to it through its standard input and output.
It can also serve over the network:
pydexpi-mcp --transport streamable-http --port 8000 --file-root ./models
pydexpi-mcp --transport sse --host 127.0.0.1 --port 8000
| Option | Meaning |
|---|---|
--transport |
stdio (default), streamable-http or sse |
--host, --port |
Where a network transport listens (default 127.0.0.1:8000) |
--file-root DIR |
Confine load_dexpi_xml_file and save_model to DIR |
What the network mode protects -- and what it does not¶
It protects clients from each other and the server's files from its clients:
- Every client connection has its own session. Models one client loads or saves are invisible to every other client, whether it asks by tool, by resource, or by guessing a model id. They are freed when the connection closes -- for streamable HTTP, when the client ends the session or when the session expires.
- An idle streamable-HTTP session expires after 30 minutes, and its models with it. The
clock runs between requests; a request in progress or an open
GETstream stops it, so a client that keeps that stream open, as the SDK's own clients do, is not expired while it stays connected. Once a session has expired the client's next request gets404 Session not found, and it must connect again and reload its models. Save work you want to keep (save_model,export_json) before a long pause. The bound is the SDK's default and is kept deliberately: without it, 64 abandoned sessions would hold their models forever and refuse every new client. - Network transports use the session-based protocol. A session is what holds a client's
models, and the 2026-07-28 MCP protocol has none over HTTP: every request stands alone. The
server therefore refuses such requests (
-32022, naming the versions it serves), and a client negotiating automatically -- the default of the SDK's clients -- falls back to theinitializehandshake. A client pinned to 2026-07-28 cannot use a networked pyDEXPI server. Over stdio either protocol works: the server serves the one client that started it. - File access follows the transport. Over stdio the file tools reach any path you can, as your
other local tools do. With
--file-rootthey are confined to that directory: relative paths are taken from it;.., absolute paths, symlinks and junctions cannot lead out of it; network and device paths (\\host\share,\\?\), reserved device names and alternate data streams are refused (access_denied); responses show paths relative to the root; and a save replaces the file rather than writing through a link. Over a network transport without--file-rootthe file tools are disabled; clients pass documents as content (load_dexpi_xml,load_dexpi_json,export_xml,export_json) instead. - Limits: 32 models per session, 64 concurrent sessions, 64 MiB per file read, and the SDK's 4 MiB request body.
It is not a multi-tenant service:
- There is no authentication. Anyone who can reach the port can use the server -- on the loopback host, any local process or user. A session id is a bearer credential; SSE puts it in the URL, where access logs record it.
- One client can slow every other one down. Tool calls and resource reads run one at a time, server-wide -- pyDEXPI's models are not safe to use from several threads at once -- so a client loading or exporting a large model repeatedly delays the rest, and an agent's parallel tool calls run in turn.
- DNS-rebinding protection is enabled by the SDK only when listening on
127.0.0.1,localhostor::1. Binding any other address (--host 0.0.0.0) has none and exposes the server to the network: put it behind something that authenticates. - The file root must not be writable by untrusted users. A local user who can write inside it could swap a directory for a link between the check and the write.
Claude Code¶
Claude Desktop¶
Add to claude_desktop_config.json:
If pydexpi-mcp is not on the PATH the client sees, give the full path to the executable in
your virtual environment instead.
Workflow¶
Every session starts by loading a model. The load tools return the model's id, a summary, what the
loader could not read (loading_issues) and the conceptual GraphML flowsheet, so the client has
the P&ID topology straight away:
→ load_dexpi_xml_file(path="/path/to/model.xml")
← {model_id: "m-def456", conceptual_model_type: "PlantModel", equipment_count: 8, ...,
loading_issues: {counts: {warning: 2}, messages: [...]}, graphml_context: "<?xml ..."}
and fetch it later with export_graphml. DEXPI process exports routinely omit the mandatory
Identifier, and under the default strict=true every such object is dropped -- loading_issues
says how many. Pass strict=false to keep them. Validation then reports each object kept without
a mandatory attribute (pydexpi_mandatory_attribute_has_value, MANDATORY) -- several hundred in a
process export.
load_dexpi_json takes the same strict: a model loaded leniently and saved as JSON still omits
Identifier. A process model's summary counts its process_step_count and its
process_connection_count, split into stream_count, energy_flow_count and
information_flow_count.
Then query and export it:
→ find_all(type="CentrifugalPump")
← {total: 2, items: [{id: "...", tag: "P-101A"}, ...]}
→ find_connections(ref="P-101A")
← {source: {tag: "P-101A"}, neighbors: [...]}
→ build_chat_context(context_mode="overview")
← # DEXPI Model Context ...
Several models can be loaded at once. The most recently loaded or created one is the active model,
which every model-dependent tool acts on unless it is given an explicit model_id;
set_active_model switches it.
Tools¶
Loading and session¶
| Tool | Description |
|---|---|
load_dexpi_xml |
Load a model from an XML string |
load_dexpi_json |
Load a model from a JSON string |
load_dexpi_xml_file |
Load a model from a file path |
list_loaded_models |
List all loaded models |
set_active_model |
Set the active model |
unload_model |
Remove a model from the session |
clear_models |
Remove all models |
Model information¶
| Tool | Description |
|---|---|
get_model_summary |
Equipment, piping, instrument, process step, process connection and object counts |
Query¶
| Tool | Description |
|---|---|
find_by_tag |
Find objects by tag name |
get_by_tag |
Get the unique object with a tag |
get_by_id |
Get an object by DEXPI id |
find_all |
Find with type and graphic filters, paginated |
find_connections |
Find connected neighbours |
find_paths |
Find paths between two objects |
find_on_sheet |
Find objects on a diagram sheet |
find_graphics_for |
Find an object's graphical representations |
Discovery¶
The ids of pipes, piping nodes and segments are not in the object summaries above. These tools return them:
| Tool | Description |
|---|---|
describe_segment |
A segment's source/target item and node, its pipes (each with source/target item and node, and the lines drawn for it: connector_id, points, the arrow_ids of the flow arrows on it and whether it is drawn with or against its flow) and its items with their node ids |
describe_item |
An item's piping nodes (including its nozzles' nodes) with free, holders, drawn and every drawn position, plus an instrument or sensing location's drawn signal_ports with node_position_id and position. For a process model's step, step detail or instrumentation activity: its ports with direction, roles and the connection each ends, each port's delegation -- the chain of super ports above it, the ports delegating it and what the delegation rule reports -- and each port's drawn positions (the node_position_id, the point and the side of its symbol it lies on), a delegated port's delegation_lines, and a step's parent_step and drawing (drawn, the box of its block, its shape and label ids, and its containment: where its block is drawn against the step hierarchy, with the offset that would put it inside its parent); a step detail's step; an instrumentation activity's drawing likewise, the system holding it and, for a measuring one, its measurement: the step, detail or connection it names and the measured_value it reads, with the object, attribute and index that hold it; for an instrumentation system, its activities; for a port, the same with its owner; for a connection, its two ends and their owners, the lines drawn for it with the node positions each is bound to (a free end is null) and its arrow_ids, and a stream's material and component order; for a material state, the streams reading it and its fraction vectors against the component order. An installed extension can add fields of its own |
describe_run |
The items along a line in flow order from a segment or an item, through tees along the run and never into a branch |
describe_segment lists pipes in the order of PipingNetworkSegment.connections (the response
says so in order); that is not guaranteed to be source-to-target order. describe_item reports a
node as free exactly when no connection or segment end names it: the check is
pydexpi.toolkits.piping_toolkit.node_holders. A node that is
not drawn has drawn: false and positions: []; a node drawn more than once lists every position
with the group that holds it.
describe_run is pydexpi.toolkits.piping_toolkit.run_items: within a segment it lists items from
source to target, then carries on into the segment that starts on the same item and node the
previous one ends on -- at a tee, the run outlet, since a branch leaves from another node. It stops
at the line's end and does not list nozzles. across_segments: false stops at the end of the start
segment. A run it cannot decide (two segments starting on one node, a segment end that names no
node, a cycle, or a segment leaving a two-node item from another node than the line ends on) is an
invalid_parameter error, never a guess or a line cut short. Its message names the segment the
walk could not continue and the item where it stopped deciding, with the node and the segments
leaving that item when the model names them; for a cycle, the segment or item the walk came back
to.
Schema¶
Discover valid DEXPI class names, attributes and values, e.g. which classes derive from a base class or which attributes a class has.
| Tool | Description |
|---|---|
list_dexpi_classes |
List classes and enums, filtered by base class, package, name or kind. A process class DEXPI declares abstract (ProcessStep, Splitting, Port, ...) is marked "abstract": true, here and in describe_dexpi_class: every object of it in a document is of a subclass |
describe_dexpi_class |
Documentation, inheritance and attributes of a class (with enum values, and for a quantity property its DEXPI binding and the units it admits; for a port class the role names its ports may perform, its default roles and the connection class joining two of them), or the values of an enum |
Export¶
| Tool | Description |
|---|---|
export_svg |
Render the diagram as SVG |
export_graphml |
Export the plant graph as GraphML |
export_llm_context |
Compact, LLM-ready text of the plant graph |
build_chat_context |
Structured chat context (overview, detailed, equipment_focus, connectivity_focus) |
export_tables |
Equipment, line, instrument and piping component tables as JSON |
export_neo4j_csv |
Neo4j-compatible CSV |
export_xml |
Round-trip export to DEXPI XML |
export_json |
Round-trip export to DEXPI JSON |
save_model |
Save a model to a DEXPI XML or JSON file |
hash_model |
Digest of the model's exact serialized state, stable across processes |
compare_models |
Compare two models, ignoring the order of unordered lists; ids must match |
Graph¶
| Tool | Description |
|---|---|
get_graph_stats |
Node and edge counts, connected components |
get_graph_nodes |
Paginated node listing: id, DEXPI type and display name |
get_graph_edges |
Paginated edge listing |
Tables¶
| Tool | Description |
|---|---|
get_equipment_list |
Equipment list, paginated |
get_nozzle_list |
Nozzle IDs, equipment parents and attachment ID arrays, paginated |
get_line_list |
Segment boundary and system IDs with line properties, paginated |
get_piping_connection_list |
Individual pipe/connection endpoint IDs and statuses, paginated |
get_instrument_list |
Instrument list, paginated |
get_instrument_relationship_list |
Instrument property edges and signal/measuring flows by ID, paginated |
get_piping_component_list |
Piping component list, paginated |
get_complete_piping_inventory |
Recommended inventory for analysis: all piping components, valves and off-page connectors, paginated |
Resources¶
Read-only endpoints a client can browse:
| URI | Description |
|---|---|
dexpi://models |
All loaded models |
dexpi://models/{model_id}/summary |
Model summary |
dexpi://models/{model_id}/equipment |
Equipment list (JSON) |
dexpi://models/{model_id}/lines |
Line list (JSON) |
dexpi://models/{model_id}/instruments |
Instrument list (JSON) |
dexpi://models/{model_id}/llm-context |
LLM-ready text description |
Errors¶
Most tools return JSON; the export tools that produce a document (export_svg, export_graphml,
export_llm_context, build_chat_context, export_xml, export_json) return it as text. A
failure is always this JSON error envelope rather than an exception, so an agent can react to it
programmatically:
{
"error": true,
"error_code": "no_active_model",
"message": "No model is loaded. Use load_dexpi_xml or load_dexpi_json first."
}
Error codes: no_active_model, model_not_found, invalid_xml, invalid_json,
object_not_found, ambiguous_tag, file_not_found, invalid_parameter, export_error,
conversion_refused, internal_error. An installed extension may add codes of its own.
conversion_refused is a strict load (strict=true, the default) of a DEXPI 2.0 document that
DEXPI 2.0.1 cannot represent without copying or dropping data: an object two owners share, one no
owner claims, a reference into a shared object, a State reference from a state that already
composes one, a MeasuredVariable given as a value, or a port's composed SubReference. details
lists all of it; strict=false converts it, and every copy and
drop is then a loading issue, a dropped object's own text in its content.
Object summaries (find_*, get_by_*) carry the object's id, DEXPI type, display_name and,
when it has one, tag -- the same caption and tag pyDEXPI's other exports use.
hash_model and compare_models both depend on object ids. Loading one DEXPI XML file twice
gives two models that differ, because objects the file leaves without an id are given a fresh one
on every load; use them to track a model through a session, not to compare two loads.
Semantic tables retain id, dexpi_class, id_kind and identity_diagnostic in
all languages, including empty tables. id is the unchanged in-memory model ID,
usable with get_by_id; a blank/missing ID is null with missing_id. Duplicate
IDs are retained with duplicate_id diagnostics (do not join those rows without
repairing the model). id_kind=generated_shape flags the UUID shape minted by
pyDEXPI; this is not proof of source provenance, and XML may mint it again on
reload. id_kind=model means another model ID spelling, not a source guarantee.
No tag or number substitutes for an ID. Instrument numbers and display tags remain
nullable even when every row lacks them; JSON exports and MCP return null.
For AI engineering inventory and safety analysis, use MCP
get_complete_piping_inventory(): it always includes valves and off-page
connectors. Omit limit to fetch all rows, or fetch every page before analysing
the inventory. This covers objects in the loaded model; it does not establish
that the source document or plant is complete. In Python, call
get_piping_component_dataframe(model, include_valves=True, include_connectors=True).
The older MCP get_piping_component_list retains False defaults for compatibility.
All PipingComponent subclasses, including inline measuring elements, appear; valves have a separate
table by default. Off-page connectors derive from PipeOffPageConnector, not
PipingComponent, and are topology objects included only with include_connectors.
With include_valves=True alone, the unpaginated total equals summary
piping_component_count; adding connectors increases it by their count. Pipes
are PipingConnection objects, not inventory components. Nozzles use the nozzle
table. Inventory is by composition identity, independent of drawings and tags;
MCP total counts the entire selected inventory before pagination.
get_nozzle_dataframe(model) and MCP get_nozzle_list supplement the existing
flat equipment table. Every nozzle retains id, dexpi_class, sub_tag_name,
parent_id (direct composition owner), parent_equipment_id (nearest tagged
ancestor), node_ids, connection_ids and segment_ids. Join the equipment ID
rather than a duplicate/empty subtag. Arrays name actual pipe/segment references
to the nozzle or its nodes; disconnected nozzles have empty arrays. Null IDs use
the identity diagnostics above. Untagged ancestry yields null equipment ID; shared
composition owners/cycles are explicit export errors rather than guessed parents.
These supplementary columns are untranslated and the five-table default is unchanged.
Line lists contain one row per segment, with piping_system_id,
source_item_id, target_item_id, source_node_id, target_node_id and endpoint
*_status columns. Source draws from upstream; target discharges downstream.
Segment boundaries retain the model's endpoints, including its item/pipe boundary
convention. System data is inherited for presentation without mutating the model.
Null endpoints are missing; detached referenced objects retain IDs with
outside_model; missing/duplicate target IDs are diagnosed separately. A loaded
model cannot recover raw unresolved XML IDs discarded by its loader: a null
reference means absent or unresolved, never a fabricated link.
get_piping_connection_dataframe(model) and MCP get_piping_connection_list
return each actual connection's identity and segment_id, with the same endpoint
columns. They preserve branches, cycles, open ends, connectors and parallel pipes.
Join IDs to the complete component/nozzle inventories; pipes have their own table.
Neither table imposes a universal item sequence. For traversal, MCP find_paths
takes explicit source/target references; describe_run follows a
well-defined piping run or reports ambiguity. Segment, system and path are distinct.
get_instrument_relationship_dataframe(model) / MCP
get_instrument_relationship_list expose instrumentation ownership and reference
edges from generated binding metadata. Property rows have source_id = owning
object and target_id = property value, exact role, category (composition or
reference), and list ordinal. Their id/relationship_id are the actual owning
DEXPI object ID; (id, role, ordinal) distinguishes property rows, without a
fabricated source ID. category=flow, role=flow rows use each signal/measuring
line's own relationship_id and its declared source -> target. Parallel lines,
including lines with identical endpoints, retain separate identities. Target
classes and source/target statuses diagnose missing, detached, blank or duplicate
IDs. Empty optional lists/references produce missing-target rows. Metadata
annotations (persistent IDs, notes, performed roles) are excluded.
Traverse instrument -> actuatingFunctions -> systems ->
operatedValveReference -> valve, or instrument ->
processSignalGeneratingFunctions -> sensingLocation / systems -> measuring
objects. These hops preserve intermediate objects and never join duplicate valve
numbers. Existing human instrument tables remain a compact display; their singular
valve/sensing columns are not a lossless relationship representation. Supplementary
DataFrames can be serialized with to_json(orient="records").
Proteus/DEXPI 1.3 inputs use the existing loader's mapping into the current 2.0.1 classes; DEXPI 2.0 is converted on load. Relationship exports query those actual loaded objects identically across inputs, without reconstructing links lost by a loader or guessing unresolved IDs. Source-file omissions remain omissions.