# MTConnect (Agent-first) Driver — Research & Design

**Status:** Research / design proposal. No code written yet.
**Author:** research pass, 2026-07-15.
**Scope:** A new standard **Equipment-kind driver** (`DriverType = "MTConnect"`) exposing an
MTConnect Agent's data under the OtOpcUa unified address space — the same shape as
Modbus / S7 / AbCip / TwinCAT / FOCAS / OpcUaClient.

---

## 0. TL;DR

- **Browseable: YES.** MTConnect `/probe` returns a fully self-describing device model
  (Device → Components → DataItems) — a natural `IDriverBrowser` tree, closest in spirit to the
  OpcUaClient browser.
- **Recommended library: TrakHound `MTConnect.NET` (client packages), MIT-licensed.** It is
  mature (v6.9.0.2, Oct 2025, 500k+ NuGet downloads), targets **netstandard2.0** (so it loads on
  .NET 10) plus net6/7/8/9, and covers MTConnect versions up to 2.5. Hand-rolling is a viable
  fallback but not recommended for v1.
- **Read-only v1** (Discover + Read + Subscribe). No Write. MTConnect *Interfaces* (request/response
  write-back) exist but are rare, optional, and out of scope for v1.
- **Top risks:** (1) MTConnect data is *loosely typed* — DataItems are strings with `type`/`units`
  metadata, so the OPC UA data-type mapping is a heuristic, not a guarantee; (2) `CONDITION`
  category has no clean scalar OPC UA analog and needs a modelling decision (fold to string vs.
  native Part 9 alarm).

---

## 1. Protocol summary + .NET library options

### 1.1 The two MTConnect layers

