Add Ecosystem Information Cluster Server implementation (#34459)

diff --git a/scripts/tools/check_includes_config.py b/scripts/tools/check_includes_config.py
index 20c853b..d897c86 100644
--- a/scripts/tools/check_includes_config.py
+++ b/scripts/tools/check_includes_config.py
@@ -128,6 +128,8 @@
     'src/app/clusters/application-launcher-server/application-launcher-server.cpp': {'string'},
     'src/app/clusters/application-launcher-server/application-launcher-delegate.h': {'list'},
     'src/app/clusters/audio-output-server/audio-output-delegate.h': {'list'},
+    # EcosystemInformationCluster is for Fabric Sync and is intended to run on device that are capable of handling these types.
+    'src/app/clusters/ecosystem-information-server/ecosystem-information-server.h': {'map', 'string', 'vector'},
     'src/app/clusters/channel-server/channel-delegate.h': {'list'},
     'src/app/clusters/content-launch-server/content-launch-delegate.h': {'list'},
     'src/app/clusters/content-launch-server/content-launch-server.cpp': {'list'},
diff --git a/src/app/clusters/ecosystem-information-server/ecosystem-information-server.cpp b/src/app/clusters/ecosystem-information-server/ecosystem-information-server.cpp
new file mode 100644
index 0000000..26f1c96
--- /dev/null
+++ b/src/app/clusters/ecosystem-information-server/ecosystem-information-server.cpp
@@ -0,0 +1,377 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#include "ecosystem-information-server.h"
+
+#include <app/AttributeAccessInterface.h>
+#include <app/AttributeAccessInterfaceRegistry.h>
+
+namespace chip {
+namespace app {
+namespace Clusters {
+namespace EcosystemInformation {
+namespace {
+
+constexpr size_t kDeviceNameMaxSize             = 64;
+constexpr size_t kUniqueLocationIdMaxSize       = 64;
+constexpr size_t kUniqueLocationIdsListMaxSize  = 64;
+constexpr size_t kLocationDescriptorNameMaxSize = 128;
+
+constexpr size_t kDeviceDirectoryMaxSize   = 256;
+constexpr size_t kLocationDirectoryMaxSize = 64;
+
+class AttrAccess : public AttributeAccessInterface
+{
+public:
+    // Register for the EcosystemInformationCluster on all endpoints.
+    AttrAccess() : AttributeAccessInterface(Optional<EndpointId>::Missing(), Clusters::EcosystemInformation::Id) {}
+
+    CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override;
+};
+
+CHIP_ERROR AttrAccess::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder)
+{
+    VerifyOrDie(aPath.mClusterId == Clusters::EcosystemInformation::Id);
+    switch (aPath.mAttributeId)
+    {
+    case Attributes::RemovedOn::Id:
+        return EcosystemInformationServer::Instance().EncodeRemovedOnAttribute(aPath.mEndpointId, aEncoder);
+    case Attributes::DeviceDirectory ::Id:
+        return EcosystemInformationServer::Instance().EncodeDeviceDirectoryAttribute(aPath.mEndpointId, aEncoder);
+    case Attributes::LocationDirectory ::Id:
+        return EcosystemInformationServer::Instance().EncodeLocationStructAttribute(aPath.mEndpointId, aEncoder);
+    default:
+        break;
+    }
+    return CHIP_NO_ERROR;
+}
+
+// WARNING: caller is expected to use the returned LocationDescriptorStruct::Type immediately. Caller must
+// be certain that the provided aLocationDescriptor has not been destroyed, prior to using the return
+// struct to encode.
+// TODO(#33223) To improve safety we could make GetEncodableLocationDescriptorStruct a private
+// memeber method where we explicitly delete member method for the parameter that matches
+// (LocationDescriptorStruct && aLocationDescriptor).
+Structs::LocationDescriptorStruct::Type GetEncodableLocationDescriptorStruct(const LocationDescriptorStruct & aLocationDescriptor)
+{
+    Structs::LocationDescriptorStruct::Type locationDescriptor;
+    // This would imply data is either not properly validated before being
+    // stored here or corruption has occurred.
+    VerifyOrDie(!aLocationDescriptor.mLocationName.empty());
+    locationDescriptor.locationName = CharSpan(aLocationDescriptor.mLocationName.c_str(), aLocationDescriptor.mLocationName.size());
+
+    if (aLocationDescriptor.mFloorNumber.has_value())
+    {
+        locationDescriptor.floorNumber.SetNonNull(aLocationDescriptor.mFloorNumber.value());
+    }
+    else
+    {
+        locationDescriptor.floorNumber.SetNull();
+    }
+
+    if (aLocationDescriptor.mAreaType.has_value())
+    {
+        locationDescriptor.areaType.SetNonNull(aLocationDescriptor.mAreaType.value());
+    }
+    else
+    {
+        locationDescriptor.areaType.SetNull();
+    }
+    return locationDescriptor;
+}
+
+} // namespace
+
+EcosystemDeviceStruct::Builder & EcosystemDeviceStruct::Builder::SetDeviceName(std::string aDeviceName,
+                                                                               uint64_t aDeviceNameLastEditEpochUs)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mDeviceName                = std::move(aDeviceName);
+    mDeviceNameLastEditEpochUs = aDeviceNameLastEditEpochUs;
+    return *this;
+}
+
+EcosystemDeviceStruct::Builder & EcosystemDeviceStruct::Builder::SetBrigedEndpoint(EndpointId aBridgedEndpoint)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mBridgedEndpoint = aBridgedEndpoint;
+    return *this;
+}
+
+EcosystemDeviceStruct::Builder & EcosystemDeviceStruct::Builder::SetOriginalEndpoint(EndpointId aOriginalEndpoint)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mOriginalEndpoint = aOriginalEndpoint;
+    return *this;
+}
+
+EcosystemDeviceStruct::Builder & EcosystemDeviceStruct::Builder::AddDeviceType(Structs::DeviceTypeStruct::Type aDeviceType)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mDeviceTypes.push_back(std::move(aDeviceType));
+    return *this;
+}
+
+EcosystemDeviceStruct::Builder & EcosystemDeviceStruct::Builder::AddUniqueLocationId(std::string aUniqueLocationId,
+                                                                                     uint64_t aUniqueLocationIdsLastEditEpochUs)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mUniqueLocationIds.push_back(std::move(aUniqueLocationId));
+    mUniqueLocationIdsLastEditEpochUs = aUniqueLocationIdsLastEditEpochUs;
+    return *this;
+}
+
+std::unique_ptr<EcosystemDeviceStruct> EcosystemDeviceStruct::Builder::Build()
+{
+    VerifyOrReturnValue(!mIsAlreadyBuilt, nullptr, ChipLogError(Zcl, "Build() already called"));
+    VerifyOrReturnValue(mDeviceName.size() <= kDeviceNameMaxSize, nullptr, ChipLogError(Zcl, "Device name too large"));
+    VerifyOrReturnValue(mOriginalEndpoint != kInvalidEndpointId, nullptr, ChipLogError(Zcl, "Invalid original endpoint"));
+    VerifyOrReturnValue(!mDeviceTypes.empty(), nullptr, ChipLogError(Zcl, "No device types added"));
+    VerifyOrReturnValue(mUniqueLocationIds.size() <= kUniqueLocationIdsListMaxSize, nullptr,
+                        ChipLogError(Zcl, "Too many location ids"));
+
+    for (auto & locationId : mUniqueLocationIds)
+    {
+        VerifyOrReturnValue(locationId.size() <= kUniqueLocationIdMaxSize, nullptr, ChipLogError(Zcl, "Location id too long"));
+    }
+
+    // std::make_unique does not have access to private constructor we workaround with using new
+    std::unique_ptr<EcosystemDeviceStruct> ret{ new EcosystemDeviceStruct(
+        std::move(mDeviceName), mDeviceNameLastEditEpochUs, mBridgedEndpoint, mOriginalEndpoint, std::move(mDeviceTypes),
+        std::move(mUniqueLocationIds), mUniqueLocationIdsLastEditEpochUs) };
+    mIsAlreadyBuilt = true;
+    return ret;
+}
+
+CHIP_ERROR EcosystemDeviceStruct::Encode(const AttributeValueEncoder::ListEncodeHelper & aEncoder, const FabricIndex & aFabricIndex)
+{
+    Structs::EcosystemDeviceStruct::Type deviceStruct;
+    if (!mDeviceName.empty())
+    {
+        deviceStruct.deviceName.SetValue(CharSpan(mDeviceName.c_str(), mDeviceName.size()));
+        // When there is a device name we also include mDeviceNameLastEditEpochUs
+        deviceStruct.deviceNameLastEdit.SetValue(mDeviceNameLastEditEpochUs);
+    }
+    deviceStruct.bridgedEndpoint  = mBridgedEndpoint;
+    deviceStruct.originalEndpoint = mOriginalEndpoint;
+    deviceStruct.deviceTypes = DataModel::List<const Structs::DeviceTypeStruct::Type>(mDeviceTypes.data(), mDeviceTypes.size());
+
+    std::vector<CharSpan> locationIds;
+    locationIds.reserve(mUniqueLocationIds.size());
+    for (auto & id : mUniqueLocationIds)
+    {
+        locationIds.push_back(CharSpan(id.c_str(), id.size()));
+    }
+    deviceStruct.uniqueLocationIDs = DataModel::List<CharSpan>(locationIds.data(), locationIds.size());
+
+    deviceStruct.uniqueLocationIDsLastEdit = mUniqueLocationIdsLastEditEpochUs;
+
+    // TODO(#33223) this is a hack, use mFabricIndex when it exists.
+    deviceStruct.SetFabricIndex(aFabricIndex);
+    return aEncoder.Encode(deviceStruct);
+}
+
+EcosystemLocationStruct::Builder & EcosystemLocationStruct::Builder::SetLocationName(std::string aLocationName)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mLocationDescriptor.mLocationName = std::move(aLocationName);
+    return *this;
+}
+
+EcosystemLocationStruct::Builder & EcosystemLocationStruct::Builder::SetFloorNumber(std::optional<int16_t> aFloorNumber)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mLocationDescriptor.mFloorNumber = aFloorNumber;
+    return *this;
+}
+
+EcosystemLocationStruct::Builder & EcosystemLocationStruct::Builder::SetAreaTypeTag(std::optional<AreaTypeTag> aAreaTypeTag)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mLocationDescriptor.mAreaType = aAreaTypeTag;
+    return *this;
+}
+
+EcosystemLocationStruct::Builder &
+EcosystemLocationStruct::Builder::SetLocationDescriptorLastEdit(uint64_t aLocationDescriptorLastEditEpochUs)
+{
+    VerifyOrDie(!mIsAlreadyBuilt);
+    mLocationDescriptorLastEditEpochUs = aLocationDescriptorLastEditEpochUs;
+    return *this;
+}
+
+std::unique_ptr<EcosystemLocationStruct> EcosystemLocationStruct::Builder::Build()
+{
+    VerifyOrReturnValue(!mIsAlreadyBuilt, nullptr, ChipLogError(Zcl, "Build() already called"));
+    VerifyOrReturnValue(!mLocationDescriptor.mLocationName.empty(), nullptr, ChipLogError(Zcl, "Must Provided Location Name"));
+    VerifyOrReturnValue(mLocationDescriptor.mLocationName.size() <= kLocationDescriptorNameMaxSize, nullptr,
+                        ChipLogError(Zcl, "Must Location Name must be less than 64 bytes"));
+
+    // std::make_unique does not have access to private constructor we workaround with using new
+    std::unique_ptr<EcosystemLocationStruct> ret{ new EcosystemLocationStruct(std::move(mLocationDescriptor),
+                                                                              mLocationDescriptorLastEditEpochUs) };
+    mIsAlreadyBuilt = true;
+    return ret;
+}
+
+CHIP_ERROR EcosystemLocationStruct::Encode(const AttributeValueEncoder::ListEncodeHelper & aEncoder,
+                                           const std::string & aUniqueLocationId, const FabricIndex & aFabricIndex)
+{
+    Structs::EcosystemLocationStruct::Type locationStruct;
+    VerifyOrDie(!aUniqueLocationId.empty());
+    locationStruct.uniqueLocationID           = CharSpan(aUniqueLocationId.c_str(), aUniqueLocationId.size());
+    locationStruct.locationDescriptor         = GetEncodableLocationDescriptorStruct(mLocationDescriptor);
+    locationStruct.locationDescriptorLastEdit = mLocationDescriptorLastEditEpochUs;
+
+    // TODO(#33223) this is a hack, use mFabricIndex when it exists.
+    locationStruct.SetFabricIndex(aFabricIndex);
+    return aEncoder.Encode(locationStruct);
+}
+
+EcosystemInformationServer EcosystemInformationServer::mInstance;
+
+EcosystemInformationServer & EcosystemInformationServer::Instance()
+{
+    return mInstance;
+}
+
+CHIP_ERROR EcosystemInformationServer::AddDeviceInfo(EndpointId aEndpoint, std::unique_ptr<EcosystemDeviceStruct> aDevice)
+{
+    VerifyOrReturnError(aDevice, CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrReturnError((aEndpoint != kRootEndpointId && aEndpoint != kInvalidEndpointId), CHIP_ERROR_INVALID_ARGUMENT);
+
+    auto & deviceInfo = mDevicesMap[aEndpoint];
+    VerifyOrReturnError((deviceInfo.mDeviceDirectory.size() < kDeviceDirectoryMaxSize), CHIP_ERROR_NO_MEMORY);
+    deviceInfo.mDeviceDirectory.push_back(std::move(aDevice));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR EcosystemInformationServer::AddLocationInfo(EndpointId aEndpoint, const std::string & aLocationId,
+                                                       std::unique_ptr<EcosystemLocationStruct> aLocation)
+{
+    VerifyOrReturnError(aLocation, CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrReturnError((aEndpoint != kRootEndpointId && aEndpoint != kInvalidEndpointId), CHIP_ERROR_INVALID_ARGUMENT);
+
+    auto & deviceInfo = mDevicesMap[aEndpoint];
+    VerifyOrReturnError((deviceInfo.mLocationDirectory.find(aLocationId) == deviceInfo.mLocationDirectory.end()),
+                        CHIP_ERROR_INVALID_ARGUMENT);
+    VerifyOrReturnError((deviceInfo.mLocationDirectory.size() < kLocationDirectoryMaxSize), CHIP_ERROR_NO_MEMORY);
+    deviceInfo.mLocationDirectory[aLocationId] = std::move(aLocation);
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR EcosystemInformationServer::RemoveDevice(EndpointId aEndpoint, uint64_t aEpochUs)
+{
+    auto it = mDevicesMap.find(aEndpoint);
+    VerifyOrReturnError((it != mDevicesMap.end()), CHIP_ERROR_INVALID_ARGUMENT);
+    auto & deviceInfo = it->second;
+    deviceInfo.mRemovedOn.SetValue(aEpochUs);
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR EcosystemInformationServer::EncodeRemovedOnAttribute(EndpointId aEndpoint, AttributeValueEncoder & aEncoder)
+{
+    auto it = mDevicesMap.find(aEndpoint);
+    if (it == mDevicesMap.end())
+    {
+        // We are always going to be given a valid endpoint. If the endpoint
+        // doesn't exist in our map that indicate that the cluster was not
+        // added on this endpoint, hence UnsupportedCluster.
+        return CHIP_IM_GLOBAL_STATUS(UnsupportedCluster);
+    }
+
+    auto & deviceInfo = it->second;
+    if (!deviceInfo.mRemovedOn.HasValue())
+    {
+        aEncoder.EncodeNull();
+        return CHIP_NO_ERROR;
+    }
+
+    aEncoder.Encode(deviceInfo.mRemovedOn.Value());
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR EcosystemInformationServer::EncodeDeviceDirectoryAttribute(EndpointId aEndpoint, AttributeValueEncoder & aEncoder)
+{
+
+    auto it = mDevicesMap.find(aEndpoint);
+    if (it == mDevicesMap.end())
+    {
+        // We are always going to be given a valid endpoint. If the endpoint
+        // doesn't exist in our map that indicate that the cluster was not
+        // added on this endpoint, hence UnsupportedCluster.
+        return CHIP_IM_GLOBAL_STATUS(UnsupportedCluster);
+    }
+
+    auto & deviceInfo = it->second;
+    if (deviceInfo.mDeviceDirectory.empty() || deviceInfo.mRemovedOn.HasValue())
+    {
+        return aEncoder.EncodeEmptyList();
+    }
+
+    FabricIndex fabricIndex = aEncoder.AccessingFabricIndex();
+    return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR {
+        for (auto & device : deviceInfo.mDeviceDirectory)
+        {
+            ReturnErrorOnFailure(device->Encode(encoder, fabricIndex));
+        }
+        return CHIP_NO_ERROR;
+    });
+}
+
+CHIP_ERROR EcosystemInformationServer::EncodeLocationStructAttribute(EndpointId aEndpoint, AttributeValueEncoder & aEncoder)
+{
+    auto it = mDevicesMap.find(aEndpoint);
+    if (it == mDevicesMap.end())
+    {
+        // We are always going to be given a valid endpoint. If the endpoint
+        // doesn't exist in our map that indicate that the cluster was not
+        // added on this endpoint, hence UnsupportedCluster.
+        return CHIP_IM_GLOBAL_STATUS(UnsupportedCluster);
+    }
+
+    auto & deviceInfo = it->second;
+    if (deviceInfo.mLocationDirectory.empty() || deviceInfo.mRemovedOn.HasValue())
+    {
+        return aEncoder.EncodeEmptyList();
+    }
+
+    FabricIndex fabricIndex = aEncoder.AccessingFabricIndex();
+    return aEncoder.EncodeList([&](const auto & encoder) -> CHIP_ERROR {
+        for (auto & [id, device] : deviceInfo.mLocationDirectory)
+        {
+            ReturnErrorOnFailure(device->Encode(encoder, id, fabricIndex));
+        }
+        return CHIP_NO_ERROR;
+    });
+    return CHIP_NO_ERROR;
+}
+
+} // namespace EcosystemInformation
+} // namespace Clusters
+} // namespace app
+} // namespace chip
+
+// -----------------------------------------------------------------------------
+// Plugin initialization
+
+chip::app::Clusters::EcosystemInformation::AttrAccess gAttrAccess;
+
+void MatterEcosystemInformationPluginServerInitCallback()
+{
+    registerAttributeAccessOverride(&gAttrAccess);
+}
diff --git a/src/app/clusters/ecosystem-information-server/ecosystem-information-server.h b/src/app/clusters/ecosystem-information-server/ecosystem-information-server.h
new file mode 100644
index 0000000..f840786
--- /dev/null
+++ b/src/app/clusters/ecosystem-information-server/ecosystem-information-server.h
@@ -0,0 +1,205 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+#pragma once
+
+// This cluster is targeted by devices that are not resource constrained, for
+// that reason we use std containers to simplify implementation of the cluster.
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <app-common/zap-generated/cluster-objects.h>
+
+#include <app/AttributeAccessInterface.h>
+
+namespace chip {
+namespace app {
+namespace Clusters {
+namespace EcosystemInformation {
+
+// This intentionally mirrors Structs::EcosystemDeviceStruct::Type but has ownership
+// of underlying types.
+class EcosystemDeviceStruct
+{
+public:
+    class Builder
+    {
+    public:
+        Builder(){};
+
+        Builder & SetDeviceName(std::string aDeviceName, uint64_t aDeviceNameLastEditEpochUs);
+        Builder & SetBrigedEndpoint(EndpointId aBridgedEndpoint);
+        Builder & SetOriginalEndpoint(EndpointId aOriginalEndpoint);
+        Builder & AddDeviceType(Structs::DeviceTypeStruct::Type aDeviceType);
+        Builder & AddUniqueLocationId(std::string aUniqueLocationId, uint64_t aUniqueLocationIdsLastEditEpochUs);
+
+        // Upon success this object will have moved all ownership of underlying
+        // types to EcosystemDeviceStruct and should not be used afterwards.
+        std::unique_ptr<EcosystemDeviceStruct> Build();
+
+    private:
+        std::string mDeviceName;
+        uint64_t mDeviceNameLastEditEpochUs = 0;
+        EndpointId mBridgedEndpoint         = kInvalidEndpointId;
+        EndpointId mOriginalEndpoint        = kInvalidEndpointId;
+        std::vector<Structs::DeviceTypeStruct::Type> mDeviceTypes;
+        std::vector<std::string> mUniqueLocationIds;
+        uint64_t mUniqueLocationIdsLastEditEpochUs = 0;
+        bool mIsAlreadyBuilt                       = false;
+    };
+
+    CHIP_ERROR Encode(const AttributeValueEncoder::ListEncodeHelper & aEncoder, const FabricIndex & aFabricIndex);
+
+private:
+    // Constructor is intentionally private. This is to ensure that it is only constructed with
+    // values that conform to the spec.
+    explicit EcosystemDeviceStruct(std::string && aDeviceName, uint64_t aDeviceNameLastEditEpochUs, EndpointId aBridgedEndpoint,
+                                   EndpointId aOriginalEndpoint, std::vector<Structs::DeviceTypeStruct::Type> && aDeviceTypes,
+                                   std::vector<std::string> && aUniqueLocationIds, uint64_t aUniqueLocationIdsLastEditEpochUs) :
+        mDeviceName(std::move(aDeviceName)),
+        mDeviceNameLastEditEpochUs(aDeviceNameLastEditEpochUs), mBridgedEndpoint(aBridgedEndpoint),
+        mOriginalEndpoint(aOriginalEndpoint), mDeviceTypes(std::move(aDeviceTypes)),
+        mUniqueLocationIds(std::move(aUniqueLocationIds)), mUniqueLocationIdsLastEditEpochUs(aUniqueLocationIdsLastEditEpochUs)
+    {}
+
+    const std::string mDeviceName;
+    uint64_t mDeviceNameLastEditEpochUs;
+    EndpointId mBridgedEndpoint;
+    EndpointId mOriginalEndpoint;
+    std::vector<Structs::DeviceTypeStruct::Type> mDeviceTypes;
+    std::vector<std::string> mUniqueLocationIds;
+    uint64_t mUniqueLocationIdsLastEditEpochUs;
+    // TODO(#33223) This structure needs to contain fabric index to be spec compliant.
+    // To keep initial PR smaller, we are going to assume that all entries
+    // here are for any fabric. This will allow follow up PR introducing
+    // fabric scoped to be more throughly reviewed with focus on fabric scoping.
+};
+
+struct LocationDescriptorStruct
+{
+    std::string mLocationName;
+    std::optional<int16_t> mFloorNumber;
+    std::optional<AreaTypeTag> mAreaType;
+};
+
+// This intentionally mirrors Structs::EcosystemLocationStruct::Type but has ownership
+// of underlying types.
+class EcosystemLocationStruct
+{
+public:
+    class Builder
+    {
+    public:
+        Builder(){};
+
+        Builder & SetLocationName(std::string aLocationName);
+        Builder & SetFloorNumber(std::optional<int16_t> aFloorNumber);
+        Builder & SetAreaTypeTag(std::optional<AreaTypeTag> aAreaTypeTag);
+        Builder & SetLocationDescriptorLastEdit(uint64_t aLocationDescriptorLastEditEpochUs);
+
+        // Upon success this object will have moved all ownership of underlying
+        // types to EcosystemDeviceStruct and should not be used afterwards.
+        std::unique_ptr<EcosystemLocationStruct> Build();
+
+    private:
+        LocationDescriptorStruct mLocationDescriptor;
+        uint64_t mLocationDescriptorLastEditEpochUs = 0;
+        bool mIsAlreadyBuilt                        = false;
+    };
+
+    CHIP_ERROR Encode(const AttributeValueEncoder::ListEncodeHelper & aEncoder, const std::string & aUniqueLocationId,
+                      const FabricIndex & aFabricIndex);
+
+private:
+    // Constructor is intentionally private. This is to ensure that it is only constructed with
+    // values that conform to the spec.
+    explicit EcosystemLocationStruct(LocationDescriptorStruct && aLocationDescriptor, uint64_t aLocationDescriptorLastEditEpochUs) :
+        mLocationDescriptor(aLocationDescriptor), mLocationDescriptorLastEditEpochUs(aLocationDescriptorLastEditEpochUs)
+    {}
+    // EcosystemLocationStruct is used as a value in a key-value map.
+    // Because UniqueLocationId is manditory when an entry exist, and
+    // it is unique, we use it as a key to the key-value pair and is why it is
+    // not explicitly in this struct.
+    LocationDescriptorStruct mLocationDescriptor;
+    uint64_t mLocationDescriptorLastEditEpochUs;
+    // TODO(#33223) This structure needs to contain fabric index to be spec compliant.
+    // To keep initial PR smaller, we are going to assume that all entries
+    // here are for any fabric. This will allow follow up PR introducing
+    // fabric scoped to be more throughly reviewed with focus on fabric scoping.
+};
+
+class EcosystemInformationServer
+{
+public:
+    static EcosystemInformationServer & Instance();
+
+    /**
+     * @brief Adds device as entry to DeviceDirectory list Attribute.
+     *
+     * @param[in] aEndpoint Which endpoint is the device being added to the device directory.
+     * @param[in] aDevice Device information.
+     * @return #CHIP_NO_ERROR on success.
+     * @return Other CHIP_ERROR associated with issue.
+     */
+    CHIP_ERROR AddDeviceInfo(EndpointId aEndpoint, std::unique_ptr<EcosystemDeviceStruct> aDevice);
+    /**
+     * @brief Adds location as entry to LocationDirectory list Attribute.
+     *
+     * @param[in] aEndpoint Which endpoint is the location being added to the location directory.
+     * @param[in] aLocationId LocationID associated with location.
+     * @param[in] aLocation Location information.
+     * @return #CHIP_NO_ERROR on success.
+     * @return Other CHIP_ERROR associated with issue.
+     */
+    CHIP_ERROR AddLocationInfo(EndpointId aEndpoint, const std::string & aLocationId,
+                               std::unique_ptr<EcosystemLocationStruct> aLocation);
+
+    /**
+     * @brief Removes device at the provided endpoint.
+     *
+     * @param aEndpoint Endpoint of the associated device that has been removed.
+     * @param aEpochUs Epoch time in micro seconds assoicated with when device was removed.
+     * @return #CHIP_NO_ERROR on success.
+     * @return Other CHIP_ERROR associated with issue.
+     */
+    CHIP_ERROR RemoveDevice(EndpointId aEndpoint, uint64_t aEpochUs);
+    // TODO(#33223) Add removal and update counterparts to AddDeviceInfo and AddLocationInfo.
+
+    CHIP_ERROR EncodeRemovedOnAttribute(EndpointId aEndpoint, AttributeValueEncoder & aEncoder);
+    CHIP_ERROR EncodeDeviceDirectoryAttribute(EndpointId aEndpoint, AttributeValueEncoder & aEncoder);
+    CHIP_ERROR EncodeLocationStructAttribute(EndpointId aEndpoint, AttributeValueEncoder & aEncoder);
+
+private:
+    struct DeviceInfo
+    {
+        Optional<uint64_t> mRemovedOn;
+        std::vector<std::unique_ptr<EcosystemDeviceStruct>> mDeviceDirectory;
+        // Map key is using the UniqueLocationId
+        std::map<std::string, std::unique_ptr<EcosystemLocationStruct>> mLocationDirectory;
+    };
+    std::map<EndpointId, DeviceInfo> mDevicesMap;
+
+    static EcosystemInformationServer mInstance;
+};
+
+} // namespace EcosystemInformation
+} // namespace Clusters
+} // namespace app
+} // namespace chip
diff --git a/src/app/zap_cluster_list.json b/src/app/zap_cluster_list.json
index 285bd6e..4852b22 100644
--- a/src/app/zap_cluster_list.json
+++ b/src/app/zap_cluster_list.json
@@ -186,7 +186,7 @@
         "DISHWASHER_MODE_CLUSTER": ["mode-base-server"],
         "MICROWAVE_OVEN_MODE_CLUSTER": ["mode-base-server"],
         "DOOR_LOCK_CLUSTER": ["door-lock-server"],
-        "ECOSYSTEM_INFORMATION_CLUSTER": [],
+        "ECOSYSTEM_INFORMATION_CLUSTER": ["ecosystem-information-server"],
         "ELECTRICAL_ENERGY_MEASUREMENT_CLUSTER": [
             "electrical-energy-measurement-server"
         ],