diff --git a/src/ZB.MOM.WW.MxGateway.Server/Sessions/ArrayAddressNormalizer.cs b/src/ZB.MOM.WW.MxGateway.Server/Sessions/ArrayAddressNormalizer.cs
new file mode 100644
index 0000000..9dfc0cd
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Server/Sessions/ArrayAddressNormalizer.cs
@@ -0,0 +1,43 @@
+using ZB.MOM.WW.MxGateway.Server.Galaxy;
+
+namespace ZB.MOM.WW.MxGateway.Server.Sessions;
+
+///
+/// Rewrites a bare MXAccess attribute address to its writable array form by appending the
+/// trailing [] suffix when Galaxy Repository metadata reports the attribute as an array.
+/// MXAccess requires the [] suffix on the AddItem address for an array attribute to be
+/// writable; the bare name registers a read-only-ish handle. This is best-effort: when metadata
+/// is cold, the address is unknown, or the attribute is not an array, the address is returned
+/// unchanged and no exception is thrown.
+///
+public sealed class ArrayAddressNormalizer(IGalaxyHierarchyCache cache)
+{
+ private const string ArraySuffix = "[]";
+
+ ///
+ /// Returns with a trailing [] appended when Galaxy metadata
+ /// reports it as an array attribute; otherwise returns it unchanged. Never throws.
+ ///
+ /// The MXAccess attribute address to normalize.
+ /// The normalized address, or the original address when no rewrite applies.
+ public string Normalize(string address)
+ {
+ if (string.IsNullOrWhiteSpace(address))
+ {
+ return address;
+ }
+
+ if (address.EndsWith(ArraySuffix, StringComparison.Ordinal))
+ {
+ return address;
+ }
+
+ // Galaxy SQL keys array attributes by their suffixed FullTagReference (e.g. "Obj.Arr[]"),
+ // so probe for the suffixed form to decide whether the bare name is an array.
+ string suffixed = address + ArraySuffix;
+ return cache.Current.Index.TagsByAddress.TryGetValue(suffixed, out GalaxyTagLookup? lookup)
+ && lookup.Attribute?.IsArray == true
+ ? suffixed
+ : address;
+ }
+}
diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/ArrayAddressNormalizerTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/ArrayAddressNormalizerTests.cs
new file mode 100644
index 0000000..ad34fcb
--- /dev/null
+++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Sessions/ArrayAddressNormalizerTests.cs
@@ -0,0 +1,105 @@
+using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
+using ZB.MOM.WW.MxGateway.Server.Galaxy;
+using ZB.MOM.WW.MxGateway.Server.Sessions;
+
+namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Sessions;
+
+public sealed class ArrayAddressNormalizerTests
+{
+ /// Verifies a bare array attribute name gains the trailing array suffix.
+ [Fact]
+ public void Normalize_BareArrayName_AppendsArraySuffix()
+ {
+ ArrayAddressNormalizer normalizer = CreateNormalizer();
+
+ Assert.Equal("Obj.Arr[]", normalizer.Normalize("Obj.Arr"));
+ }
+
+ /// Verifies an already-suffixed address is returned unchanged.
+ [Fact]
+ public void Normalize_AlreadySuffixed_ReturnsUnchanged()
+ {
+ ArrayAddressNormalizer normalizer = CreateNormalizer();
+
+ Assert.Equal("Obj.Arr[]", normalizer.Normalize("Obj.Arr[]"));
+ }
+
+ /// Verifies a scalar attribute is returned unchanged.
+ [Fact]
+ public void Normalize_ScalarAttribute_ReturnsUnchanged()
+ {
+ ArrayAddressNormalizer normalizer = CreateNormalizer();
+
+ Assert.Equal("Obj.Scalar", normalizer.Normalize("Obj.Scalar"));
+ }
+
+ /// Verifies an address absent from the cache is returned unchanged.
+ [Fact]
+ public void Normalize_UnknownAddress_ReturnsUnchanged()
+ {
+ ArrayAddressNormalizer normalizer = CreateNormalizer();
+
+ Assert.Equal("Obj.Unknown", normalizer.Normalize("Obj.Unknown"));
+ }
+
+ /// Verifies null, empty, and whitespace addresses are returned unchanged.
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Normalize_BlankAddress_ReturnsUnchanged(string address)
+ {
+ ArrayAddressNormalizer normalizer = CreateNormalizer();
+
+ Assert.Equal(address, normalizer.Normalize(address));
+ }
+
+ private static ArrayAddressNormalizer CreateNormalizer()
+ {
+ IReadOnlyList objects =
+ [
+ new GalaxyObject
+ {
+ GobjectId = 1,
+ TagName = "Obj",
+ ContainedName = "Obj",
+ Attributes =
+ {
+ new GalaxyAttribute
+ {
+ AttributeName = "Arr",
+ // Galaxy SQL already appends "[]" to array attribute references.
+ FullTagReference = "Obj.Arr[]",
+ IsArray = true,
+ },
+ new GalaxyAttribute
+ {
+ AttributeName = "Scalar",
+ FullTagReference = "Obj.Scalar",
+ IsArray = false,
+ },
+ },
+ },
+ ];
+
+ GalaxyHierarchyCacheEntry entry = GalaxyHierarchyCacheEntry.Empty with
+ {
+ Status = GalaxyCacheStatus.Healthy,
+ Objects = objects,
+ Index = GalaxyHierarchyIndex.Build(objects),
+ };
+
+ return new ArrayAddressNormalizer(new StubGalaxyHierarchyCache(entry));
+ }
+
+ private sealed class StubGalaxyHierarchyCache(GalaxyHierarchyCacheEntry current) : IGalaxyHierarchyCache
+ {
+ /// Gets the current cache entry.
+ public GalaxyHierarchyCacheEntry Current { get; } = current;
+
+ ///
+ public Task RefreshAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ ///
+ public Task WaitForFirstLoadAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+ }
+}