MTConnect ([standard docs](https://docs.mtconnect.org/), [SysML model v2.5](https://model.mtconnect.org/Version2.5/Fundamentals/))
has two wire layers:

| Layer | Transport | Device model? | Role |
|---|---|---|---|
| **Agent** (primary) | HTTP REST, XML (or JSON) | **Yes** — self-describing | The queryable server. What apps talk to. |
| **Adapter / SHDR** | Raw pipe-delimited TCP (default `:7878`) | **No** | Feeds an Agent from a machine; no model, no discovery. |

We target the **Agent** as the primary source. SHDR is a possible phase-2 ingest mode (§7) but
loses auto-discovery.

### 1.2 Agent REST verbs

The Agent exposes three request types (MTConnect Standard Part 1, §5.4 / §8):

- **`/probe`** (optionally `/{device}/probe`) → an **`MTConnectDevices`** response document: the
  Device Information Model — every `Device`, its nested `Components`, and each `DataItem`
  definition (`id`, `type`, `category`, `subType`, `units`, `nativeUnits`, `name`).
  *This is the discovery + browse surface.*
- **`/current`** (optionally `?at=<sequence>`) → an **`MTConnectStreams`** response document: a
  snapshot of the latest `Observation` for every DataItem at the moment of the request. *This is the
  Read surface.*
- **`/sample?from=<seq>&count=<n>&interval=<ms>`** → an **`MTConnectStreams`** document containing a
  *time-ordered set* of observations since sequence `from`. *This is the Subscribe surface.*

**Streaming / long-poll.** When `interval` is supplied, the Agent holds the HTTP connection open and
pushes successive `MTConnectStreams` chunks as a **`multipart/x-mixed-replace`** boundary stream —
each chunk is one batch of new observations. Each response `Header` carries `nextSequence`; a poller
issues the next `/sample` with `from=nextSequence` to get a contiguous, gap-free stream. The Agent's
ring buffer has a finite `bufferSize`; if a consumer falls behind past the buffer, it must re-`/current`
to re-baseline. (Sources:
[MTConnect.NET README](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md),
[cppagent README](https://github.com/mtconnect/cppagent/blob/master/README.md),
[Part 1 Overview](https://docs.mtconnect.org/MTC_Part_1_Overview+V1.2.pdf).)

### 1.3 The information model (probe)

`Device → Component(s) → DataItem(s)`. A `DataItem` has three **categories** (from the
[SysML model](https://model.mtconnect.org/Version2.5/Fundamentals/)):

- **SAMPLE** — continuous numeric measurement over time. Types e.g. `Position`, `Temperature`,
  `SpindleSpeed`, `PathFeedrate`, `Load`, `Acceleration`. Carries `units`/`nativeUnits`.
- **EVENT** — discrete state / value change. Types e.g. `Availability` (AVAILABLE/UNAVAILABLE),
  `Execution` (READY/ACTIVE/INTERRUPTED/STOPPED), `ControllerMode` (AUTOMATIC/MANUAL/...),
  `Program`, `EmergencyStop`, `Block`. Values are typically controlled vocabularies (enums) or free
  strings/ints.
- **CONDITION** — health/alarm state. An observation is reported as one of `Normal` / `Warning` /
  `Fault` / `Unavailable`, with optional `nativeCode`, `nativeSeverity`, `qualifier`, and message
  text. Subtypes: `Actuator`, `Communications`, `System`, `Temperature`, `LogicProgram`, etc.

An observation in an `MTConnectStreams` document references its definition by **`dataItemId`** (and
carries a `sequence`, `timestamp`, and value). This `dataItemId` is the stable per-tag address (§3).

### 1.4 .NET library options

**Recommended: TrakHound `MTConnect.NET` client packages (MIT).**

| Fact | Value | Source |
|---|---|---|
| Latest version | 6.9.0.2 (published 2025-10-16) | [NuGet MTConnect.NET-HTTP](https://www.nuget.org/packages/MTConnect.NET-HTTP/) |
| License | **MIT** (per latest NuGet package metadata) | [NuGet MTConnect.NET-HTTP](https://www.nuget.org/packages/MTConnect.NET-HTTP/) |
| TFMs | netstandard2.0, net6.0–net9.0, net48 (**runs on .NET 10 via netstandard2.0**) | [README](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md) |
| MTConnect versions | up to 2.5; auto-strips data not valid for requested version | [README](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md) |
| Maturity | 500k+ NuGet downloads; actively maintained | [README](https://github.com/TrakHound/MTConnect.NET/blob/master/README.md) |

Packages we would consume (client side only — **not** the Agent/Adapter host packages):

- **`MTConnect.NET-Common`** — model types (`IDevice`, `IComponent`, `IDataItem`, `IObservation`),
  the `IMTConnectClient` / `IMTConnectEntityClient` interfaces.
- **`MTConnect.NET-HTTP`** — `MTConnectHttpClient`: probe/current/sample with polling **and**
  streaming, gzip compression, XML **and** JSON. Exposes events for received documents
  (`OnProbeReceived`, `OnCurrentReceived`, `OnSampleReceived`, plus a per-observation callback) —
  a clean fit for pushing into `ISubscribable.OnDataChange`.
- (`MTConnect.NET-XML` / `-JSON` are pulled transitively for serialization.)

> ⚠️ **License due-diligence caveat:** older release notes and the repo copyright header have shown
> mixed "MIT" / "Apache-2.0" / "© TrakHound, All Rights Reserved" strings across versions. The
> *current* NuGet package metadata for 6.9.0.2 declares **MIT**. Before taking the dependency,
> pin a specific version and confirm that exact version's embedded `LICENSE` / package license
> expression (a legal-review checkbox, not a blocker).

**Fallback: hand-roll a minimal XML client.** The Agent protocol is just three HTTP GETs returning
XML. A hand-rolled client (`HttpClient` + `System.Xml.Linq` for probe, a `multipart/x-mixed-replace`
boundary reader for sample) is ~300–500 LoC and removes a third-party dependency + its transitive
`System.Text.Json` version constraints. **Recommendation: use the library for v1** (it handles version
negotiation, the multipart framing, buffer-overflow re-baselining, and JSON/XML dual-format — all
fiddly to reimplement correctly), and keep hand-roll as a contingency if the license review fails.

---

## 2. Capability mapping to the Equipment-kind seams

The driver implements the composable capability interfaces in
`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/`. Mapping:

| OtOpcUa seam | MTConnect mechanism | Notes |
|---|---|---|
| `IDriver` (required) | Construct `MTConnectHttpClient` from config, hold `/probe` model | `InitializeAsync` does one probe; `GetHealth` reflects last successful poll + Agent `instanceId` |
| `ITagDiscovery.DiscoverAsync` | Walk the `/probe` DeviceModel, stream `Device→Component→DataItem` into `IAddressSpaceBuilder` | `RediscoverPolicy = Once` (probe is synchronous & complete). Redeploy on Agent `instanceId` change (see `IRediscoverable`, §below) |
| `IReadable.ReadAsync(fullRefs)` | `/current` snapshot, index observations by `dataItemId`, return one `DataValueSnapshot` per requested ref | Reads are idempotent (matches `IReadable` contract). Per-ref miss → `Bad`-coded snapshot, not a throw |
| `ISubscribable.SubscribeAsync` | Start the `MTConnectHttpClient` **sample stream** (`from`/`interval`); fan each received observation to `OnDataChange` keyed by `dataItemId` | One shared stream per driver instance (not per tag) — the Agent streams the whole device; filter to subscribed refs. Fire initial values from `/current` on subscribe (OPC UA convention) |
| `IWritable` | **NOT implemented (v1)** | See verdict below |
| `IRediscoverable.OnRediscoveryNeeded` | Raise when the Agent's `Header/@instanceId` or `@assetBufferSize` changes (Agent restarted / model changed) → Host rebuilds address space | Mirrors Galaxy's `DeployWatcher` pattern |
| `IHostConnectivityProbe` | Cheap periodic `/probe` HEAD or `/current` to flip Running↔Stopped | Mirrors `ModbusProbeOptions` |
| `IAlarmSource` | *(optional, phase 1.5)* map `CONDITION` Fault/Warning → Part 9 native alarm | See §2.3 |

### 2.1 Write verdict: **read-only for v1**

Justification:
1. The mainstream MTConnect surface (`/probe`, `/current`, `/sample`) is **strictly read-only** by
   design — MTConnect was conceived as a read-only telemetry standard.
2. Write-back exists only via **MTConnect *Interfaces*** (a request/response handshake modelled as
   its own DataItem categories — `Request`/`Response` events) which is **optional, rarely deployed**,
   and semantically a state-machine handshake, not a simple "set value." Modelling it as OPC UA
   `IWritable` would be misleading.
3. The Equipment-kind seams are composable — a driver implements only what its backend supports
   (`IDriver` doc-comment). Omitting `IWritable` is idiomatic (Galaxy's write path is even
   fire-and-forget; several drivers are read-mostly). Nodes materialize without the
   `AccessLevels.CurrentWrite` bit.

Revisit Interfaces write-back in a later phase only if a concrete deployment needs it.

### 2.2 Data-type mapping (MTConnect → `DriverDataType` → OPC UA)

MTConnect DataItems are **weakly typed on the wire** (values arrive as strings). The mapping is
inferred from `category` + `type` + `units`, and stored per-tag in the TagConfig (§3) so a wrong
inference can be corrected by the author without a code change.

| MTConnect | Inferred `DriverDataType` | Rationale |
|---|---|---|
| `SAMPLE` (numeric, has `units`) | `Float64` | Continuous analog; `Double` is the safe superset |
| `SAMPLE` w/ `representation="TIME_SERIES"` | `Float64` array (ValueRank=1) | Time-series sample bursts |
| `EVENT` controlled-vocab (Execution, ControllerMode, Availability, …) | `String` | Enum-like; keep the vocab string. (Optional: OPC UA enum modelling later) |
| `EVENT` numeric (e.g. `PartCount`, `Line`) | `Int64` | Integer counters |
| `EVENT` free text (`Program`, `Block`, `Message`) | `String` | |
| `CONDITION` | `String` (v1: the state word `Normal`/`Warning`/`Fault`/`Unavailable`) **or** native alarm (§2.3) | No clean scalar analog |
| `timestamp` on every observation | → OPC UA `SourceTimestamp` | Not a separate node |

`DataValueSnapshot.StatusCode` mapping: an observation value of `UNAVAILABLE` (MTConnect's explicit
"no data" sentinel) → OPC UA `Bad`/`Uncertain` quality rather than a literal string, so downstream
sees proper quality. Empty CONDITION or missing dataItem → `Bad`.

### 2.3 CONDITION modelling (decision point)

`CONDITION` is the awkward one. Two options:
- **v1 simple:** expose each CONDITION DataItem as a `String` variable node whose value is the current
  condition state (`Fault`/`Warning`/`Normal`/`Unavailable`), optionally suffixed with `nativeCode`.
  Zero alarm plumbing.
- **v1.5 native:** treat a Fault/Warning CONDITION as an OtOpcUa **native alarm** by emitting a
  TagConfig `alarm` object and implementing `IAlarmSource` — matching the Galaxy/Phase-B native-alarm
  pattern (route on the authored dotted reference, per the alarms memory). Higher value, more work.

Recommend **v1 simple, v1.5 native** as a fast-follow.

---

## 3. TagConfig JSON shape

Consistent with the Equipment-kind model: **connection-level settings live in the driver config**
(agent base URL, auth, poll interval); **per-tag addressing lives in `TagConfig`**, and the platform
`FullName` binds the tag to a driver-side reference. For MTConnect the natural stable reference is the
**`dataItemId`** (globally unique within an Agent), so `FullName = "<dataItemId>"` (or, for
readability, the driver can also accept a `device/component/dataItemName` path and resolve it to the id
at discovery time).

### 3.1 Driver config (per driver instance) — `MTConnectDriverOptions`

```jsonc
{
  "AgentUri": "https://demo.mtconnect.org",   // Agent base URL (http or https)
  "DeviceName": "",                            // optional: scope to one device (else all devices)
  "PreferJson": false,                          // request application/json instead of XML if Agent supports it
  "SampleIntervalMs": 1000,                     // long-poll interval passed to /sample
  "SampleCount": 500,                           // max observations per sample chunk
  "HeartbeatMs": 10000,                         // Agent keep-alive heartbeat
  "RequestTimeoutMs": 30000,
  "AllowUntrustedTls": false,                   // dev/on-prem self-signed agents
  "Probe": { "Enabled": true, "IntervalMs": 5000 }  // IHostConnectivityProbe
}
```

### 3.2 Per-tag `TagConfig` (authored on the /uns Tags tab)

```jsonc
{
  "FullName": "avail",                       // <-- MTConnect dataItemId; the driver-side reference
  "mtDevice": "OKUMA.Lathe",                 // optional human context (device name from probe)
  "mtComponent": "Controller",               // optional
  "mtCategory": "EVENT",                     // SAMPLE | EVENT | CONDITION (from probe; drives typing)
  "mtType": "AVAILABILITY",                  // MTConnect DataItem type
  "mtSubType": null,
  "dataType": "String",                      // resolved DriverDataType (overridable by author)
  "units": null,                             // e.g. "CELSIUS" for a SAMPLE
  "writable": false                          // always false for MTConnect v1
}
```

A picked SAMPLE example:

```jsonc
{
  "FullName": "Xact",
  "mtDevice": "OKUMA.Lathe",
  "mtComponent": "Linear[X]",
  "mtCategory": "SAMPLE",
  "mtType": "POSITION",
  "mtSubType": "ACTUAL",
  "dataType": "Float64",
  "units": "MILLIMETER",
  "writable": false
}
```

The runtime driver resolves reads/subscriptions by `FullName` (= `dataItemId`) against the observation
index it maintains from `/current` and `/sample` — exactly the "bind by `TagConfig.FullName`" pattern
the other drivers use (`EquipmentTagRefResolver<TDef>`). Only `FullName` is load-bearing at runtime;
the `mt*`/`units` fields are author-facing metadata + type inference inputs preserved across edits.

---

## 4. Browseability verdict + browse design

### 4.1 Verdict: **YES — fully browseable.**

`/probe` returns the complete `MTConnectDevices` model in one call — a static, self-describing tree.
This is the *cleanest possible* browse source (better than Galaxy, which needs per-node expansion;
comparable to a single-shot OPC UA browse). It maps directly onto `IDriverBrowser`/`IBrowseSession`.

### 4.2 New project: `ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Browser`

Mirror `OpcUaClient.Browser` layout exactly:

```
src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Browser/
  MTConnectDriverBrowser.cs     // IDriverBrowser: DriverType="MTConnect"; OpenAsync(configJson) -> session
  MTConnectBrowseSession.cs     // IBrowseSession over the fetched probe model
  ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Browser.csproj
```

Register in AdminUI DI alongside the others
(`src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs`, ~line 49-50):

```csharp
services.AddSingleton<IDriverBrowser, MTConnectDriverBrowser>();
```

### 4.3 Session behaviour (map to `IBrowseSession`)

Unlike OPC UA (live one-level-per-call), MTConnect probe is fetched **once** in
`OpenAsync` and cached in the session; expansion is served from the in-memory tree (no further network
calls). This is simpler and faster than the OpcUaClient browser.

| Method | Returns |
|---|---|
| `OpenAsync(configJson)` | Deserialize `MTConnectDriverOptions`, `GET {AgentUri}/probe`, hold the parsed `IDevice[]` model in the session |
| `RootAsync()` | One `BrowseNode(Kind=Folder)` per **Device** (or, if `DeviceName` is scoped, the top-level **Components** of that device) |
| `ExpandAsync(nodeId)` | Children of a Device/Component: nested **Components** → `Folder` nodes; **DataItems** → `Leaf` nodes. `NodeId` encodes the path (e.g. `dev:OKUMA.Lathe`, `comp:<componentId>`, `di:<dataItemId>`) |
| `AttributesAsync(nodeId)` | For a DataItem leaf, one-or-few `AttributeInfo` rows describing it (`Name=dataItemName`, `DriverDataType=<inferred>`, `IsArray=<TIME_SERIES?>`, `SecurityClass`, `IsAlarm = (category==CONDITION)`). For Device/Component folders, empty. This drives the picker's side-panel and lets a CONDITION pre-fill a native-alarm TagConfig (like Galaxy alarm attrs) |

- `BrowseNode.NodeId` for a DataItem leaf is `di:<dataItemId>` → the picker commits it, and the
  TagModal maps it to `TagConfig.FullName = <dataItemId>` plus the `mt*` metadata read from the cached
  probe model.
- `BrowseNodeKind.Leaf` = DataItem (terminal, commit-on-select); `Folder` = Device/Component.
- `HasChildrenHint = true` for Devices/Components that have child components or DataItems.

### 4.4 Picked-DataItem → TagConfig

When the picker commits a `di:<dataItemId>` leaf, the AdminUI builds the TagConfig in §3.2 from the
cached probe DataItem: `FullName=id`, `mtCategory/mtType/mtSubType/units` copied verbatim, and
`dataType` set from the §2.2 inference table (author can override in the typed editor).

---

## 5. Typed tag-editor (AdminUI)

To avoid the raw-JSON fallback, add a driver-typed editor (mirrors the driver-typed tag-editor pattern):

- `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/MTConnectTagConfigModel.cs`
  (pure `FromJson`/`ToJson`/`Validate`, preserves unknown keys) — copy the Modbus template.
- `.../Components/Shared/Uns/TagEditors/MTConnectTagConfigEditor.razor` — a thin shell:
  fields for `FullName` (dataItemId), read-only `mtCategory/mtType`, and a `dataType` override dropdown.
- Register `["MTConnect"] = typeof(...MTConnectTagConfigEditor)` in
  `Uns/TagEditors/TagConfigEditorMap.cs`, and add validation in `TagConfigValidator`.

Because most tags are authored via the **browse picker** (which fills the model), the editor is mostly
a confirm/override surface.

---

## 6. Test-fixture strategy

Ranked by value:

1. **Public demo Agents (fastest smoke path).**
   - `https://demo.mtconnect.org/` — the MTConnect Institute reference demo (probe/current/sample).
   - `http://mtconnect.mazakcorp.com/` — Mazak demo agents.
   - NIST **smstestbed** publishes real agent configs + `Devices.xml`
     ([usnistgov/smstestbed](https://github.com/usnistgov/smstestbed/blob/master/mtconnect/agent/Devices.xml)).
   These give a live, real-shape model for browse + read + streaming verification with zero infra.
   (Caveat: public agents are internet-dependent and not reproducible in CI.)

2. **Dockerized reference C++ Agent (reproducible integration fixture).**
   The MTConnect reference Agent ([mtconnect/cppagent](https://github.com/mtconnect/cppagent)) has an
   official Docker image. Add a fixture under `tests/.../Docker/docker-compose.yml` with the
   `project: lmxopcua` label (per the repo's Docker workflow), seeded with a canned `Devices.xml` and
   an SHDR simulator or the built-in agent adapter, exposed on the shared docker host `10.100.0.35`.
   This is the integration-test analog of the Modbus/S7 sims.

3. **Canned probe/current/sample XML fixtures (unit tests).**
   Capture one `MTConnectDevices` (probe) and a couple of `MTConnectStreams` (current + sample) XML
   documents from a demo agent into `tests/.../MTConnect.Tests/Fixtures/`. Drive the parser,
   type-inference, browse-tree, and observation-indexing logic with **no network** — the bulk of unit
   coverage. This is where the tricky bits (type inference, `nextSequence` paging, `UNAVAILABLE`→Bad,
   CONDITION handling, multipart chunk framing) get pinned.

Recommended: **#3 for unit CI + #2 for the env-gated integration suite + #1 for manual live smoke.**

---

## 7. Effort / risk / phasing

### Phase 1 — Agent read + subscribe + browse (the MVP)
- Projects: `Driver.MTConnect` (+ `.Contracts` for `MTConnectDriverOptions`/DTOs),
  `Driver.MTConnect.Browser`, `MTConnect.Tests`.
- Implement `IDriver` + `ITagDiscovery` (probe→builder) + `IReadable` (current) +
  `ISubscribable` (sample stream) + `IHostConnectivityProbe` + `IRediscoverable` (instanceId change).
- `IDriverProbe` for the AdminUI Test-Connect button (probe reachability).
- `IDriverBrowser`/`IBrowseSession` from cached probe model; register in AdminUI DI.
- Typed tag editor + `TagConfigEditorMap`/`TagConfigValidator` entries.
- Register the factory in the Host bootstrapper (mirror `ModbusDriverFactoryExtensions.Register`).
- CONDITION as `String` (simple).
- **Effort estimate:** ~1–1.5 weeks with the TrakHound library (it removes the protocol grind).
  ~2.5–3 weeks hand-rolled.

### Phase 1.5 (fast-follow)
- CONDITION → native OPC UA Part 9 alarms via `IAlarmSource` (Galaxy native-alarm pattern).
- `TIME_SERIES` SAMPLE arrays; EVENT controlled-vocab → OPC UA enumerations.

### Phase 2 — SHDR adapter ingest (second source mode)
- Add an optional `SourceMode: "Agent" | "Shdr"` to the driver config. In SHDR mode the driver opens
  the raw pipe-delimited TCP socket (default `:7878`) and parses `<timestamp>|<key>|<value>` lines.
- **Loses auto-discovery**: SHDR has no device model, so `ITagDiscovery` cannot self-populate — tags
  must be **manually authored** (like Modbus), and the browser is unavailable in SHDR mode. Type
  inference falls back to author-supplied `dataType`.
- Value: connect directly to a machine adapter with no Agent deployed. Niche; do only on demand.

### Risk register
| Risk | Sev | Mitigation |
|---|---|---|
| Weak wire typing → wrong OPC UA type inference | Med | Store resolved `dataType` in TagConfig; author-overridable; unit-test the inference table against real probe XML |
| CONDITION has no scalar analog | Med | v1 = String state; v1.5 = native alarm |
| TrakHound license string inconsistent across versions | Low-Med | Pin a version; confirm its embedded MIT license in legal review before merge |
| Public demo agents unreliable for CI | Low | Docker cppagent fixture + canned XML for CI; demo agents only for manual smoke |
| Agent ring-buffer overflow if consumer stalls | Low | Library re-baselines via `/current` on sequence gap; verify + test the fall-behind path |
| `netstandard2.0`-only lib on .NET 10 | Low | netstandard2.0 loads on net10.0; validate no trim/AOT issues at build |

---

## 8. Source references

- MTConnect Standard Part 1 (Overview & Protocol): https://docs.mtconnect.org/MTC_Part_1_Overview+V1.2.pdf
- MTConnect SysML Model v2.5 (Fundamentals — Device/Component/DataItem, categories): https://model.mtconnect.org/Version2.5/Fundamentals/
- TrakHound MTConnect.NET (README, client API, TFMs, versions): https://github.com/TrakHound/MTConnect.NET/blob/master/README.md
- NuGet MTConnect.NET-HTTP (license=MIT, v6.9.0.2 2025-10-16, TFMs, deps): https://www.nuget.org/packages/MTConnect.NET-HTTP/
- MTConnect reference C++ Agent (Docker fixture): https://github.com/mtconnect/cppagent/blob/master/README.md
- NIST smstestbed agent config + Devices.xml (real fixtures): https://github.com/usnistgov/smstestbed/blob/master/mtconnect/agent/Devices.xml
- Public demo agent: https://demo.mtconnect.org/  ·  Mazak demo agents: http://mtconnect.mazakcorp.com/

### OtOpcUa code seams referenced
- Capability interfaces: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/` (`IDriver`, `ITagDiscovery`, `IReadable`, `ISubscribable`, `IWritable`, `IRediscoverable`, `IHostConnectivityProbe`, `IDriverFactory`, `DriverDataType`).
- Browse seam: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/` (`IDriverBrowser`, `IBrowseSession`, `BrowseNode`, `AttributeInfo`).
- Browser template: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/`.
- Driver template: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/` (+ `.Contracts`), factory `ModbusDriverFactoryExtensions`.
- AdminUI browser DI registration: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs:49-50`.
- Typed tag editors: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs` + `TagConfigValidator.cs`.
- Browse design doc: `docs/plans/2026-05-28-driver-browsers-design.md`.
