diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs
new file mode 100644
index 00000000..8ba73869
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs
@@ -0,0 +1,67 @@
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
+
+///
+/// An that RECORDS the streamed tree instead of creating OPC UA
+/// nodes — used to capture an driver's discovered hierarchy so the
+/// runtime can graft it under an equipment node. Folder nesting is tracked (each child builder
+/// carries its accumulated path), so every variable records its full .
+/// Value nodes only: is ignored and alarm marking returns a no-op sink
+/// (discovered alarms are out of scope — alarms come via the config path).
+/// Single-threaded: a driver's DiscoverAsync streams on one caller; the root and its child
+/// builders share one . Not thread-safe by design.
+///
+public sealed class CapturingAddressSpaceBuilder : IAddressSpaceBuilder
+{
+ private readonly List _nodes;
+ private readonly IReadOnlyList _path;
+
+ /// Create a root capturing builder with an empty folder path and a fresh node list.
+ public CapturingAddressSpaceBuilder() : this([], []) { }
+
+ private CapturingAddressSpaceBuilder(List nodes, IReadOnlyList path)
+ {
+ _nodes = nodes;
+ _path = path;
+ }
+
+ /// All variables captured across the whole tree (shared by the root and every child scope).
+ public IReadOnlyList Nodes => _nodes;
+
+ ///
+ public IAddressSpaceBuilder Folder(string browseName, string displayName)
+ => new CapturingAddressSpaceBuilder(_nodes, [.. _path, browseName]);
+
+ ///
+ public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
+ {
+ _nodes.Add(new DiscoveredNode(
+ FolderPathSegments: _path,
+ BrowseName: browseName,
+ DisplayName: displayName,
+ FullReference: attributeInfo.FullName,
+ DataType: attributeInfo.DriverDataType,
+ IsArray: attributeInfo.IsArray,
+ ArrayDim: attributeInfo.ArrayDim,
+ Writable: attributeInfo.SecurityClass != SecurityClassification.ViewOnly,
+ IsHistorized: attributeInfo.IsHistorized));
+ return new NullHandle(attributeInfo.FullName);
+ }
+
+ ///
+ public void AddProperty(string browseName, DriverDataType dataType, object? value) { /* metadata only — ignored */ }
+
+ /// A variable handle whose alarm marking is a no-op (discovered alarms are out of scope).
+ private sealed class NullHandle(string fullRef) : IVariableHandle
+ {
+ public string FullReference => fullRef;
+ public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink();
+ }
+
+ /// A null sink that ignores alarm condition transitions.
+ private sealed class NullSink : IAlarmConditionSink
+ {
+ public void OnTransition(AlarmEventArgs args) { }
+ }
+}
diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DiscoveredNode.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DiscoveredNode.cs
new file mode 100644
index 00000000..7ff7d43e
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DiscoveredNode.cs
@@ -0,0 +1,19 @@
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
+
+///
+/// A flattened variable captured from a driver's stream
+/// by . Folder nesting is preserved in
+/// so the injector can re-root the node under an equipment.
+///
+public sealed record DiscoveredNode(
+ IReadOnlyList FolderPathSegments,
+ string BrowseName,
+ string DisplayName,
+ string FullReference,
+ DriverDataType DataType,
+ bool IsArray,
+ uint? ArrayDim,
+ bool Writable,
+ bool IsHistorized);
diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/CapturingAddressSpaceBuilderTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/CapturingAddressSpaceBuilderTests.cs
new file mode 100644
index 00000000..f1da0dfd
--- /dev/null
+++ b/tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/CapturingAddressSpaceBuilderTests.cs
@@ -0,0 +1,44 @@
+using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
+using ZB.MOM.WW.OtOpcUa.Runtime.Drivers;
+using Shouldly;
+using Xunit;
+
+namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Drivers;
+
+[Trait("Category", "Unit")]
+public sealed class CapturingAddressSpaceBuilderTests
+{
+ [Fact]
+ public void Records_nested_path_segments_full_reference_and_metadata()
+ {
+ var b = new CapturingAddressSpaceBuilder();
+ var focas = b.Folder("FOCAS", "FOCAS");
+ var device = focas.Folder("10.0.0.5:8193", "cnc");
+ var identity = device.Folder("Identity", "Identity");
+ identity.Variable("SeriesNumber", "SeriesNumber", new DriverAttributeInfo(
+ FullName: "10.0.0.5:8193/Identity/SeriesNumber",
+ DriverDataType: DriverDataType.String, IsArray: false, ArrayDim: null,
+ SecurityClass: SecurityClassification.ViewOnly, IsHistorized: false));
+
+ b.Nodes.Count.ShouldBe(1);
+ var n = b.Nodes[0];
+ n.FolderPathSegments.ShouldBe(new[] { "FOCAS", "10.0.0.5:8193", "Identity" });
+ n.BrowseName.ShouldBe("SeriesNumber");
+ n.FullReference.ShouldBe("10.0.0.5:8193/Identity/SeriesNumber");
+ n.DataType.ShouldBe(DriverDataType.String);
+ n.Writable.ShouldBeFalse(); // ViewOnly -> read-only
+ }
+
+ [Fact]
+ public void AddProperty_is_ignored_and_alarm_marking_is_a_noop_sink()
+ {
+ var b = new CapturingAddressSpaceBuilder();
+ var f = b.Folder("FOCAS", "FOCAS");
+ f.AddProperty("Manufacturer", DriverDataType.String, "FANUC"); // ignored, no throw
+ var h = f.Variable("V", "V", new DriverAttributeInfo("ref", DriverDataType.Int32, false, null,
+ SecurityClassification.ViewOnly, false, IsAlarm: true));
+ var sink = h.MarkAsAlarmCondition(new AlarmConditionInfo("src", AlarmSeverity.Low, null));
+ sink.ShouldNotBeNull(); // no-op sink, alarms out of scope
+ b.Nodes.Count.ShouldBe(1);
+ }
+}