Architecture#
This document describes the internal architecture of the OpenSWMM MCP Server.
Overview#
The server is built on FastMCP 3.x and follows a composition-based architecture where the root server mounts domain-specific sub-servers, each providing tools scoped to a particular concern.
Root FastMCP ("OpenSWMM MCP Server")
|
|-- lifespan --> ServerSettings + SessionManager
|
| # Lifecycle, query, and forcing
|-- mount(lifecycle_mcp, namespace="lifecycle")
|-- mount(query_mcp, namespace="query")
|-- mount(forcing_mcp, namespace="forcing")
|
| # Analysis + post-processing
|-- mount(analysis_mcp, namespace="analysis")
|
| # Model construction + editing
|-- mount(building_mcp, namespace="building")
|-- mount(editing_mcp, namespace="editing")
|-- mount(model_mcp, namespace="model")
|
| # Fine-grained element accessors
|-- mount(nodes_mcp, namespace="nodes")
|-- mount(links_mcp, namespace="links")
|-- mount(subcatchments_mcp, namespace="subcatchments")
|
| # Forcing / control inputs
|-- mount(inflows_mcp, namespace="inflows")
|-- mount(controls_mcp, namespace="controls")
|
| # Water quality + hydrology configuration
|-- mount(pollutants_mcp, namespace="pollutants")
|-- mount(quality_mcp, namespace="quality")
|-- mount(tables_mcp, namespace="tables")
|-- mount(infrastructure_mcp, namespace="infrastructure")
|
| # State / IO
|-- mount(hotstart_mcp, namespace="hotstart")
|-- mount(spatial_quality_mcp, namespace="spatial")
|-- mount(geopackage_mcp, namespace="geopackage")
|
| # 2D overland-flow surface
|-- mount(twod_mcp, namespace="twod")
|
|-- mount(resources_mcp) # no namespace
|-- mount(prompts_mcp) # no namespace
Twenty tool sub-servers are mounted in total — each declared in its
own openswmm_mcp/tools/<name>.py module as a FastMCP("<name>")
instance. The full list (and mount order) is in
openswmm_mcp/server.py.
Server Composition via mount()#
Each tool phase is defined as its own FastMCP instance in a separate module
under openswmm_mcp/tools/. The root server in server.py mounts these
sub-servers with namespace prefixes, so tool names are automatically
scoped:
lifecycle_mcptools becomelifecycle_open_model,lifecycle_run_simulation, etc.query_mcptools becomequery_get_node_info,query_get_link_info, etc.
Resources and prompts are mounted without namespaces so their URIs remain
short (e.g. swmm://sessions instead of resources_swmm://sessions).
Lifespan and Session Lifecycle#
Server Lifespan#
The server_lifespan async context manager (in dependencies.py) runs once
when the server starts and:
Creates a
ServerSettingsinstance from environment variables.Creates a
SessionManagerwith the configured working directory and max session count.Yields a context dict containing both objects.
On shutdown, calls
SessionManager.cleanup_all()to close all sessions.
All mounted sub-servers inherit this lifespan context through the mount()
mechanism, so every tool can access the SessionManager via
ctx.lifespan_context["session_manager"].
Session States#
A SimSession progresses through these states:
created --> opened --> initialized --> running --> ended --> closed
|
building ----> (finalized via write_model) ---------+
created: Session object exists but solver not yet opened.
opened:
.inpfile parsed.initialized: Engine memory allocated and ready to simulate.
running: Simulation in progress (stepping).
ended: Simulation complete, results available.
closed: All resources released.
building: Model being constructed programmatically via
ModelBuilder.
SimSession#
The SimSession dataclass holds:
backend: ABackendsubclass (OpenSWMMBackendorLegacyBackend) that wraps either the refactoredopenswmm.engine.Solveror the SWMM 5 solver. This is the engine- abstraction seam — see Backends below.state: Current lifecycle state string.working_dir: Path for session-specific files.inp_path/rpt_path/out_path: Resolved file paths.model_builder: AModelBuilderinstance while the session is in thebuildingstate (before a.inpis parsed).
SimSession.__getattr__ delegates unknown attributes (nodes,
links, subcatchments, gages, forcing, mass_balance,
pollutants, statistics, spatial, tables, controls,
inflows, infrastructure, quality, hotstart, model,
editor, …) to the active backend, so tools call
session.nodes.get_depth(...) regardless of which engine is in use.
The backend itself owns the domain accessor objects and caches them
on first access.
Backends#
The backends/ subpackage holds the engine-abstraction layer:
backends.base.Backend— abstract base class declaring the domain-accessor surface (nodes,links, …), lifecycle delegation (open,initialize,start,step,end,report,close), andengine_kind.backends.openswmm.OpenSWMMBackend— wrapsopenswmm.engine.Solverand exposes the v6.0 domain classes.backends.legacy.LegacyBackend— wraps the SWMM 5 solver viaopenswmm.legacy.engine.
When a tool calls lifecycle_open_model(engine="openswmm") the
matching Backend subclass is instantiated and attached to the
session; switching to engine="legacy" swaps in LegacyBackend
without any tool needing to know which solver is underneath.
Threading Model#
All openswmm.engine Cython bindings are synchronous. The server wraps every
engine call in asyncio.to_thread() to avoid blocking the async event loop.
This ensures that long-running operations (e.g. solver.step() during
run_simulation) do not prevent other MCP requests from being processed.
Background Tasks#
The run_simulation tool is decorated with @mcp.tool(task=True), which
means:
The client receives a task ID immediately.
The server runs the simulation loop in the background.
Progress is reported via
ctx.report_progress(pct, 100)at each timestep.The client can poll for the task result.
This is essential for large models where simulation can take minutes.
Error Handling#
All tool errors are raised as ToolError exceptions with structured error
codes defined in errors.py:
SESSION_NOT_FOUNDINVALID_STATEELEMENT_NOT_FOUNDENGINE_ERRORVALIDATION_ERRORMAX_SESSIONS_REACHED
The require_state() helper validates that a session is in an acceptable
state before proceeding, raising ToolError with INVALID_STATE otherwise.
Authentication#
Authentication is only active when the transport is HTTP or SSE. The auth.py
module provides:
OAuth: For interactive users authenticating through browser-based flows.
JWT: For service-to-service authentication using signed tokens.
Both are composed via MultiAuth. When running in stdio mode (the default for
Claude Code), authentication is bypassed entirely.
Module Map#
Module |
Purpose |
|---|---|
|
Package version |
|
CLI entry point ( |
|
Root FastMCP instance, mounts sub-servers |
|
|
|
|
|
Pydantic response models |
|
Error codes and |
|
Lifespan context manager and dependency functions |
|
OAuth/JWT authentication providers |
|
Nineteen tool sub-servers (one FastMCP per namespace) |
|
|
|
OpenSWMM v6.0 backend (refactored engine) |
|
Legacy SWMM 5 backend |
|
|
|
Seven guided-workflow prompt templates |
|
numpy-to-list, time formatting utilities |
|
Path resolution, element validation helpers |