Skip to content

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:

pip install "pydexpi[mcp]"

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

pydexpi-mcp

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 GET stream 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 gets 404 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 the initialize handshake. 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-root they 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-root the 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, localhost or ::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 mcp add pydexpi -- pydexpi-mcp

Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "pydexpi": {
      "command": "pydexpi-mcp"
    }
  }
}

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.