Implement support for more configurable server endpoints in Matter.framework. (#31814)

* Implement support for more configurable server endpoints in Matter.framework.

* Public APIs on MTRDeviceController to add/remove an endpoint.
* Internal APIs on MTRDeviceController to query the access grants for a cluster
  path and the declared "minimum privilege needed" to read a given attribute.
* Changes to the controller factory to stop using app/dynamic_server and instead
  use the new infrastructure to expose OTA Provider on endpoint 0.
* Internal APIs on the controller factory to query access grants and declared
  privileges.
* An implemenation of AccessControl::Delegate to do ACL checks using the
  above-mentioned APIs.
* A fix to MTRServerAttribute's setValue for arrays: it was not expecting the
  correct data-value structure for an array.  This requires fixing some tests
  too, to provide the right structures.
* Changes to the MTRServer* data structures to allow passing nil to
  associateWithController, to support the OTA endpoint which is not associated
  with any controller.
* Changes to MTRServerCluster to create an AttributeAccessInterface, the set of
  EmberAfAttributeMetadata needed to represent its attributes, and various other
  things needed to register a cluster with the "ember" bits.
* Changes to MTRServerEndpoint to create an EmberAfEndpointType, a list of
  EmberAfCluster, and various other things needed to register an endpoint with
  the "ember" bits.
* (Re-)addition of MTRIMDispatch to handle command dispatch for OTA and host a
  few other functions the "ember" bits expect to exist.
* Addition of some headers that the "ember" bits expect to exist at specific
  paths and with some specific content: "app/PluginApplicationCallbacks.h" and
  "zap-generated/endpoint_config.h".  Importantly, the latter sets
  FIXED_ENDPOINT_COUNT to 0.
* Addition of unit tests that exercise the non-OTA bits of the above (OTA is
  covered by existing tests), including the ACL checks and so on.
* Including a bunch of src/app and src/app/util files needed for the "ember"
  stuff to work in the framework.
* Turning off the chip_build_controller_dynamic_server bit that we are no longer
  using (and which conflicts with the above bits).
* Configure Darwin to support 254 dynamic endpoints (the maximum that makes
  sense) by default.
* Adjusting include paths for the Xcode darwin-framework-tool project, so that
  it sees the new headers that were added.

* Address review comments.

* Fix test timeout due to resolving IPv4 non-locahost addresses.

* Remove stale comment.
diff --git a/src/darwin/Framework/CHIP/MTRCallbackBridgeBase.h b/src/darwin/Framework/CHIP/MTRCallbackBridgeBase.h
index ab65655..d113b5d 100644
--- a/src/darwin/Framework/CHIP/MTRCallbackBridgeBase.h
+++ b/src/darwin/Framework/CHIP/MTRCallbackBridgeBase.h
@@ -215,6 +215,8 @@
         }
 
         if (!callbackBridge->mQueue) {
+            ChipLogDetail(Controller, "%s %f seconds: can't dispatch response; no queue", callbackBridge->mCookie.UTF8String,
+                -[callbackBridge->mRequestTime timeIntervalSinceNow]);
             if (!callbackBridge->mKeepAlive) {
                 delete callbackBridge;
             }
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.h b/src/darwin/Framework/CHIP/MTRDeviceController.h
index a3757e6..ea2f649 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceController.h
@@ -22,6 +22,7 @@
 #import <Matter/MTROperationalCertificateIssuer.h>
 
 @class MTRBaseDevice;
+@class MTRServerEndpoint; // Defined in MTRServerEndpoint.h, which imports MTRAccessGrant.h, which imports MTRBaseClusters.h, which imports this file, so we can't import it.
 
 #if MTR_PER_CONTROLLER_STORAGE_ENABLED
 @class MTRDeviceControllerAbstractParameters;
@@ -229,6 +230,29 @@
     MTR_AVAILABLE(ios(16.4), macos(13.3), watchos(9.4), tvos(16.4));
 
 /**
+ * Add a server endpoint for this controller.  The endpoint starts off enabled.
+ *
+ * Will fail in the following cases:
+ *
+ * 1) There is already an endpoint defined with the given endpoint id.
+ * 2) There are too many endpoints defined already.
+ */
+- (BOOL)addServerEndpoint:(MTRServerEndpoint *)endpoint MTR_NEWLY_AVAILABLE;
+
+/**
+ * Remove the given server endpoint from this controller.  If the endpoint is
+ * not attached to this controller, will just call the completion and do nothing
+ * else.
+ */
+- (void)removeServerEndpoint:(MTRServerEndpoint *)endpoint queue:(dispatch_queue_t)queue completion:(dispatch_block_t)completion MTR_NEWLY_AVAILABLE;
+
+/**
+ * Remove the given server endpoint without being notified when the removal
+ * completes.
+ */
+- (void)removeServerEndpoint:(MTRServerEndpoint *)endpoint MTR_NEWLY_AVAILABLE;
+
+/**
  * Compute a PASE verifier for the desired setup passcode.
  *
  * @param[in] setupPasscode   The desired passcode to use.
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm
index decd879..9621b44 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm
@@ -41,6 +41,7 @@
 #import "MTROperationalCredentialsDelegate.h"
 #import "MTRP256KeypairBridge.h"
 #import "MTRPersistentStorageDelegateBridge.h"
+#import "MTRServerEndpoint_Internal.h"
 #import "MTRSetupPayload.h"
 #import "NSDataSpanConversion.h"
 #import "NSStringSpanConversion.h"
@@ -55,6 +56,7 @@
 
 #include <app-common/zap-generated/cluster-objects.h>
 #include <app/data-model/List.h>
+#include <app/server/Dnssd.h>
 #include <controller/CHIPDeviceController.h>
 #include <controller/CHIPDeviceControllerFactory.h>
 #include <controller/CommissioningWindowOpener.h>
@@ -62,6 +64,7 @@
 #include <credentials/GroupDataProvider.h>
 #include <credentials/attestation_verifier/DacOnlyPartialAttestationVerifier.h>
 #include <credentials/attestation_verifier/DefaultDeviceAttestationVerifier.h>
+#include <inet/InetInterface.h>
 #include <lib/core/CHIPVendorIdentifiers.hpp>
 #include <platform/LockTracker.h>
 #include <platform/PlatformManager.h>
@@ -69,6 +72,7 @@
 #include <system/SystemClock.h>
 
 #include <atomic>
+#include <dns_sd.h>
 
 #import <os/lock.h>
 
@@ -121,6 +125,9 @@
     os_unfair_lock _deviceMapLock; // protects nodeIDToDeviceMap
     MTRCommissionableBrowser * _commissionableBrowser;
     MTRAttestationTrustStoreBridge * _attestationTrustStoreBridge;
+
+    // _serverEndpoints is only touched on the Matter queue.
+    NSMutableArray<MTRServerEndpoint *> * _serverEndpoints;
 }
 
 - (nullable instancetype)initWithParameters:(MTRDeviceControllerAbstractParameters *)parameters error:(NSError * __autoreleasing *)error
@@ -221,6 +228,7 @@
         _factory = factory;
         _deviceMapLock = OS_UNFAIR_LOCK_INIT;
         _nodeIDToDeviceMap = [NSMutableDictionary dictionary];
+        _serverEndpoints = [[NSMutableArray alloc] init];
         _commissionableBrowser = nil;
 
         _deviceControllerDelegateBridge = new MTRDeviceControllerDelegateBridge();
@@ -285,6 +293,11 @@
 {
     assertChipStackLockedByCurrentThread();
 
+    // Shut down all our endpoints.
+    for (MTRServerEndpoint * endpoint in [_serverEndpoints copy]) {
+        [self removeServerEndpointOnMatterQueue:endpoint];
+    }
+
     if (_cppCommissioner) {
         auto * commissionerToShutDown = _cppCommissioner;
         // Flag ourselves as not running before we start shutting down
@@ -947,6 +960,71 @@
     return [self syncRunOnWorkQueueWithReturnValue:block error:nil];
 }
 
+- (BOOL)addServerEndpoint:(MTRServerEndpoint *)endpoint
+{
+    VerifyOrReturnValue([self checkIsRunning], NO);
+
+    if (![_factory addServerEndpoint:endpoint]) {
+        return NO;
+    }
+
+    if (![endpoint associateWithController:self]) {
+        MTR_LOG_ERROR("Failed to associate MTRServerEndpoint with MTRDeviceController");
+        [_factory removeServerEndpoint:endpoint];
+        return NO;
+    }
+
+    [self asyncDispatchToMatterQueue:^() {
+        [self->_serverEndpoints addObject:endpoint];
+        [endpoint registerMatterEndpoint];
+    }
+        errorHandler:^(NSError * error) {
+            MTR_LOG_ERROR("Unexpected failure dispatching to Matter queue on running controller in addServerEndpoint");
+        }];
+    return YES;
+}
+
+- (void)removeServerEndpoint:(MTRServerEndpoint *)endpoint queue:(dispatch_queue_t)queue completion:(dispatch_block_t)completion
+{
+    [self removeServerEndpointInternal:endpoint queue:queue completion:completion];
+}
+
+- (void)removeServerEndpoint:(MTRServerEndpoint *)endpoint
+{
+    [self removeServerEndpointInternal:endpoint queue:nil completion:nil];
+}
+
+- (void)removeServerEndpointInternal:(MTRServerEndpoint *)endpoint queue:(dispatch_queue_t _Nullable)queue completion:(dispatch_block_t _Nullable)completion
+{
+    VerifyOrReturn([self checkIsRunning]);
+
+    // We need to unhook the endpoint from the Matter side before we can start
+    // tearing it down.
+    [self asyncDispatchToMatterQueue:^() {
+        [self removeServerEndpointOnMatterQueue:endpoint];
+        if (queue != nil && completion != nil) {
+            dispatch_async(queue, completion);
+        }
+    }
+        errorHandler:^(NSError * error) {
+            // Error means we got shut down, so the endpoint is removed now.
+            if (queue != nil && completion != nil) {
+                dispatch_async(queue, completion);
+            }
+        }];
+}
+
+- (void)removeServerEndpointOnMatterQueue:(MTRServerEndpoint *)endpoint
+{
+    assertChipStackLockedByCurrentThread();
+
+    [endpoint unregisterMatterEndpoint];
+    [_serverEndpoints removeObject:endpoint];
+    [endpoint invalidate];
+
+    [_factory removeServerEndpoint:endpoint];
+}
+
 - (BOOL)checkForInitError:(BOOL)condition logMsg:(NSString *)logMsg
 {
     if (condition) {
@@ -1230,6 +1308,52 @@
                              completion:completion];
 }
 
+- (NSArray<MTRAccessGrant *> *)accessGrantsForClusterPath:(MTRClusterPath *)clusterPath
+{
+    assertChipStackLockedByCurrentThread();
+
+    for (MTRServerEndpoint * endpoint in _serverEndpoints) {
+        if ([clusterPath.endpoint isEqual:endpoint.endpointID]) {
+            return [endpoint matterAccessGrantsForCluster:clusterPath.cluster];
+        }
+    }
+
+    // Nothing matched, no grants.
+    return @[];
+}
+
+- (nullable NSNumber *)neededReadPrivilegeForClusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID
+{
+    assertChipStackLockedByCurrentThread();
+
+    for (MTRServerEndpoint * endpoint in _serverEndpoints) {
+        for (MTRServerCluster * cluster in endpoint.serverClusters) {
+            if (![cluster.clusterID isEqual:clusterID]) {
+                continue;
+            }
+
+            for (MTRServerAttribute * attr in cluster.attributes) {
+                if (![attr.attributeID isEqual:attributeID]) {
+                    continue;
+                }
+
+                return @(attr.requiredReadPrivilege);
+            }
+        }
+    }
+
+    return nil;
+}
+
+#ifdef DEBUG
++ (void)forceLocalhostAdvertisingOnly
+{
+    auto interfaceIndex = chip::Inet::InterfaceId::PlatformType(kDNSServiceInterfaceIndexLocalOnly);
+    auto interfaceId = chip::Inet::InterfaceId(interfaceIndex);
+    chip::app::DnssdServer::Instance().SetInterfaceId(interfaceId);
+}
+#endif // DEBUG
+
 @end
 
 /**
diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm
index 319c33f..457abea 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory.mm
@@ -25,6 +25,9 @@
 #import "MTRDeviceControllerParameters_Wrapper.h"
 #endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
 
+#import <Matter/MTRClusterConstants.h>
+#import <Matter/MTRServerCluster.h>
+
 #import "MTRCertificates.h"
 #import "MTRDemuxingStorage.h"
 #import "MTRDeviceController.h"
@@ -40,12 +43,15 @@
 #import "MTROperationalBrowser.h"
 #import "MTRP256KeypairBridge.h"
 #import "MTRPersistentStorageDelegateBridge.h"
+#import "MTRServerAccessControl.h"
+#import "MTRServerCluster_Internal.h"
+#import "MTRServerEndpoint_Internal.h"
 #import "MTRSessionResumptionStorageBridge.h"
 #import "NSDataSpanConversion.h"
 
 #import <os/lock.h>
 
-#include <app/dynamic_server/AccessControl.h>
+#include <app/util/af.h>
 #include <controller/CHIPDeviceControllerFactory.h>
 #include <credentials/CHIPCert.h>
 #include <credentials/FabricTable.h>
@@ -75,12 +81,14 @@
 static bool sExitHandlerRegistered = false;
 static void ShutdownOnExit() { [[MTRDeviceControllerFactory sharedInstance] stopControllerFactory]; }
 
-@interface MTRDeviceControllerFactory ()
+@interface MTRDeviceControllerFactory () {
+    MTRServerEndpoint * _otaProviderEndpoint;
+    std::unique_ptr<MTROTAProviderDelegateBridge> _otaProviderDelegateBridge;
+}
 
 @property (atomic, readonly) dispatch_queue_t chipWorkQueue;
 @property (readonly) DeviceControllerFactory * controllerFactory;
 @property (readonly) PersistentStorageDelegate * persistentStorageDelegate;
-@property (readonly) MTROTAProviderDelegateBridge * otaProviderDelegateBridge;
 @property (readonly) Crypto::RawKeySessionKeystore * sessionKeystore;
 // We use TestPersistentStorageDelegate just to get an in-memory store to back
 // our group data provider impl.  We initialize this store correctly on every
@@ -170,6 +178,11 @@
     // is only accessed on the Matter queue or after the Matter queue has shut
     // down.
     FabricIndex _nextAvailableFabricIndex;
+
+    // Array of all server endpoints across all controllers, used to ensure
+    // in an atomic way that endpoint IDs are unique.
+    NSMutableArray<MTRServerEndpoint *> * _serverEndpoints;
+    os_unfair_lock _serverEndpointsLock; // Protects access to _serverEndpoints.
 }
 
 + (void)initialize
@@ -232,6 +245,9 @@
         return nil;
     }
 
+    _serverEndpoints = [[NSMutableArray alloc] init];
+    _serverEndpointsLock = OS_UNFAIR_LOCK_INIT;
+
     return self;
 }
 
@@ -319,10 +335,6 @@
         _keystore = nullptr;
     }
 
-    if (_otaProviderDelegateBridge) {
-        delete _otaProviderDelegateBridge;
-        _otaProviderDelegateBridge = nullptr;
-    }
     _otaProviderDelegateQueue = nil;
     _otaProviderDelegate = nil;
 
@@ -410,7 +422,7 @@
             return;
         }
 
-        app::dynamic_server::InitAccessControl();
+        InitializeServerAccessControl();
 
         if (startupParams.hasStorage) {
             _persistentStorageDelegate = new (std::nothrow) MTRPersistentStorageDelegateBridge(startupParams.storage);
@@ -439,7 +451,6 @@
             _otaProviderDelegateQueue = dispatch_queue_create(
                 "org.csa-iot.matter.framework.otaprovider.workqueue", DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL);
         }
-        _otaProviderDelegateBridge = new MTROTAProviderDelegateBridge();
 
         // TODO: Allow passing a different keystore implementation via startupParams.
         _keystore = new PersistentStorageOperationalKeystore();
@@ -900,11 +911,44 @@
 {
     [self _assertCurrentQueueIsNotMatterQueue];
 
-    VerifyOrReturnValue(_otaProviderDelegateBridge != nil, controller);
     VerifyOrReturnValue([_controllers count] == 1, controller);
 
+    _otaProviderEndpoint = [MTRServerEndpoint rootNodeEndpoint];
+
+    // TODO: Have the OTA Provider cluster revision accessible somewhere?
+    auto * otaProviderCluster = [[MTRServerCluster alloc] initWithClusterID:@(MTRClusterIDTypeOTASoftwareUpdateProviderID) revision:@(1)];
+    otaProviderCluster.acceptedCommands = @[
+        @(MTRCommandIDTypeClusterOTASoftwareUpdateProviderCommandQueryImageID),
+        @(MTRCommandIDTypeClusterOTASoftwareUpdateProviderCommandApplyUpdateRequestID),
+        @(MTRCommandIDTypeClusterOTASoftwareUpdateProviderCommandNotifyUpdateAppliedID),
+    ];
+    otaProviderCluster.generatedCommands = @[
+        @(MTRCommandIDTypeClusterOTASoftwareUpdateProviderCommandQueryImageResponseID),
+        @(MTRCommandIDTypeClusterOTASoftwareUpdateProviderCommandApplyUpdateResponseID),
+    ];
+    [otaProviderCluster addAccessGrant:[MTRAccessGrant accessGrantForAllNodesWithPrivilege:MTRAccessControlEntryPrivilegeOperate]];
+
+    // Not expected to fail, since we are following the rules for clusters here.
+    [_otaProviderEndpoint addServerCluster:otaProviderCluster];
+
+    if (![self addServerEndpoint:_otaProviderEndpoint]) {
+        MTR_LOG_ERROR("Failed to add OTA endpoint on factory.  Why?");
+        [controller shutdown];
+        return nil;
+    }
+
+    // This endpoint is not actually associated with a specific controller; we
+    // just need to have a working Matter event loop to bring it up.
+    [_otaProviderEndpoint associateWithController:nil];
+
     __block CHIP_ERROR err;
     dispatch_sync(_chipWorkQueue, ^{
+        [self->_otaProviderEndpoint registerMatterEndpoint];
+
+        // Now that our endpoint exists, go ahead and create the OTA delegate
+        // bridge.  Its constructor relies on the endpoint existing.
+        _otaProviderDelegateBridge = std::make_unique<MTROTAProviderDelegateBridge>();
+
         auto systemState = _controllerFactory->GetSystemState();
         err = _otaProviderDelegateBridge->Init(systemState->SystemLayer(), systemState->ExchangeMgr());
     });
@@ -982,6 +1026,16 @@
 
         if (_otaProviderDelegateBridge) {
             _otaProviderDelegateBridge->Shutdown();
+            _otaProviderDelegateBridge.reset();
+        }
+
+        if (_otaProviderEndpoint != nil) {
+            [_otaProviderEndpoint unregisterMatterEndpoint];
+            [_otaProviderEndpoint invalidate];
+
+            [self removeServerEndpoint:_otaProviderEndpoint];
+
+            _otaProviderEndpoint = nil;
         }
 
         sharedCleanupBlock();
@@ -1071,6 +1125,94 @@
     return [self runningControllerForFabricIndex:fabricIndex includeControllerStartingUp:YES includeControllerShuttingDown:YES];
 }
 
+- (BOOL)addServerEndpoint:(MTRServerEndpoint *)endpoint
+{
+    os_unfair_lock_lock(&_serverEndpointsLock);
+    if (_serverEndpoints.count == CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT) {
+        os_unfair_lock_unlock(&_serverEndpointsLock);
+
+        MTR_LOG_ERROR("Can't add a server endpoint with endpoint ID %u, because we already have %u endpoints defined", static_cast<EndpointId>(endpoint.endpointID.unsignedLongLongValue), CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT);
+
+        return NO;
+    }
+
+    BOOL haveExisting = NO;
+    for (MTRServerEndpoint * existing in _serverEndpoints) {
+        if ([endpoint.endpointID isEqual:existing.endpointID]) {
+            haveExisting = YES;
+            break;
+        }
+    }
+
+    if (!haveExisting) {
+        [_serverEndpoints addObject:endpoint];
+    }
+    os_unfair_lock_unlock(&_serverEndpointsLock);
+
+    if (haveExisting) {
+        MTR_LOG_ERROR("Trying to add a server endpoint with endpoint ID %u, which already exists", static_cast<EndpointId>(endpoint.endpointID.unsignedLongLongValue));
+    }
+
+    return !haveExisting;
+}
+
+- (void)removeServerEndpoint:(MTRServerEndpoint *)endpoint
+{
+    os_unfair_lock_lock(&_serverEndpointsLock);
+    [_serverEndpoints removeObject:endpoint];
+    os_unfair_lock_unlock(&_serverEndpointsLock);
+}
+
+- (NSArray<MTRAccessGrant *> *)accessGrantsForFabricIndex:(chip::FabricIndex)fabricIndex clusterPath:(MTRClusterPath *)clusterPath
+{
+    assertChipStackLockedByCurrentThread();
+
+    if ([clusterPath.endpoint isEqual:_otaProviderEndpoint.endpointID]) {
+        return [_otaProviderEndpoint matterAccessGrantsForCluster:clusterPath.cluster];
+    }
+
+    // We do not want to use _serverEndpoints here, because that might contain
+    // endpoints that are still being set up and whatnot.  Ask the controller
+    // for the relevant fabric index what the relevant access grants are.
+
+    // Include controllers that are shutting down, since this may be an accesss
+    // check for event reports they emit as they shut down.
+    auto * controller = [self runningControllerForFabricIndex:fabricIndex includeControllerStartingUp:NO includeControllerShuttingDown:YES];
+    if (controller == nil) {
+        return @[];
+    }
+
+    return [controller accessGrantsForClusterPath:clusterPath];
+}
+
+- (nullable NSNumber *)neededReadPrivilegeForClusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID
+{
+    assertChipStackLockedByCurrentThread();
+
+    for (MTRServerCluster * cluster in _otaProviderEndpoint.serverClusters) {
+        if (![cluster.clusterID isEqual:clusterID]) {
+            continue;
+        }
+
+        for (MTRServerAttribute * attr in cluster.attributes) {
+            if (![attr.attributeID isEqual:attributeID]) {
+                continue;
+            }
+
+            return @(attr.requiredReadPrivilege);
+        }
+    }
+
+    for (MTRDeviceController * controller in [self getRunningControllers]) {
+        NSNumber * _Nullable neededPrivilege = [controller neededReadPrivilegeForClusterID:clusterID attributeID:attributeID];
+        if (neededPrivilege != nil) {
+            return neededPrivilege;
+        }
+    }
+
+    return nil;
+}
+
 - (void)downloadLogFromNodeWithID:(NSNumber *)nodeID
                        controller:(MTRDeviceController *)controller
                              type:(MTRDiagnosticLogType)type
diff --git a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h
index e413001..4e0290b 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceControllerFactory_Internal.h
@@ -20,9 +20,12 @@
  */
 
 #import <Foundation/Foundation.h>
+#import <Matter/MTRAccessGrant.h>
+#import <Matter/MTRBaseDevice.h> // for MTRClusterPath
 #import <Matter/MTRDefines.h>
 #import <Matter/MTRDeviceController.h>
 #import <Matter/MTRDiagnosticLogsType.h>
+#import <Matter/MTRServerEndpoint.h>
 
 #if MTR_PER_CONTROLLER_STORAGE_ENABLED
 #import <Matter/MTRDeviceControllerParameters.h>
@@ -93,6 +96,38 @@
                                         withParameters:(MTRDeviceControllerParameters *)parameters
                                                  error:(NSError * __autoreleasing *)error;
 
+/**
+ * Add a server endpoint.  This will verify that there is no existing server
+ * endpoint with the provided endpoint ID and return NO if there is one.  Can be
+ * called on any thread.
+ */
+- (BOOL)addServerEndpoint:(MTRServerEndpoint *)endpoint;
+
+/**
+ * Remove a server endpoint.  This must happen after all other teardown for the
+ * endpoint is complete.  Can be called on any thread.
+ */
+- (void)removeServerEndpoint:(MTRServerEndpoint *)endpoint;
+
+/**
+ * Get the access grants that apply for the given fabric index and cluster path.
+ *
+ * Only called on the Matter queue.
+ */
+- (NSArray<MTRAccessGrant *> *)accessGrantsForFabricIndex:(chip::FabricIndex)fabricIndex clusterPath:(MTRClusterPath *)clusterPath;
+
+/**
+ * Get the privilege level needed to read the given attribute.  There's no
+ * endpoint provided because the expectation is that this information is the
+ * same for all cluster instances.
+ *
+ * Returns nil if we have no such attribute defined on any endpoint, otherwise
+ * one of MTRAccessControlEntry* constants wrapped in NSNumber.
+ *
+ * Only called on the Matter queue.
+ */
+- (nullable NSNumber *)neededReadPrivilegeForClusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID;
+
 @property (readonly) chip::PersistentStorageDelegate * storageDelegate;
 @property (readonly) chip::Credentials::GroupDataProvider * groupData;
 
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
index 92873cf..b55d41b 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
@@ -20,6 +20,8 @@
  */
 
 #import <Foundation/Foundation.h>
+#import <Matter/MTRAccessGrant.h>
+#import <Matter/MTRBaseDevice.h> // for MTRClusterPath
 
 #import "MTRDeviceConnectionBridge.h" // For MTRInternalDeviceConnectionCallback
 #import "MTRDeviceController.h"
@@ -243,6 +245,23 @@
                             queue:(dispatch_queue_t)queue
                        completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion;
 
+/**
+ * Get the access grants that apply for the given cluster path.
+ */
+- (NSArray<MTRAccessGrant *> *)accessGrantsForClusterPath:(MTRClusterPath *)clusterPath;
+
+/**
+ * Get the privilege level needed to read the given attribute.  There's no
+ * endpoint provided because the expectation is that this information is the
+ * same for all cluster instances.
+ *
+ * Returns nil if we have no such attribute defined on any endpoint, otherwise
+ * one of MTRAccessControlEntry* constants wrapped in NSNumber.
+ *
+ * Only called on the Matter queue.
+ */
+- (nullable NSNumber *)neededReadPrivilegeForClusterID:(NSNumber *)clusterID attributeID:(NSNumber *)attributeID;
+
 #pragma mark - Device-specific data and SDK access
 // DeviceController will act as a central repository for this opaque dictionary that MTRDevice manages
 - (MTRDevice *)deviceForNodeID:(NSNumber *)nodeID;
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRIMDispatch.mm b/src/darwin/Framework/CHIP/ServerEndpoint/MTRIMDispatch.mm
new file mode 100644
index 0000000..a1f1f47
--- /dev/null
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRIMDispatch.mm
@@ -0,0 +1,104 @@
+/**
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    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 <app-common/zap-generated/callback.h>
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/CommandHandler.h>
+#include <app/ConcreteCommandPath.h>
+#include <app/att-storage.h>
+#include <app/util/af-enums.h>
+#include <app/util/af-types.h>
+#include <app/util/privilege-storage.h>
+#include <lib/core/Optional.h>
+#include <lib/core/TLVReader.h>
+#include <platform/LockTracker.h>
+#include <protocols/interaction_model/StatusCode.h>
+
+using namespace chip;
+using namespace chip::app;
+using namespace chip::app::Clusters;
+
+void emberAfClusterInitCallback(EndpointId endpoint, ClusterId clusterId)
+{
+    assertChipStackLockedByCurrentThread();
+
+    // No-op: Descriptor and OTA do not need this, and our client-defined
+    // clusters dont use it.
+}
+
+EmberAfStatus emAfWriteAttributeExternal(EndpointId endpoint, ClusterId cluster, AttributeId attributeID, uint8_t * dataPtr,
+    EmberAfAttributeType dataType)
+{
+    assertChipStackLockedByCurrentThread();
+
+    // All of our attributes are handled via AttributeAccessInterface, so this
+    // should be unreached.
+    return EMBER_ZCL_STATUS_UNSUPPORTED_ATTRIBUTE;
+}
+
+namespace chip {
+namespace app {
+
+    void DispatchSingleClusterCommand(const ConcreteCommandPath & aPath, TLV::TLVReader & aReader, CommandHandler * aCommandObj)
+    {
+        // TODO: Consider having MTRServerCluster register a
+        // CommandHandlerInterface for command dispatch.  But OTA would need
+        // some special-casing in any case, to call into the existing cluster
+        // implementation.
+        using Protocols::InteractionModel::Status;
+        // This command passed ServerClusterCommandExists so we know it's one of our
+        // supported commands.
+        using namespace OtaSoftwareUpdateProvider::Commands;
+
+        bool wasHandled = false;
+        CHIP_ERROR err = CHIP_NO_ERROR;
+
+        switch (aPath.mCommandId) {
+        case QueryImage::Id: {
+            QueryImage::DecodableType commandData;
+            err = DataModel::Decode(aReader, commandData);
+            if (err == CHIP_NO_ERROR) {
+                wasHandled = emberAfOtaSoftwareUpdateProviderClusterQueryImageCallback(aCommandObj, aPath, commandData);
+            }
+            break;
+        }
+        case ApplyUpdateRequest::Id: {
+            ApplyUpdateRequest::DecodableType commandData;
+            err = DataModel::Decode(aReader, commandData);
+            if (err == CHIP_NO_ERROR) {
+                wasHandled = emberAfOtaSoftwareUpdateProviderClusterApplyUpdateRequestCallback(aCommandObj, aPath, commandData);
+            }
+            break;
+        }
+        case NotifyUpdateApplied::Id: {
+            NotifyUpdateApplied::DecodableType commandData;
+            err = DataModel::Decode(aReader, commandData);
+            if (err == CHIP_NO_ERROR) {
+                wasHandled = emberAfOtaSoftwareUpdateProviderClusterNotifyUpdateAppliedCallback(aCommandObj, aPath, commandData);
+            }
+            break;
+        }
+        default:
+            break;
+        }
+
+        if (CHIP_NO_ERROR != err || !wasHandled) {
+            aCommandObj->AddStatus(aPath, Status::InvalidCommand);
+        }
+    }
+
+} // namespace app
+} // namespace chip
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAccessControl.h b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAccessControl.h
new file mode 100644
index 0000000..49d542a
--- /dev/null
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAccessControl.h
@@ -0,0 +1,28 @@
+/**
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    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.
+ */
+
+#import <Foundation/Foundation.h>
+#import <Matter/MTRDefines.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Initialize the access control module. Must be called on the Matter task
+ * queue.
+ */
+void InitializeServerAccessControl();
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAccessControl.mm b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAccessControl.mm
new file mode 100644
index 0000000..9847165
--- /dev/null
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAccessControl.mm
@@ -0,0 +1,192 @@
+/**
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    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.
+ */
+
+#import "MTRServerAccessControl.h"
+
+#import <Matter/MTRAccessGrant.h>
+#import <Matter/MTRBaseDevice.h> // for MTRClusterPath
+#import <Matter/MTRDeviceControllerFactory.h>
+
+#import "MTRDeviceControllerFactory_Internal.h"
+#import "MTRLogging_Internal.h"
+
+#include <access/AccessControl.h>
+#include <access/Privilege.h>
+#include <access/RequestPath.h>
+#include <access/SubjectDescriptor.h>
+#include <app/InteractionModelEngine.h>
+#include <lib/core/CHIPError.h>
+#include <lib/core/Global.h>
+#include <lib/core/NodeId.h>
+
+#include <app/util/privilege-storage.h>
+
+using namespace chip;
+using namespace chip::Access;
+
+namespace {
+
+class DeviceTypeResolver : public Access::AccessControl::DeviceTypeResolver {
+public:
+    bool IsDeviceTypeOnEndpoint(DeviceTypeId deviceType, EndpointId endpoint) override
+    {
+        return app::IsDeviceTypeOnEndpoint(deviceType, endpoint);
+    }
+};
+
+class AccessControlDelegate : public AccessControl::Delegate {
+    CHIP_ERROR Check(const SubjectDescriptor & subjectDescriptor, const RequestPath & requestPath,
+        Privilege requestPrivilege) override
+    {
+        auto * clusterPath = [MTRClusterPath clusterPathWithEndpointID:@(requestPath.endpoint) clusterID:@(requestPath.cluster)];
+        auto * grants = [[MTRDeviceControllerFactory sharedInstance] accessGrantsForFabricIndex:subjectDescriptor.fabricIndex clusterPath:clusterPath];
+
+        for (MTRAccessGrant * grant in grants) {
+            if (!GrantSubjectMatchesDescriptor(grant, subjectDescriptor)) {
+                continue;
+            }
+
+            // Check whether the desired privilege is granted.  See the Access Control "Overall
+            // Algorithm" section in the spec for which privileges imply which other privileges.
+            switch (grant.grantedPrivilege) {
+            case MTRAccessControlEntryPrivilegeView:
+                if (requestPrivilege == Privilege::kView) {
+                    return CHIP_NO_ERROR;
+                }
+                break;
+            case MTRAccessControlEntryPrivilegeProxyView:
+                if (requestPrivilege == Privilege::kView || requestPrivilege == Privilege::kProxyView) {
+                    return CHIP_NO_ERROR;
+                }
+                break;
+            case MTRAccessControlEntryPrivilegeOperate:
+                if (requestPrivilege == Privilege::kView || requestPrivilege == Privilege::kOperate) {
+                    return CHIP_NO_ERROR;
+                }
+                break;
+            case MTRAccessControlEntryPrivilegeManage:
+                if (requestPrivilege == Privilege::kView || requestPrivilege == Privilege::kOperate || requestPrivilege == Privilege::kManage) {
+                    return CHIP_NO_ERROR;
+                }
+                break;
+            case MTRAccessControlEntryPrivilegeAdminister:
+                if (requestPrivilege == Privilege::kView || requestPrivilege == Privilege::kProxyView || requestPrivilege == Privilege::kOperate || requestPrivilege == Privilege::kManage || requestPrivilege == Privilege::kAdminister) {
+                    return CHIP_NO_ERROR;
+                }
+                break;
+            default:
+                MTR_LOG_ERROR("Uknown granted privilege %u, ignoring", grant.grantedPrivilege);
+                break;
+            }
+
+            // If this grant did not match, just move on to the next one.
+        }
+
+        // None of the grants matched.
+        return CHIP_ERROR_ACCESS_DENIED;
+    }
+
+    bool GrantSubjectMatchesDescriptor(MTRAccessGrant * grant, const SubjectDescriptor & descriptor)
+    {
+        if (grant.subjectID == nil) {
+            // This is an all-nodes grant for CASE access only.
+            return descriptor.authMode == AuthMode::kCase;
+        }
+
+        NodeId grantSubjectNodeId = grant.subjectID.unsignedLongLongValue;
+        if (IsOperationalNodeId(grantSubjectNodeId)) {
+            return descriptor.authMode == AuthMode::kCase && descriptor.subject == grantSubjectNodeId;
+        }
+
+        if (IsGroupId(grantSubjectNodeId)) {
+            return descriptor.authMode == AuthMode::kGroup && descriptor.subject == grantSubjectNodeId;
+        }
+
+        if (IsCASEAuthTag(grantSubjectNodeId)) {
+            return descriptor.cats.CheckSubjectAgainstCATs(grantSubjectNodeId);
+        }
+
+        MTR_LOG_ERROR("Unexpected grant subject: 0x%llx", grantSubjectNodeId);
+        return false;
+    }
+};
+
+struct ControllerAccessControl {
+    DeviceTypeResolver mDeviceTypeResolver;
+    AccessControlDelegate mDelegate;
+    ControllerAccessControl() { GetAccessControl().Init(&mDelegate, mDeviceTypeResolver); }
+};
+
+Global<ControllerAccessControl> gControllerAccessControl;
+
+} // anonymous namespace
+
+int MatterGetAccessPrivilegeForReadEvent(ClusterId cluster, EventId event)
+{
+    // We don't support any event bits yet.
+    return kMatterAccessPrivilegeAdminister;
+}
+
+int MatterGetAccessPrivilegeForInvokeCommand(ClusterId cluster, CommandId command)
+{
+    // For now we only have OTA, which uses Operate.
+    return kMatterAccessPrivilegeOperate;
+}
+
+int MatterGetAccessPrivilegeForReadAttribute(ClusterId cluster, AttributeId attribute)
+{
+    NSNumber * _Nullable neededPrivilege = [[MTRDeviceControllerFactory sharedInstance] neededReadPrivilegeForClusterID:@(cluster) attributeID:@(attribute)];
+    if (neededPrivilege == nil) {
+        // No privileges declared for this attribute on this cluster.  Treat as
+        // "needs admin privileges", so we fail closed.
+        return kMatterAccessPrivilegeAdminister;
+    }
+
+    switch (neededPrivilege.unsignedLongLongValue) {
+    case MTRAccessControlEntryPrivilegeView:
+        return kMatterAccessPrivilegeView;
+    case MTRAccessControlEntryPrivilegeOperate:
+        return kMatterAccessPrivilegeOperate;
+    case MTRAccessControlEntryPrivilegeManage:
+        return kMatterAccessPrivilegeManage;
+    case MTRAccessControlEntryPrivilegeAdminister:
+        return kMatterAccessPrivilegeAdminister;
+    case MTRAccessControlEntryPrivilegeProxyView:
+        // Just treat this as an unknown value; there is no value for this in privilege-storage.
+        FALLTHROUGH;
+    default:
+        break;
+    }
+
+    // To be safe, treat unknown values as "needs admin privileges".  That way the failure case
+    // disallows access that maybe should be allowed, instead of allowing access that maybe
+    // should be disallowed.
+    return kMatterAccessPrivilegeAdminister;
+}
+
+int MatterGetAccessPrivilegeForWriteAttribute(ClusterId cluster, AttributeId attribute)
+{
+    // We don't have any writable attributes yet, but default to Operate.
+    return kMatterAccessPrivilegeOperate;
+}
+
+void InitializeServerAccessControl()
+{
+    assertChipStackLockedByCurrentThread();
+
+    // Ensure the access control bits are created.  No-op after the first call.
+    gControllerAccessControl.get();
+}
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm
index 0337d55..22fd267 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute.mm
@@ -95,8 +95,13 @@
             return NO;
         }
         for (id item in dataValueList) {
+            if (![item isKindOfClass:NSDictionary.class]) {
+                MTR_LOG_ERROR("MTRServerAttribute value array should contain dictionaries");
+            }
+            NSDictionary<NSString *, id> * itemDictionary = item;
+
             NSError * encodingError;
-            NSData * encodedItem = MTREncodeTLVFromDataValueDictionary(item, &encodingError);
+            NSData * encodedItem = MTREncodeTLVFromDataValueDictionary(itemDictionary[MTRDataKey], &encodingError);
             if (encodedItem == nil) {
                 return NO;
             }
@@ -132,7 +137,7 @@
     return YES;
 }
 
-- (BOOL)associateWithController:(MTRDeviceController *)controller
+- (BOOL)associateWithController:(nullable MTRDeviceController *)controller
 {
     MTRDeviceController * existingController = _deviceController;
     if (existingController != nil) {
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h
index dbc7ceb..f6b423f 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerAttribute_Internal.h
@@ -21,15 +21,19 @@
 
 #include <app/ConcreteClusterPath.h>
 
+NS_ASSUME_NONNULL_BEGIN
+
 @interface MTRServerAttribute ()
 
 /**
- * Mark this attribute as associated with a particular controller.
+ * Mark this attribute as associated with a particular controller.  The
+ * controller can be nil to indicate that the endpoint is not associated with a
+ * specific controller but rather with the controller factory.
  */
-- (BOOL)associateWithController:(MTRDeviceController *)controller;
+- (BOOL)associateWithController:(nullable MTRDeviceController *)controller;
 
 /**
- * Mark this attribute as part of an Defunct-state endpoint.
+ * Mark this attribute as part of an endpoint that is no longer being used.
  */
 - (void)invalidate;
 
@@ -46,3 +50,5 @@
 @property (nonatomic, assign) chip::app::ConcreteClusterPath parentCluster;
 
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm
index 8af4ca9..8de78a6 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster.mm
@@ -23,12 +23,40 @@
 #import <Matter/MTRClusterConstants.h>
 #import <Matter/MTRServerCluster.h>
 
+#import "NSDataSpanConversion.h"
+
+#include <app/AttributeAccessInterface.h>
 #include <app/clusters/descriptor/descriptor.h>
+#include <app/data-model/PreEncodedValue.h>
 #include <lib/core/CHIPError.h>
 #include <lib/core/DataModelTypes.h>
+#include <lib/support/CodeUtils.h>
 #include <lib/support/SafeInt.h>
+#include <protocols/interaction_model/StatusCode.h>
+
+// TODO: These attribute-*.h bits are a hack that should eventually go away.
+#include <app/util/attribute-metadata.h>
+#include <app/util/attribute-storage.h>
 
 using namespace chip;
+using namespace chip::app;
+
+class MTRServerAttributeAccessInterface : public AttributeAccessInterface {
+public:
+    MTRServerAttributeAccessInterface(EndpointId aEndpointID, ClusterId aClusterID, NSArray<MTRServerAttribute *> * aAttributes,
+        NSNumber * aClusterRevision)
+        : AttributeAccessInterface(MakeOptional(aEndpointID), aClusterID)
+        , mAttributes(aAttributes)
+        , mClusterRevision(aClusterRevision)
+    {
+    }
+
+    CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override;
+
+private:
+    NSArray<MTRServerAttribute *> * mAttributes;
+    NSNumber * mClusterRevision;
+};
 
 MTR_DIRECT_MEMBERS
 @implementation MTRServerCluster {
@@ -39,6 +67,15 @@
     NSMutableSet<MTRAccessGrant *> * _accessGrants;
     NSMutableArray<MTRServerAttribute *> * _attributes;
     MTRDeviceController * __weak _deviceController;
+
+    std::unique_ptr<MTRServerAttributeAccessInterface> _attributeAccessInterface;
+    // We can't use something like std::unique_ptr<EmberAfAttributeMetadata[]>
+    // because EmberAfAttributeMetadata does not have a default constructor, so
+    // we can't alloc and then initializer later.
+    std::vector<EmberAfAttributeMetadata> _matterAttributeMetadata;
+
+    std::unique_ptr<CommandId[]> _matterAcceptedCommandList;
+    std::unique_ptr<CommandId[]> _matterGeneratedCommandList;
 }
 
 - (nullable instancetype)initWithClusterID:(NSNumber *)clusterID revision:(NSNumber *)revision
@@ -71,7 +108,7 @@
 
 + (MTRServerCluster *)newDescriptorCluster
 {
-    return [[MTRServerCluster alloc] initInternalWithClusterID:@(MTRClusterIDTypeDescriptorID) revision:@(app::Clusters::Descriptor::kClusterRevision) accessGrants:[NSSet set] attributes:@[]];
+    return [[MTRServerCluster alloc] initInternalWithClusterID:@(MTRClusterIDTypeDescriptorID) revision:@(Clusters::Descriptor::kClusterRevision) accessGrants:[NSSet set] attributes:@[]];
 }
 
 - (instancetype)initInternalWithClusterID:(NSNumber *)clusterID revision:(NSNumber *)revision accessGrants:(NSSet *)accessGrants attributes:(NSArray *)attributes
@@ -165,11 +202,18 @@
     }
 
     [_attributes addObject:attribute];
-    attribute.parentCluster = app::ConcreteClusterPath(_parentEndpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue));
+    attribute.parentCluster = ConcreteClusterPath(_parentEndpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue));
     return YES;
 }
 
-- (BOOL)associateWithController:(MTRDeviceController *)controller
+static constexpr EmberAfAttributeMetadata sDescriptorAttributesMetadata[] = {
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributeDeviceTypeListID, ARRAY, 0, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributeServerListID, ARRAY, 0, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributeClientListID, ARRAY, 0, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributePartsListID, ARRAY, 0, 0),
+};
+
+- (BOOL)associateWithController:(nullable MTRDeviceController *)controller
 {
     MTRDeviceController * existingController = _deviceController;
     if (existingController != nil) {
@@ -191,6 +235,84 @@
     // Snapshot _matterAccessGrants now; after this point it will only be
     // updated on the Matter queue.
     _matterAccessGrants = [_accessGrants copy];
+
+    // _attributes shouldn't be able to change anymore, so we can now construct
+    // our EmberAfAttributeMetadata array.
+    size_t attributeCount = _attributes.count;
+
+    // Figure out whether we need to synthesize a FeatureMap attribute.
+    bool needsFeatureMap = true;
+    for (MTRServerAttribute * attr in _attributes) {
+        if ([attr.attributeID isEqual:@(MTRClusterGlobalAttributeFeatureMapID)]) {
+            needsFeatureMap = false;
+            break;
+        }
+    }
+
+    bool needsDescriptorAttributes = [_clusterID isEqual:@(MTRClusterIDTypeDescriptorID)];
+
+    if (needsFeatureMap) {
+        ++attributeCount;
+    }
+
+    if (needsDescriptorAttributes) {
+        attributeCount += ArraySize(sDescriptorAttributesMetadata);
+    }
+
+    // And add one for ClusterRevision
+    ++attributeCount;
+
+    if (attributeCount >= UINT16_MAX) {
+        MTR_LOG_ERROR("Unable to have %llu attributes in a single cluster (clusterID: " ChipLogFormatMEI ")",
+            static_cast<unsigned long long>(attributeCount), ChipLogValueMEI(_clusterID.unsignedLongLongValue));
+        return NO;
+    }
+
+    size_t attrIndex = 0;
+    for (; attrIndex < _attributes.count; ++attrIndex) {
+        auto * attr = _attributes[attrIndex];
+        _matterAttributeMetadata.emplace_back(EmberAfAttributeMetadata(DECLARE_DYNAMIC_ATTRIBUTE(static_cast<AttributeId>(attr.attributeID.unsignedLongLongValue),
+            // The type does not actually matter, since we plan to
+            // handle this entirely via AttributeAccessInterface.
+            // Claim Array because that one will keep random IM
+            // code from trying to do things with the attribute
+            // store.
+            ARRAY,
+            // Size in bytes does not matter, since we plan to
+            // handle this entirely via AttributeAccessInterface.
+            0,
+            // ATTRIBUTE_MASK_NULLABLE is not relevant because we
+            // are handling this all via AttributeAccessInterface.
+            0)));
+    }
+
+    if (needsFeatureMap) {
+        _matterAttributeMetadata.emplace_back(EmberAfAttributeMetadata(DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeGlobalAttributeFeatureMapID,
+            BITMAP32, 4, 0)));
+        ++attrIndex;
+    }
+
+    if (needsDescriptorAttributes) {
+        for (auto & data : sDescriptorAttributesMetadata) {
+            _matterAttributeMetadata.emplace_back(data);
+            ++attrIndex;
+        }
+    }
+
+    // Add our ClusterRevision bit.
+    _matterAttributeMetadata.emplace_back(EmberAfAttributeMetadata(DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeGlobalAttributeClusterRevisionID,
+        INT16U, 2, 0)));
+    ++attrIndex;
+
+    _attributeAccessInterface = std::make_unique<MTRServerAttributeAccessInterface>(_parentEndpoint,
+        static_cast<ClusterId>(_clusterID.unsignedLongLongValue),
+        _attributes,
+        _clusterRevision);
+    // _attributeAccessInterface needs to be registered on the Matter queue; that will happen later.
+
+    _matterAcceptedCommandList = [MTRServerCluster makeMatterCommandList:_acceptedCommands];
+    _matterGeneratedCommandList = [MTRServerCluster makeMatterCommandList:_generatedCommands];
+
     _deviceController = controller;
 
     return YES;
@@ -198,13 +320,44 @@
 
 - (void)invalidate
 {
+    // Undo any work associateWithController did.
     for (MTRServerAttribute * attr in _attributes) {
         [attr invalidate];
     }
 
+    // We generally promise to only touch _matterAccessGrants on the Matter
+    // queue after associateWithController succeeds, but we are no longer being
+    // looked at from that queue, so it's safe to reset it here.
+    _matterAccessGrants = [NSSet set];
+    _matterAttributeMetadata.clear();
+    _attributeAccessInterface.reset();
+    _matterAcceptedCommandList.reset();
+    _matterGeneratedCommandList.reset();
+
     _deviceController = nil;
 }
 
+- (void)registerMatterCluster
+{
+    assertChipStackLockedByCurrentThread();
+
+    if (!registerAttributeAccessOverride(_attributeAccessInterface.get())) {
+        // This should only happen if we somehow managed to register an
+        // AttributeAccessInterface for the same (endpoint, cluster) pair.
+        MTR_LOG_ERROR("Could not register AttributeAccessInterface for endpoint %u, cluster 0x%llx",
+            _parentEndpoint, _clusterID.unsignedLongLongValue);
+    }
+}
+
+- (void)unregisterMatterCluster
+{
+    assertChipStackLockedByCurrentThread();
+
+    if (_attributeAccessInterface != nullptr) {
+        unregisterAttributeAccessOverride(_attributeAccessInterface.get());
+    }
+}
+
 - (NSArray<MTRAccessGrant *> *)accessGrants
 {
     return [_accessGrants allObjects];
@@ -221,8 +374,87 @@
     // Update it on all the attributes, in case the attributes were added to us
     // before we were added to the endpoint.
     for (MTRServerAttribute * attr in _attributes) {
-        attr.parentCluster = app::ConcreteClusterPath(endpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue));
+        attr.parentCluster = ConcreteClusterPath(endpoint, static_cast<ClusterId>(_clusterID.unsignedLongLongValue));
     }
 }
 
+- (Span<const EmberAfAttributeMetadata>)matterAttributeMetadata
+{
+    // This is always called after our _matterAttributeMetadata has been set up
+    // by associateWithController.
+    return Span<const EmberAfAttributeMetadata>(_matterAttributeMetadata.data(), _matterAttributeMetadata.size());
+}
+
+- (CommandId *)matterAcceptedCommands
+{
+    return _matterAcceptedCommandList.get();
+}
+
+- (CommandId *)matterGeneratedCommands
+{
+    return _matterGeneratedCommandList.get();
+}
+
++ (std::unique_ptr<CommandId[]>)makeMatterCommandList:(NSArray<NSNumber *> * _Nullable)commandList
+{
+    if (commandList.count == 0) {
+        return nullptr;
+    }
+
+    // Lists of accepted/generated commands are terminated by kInvalidClusterId.
+    auto matterCommandList = std::make_unique<CommandId[]>(commandList.count + 1);
+    for (size_t index = 0; index < commandList.count; ++index) {
+        matterCommandList[index] = static_cast<CommandId>(commandList[index].unsignedLongLongValue);
+    }
+    matterCommandList[commandList.count] = kInvalidClusterId;
+    return matterCommandList;
+}
+
 @end
+
+CHIP_ERROR MTRServerAttributeAccessInterface::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder)
+{
+    using DataModel::PreEncodedValue;
+
+    // Find the right attribute in our list.
+    MTRServerAttribute * foundAttr = nil;
+    for (MTRServerAttribute * attr in mAttributes) {
+        if ([attr.attributeID isEqual:@(aPath.mAttributeId)]) {
+            foundAttr = attr;
+            break;
+        }
+    }
+
+    if (foundAttr) {
+        id value = foundAttr.serializedValue;
+        if (![value isKindOfClass:NSArray.class]) {
+            // It's a single value, so NSData.
+            NSData * data = value;
+            return aEncoder.Encode(PreEncodedValue(AsByteSpan(data)));
+        }
+
+        // It's a list of data values.
+        NSArray<NSData *> * dataList = value;
+        return aEncoder.EncodeList([dataList](const auto & itemEncoder) {
+            for (NSData * item in dataList) {
+                ReturnErrorOnFailure(itemEncoder.Encode(PreEncodedValue(AsByteSpan(item))));
+            }
+            return CHIP_NO_ERROR;
+        });
+    }
+
+    // This must be the FeatureMap attribute we synthesized.
+    if (aPath.mAttributeId == MTRAttributeIDTypeGlobalAttributeFeatureMapID) {
+        // Feature map defaults to 0.
+        constexpr uint32_t defaultFeatureMap = 0;
+        return aEncoder.Encode(defaultFeatureMap);
+    }
+
+    if (aPath.mAttributeId == MTRAttributeIDTypeGlobalAttributeClusterRevisionID) {
+        return aEncoder.Encode(mClusterRevision.unsignedLongLongValue);
+    }
+
+    // Note: This code is not reached for the descriptor cluster, which uses its own AttributeAccessInterface.
+
+    return CHIP_IM_GLOBAL_STATUS(UnsupportedAttribute);
+}
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster_Internal.h b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster_Internal.h
index 5bd9ba0..4ae3d0d 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster_Internal.h
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerCluster_Internal.h
@@ -21,15 +21,38 @@
 
 #include <lib/core/DataModelTypes.h>
 
+// TODO: These attribute-*.h and Span bits are a hack that should eventually go away.
+#include <app/util/attribute-metadata.h>
+#include <lib/support/Span.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
 @interface MTRServerCluster ()
 
 /**
- * Mark this cluster as associated with a particular controller.
+ * Mark this cluster as associated with a particular controller.  The
+ * controller can be nil to indicate that the endpoint is not associated with a
+ * specific controller but rather with the controller factory.  This method does
+ * NOT perform any cleanup on failure; it's the caller's responsibility to call
+ * invalidate if it fails.
  */
-- (BOOL)associateWithController:(MTRDeviceController *)controller;
+- (BOOL)associateWithController:(nullable MTRDeviceController *)controller;
 
 /**
- * Mark this cluster as part of an Defunct-state endpoint.
+ * Register this cluster.  Always called on the Matter queue.
+ */
+- (void)registerMatterCluster;
+
+/**
+ * Unregister this cluster.  Always called on the Matter queue.
+ */
+- (void)unregisterMatterCluster;
+
+/**
+ * Mark this cluster as part of an endpoint that is no longer being used.  Can
+ * run on any thread, but will either be called before registerMatterCluster or
+ * after unregisterMatterCluster.  This undoes anything associateWithController
+ * did.
  */
 - (void)invalidate;
 
@@ -44,4 +67,31 @@
  */
 @property (nonatomic, assign) chip::EndpointId parentEndpoint;
 
+/**
+ * The attribute metadata for the cluster.  Only valid after associateWithController: has succeeded.
+ */
+@property (nonatomic, assign, readonly) chip::Span<const EmberAfAttributeMetadata> matterAttributeMetadata;
+
+/**
+ * The list of accepted command IDs.
+ */
+@property (nonatomic, copy, nullable) NSArray<NSNumber *> * acceptedCommands;
+
+/**
+ * The list of generated command IDs.
+ */
+@property (nonatomic, copy, nullable) NSArray<NSNumber *> * generatedCommands;
+
+/**
+ * The list of accepted commands IDs in the format the Matter stack needs.
+ */
+@property (nonatomic, assign, nullable, readonly) chip::CommandId * matterAcceptedCommands;
+
+/**
+ * The list of generated commands IDs in the format the Matter stack needs.
+ */
+@property (nonatomic, assign, nullable, readonly) chip::CommandId * matterGeneratedCommands;
+
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.h b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.h
index 642dc5fb..13dbe84 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.h
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.h
@@ -77,8 +77,9 @@
 /**
  * A list of server clusters supported on this endpoint.  The Descriptor cluster
  * does not need to be included unless a TagList attribute is desired on it or
- * unless it has a non-empty PartsList.  If not included, the Descriptor cluster
- * will be generated automatically.
+ * it has a non-empty PartsList, or it needs to have cluster-specific access
+ * grants.  If not included, the Descriptor cluster will be generated
+ * automatically.
  */
 @property (nonatomic, copy, readonly) NSArray<MTRServerCluster *> * serverClusters;
 
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.mm b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.mm
index ee35e4d..5e6df86 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.mm
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint.mm
@@ -19,11 +19,22 @@
 #import "MTRLogging_Internal.h"
 #import "MTRServerCluster_Internal.h"
 #import "MTRServerEndpoint_Internal.h"
+#import <Matter/MTRClusterConstants.h>
 #import <Matter/MTRServerEndpoint.h>
 
 #include <lib/core/CHIPError.h>
 #include <lib/core/DataModelTypes.h>
 #include <lib/support/SafeInt.h>
+#include <platform/LockTracker.h>
+
+// TODO: These af-types.h and att-storage.h and attribute-storage.h and
+// endpoint-config-api.h and probably CodeUtils.h bits are a hack that should
+// eventually go away.
+#include <app/att-storage.h>
+#include <app/util/af-types.h>
+#include <app/util/attribute-storage.h>
+#include <app/util/endpoint-config-api.h>
+#include <lib/support/CodeUtils.h>
 
 using namespace chip;
 
@@ -36,6 +47,13 @@
     NSMutableSet<MTRAccessGrant *> * _accessGrants;
     NSMutableArray<MTRServerCluster *> * _serverClusters;
     MTRDeviceController * __weak _deviceController;
+    std::unique_ptr<EmberAfCluster[]> _matterClusterMetadata;
+    EmberAfEndpointType _matterEndpointMetadata;
+    std::unique_ptr<EmberAfDeviceType[]> _matterDeviceTypes;
+    std::unique_ptr<DataVersion[]> _matterDataVersions;
+
+    // _endpointIndex has a value only when we have the endpoint configured.
+    std::optional<uint16_t> _endpointIndex;
 }
 
 - (nullable instancetype)initWithEndpointID:(NSNumber *)endpointID deviceTypes:(NSArray<MTRDeviceTypeRevision *> *)deviceTypes
@@ -67,6 +85,11 @@
     return [self initInternalWithEndpointID:endpointID deviceTypes:deviceTypes accessGrants:[NSSet set] clusters:@[]];
 }
 
++ (MTRServerEndpoint *)rootNodeEndpoint
+{
+    return [[MTRServerEndpoint alloc] initInternalWithEndpointID:@(kRootEndpointId) deviceTypes:@[] accessGrants:[NSSet set] clusters:@[]];
+}
+
 - (instancetype)initInternalWithEndpointID:(NSNumber *)endpointID deviceTypes:(NSArray<MTRDeviceTypeRevision *> *)deviceTypes accessGrants:(NSSet *)accessGrants clusters:(NSArray *)clusters
 {
     if (!(self = [super init])) {
@@ -148,7 +171,21 @@
     return YES;
 }
 
-- (BOOL)associateWithController:(MTRDeviceController *)controller
+#define MTR_DECLARE_LIST_ATTRIBUTE(attrID) \
+    DECLARE_DYNAMIC_ATTRIBUTE(attrID, ARRAY, 0, 0)
+
+static constexpr EmberAfAttributeMetadata sDescriptorAttributesMetadata[] = {
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributeDeviceTypeListID, ARRAY, 0, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributeServerListID, ARRAY, 0, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributeClientListID, ARRAY, 0, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeClusterDescriptorAttributePartsListID, ARRAY, 0, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeGlobalAttributeFeatureMapID, BITMAP32, 4, 0),
+    DECLARE_DYNAMIC_ATTRIBUTE(MTRAttributeIDTypeGlobalAttributeClusterRevisionID, INT16U, 2, 0),
+};
+
+#undef MTR_DECLARE_LIST_ATTRIBUTE
+
+- (BOOL)associateWithController:(nullable MTRDeviceController *)controller
 {
     MTRDeviceController * existingController = _deviceController;
     if (existingController != nil) {
@@ -161,6 +198,17 @@
         return NO;
     }
 
+    // After this point we have to make sure we clean up on any failures.
+    if (![self finishAssociationWithController:controller]) {
+        [self invalidate];
+        return NO;
+    }
+
+    return YES;
+}
+
+- (BOOL)finishAssociationWithController:(nullable MTRDeviceController *)controller
+{
     for (MTRServerCluster * cluster in _serverClusters) {
         if (![cluster associateWithController:controller]) {
             return NO;
@@ -170,20 +218,193 @@
     // Snapshot _matterAccessGrants now; after this point it will only be
     // updated on the Matter queue.
     _matterAccessGrants = [_accessGrants copy];
+
+    // _serverClusters shouldn't be able to change anymore, so we can now
+    // construct our EmberAfCluster array.
+    size_t clusterCount = _serverClusters.count;
+
+    // Figure out whether we need to synthesize a Descriptor cluster.
+    bool needsDescriptor = true;
+    for (MTRServerCluster * cluster in _serverClusters) {
+        if ([cluster.clusterID isEqual:@(MTRClusterIDTypeDescriptorID)]) {
+            needsDescriptor = false;
+            break;
+        }
+    }
+
+    if (needsDescriptor) {
+        ++clusterCount;
+    }
+
+    if (clusterCount >= 0xFF) {
+        // The ember bits don't allow this many clusters (they use 0xFF to mean
+        // "no such cluster" in various places.
+        MTR_LOG_ERROR("Unable to create endpoint with %llu clusters; it's too many",
+            static_cast<unsigned long long>(clusterCount));
+        return NO;
+    }
+
+    _matterClusterMetadata = std::make_unique<EmberAfCluster[]>(clusterCount);
+    // std::make_unique never returns null; it will try to throw an exception
+    // and likely crash on OOM.
+
+    size_t clusterIndex = 0;
+    for (; clusterIndex < _serverClusters.count; ++clusterIndex) {
+        auto * cluster = _serverClusters[clusterIndex];
+        auto & metadata = _matterClusterMetadata[clusterIndex];
+
+        metadata.clusterId = static_cast<ClusterId>(cluster.clusterID.unsignedLongLongValue);
+
+        auto attrMetadata = cluster.matterAttributeMetadata;
+        metadata.attributes = attrMetadata.data();
+        // This cast is safe because clusters check for this constraint on
+        // number of attributes.
+        metadata.attributeCount = static_cast<uint16_t>(attrMetadata.size());
+
+        metadata.clusterSize = 0; // All our attributes are external.
+
+        metadata.mask = CLUSTER_MASK_SERVER;
+
+        metadata.functions = nullptr; // None of our clusters, including Descriptor, uses these.
+
+        metadata.acceptedCommandList = cluster.matterAcceptedCommands;
+        metadata.generatedCommandList = cluster.matterGeneratedCommands;
+
+        metadata.eventList = nullptr;
+        metadata.eventCount = 0;
+    }
+
+    if (needsDescriptor) {
+        auto & metadata = _matterClusterMetadata[clusterIndex];
+
+        metadata.clusterId = MTRClusterIDTypeDescriptorID;
+
+        metadata.attributes = sDescriptorAttributesMetadata;
+        metadata.attributeCount = ArraySize(sDescriptorAttributesMetadata);
+
+        metadata.clusterSize = 0; // All our attributes are external.
+
+        metadata.mask = CLUSTER_MASK_SERVER;
+
+        metadata.functions = nullptr; // Descriptor does not use these.
+
+        metadata.acceptedCommandList = nullptr;
+        metadata.generatedCommandList = nullptr;
+
+        metadata.eventList = nullptr;
+        metadata.eventCount = 0;
+
+        ++clusterIndex;
+    }
+
+    _matterEndpointMetadata.cluster = _matterClusterMetadata.get();
+    // Cast is safe, because we did a range check above.
+    _matterEndpointMetadata.clusterCount = static_cast<decltype(_matterEndpointMetadata.clusterCount)>(clusterCount);
+    _matterEndpointMetadata.endpointSize = 0; // All our attributes are external.
+
+    _matterDeviceTypes = std::make_unique<EmberAfDeviceType[]>(_deviceTypes.count);
+    for (size_t index = 0; index < _deviceTypes.count; ++index) {
+        auto * deviceType = _deviceTypes[index];
+        auto & matterType = _matterDeviceTypes[index];
+
+        matterType.deviceId = static_cast<DeviceTypeId>(deviceType.deviceTypeID.unsignedLongLongValue);
+        // TODO: The spec allows 16-bit revisions, but the Ember bits only
+        // support 8-bit....
+        matterType.deviceVersion = static_cast<uint8_t>(deviceType.deviceTypeRevision.unsignedLongLongValue);
+    }
+
+    _matterDataVersions = std::make_unique<DataVersion[]>(clusterCount);
+
     _deviceController = controller;
 
     return YES;
 }
 
+- (void)registerMatterEndpoint
+{
+    assertChipStackLockedByCurrentThread();
+
+    static_assert(FIXED_ENDPOINT_COUNT == 0, "Indexing will be off");
+
+    // We can't use emberAfEndpointCount here, because that returns just the
+    // count of fixed endpoints up until the first call to
+    // emberAfSetDynamicEndpoint().
+    uint16_t possibleEndpointCount = MAX_ENDPOINT_COUNT;
+    uint16_t index = 0;
+    for (; index < possibleEndpointCount; ++index) {
+        if (emberAfEndpointFromIndex(index) == kInvalidEndpointId) {
+            break;
+        }
+    }
+
+    if (index == possibleEndpointCount) {
+        // Something is very broken.  We shouldn't have this many endpoints!
+        MTR_LOG_ERROR("We somehow ran out of endpoint slots.");
+        return;
+    }
+
+    auto status = emberAfSetDynamicEndpoint(index, static_cast<EndpointId>(_endpointID.unsignedLongLongValue),
+        &_matterEndpointMetadata,
+        Span<DataVersion>(_matterDataVersions.get(), _matterEndpointMetadata.clusterCount),
+        Span<EmberAfDeviceType>(_matterDeviceTypes.get(), _deviceTypes.count));
+    if (status != EMBER_ZCL_STATUS_SUCCESS) {
+        MTR_LOG_ERROR("Unexpected failure to define our Matter endpoint");
+    }
+
+    _endpointIndex.emplace(index);
+
+    for (MTRServerCluster * cluster in _serverClusters) {
+        [cluster registerMatterCluster];
+    }
+}
+
+- (void)unregisterMatterEndpoint
+{
+    assertChipStackLockedByCurrentThread();
+
+    if (_endpointIndex.has_value()) {
+        emberAfClearDynamicEndpoint(_endpointIndex.value());
+        _endpointIndex.reset();
+    }
+
+    for (MTRServerCluster * cluster in _serverClusters) {
+        [cluster unregisterMatterCluster];
+    }
+}
+
 - (void)invalidate
 {
+    // Undo any work associateWithController did.
     for (MTRServerCluster * cluster in _serverClusters) {
         [cluster invalidate];
     }
 
+    // We generally promise to only touch _matterAccessGrants on the Matter
+    // queue after associateWithController succeeds, but we are no longer being
+    // looked at from that queue, so it's safe to reset it here.
+    _matterAccessGrants = [NSSet set];
+    _matterEndpointMetadata.cluster = nullptr;
+    _matterEndpointMetadata.clusterCount = 0;
+    _matterClusterMetadata.reset();
+    _matterDeviceTypes.reset();
+    _matterDataVersions.reset();
     _deviceController = nil;
 }
 
+- (NSArray<MTRAccessGrant *> *)matterAccessGrantsForCluster:(NSNumber *)clusterID
+{
+    assertChipStackLockedByCurrentThread();
+
+    NSMutableArray<MTRAccessGrant *> * grants = [[_matterAccessGrants allObjects] mutableCopy];
+    for (MTRServerCluster * cluster in _serverClusters) {
+        if ([cluster.clusterID isEqual:clusterID]) {
+            [grants addObjectsFromArray:[cluster.matterAccessGrants allObjects]];
+        }
+    }
+
+    return [grants copy];
+}
+
 - (NSArray<MTRAccessGrant *> *)accessGrants
 {
     return [_accessGrants allObjects];
diff --git a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint_Internal.h b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint_Internal.h
index 97725c2..1ca4d4d 100644
--- a/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint_Internal.h
+++ b/src/darwin/Framework/CHIP/ServerEndpoint/MTRServerEndpoint_Internal.h
@@ -18,22 +18,55 @@
 #import <Matter/MTRDeviceController.h>
 #import <Matter/MTRServerEndpoint.h>
 
+NS_ASSUME_NONNULL_BEGIN
+
 @interface MTRServerEndpoint ()
 
 /**
- * Mark this endpoint as associated with a particular controller.
+ * Mark this endpoint as associated with a particular controller.  The
+ * controller can be nil to indicate that the endpoint is not associated with a
+ * specific controller but rather with the controller factory.
+ *
+ * On failure, this method ensures that it undoes any state changes it made.
  */
-- (BOOL)associateWithController:(MTRDeviceController *)controller;
+- (BOOL)associateWithController:(nullable MTRDeviceController *)controller;
 
 /**
- * Mark this endpoint as being in a Defunct state.
+ * Register this endpoint.  Always called on the Matter queue.
+ */
+- (void)registerMatterEndpoint;
+
+/**
+ * Unregister this endpoint.  Always called on the Matter queue.
+ */
+- (void)unregisterMatterEndpoint;
+
+/**
+ * Mark this endpoint as no longer being in use.  Can run on any thread, but
+ * will either be called before registerMatterEndpoint or after
+ * unregisterMatterEndpoint.  This undoes anything associateWithController did.
  */
 - (void)invalidate;
 
 /**
+ * Get an MTRServerEndpoint for the root node endpoint.  This can't be done via
+ * the public initializer, since we don't allow that to create an
+ * MTRServerEndpoint for endpoint 0.
+ */
++ (MTRServerEndpoint *)rootNodeEndpoint;
+
+/**
+ * Returns the list of access grants applicable to the given cluster ID on this
+ * endpoint.  Only called on the Matter queue.
+ */
+- (NSArray<MTRAccessGrant *> *)matterAccessGrantsForCluster:(NSNumber *)clusterID;
+
+/**
  * The access grants the Matter stack can observe.  Only modified while in
  * Initializing state or on the Matter queue.
  */
 @property (nonatomic, strong, readonly) NSSet<MTRAccessGrant *> * matterAccessGrants;
 
 @end
+
+NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/app/PluginApplicationCallbacks.h b/src/darwin/Framework/CHIP/app/PluginApplicationCallbacks.h
new file mode 100644
index 0000000..f21f1e1
--- /dev/null
+++ b/src/darwin/Framework/CHIP/app/PluginApplicationCallbacks.h
@@ -0,0 +1,27 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    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.
+ */
+
+/**
+ * This file is only here to satisfy includes in core files that expect an
+ * app/PluginApplicationCallbacks.h.  This file is NOT generated by ZAP, and
+ * provides the bare minimum information needed to allow things to compile.
+ *
+ * TODO: This needs a better setup.
+ */
+
+void MatterDescriptorPluginServerInitCallback();
+
+#define MATTER_PLUGINS_INIT MatterDescriptorPluginServerInitCallback();
diff --git a/src/darwin/Framework/CHIP/zap-generated/endpoint_config.h b/src/darwin/Framework/CHIP/zap-generated/endpoint_config.h
new file mode 100644
index 0000000..27cfbdb
--- /dev/null
+++ b/src/darwin/Framework/CHIP/zap-generated/endpoint_config.h
@@ -0,0 +1,51 @@
+/*
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    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.
+ */
+
+/**
+ * This file is only here to satisfy includes in core files that expect an
+ * endpoint config.  This file is NOT generated by ZAP, and provides the bare
+ * minimum information needed to allow things to compile.
+ *
+ * TO DO: This needs a better setup.
+ */
+#include <app/util/endpoint-config-defines.h>
+
+/**
+ * We don't have any fixed endpoints.
+ */
+#define FIXED_ENDPOINT_COUNT 0
+
+/**
+ * We don't have any attributes not implemented by AttributeAccessInterface. But
+ * using 0 here does not work, so just claim 1.
+ */
+#define ATTRIBUTE_LARGEST 1
+
+#define GENERATED_ATTRIBUTES {}
+
+#define GENERATED_ENDPOINT_TYPES {}
+
+#define FIXED_DEVICE_TYPES {}
+
+#define ZAP_FIXED_ENDPOINT_DATA_VERSION_COUNT 0
+
+#define FIXED_ENDPOINT_ARRAY {}
+
+#define FIXED_DEVICE_TYPE_LENGTHS {}
+
+#define FIXED_DEVICE_TYPE_OFFSETS {}
+
+#define FIXED_ENDPOINT_TYPES {}
diff --git a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m
index 8bbb529..effce2e 100644
--- a/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m
@@ -32,6 +32,12 @@
 static NSString * kOnboardingPayload = @"MT:-24J0AFN00KA0648G00";
 static const uint16_t kTestVendorId = 0xFFF1u;
 
+#ifdef DEBUG
+@interface MTRDeviceController (Test)
++ (void)forceLocalhostAdvertisingOnly;
+@end
+#endif // DEBUG
+
 @interface MTRPerControllerStorageTestsControllerDelegate : NSObject <MTRDeviceControllerDelegate>
 @property (nonatomic, strong) XCTestExpectation * expectation;
 @property (nonatomic, strong) NSNumber * deviceID;
@@ -270,6 +276,9 @@
                                                                                      intermediateCertificate:nil
                                                                                              rootCertificate:root];
     XCTAssertNotNil(params);
+    // TODO: This is only used by testControllerServer.  If that moves
+    // elsewhere, take this back out again.
+    params.shouldAdvertiseOperational = YES;
 
     __auto_type * ourCertificateIssuer = [[MTRPerControllerStorageTestsCertificateIssuer alloc] initWithRootCertificate:root
                                                                                                 intermediateCertificate:nil
@@ -1035,6 +1044,471 @@
     XCTAssertFalse([controller3 isRunning]);
 }
 
+// TODO: This might want to go in a separate test file, with some shared setup
+// across multiple tests, maybe.  Would need to factor out
+// startControllerWithRootKeys into a test helper.
+- (void)testControllerServer
+{
+#ifdef DEBUG
+    // Force our controllers to only advertise on localhost, to avoid DNS-SD
+    // crosstalk.
+    [MTRDeviceController forceLocalhostAdvertisingOnly];
+#endif // DEBUG
+
+    __auto_type queue = dispatch_get_main_queue();
+
+    __auto_type * rootKeys = [[MTRTestKeys alloc] init];
+    XCTAssertNotNil(rootKeys);
+
+    NSNumber * fabricID = @(456);
+
+    __auto_type * operationalKeysServer = [[MTRTestKeys alloc] init];
+    XCTAssertNotNil(operationalKeysServer);
+
+    __auto_type * storageDelegateServer = [[MTRTestPerControllerStorage alloc] initWithControllerID:[NSUUID UUID]];
+    XCTAssertNotNil(storageDelegateServer);
+
+    NSNumber * nodeIDServer = @(123);
+
+    NSError * error;
+    MTRDeviceController * controllerServer = [self startControllerWithRootKeys:rootKeys
+                                                               operationalKeys:operationalKeysServer
+                                                                      fabricID:fabricID
+                                                                        nodeID:nodeIDServer
+                                                                       storage:storageDelegateServer
+                                                                         error:&error];
+    XCTAssertNil(error);
+    XCTAssertNotNil(controllerServer);
+    XCTAssertTrue([controllerServer isRunning]);
+    XCTAssertEqualObjects(controllerServer.controllerNodeID, nodeIDServer);
+
+    __auto_type * endpointId1 = @(10);
+    __auto_type * endpointId2 = @(20);
+    __auto_type * endpointId3 = @(30);
+    __auto_type * clusterId1 = @(0xFFF1FC02);
+    __auto_type * clusterId2 = @(0xFFF1FC10);
+    __auto_type * clusterRevision1 = @(3);
+    __auto_type * clusterRevision2 = @(4);
+    __auto_type * attributeId1 = @(0);
+    __auto_type * attributeId2 = @(0xFFF10002);
+
+    __auto_type * unsignedIntValue1 = @{
+        MTRTypeKey : MTRUnsignedIntegerValueType,
+        MTRValueKey : @(5),
+    };
+
+    __auto_type * unsignedIntValue2 = @{
+        MTRTypeKey : MTRUnsignedIntegerValueType,
+        MTRValueKey : @(7),
+    };
+
+    __auto_type * structValue1 = @{
+        MTRTypeKey : MTRStructureValueType,
+        MTRValueKey : @[
+            @{
+                MTRContextTagKey : @(1),
+                MTRDataKey : @ {
+                    MTRTypeKey : MTRUnsignedIntegerValueType,
+                    MTRValueKey : @(1),
+                },
+            },
+            @{
+                MTRContextTagKey : @(2),
+                MTRDataKey : @ {
+                    MTRTypeKey : MTRUTF8StringValueType,
+                    MTRValueKey : @"struct1",
+                },
+            },
+        ],
+    };
+
+    __auto_type * structValue2 = @{
+        MTRTypeKey : MTRStructureValueType,
+        MTRValueKey : @[
+            @{
+                MTRContextTagKey : @(1),
+                MTRDataKey : @ {
+                    MTRTypeKey : MTRUnsignedIntegerValueType,
+                    MTRValueKey : @(2),
+                },
+            },
+            @{
+                MTRContextTagKey : @(2),
+                MTRDataKey : @ {
+                    MTRTypeKey : MTRUTF8StringValueType,
+                    MTRValueKey : @"struct2",
+                },
+            },
+        ],
+    };
+
+    __auto_type * listOfStructsValue1 = @{
+        MTRTypeKey : MTRArrayValueType,
+        MTRValueKey : @[
+            @{
+                MTRDataKey : structValue1,
+            },
+            @{
+                MTRDataKey : structValue2,
+            },
+        ],
+    };
+
+#if 0
+    __auto_type * listOfStructsValue2 = @{
+        MTRTypeKey : MTRArrayValueType,
+        MTRValueKey : @[
+                        @{ MTRDataKey: structValue2, },
+                        ],
+    };
+#endif
+
+    __auto_type responsePathFromRequestPath = ^(MTRAttributeRequestPath * path) {
+        return [MTRAttributePath attributePathWithEndpointID:path.endpoint clusterID:path.cluster attributeID:path.attribute];
+    };
+
+    // Set up an endpoint on the server.
+    __auto_type * deviceType1 = [[MTRDeviceTypeRevision alloc] initWithDeviceTypeID:@(0xFFF10001) revision:@(1)];
+    XCTAssertNotNil(deviceType1);
+
+    __auto_type * endpoint1 = [[MTRServerEndpoint alloc] initWithEndpointID:endpointId1 deviceTypes:@[ deviceType1 ]];
+    XCTAssertNotNil(endpoint1);
+
+    __auto_type * cluster1 = [[MTRServerCluster alloc] initWithClusterID:clusterId1 revision:clusterRevision1];
+    XCTAssertNotNil(cluster1);
+
+    __auto_type * cluster2 = [[MTRServerCluster alloc] initWithClusterID:clusterId2 revision:clusterRevision2];
+    XCTAssertNotNil(cluster1);
+
+    __auto_type * attribute1 = [[MTRServerAttribute alloc] initReadonlyAttributeWithID:attributeId1 initialValue:unsignedIntValue1 requiredPrivilege:MTRAccessControlEntryPrivilegeView];
+    XCTAssertNotNil(attribute1);
+    __auto_type * attribute1RequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:endpointId1
+                                                                                   clusterID:clusterId1
+                                                                                 attributeID:attributeId1];
+    XCTAssertNotNil(attribute1RequestPath);
+    __auto_type * attribute1ResponsePath = responsePathFromRequestPath(attribute1RequestPath);
+    XCTAssertNotNil(attribute1ResponsePath);
+
+    __auto_type * attribute2 = [[MTRServerAttribute alloc] initReadonlyAttributeWithID:attributeId2 initialValue:listOfStructsValue1 requiredPrivilege:MTRAccessControlEntryPrivilegeManage];
+    XCTAssertNotNil(attribute2);
+    __auto_type * attribute2RequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:endpointId1
+                                                                                   clusterID:clusterId2
+                                                                                 attributeID:attributeId2];
+    XCTAssertNotNil(attribute2RequestPath);
+    __auto_type * attribute2ResponsePath = responsePathFromRequestPath(attribute2RequestPath);
+    XCTAssertNotNil(attribute2ResponsePath);
+
+    __auto_type * attribute3 = [[MTRServerAttribute alloc] initReadonlyAttributeWithID:attributeId2 initialValue:unsignedIntValue1 requiredPrivilege:MTRAccessControlEntryPrivilegeOperate];
+    XCTAssertNotNil(attribute3);
+    __auto_type * attribute3RequestPath = [MTRAttributeRequestPath requestPathWithEndpointID:endpointId1
+                                                                                   clusterID:clusterId1
+                                                                                 attributeID:attributeId2];
+    XCTAssertNotNil(attribute3RequestPath);
+    __auto_type * attribute3ResponsePath = responsePathFromRequestPath(attribute3RequestPath);
+    XCTAssertNotNil(attribute3ResponsePath);
+
+    XCTAssertTrue([cluster1 addAttribute:attribute1]);
+    XCTAssertTrue([cluster1 addAttribute:attribute3]);
+
+    XCTAssertTrue([cluster2 addAttribute:attribute2]);
+
+    XCTAssertTrue([endpoint1 addServerCluster:cluster1]);
+    XCTAssertTrue([endpoint1 addServerCluster:cluster2]);
+
+    [endpoint1 addAccessGrant:[MTRAccessGrant accessGrantForAllNodesWithPrivilege:MTRAccessControlEntryPrivilegeView]];
+
+    XCTAssertTrue([controllerServer addServerEndpoint:endpoint1]);
+
+    __auto_type * endpoint2 = [[MTRServerEndpoint alloc] initWithEndpointID:endpointId2 deviceTypes:@[ deviceType1 ]];
+    XCTAssertNotNil(endpoint2);
+    // Should be able to add this endpoint as well.
+    XCTAssertTrue([controllerServer addServerEndpoint:endpoint2]);
+
+    __auto_type * endpoint3 = [[MTRServerEndpoint alloc] initWithEndpointID:endpointId2 deviceTypes:@[ deviceType1 ]];
+    XCTAssertNotNil(endpoint3);
+    // Should not be able to add this endpoint, since it's got a duplicate
+    // endpoint id.
+    XCTAssertFalse([controllerServer addServerEndpoint:endpoint3]);
+
+    __auto_type * operationalKeysClient = [[MTRTestKeys alloc] init];
+    XCTAssertNotNil(operationalKeysClient);
+
+    __auto_type * storageDelegateClient = [[MTRTestPerControllerStorage alloc] initWithControllerID:[NSUUID UUID]];
+    XCTAssertNotNil(storageDelegateClient);
+
+    NSNumber * nodeIDClient = @(789);
+
+    MTRDeviceController * controllerClient = [self startControllerWithRootKeys:rootKeys
+                                                               operationalKeys:operationalKeysClient
+                                                                      fabricID:fabricID
+                                                                        nodeID:nodeIDClient
+                                                                       storage:storageDelegateClient
+                                                                         error:&error];
+    XCTAssertNil(error);
+    XCTAssertNotNil(controllerClient);
+    XCTAssertTrue([controllerClient isRunning]);
+    XCTAssertEqualObjects(controllerClient.controllerNodeID, nodeIDClient);
+
+    __auto_type * endpoint4 = [[MTRServerEndpoint alloc] initWithEndpointID:endpointId2 deviceTypes:@[ deviceType1 ]];
+    XCTAssertNotNil(endpoint4);
+    // Should not be able to add this endpoint, since it's got a duplicate
+    // endpoint id, even though we are adding on a different controller.
+    XCTAssertFalse([controllerClient addServerEndpoint:endpoint4]);
+
+    __auto_type * endpoint5 = [[MTRServerEndpoint alloc] initWithEndpointID:endpointId3 deviceTypes:@[ deviceType1 ]];
+    XCTAssertNotNil(endpoint5);
+    // Should be able to add this one, though; it's unrelated to any existing endpoints.
+    XCTAssertTrue([controllerClient addServerEndpoint:endpoint5]);
+
+    __auto_type * device = [MTRBaseDevice deviceWithNodeID:nodeIDServer controller:controllerClient];
+
+    __auto_type * requestPath = attribute1RequestPath;
+    __block __auto_type * responsePath = attribute1ResponsePath;
+
+    __auto_type checkSingleValue = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error, NSDictionary<NSString *, id> * expectedValue) {
+        // The overall interaction should succeed.
+        XCTAssertNil(error);
+        XCTAssertNotNil(values);
+
+        // And we should get a value for our attribute.
+        XCTAssertEqual(values.count, 1);
+
+        NSDictionary<NSString *, id> * value = values[0];
+        XCTAssertEqualObjects(value[MTRAttributePathKey], responsePath);
+
+        XCTAssertNil(value[MTRErrorKey]);
+        XCTAssertNotNil(value[MTRDataKey]);
+
+        XCTAssertEqualObjects(value[MTRDataKey], expectedValue);
+    };
+
+    __auto_type checkSinglePathError = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error, MTRInteractionErrorCode expectedError) {
+        // The overall interaction should succeed.
+        XCTAssertNil(error);
+        XCTAssertNotNil(values);
+
+        // And we should get a value for our attribute.
+        XCTAssertEqual(values.count, 1);
+
+        NSDictionary<NSString *, id> * value = values[0];
+        XCTAssertEqualObjects(value[MTRAttributePathKey], responsePath);
+
+        XCTAssertNil(value[MTRDataKey]);
+        XCTAssertNotNil(value[MTRErrorKey]);
+
+        NSError * pathError = value[MTRErrorKey];
+        XCTAssertEqual(pathError.domain, MTRInteractionErrorDomain);
+        XCTAssertEqual(pathError.code, expectedError);
+    };
+
+    // First try a basic read.
+    XCTestExpectation * readExpectation1 = [self expectationWithDescription:@"Read 1 of attribute complete"];
+    [device readAttributePaths:@[ requestPath ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        checkSingleValue(values, error, unsignedIntValue1);
+                        [readExpectation1 fulfill];
+                    }];
+    [self waitForExpectations:@[ readExpectation1 ] timeout:kTimeoutInSeconds];
+
+    // Now try a basic subscribe.
+    __block void (^reportHandler)(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error);
+
+    XCTestExpectation * initialValueExpectation = [self expectationWithDescription:@"Got initial value"];
+    reportHandler = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+        checkSingleValue(values, error, unsignedIntValue1);
+        [initialValueExpectation fulfill];
+    };
+
+    XCTestExpectation * subscriptionEstablishedExpectation = [self expectationWithDescription:@"Basic subscription established"];
+    __auto_type * subscribeParams = [[MTRSubscribeParams alloc] initWithMinInterval:@(0) maxInterval:@(10)];
+    [device subscribeToAttributesWithEndpointID:requestPath.endpoint clusterID:requestPath.cluster attributeID:requestPath.attribute
+        params:subscribeParams
+        queue:queue
+        reportHandler:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+            reportHandler(values, error);
+        }
+        subscriptionEstablished:^() {
+            [subscriptionEstablishedExpectation fulfill];
+        }];
+    [self waitForExpectations:@[ subscriptionEstablishedExpectation, initialValueExpectation ] timeout:kTimeoutInSeconds];
+
+    // Now change the value and expect to see it on our subscription.
+    XCTestExpectation * valueUpdateExpectation = [self expectationWithDescription:@"We see the new value"];
+    reportHandler = ^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+        checkSingleValue(values, error, unsignedIntValue2);
+        [valueUpdateExpectation fulfill];
+    };
+
+    [attribute1 setValue:unsignedIntValue2];
+
+    [self waitForExpectations:@[ valueUpdateExpectation ] timeout:kTimeoutInSeconds];
+
+    // Now try a read of an attribute we do not have permissions for.
+    requestPath = attribute2RequestPath;
+    responsePath = attribute2ResponsePath;
+    XCTestExpectation * readNoPermissionsExpectation1 = [self expectationWithDescription:@"Read 1 of attribute with no permissions complete"];
+    [device readAttributePaths:@[ requestPath ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        checkSinglePathError(values, error, MTRInteractionErrorCodeUnsupportedAccess);
+                        [readNoPermissionsExpectation1 fulfill];
+                    }];
+    [self waitForExpectations:@[ readNoPermissionsExpectation1 ] timeout:kTimeoutInSeconds];
+
+    // Change the permissions to give Manage access on the cluster to some
+    // random node ID and try again.  Should still have no permissions.
+    __auto_type * unrelatedGrant = [MTRAccessGrant accessGrantForNodeID:@(0xabc) privilege:MTRAccessControlEntryPrivilegeManage];
+    XCTAssertNotNil(unrelatedGrant);
+    [cluster2 addAccessGrant:unrelatedGrant];
+
+    XCTestExpectation * readNoPermissionsExpectation2 = [self expectationWithDescription:@"Read 2 of attribute with no permissions complete"];
+    [device readAttributePaths:@[ requestPath ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        checkSinglePathError(values, error, MTRInteractionErrorCodeUnsupportedAccess);
+                        [readNoPermissionsExpectation2 fulfill];
+                    }];
+    [self waitForExpectations:@[ readNoPermissionsExpectation2 ] timeout:kTimeoutInSeconds];
+
+    // Change the permissions to give Manage access on the cluster to our client
+    // node ID and try again.  Should be able to read the attribute now.
+    __auto_type * clientManageGrant = [MTRAccessGrant accessGrantForNodeID:nodeIDClient privilege:MTRAccessControlEntryPrivilegeManage];
+    XCTAssertNotNil(clientManageGrant);
+    [cluster2 addAccessGrant:clientManageGrant];
+
+    XCTestExpectation * readExpectation2 = [self expectationWithDescription:@"Read 2 of attribute complete"];
+    [device readAttributePaths:@[ requestPath ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        checkSingleValue(values, error, listOfStructsValue1);
+                        [readExpectation2 fulfill];
+                    }];
+    [self waitForExpectations:@[ readExpectation2 ] timeout:kTimeoutInSeconds];
+
+    // Adding Manage permissions to one cluster should not affect another one.
+    requestPath = attribute3RequestPath;
+    responsePath = attribute3ResponsePath;
+
+    XCTestExpectation * readNoPermissionsExpectation3 = [self expectationWithDescription:@"Read 3 of attribute with no permissions complete"];
+    [device readAttributePaths:@[ requestPath ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        checkSinglePathError(values, error, MTRInteractionErrorCodeUnsupportedAccess);
+                        [readNoPermissionsExpectation3 fulfill];
+                    }];
+    [self waitForExpectations:@[ readNoPermissionsExpectation3 ] timeout:kTimeoutInSeconds];
+
+    // But adding Manage permissions on the endpoint should grant Operate on
+    // the cluster.
+    [endpoint1 addAccessGrant:clientManageGrant];
+
+    XCTestExpectation * readExpectation3 = [self expectationWithDescription:@"Read 3 of attribute complete"];
+    [device readAttributePaths:@[ requestPath ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        checkSingleValue(values, error, unsignedIntValue1);
+                        [readExpectation3 fulfill];
+                    }];
+    [self waitForExpectations:@[ readExpectation3 ] timeout:kTimeoutInSeconds];
+
+    // And removing that grant should remove the permissions again.
+    [endpoint1 removeAccessGrant:clientManageGrant];
+
+    XCTestExpectation * readNoPermissionsExpectation4 = [self expectationWithDescription:@"Read 4 of attribute with no permissions complete"];
+    [device readAttributePaths:@[ requestPath ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        checkSinglePathError(values, error, MTRInteractionErrorCodeUnsupportedAccess);
+                        [readNoPermissionsExpectation4 fulfill];
+                    }];
+    [self waitForExpectations:@[ readNoPermissionsExpectation4 ] timeout:kTimeoutInSeconds];
+
+    // Now do a wildcard read on the endpoint and check that this does the right
+    // thing (gets the right things from descriptor, gets both clusters, etc).
+#if 0
+    // Unused bits ifdefed out until we doing more testing on the actual values
+    // we get back.
+    __auto_type globalAttributePath = ^(NSNumber * clusterID, MTRAttributeIDType attributeID) {
+        return [MTRAttributePath attributePathWithEndpointID:endpointId1 clusterID:clusterID attributeID:@(attributeID)];
+    };
+    __auto_type unsignedIntValue = ^(NSUInteger value) {
+        return @{
+        MTRTypeKey: MTRUnsignedIntegerValueType,
+        MTRValueKey: @(value),
+        };
+    };
+    __auto_type arrayOfUnsignedIntegersValue = ^(NSArray<NSNumber *> * values) {
+        __auto_type * mutableArray = [[NSMutableArray alloc] init];
+        for (NSNumber * value in values) {
+            [mutableArray addObject:@{ MTRDataKey: @{
+                    MTRTypeKey: MTRUnsignedIntegerValueType,
+                            MTRValueKey: value,
+                            }, }];
+        }
+        return @{
+        MTRTypeKey: MTRArrayValueType,
+                MTRValueKey: [mutableArray copy],
+                };
+    };
+#endif
+    XCTestExpectation * wildcardReadExpectation = [self expectationWithDescription:@"Wildcard read of our endpoint"];
+    [device readAttributePaths:@[ [MTRAttributeRequestPath requestPathWithEndpointID:endpointId1 clusterID:nil attributeID:nil] ]
+                    eventPaths:nil
+                        params:nil
+                         queue:queue
+                    completion:^(NSArray<NSDictionary<NSString *, id> *> * _Nullable values, NSError * _Nullable error) {
+                        XCTAssertNil(error);
+                        XCTAssertNotNil(values);
+
+                        // TODO: Figure out how to test that values is correct that's not
+                        // too fragile if things get returned in different valid order.
+                        // For now just check that every path we got has a value, not an
+                        // error.
+                        for (NSDictionary<NSString *, id> * value in values) {
+                            XCTAssertNotNil(value[MTRAttributePathKey]);
+                            XCTAssertNil(value[MTRErrorKey]);
+                            XCTAssertNotNil(value[MTRDataKey]);
+                        }
+#if 0
+            XCTAssertEqualObjects(values, @[
+                                            // cluster1
+                                            @{ MTRAttributePathKey: attribute1ResponsePath,
+                                                    MTRDataKey: unsignedIntValue2, },
+                                               @{ MTRAttributePathKey: globalAttributePath(clusterId1, MTRAttributeIDTypeGlobalAttributeFeatureMapID),
+                                                    MTRDataKey: unsignedIntValue(0), },
+                                               @{ MTRAttributePathKey: globalAttributePath(clusterId1, MTRAttributeIDTypeGlobalAttributeClusterRevisionID),
+                                                    MTRDataKey: clusterRevision1, },
+                                               @{ MTRAttributePathKey: globalAttributePath(clusterId1, MTRAttributeIDTypeGlobalAttributeGeneratedCommandListID),
+                                                    MTRDataKey: arrayOfUnsignedIntegersValue(@[]), },
+                                               @{ MTRAttributePathKey: globalAttributePath(clusterId1, MTRAttributeIDTypeGlobalAttributeAcceptedCommandListID),
+                                                    MTRDataKey: arrayOfUnsignedIntegersValue(@[]), },
+                                             // etc
+
+                                            ]);
+#endif
+                        [wildcardReadExpectation fulfill];
+                    }];
+    [self waitForExpectations:@[ wildcardReadExpectation ] timeout:kTimeoutInSeconds];
+
+    [controllerClient shutdown];
+    [controllerServer shutdown];
+}
+
 @end
 
 #endif // MTR_PER_CONTROLLER_STORAGE_ENABLED
diff --git a/src/darwin/Framework/CHIPTests/MTRServerEndpointTests.m b/src/darwin/Framework/CHIPTests/MTRServerEndpointTests.m
index 57b47f8..4a7c31a 100644
--- a/src/darwin/Framework/CHIPTests/MTRServerEndpointTests.m
+++ b/src/darwin/Framework/CHIPTests/MTRServerEndpointTests.m
@@ -130,12 +130,16 @@
         MTRTypeKey : MTRArrayValueType,
         MTRValueKey : @[
             @{
-                MTRTypeKey : MTRUTF8StringValueType,
-                MTRValueKey : @"str1",
+                MTRDataKey : @ {
+                    MTRTypeKey : MTRUTF8StringValueType,
+                    MTRValueKey : @"str1",
+                },
             },
             @{
-                MTRTypeKey : MTRUTF8StringValueType,
-                MTRValueKey : @"str2",
+                MTRDataKey : @ {
+                    MTRTypeKey : MTRUTF8StringValueType,
+                    MTRValueKey : @"str2",
+                },
             },
         ],
     };
diff --git a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
index 5c735e1..552a797 100644
--- a/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
+++ b/src/darwin/Framework/Matter.xcodeproj/project.pbxproj
@@ -150,6 +150,18 @@
 		5143851E2A65885500EDC8E6 /* MTRSwiftPairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5143851D2A65885500EDC8E6 /* MTRSwiftPairingTests.swift */; };
 		514654492A72F9DF00904E61 /* MTRDemuxingStorage.mm in Sources */ = {isa = PBXBuildFile; fileRef = 514654482A72F9DF00904E61 /* MTRDemuxingStorage.mm */; };
 		5146544B2A72F9F500904E61 /* MTRDemuxingStorage.h in Headers */ = {isa = PBXBuildFile; fileRef = 5146544A2A72F9F500904E61 /* MTRDemuxingStorage.h */; };
+		514C79ED2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79EC2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp */; };
+		514C79EE2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79EC2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp */; };
+		514C79F02B62ADDA00DD6D7B /* descriptor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79EF2B62ADDA00DD6D7B /* descriptor.cpp */; };
+		514C79F12B62ADDA00DD6D7B /* descriptor.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79EF2B62ADDA00DD6D7B /* descriptor.cpp */; };
+		514C79F32B62ED5500DD6D7B /* attribute-storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79F22B62ED5500DD6D7B /* attribute-storage.cpp */; };
+		514C79F42B62ED5500DD6D7B /* attribute-storage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79F22B62ED5500DD6D7B /* attribute-storage.cpp */; };
+		514C79F62B62F0B900DD6D7B /* util.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79F52B62F0B900DD6D7B /* util.cpp */; };
+		514C79F72B62F0B900DD6D7B /* util.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79F52B62F0B900DD6D7B /* util.cpp */; };
+		514C79F92B62F60100DD6D7B /* message.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79F82B62F60100DD6D7B /* message.cpp */; };
+		514C79FA2B62F60100DD6D7B /* message.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79F82B62F60100DD6D7B /* message.cpp */; };
+		514C79FC2B62F94C00DD6D7B /* ota-provider.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79FB2B62F94C00DD6D7B /* ota-provider.cpp */; };
+		514C79FD2B62F94C00DD6D7B /* ota-provider.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 514C79FB2B62F94C00DD6D7B /* ota-provider.cpp */; };
 		514C7A012B64223400DD6D7B /* MTRServerAttribute_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 514C79FF2B64223400DD6D7B /* MTRServerAttribute_Internal.h */; };
 		514C7A022B64223400DD6D7B /* MTRServerEndpoint_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 514C7A002B64223400DD6D7B /* MTRServerEndpoint_Internal.h */; };
 		514C7A042B6436D500DD6D7B /* MTRServerCluster_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 514C7A032B6436D500DD6D7B /* MTRServerCluster_Internal.h */; };
@@ -160,6 +172,13 @@
 		51565CB62A7B0D6600469F18 /* MTRDeviceControllerParameters.h in Headers */ = {isa = PBXBuildFile; fileRef = 51565CB52A7B0D6600469F18 /* MTRDeviceControllerParameters.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		515C1C6F284F9FFB00A48F0C /* MTRFramework.mm in Sources */ = {isa = PBXBuildFile; fileRef = 515C1C6D284F9FFB00A48F0C /* MTRFramework.mm */; };
 		515C1C70284F9FFB00A48F0C /* MTRFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 515C1C6E284F9FFB00A48F0C /* MTRFramework.h */; };
+		516411312B6BF70300E67C05 /* DataModelHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 516415FE2B6B132200D5CE11 /* DataModelHandler.cpp */; };
+		516411322B6BF75700E67C05 /* MTRIMDispatch.mm in Sources */ = {isa = PBXBuildFile; fileRef = 516416002B6B483C00D5CE11 /* MTRIMDispatch.mm */; };
+		516411332B6BF77700E67C05 /* MTRServerAccessControl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 516415FA2B6ACA8300D5CE11 /* MTRServerAccessControl.mm */; };
+		516415FC2B6ACA8300D5CE11 /* MTRServerAccessControl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 516415FA2B6ACA8300D5CE11 /* MTRServerAccessControl.mm */; };
+		516415FD2B6ACA8300D5CE11 /* MTRServerAccessControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 516415FB2B6ACA8300D5CE11 /* MTRServerAccessControl.h */; };
+		516415FF2B6B132200D5CE11 /* DataModelHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 516415FE2B6B132200D5CE11 /* DataModelHandler.cpp */; };
+		516416012B6B483C00D5CE11 /* MTRIMDispatch.mm in Sources */ = {isa = PBXBuildFile; fileRef = 516416002B6B483C00D5CE11 /* MTRIMDispatch.mm */; };
 		51669AF02913204400F4AA36 /* MTRBackwardsCompatTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51669AEF2913204400F4AA36 /* MTRBackwardsCompatTests.m */; };
 		5173A47529C0E2ED00F67F48 /* MTRFabricInfo_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 5173A47229C0E2ED00F67F48 /* MTRFabricInfo_Internal.h */; };
 		5173A47629C0E2ED00F67F48 /* MTRFabricInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5173A47329C0E2ED00F67F48 /* MTRFabricInfo.mm */; };
@@ -183,8 +202,6 @@
 		51B22C2A2740CB47008D5055 /* MTRCommandPayloadsObjc.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51B22C292740CB47008D5055 /* MTRCommandPayloadsObjc.mm */; };
 		51C8E3F82825CDB600D47D00 /* MTRTestKeys.m in Sources */ = {isa = PBXBuildFile; fileRef = 51C8E3F72825CDB600D47D00 /* MTRTestKeys.m */; };
 		51C984622A61CE2A00B0AD9A /* MTRFabricInfoChecker.m in Sources */ = {isa = PBXBuildFile; fileRef = 51C984602A61CE2A00B0AD9A /* MTRFabricInfoChecker.m */; };
-		51CFDDB12AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 51CFDDB02AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp */; };
-		51CFDDB22AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 51CFDDB02AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp */; };
 		51D0B1272B617246006E3511 /* MTRServerEndpoint.mm in Sources */ = {isa = PBXBuildFile; fileRef = 51D0B1252B617246006E3511 /* MTRServerEndpoint.mm */; };
 		51D0B1282B617246006E3511 /* MTRServerEndpoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 51D0B1262B617246006E3511 /* MTRServerEndpoint.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		51D0B12A2B61766F006E3511 /* MTRServerEndpointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 51D0B1292B61766F006E3511 /* MTRServerEndpointTests.m */; };
@@ -535,6 +552,12 @@
 		5143851D2A65885500EDC8E6 /* MTRSwiftPairingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MTRSwiftPairingTests.swift; sourceTree = "<group>"; };
 		514654482A72F9DF00904E61 /* MTRDemuxingStorage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRDemuxingStorage.mm; sourceTree = "<group>"; };
 		5146544A2A72F9F500904E61 /* MTRDemuxingStorage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDemuxingStorage.h; sourceTree = "<group>"; };
+		514C79EC2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "ember-compatibility-functions.cpp"; path = "util/ember-compatibility-functions.cpp"; sourceTree = "<group>"; };
+		514C79EF2B62ADDA00DD6D7B /* descriptor.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = descriptor.cpp; path = clusters/descriptor/descriptor.cpp; sourceTree = "<group>"; };
+		514C79F22B62ED5500DD6D7B /* attribute-storage.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "attribute-storage.cpp"; path = "util/attribute-storage.cpp"; sourceTree = "<group>"; };
+		514C79F52B62F0B900DD6D7B /* util.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = util.cpp; path = util/util.cpp; sourceTree = "<group>"; };
+		514C79F82B62F60100DD6D7B /* message.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = message.cpp; path = util/message.cpp; sourceTree = "<group>"; };
+		514C79FB2B62F94C00DD6D7B /* ota-provider.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = "ota-provider.cpp"; path = "clusters/ota-provider/ota-provider.cpp"; sourceTree = "<group>"; };
 		514C79FF2B64223400DD6D7B /* MTRServerAttribute_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRServerAttribute_Internal.h; sourceTree = "<group>"; };
 		514C7A002B64223400DD6D7B /* MTRServerEndpoint_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRServerEndpoint_Internal.h; sourceTree = "<group>"; };
 		514C7A032B6436D500DD6D7B /* MTRServerCluster_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRServerCluster_Internal.h; sourceTree = "<group>"; };
@@ -545,6 +568,10 @@
 		51565CB52A7B0D6600469F18 /* MTRDeviceControllerParameters.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRDeviceControllerParameters.h; sourceTree = "<group>"; };
 		515C1C6D284F9FFB00A48F0C /* MTRFramework.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRFramework.mm; sourceTree = "<group>"; };
 		515C1C6E284F9FFB00A48F0C /* MTRFramework.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRFramework.h; sourceTree = "<group>"; };
+		516415FA2B6ACA8300D5CE11 /* MTRServerAccessControl.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRServerAccessControl.mm; sourceTree = "<group>"; };
+		516415FB2B6ACA8300D5CE11 /* MTRServerAccessControl.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRServerAccessControl.h; sourceTree = "<group>"; };
+		516415FE2B6B132200D5CE11 /* DataModelHandler.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = DataModelHandler.cpp; path = util/DataModelHandler.cpp; sourceTree = "<group>"; };
+		516416002B6B483C00D5CE11 /* MTRIMDispatch.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRIMDispatch.mm; sourceTree = "<group>"; };
 		51669AEF2913204400F4AA36 /* MTRBackwardsCompatTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRBackwardsCompatTests.m; sourceTree = "<group>"; };
 		5173A47229C0E2ED00F67F48 /* MTRFabricInfo_Internal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRFabricInfo_Internal.h; sourceTree = "<group>"; };
 		5173A47329C0E2ED00F67F48 /* MTRFabricInfo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRFabricInfo.mm; sourceTree = "<group>"; };
@@ -575,7 +602,6 @@
 		51C8E3F72825CDB600D47D00 /* MTRTestKeys.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRTestKeys.m; sourceTree = "<group>"; };
 		51C984602A61CE2A00B0AD9A /* MTRFabricInfoChecker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRFabricInfoChecker.m; sourceTree = "<group>"; };
 		51C984612A61CE2A00B0AD9A /* MTRFabricInfoChecker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRFabricInfoChecker.h; sourceTree = "<group>"; };
-		51CFDDB02AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = EmptyDataModelHandler.cpp; path = ../controller/EmptyDataModelHandler.cpp; sourceTree = "<group>"; };
 		51D0B1252B617246006E3511 /* MTRServerEndpoint.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = MTRServerEndpoint.mm; sourceTree = "<group>"; };
 		51D0B1262B617246006E3511 /* MTRServerEndpoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTRServerEndpoint.h; sourceTree = "<group>"; };
 		51D0B1292B61766F006E3511 /* MTRServerEndpointTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTRServerEndpointTests.m; sourceTree = "<group>"; };
@@ -1024,7 +1050,13 @@
 			isa = PBXGroup;
 			children = (
 				5143041F2914CED9004DC7FE /* generic-callback-stubs.cpp */,
-				51CFDDB02AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp */,
+				514C79F22B62ED5500DD6D7B /* attribute-storage.cpp */,
+				514C79EF2B62ADDA00DD6D7B /* descriptor.cpp */,
+				514C79EC2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp */,
+				516415FE2B6B132200D5CE11 /* DataModelHandler.cpp */,
+				514C79F52B62F0B900DD6D7B /* util.cpp */,
+				514C79F82B62F60100DD6D7B /* message.cpp */,
+				514C79FB2B62F94C00DD6D7B /* ota-provider.cpp */,
 			);
 			name = app;
 			path = ../../../app;
@@ -1138,6 +1170,9 @@
 			children = (
 				51D0B12C2B6177D9006E3511 /* MTRAccessGrant.h */,
 				51D0B12B2B6177D9006E3511 /* MTRAccessGrant.mm */,
+				516416002B6B483C00D5CE11 /* MTRIMDispatch.mm */,
+				516415FB2B6ACA8300D5CE11 /* MTRServerAccessControl.h */,
+				516415FA2B6ACA8300D5CE11 /* MTRServerAccessControl.mm */,
 				51D0B1362B618CC6006E3511 /* MTRServerAttribute.h */,
 				51D0B1372B618CC6006E3511 /* MTRServerAttribute.mm */,
 				514C79FF2B64223400DD6D7B /* MTRServerAttribute_Internal.h */,
@@ -1541,6 +1576,7 @@
 				754F3DF427FBB94B00E60580 /* MTREventTLVValueDecoder_Internal.h in Headers */,
 				3CF134AF289D90FF0017A19E /* MTROperationalCertificateIssuer.h in Headers */,
 				5178E6822AE098520069DF72 /* MTRCommissionableBrowserResult_Internal.h in Headers */,
+				516415FD2B6ACA8300D5CE11 /* MTRServerAccessControl.h in Headers */,
 				3CF134AB289D8DF70017A19E /* MTRDeviceAttestationInfo.h in Headers */,
 				B2E0D7B2245B0B5C003C5B48 /* MTRManualSetupPayloadParser.h in Headers */,
 				3CF134A7289D8ADA0017A19E /* MTRCSRInfo.h in Headers */,
@@ -1757,8 +1793,10 @@
 				B45373F32A9FEC1A00807602 /* server-ws.c in Sources */,
 				03F430AA2994113500166449 /* sysunix.c in Sources */,
 				B45373C42A9FEA9100807602 /* dummy-callback.c in Sources */,
+				514C79EE2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp in Sources */,
 				039145E82993179300257B3E /* GetCommissionerNodeIdCommand.mm in Sources */,
 				0395469F2991DFC5006D42A8 /* json_reader.cpp in Sources */,
+				514C79F42B62ED5500DD6D7B /* attribute-storage.cpp in Sources */,
 				B45373D22A9FEB0C00807602 /* buflist.c in Sources */,
 				B45373D72A9FEB0C00807602 /* lws_dll2.c in Sources */,
 				B45373FE2A9FEC4F00807602 /* unix-fds.c in Sources */,
@@ -1768,8 +1806,10 @@
 				0395469E2991DFC5006D42A8 /* json_writer.cpp in Sources */,
 				03FB93E02A46200A0048CB35 /* DiscoverCommissionablesCommand.mm in Sources */,
 				B45373EF2A9FEBFE00807602 /* ops-raw-skt.c in Sources */,
+				516411332B6BF77700E67C05 /* MTRServerAccessControl.mm in Sources */,
 				037C3DD52991C2E200B7EEE2 /* CHIPCommandBridge.mm in Sources */,
 				039546BC2991E1CB006D42A8 /* LogCommands.cpp in Sources */,
+				516411312B6BF70300E67C05 /* DataModelHandler.cpp in Sources */,
 				B45373E12A9FEB7F00807602 /* ops-h1.c in Sources */,
 				B45373EB2A9FEBDB00807602 /* ops-listen.c in Sources */,
 				0382FA2C2992F06C00247BBB /* Commands.cpp in Sources */,
@@ -1804,25 +1844,29 @@
 				039145E12993102B00257B3E /* main.mm in Sources */,
 				037C3DD42991BD5200B7EEE2 /* logging.mm in Sources */,
 				B45374012A9FEC4F00807602 /* unix-sockets.c in Sources */,
-				51CFDDB22AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp in Sources */,
 				03F430A82994112B00166449 /* editline.c in Sources */,
 				B45373E92A9FEBC100807602 /* server.c in Sources */,
 				037C3DB32991BD5000B7EEE2 /* OpenCommissioningWindowCommand.mm in Sources */,
 				037C3DAE2991BD4F00B7EEE2 /* PairingCommandBridge.mm in Sources */,
+				514C79FD2B62F94C00DD6D7B /* ota-provider.cpp in Sources */,
 				B45373FB2A9FEC4F00807602 /* unix-service.c in Sources */,
 				B45373F22A9FEC1A00807602 /* ops-ws.c in Sources */,
 				037C3DCA2991BD5100B7EEE2 /* CHIPCommandStorageDelegate.mm in Sources */,
 				037C3DCF2991BD5200B7EEE2 /* MTRError.mm in Sources */,
 				037C3DC72991BD5100B7EEE2 /* CHIPToolKeypair.mm in Sources */,
 				B45373E52A9FEBA400807602 /* date.c in Sources */,
+				514C79FA2B62F60100DD6D7B /* message.cpp in Sources */,
+				514C79F72B62F0B900DD6D7B /* util.cpp in Sources */,
 				B45373DC2A9FEB5300807602 /* sha-1.c in Sources */,
 				B45373D12A9FEB0C00807602 /* alloc.c in Sources */,
 				B45373C62A9FEA9100807602 /* sorted-usec-list.c in Sources */,
 				037C3DB62991BD5000B7EEE2 /* ModelCommandBridge.mm in Sources */,
+				516411322B6BF75700E67C05 /* MTRIMDispatch.mm in Sources */,
 				B45373C22A9FEA9100807602 /* vhost.c in Sources */,
 				037C3DB42991BD5000B7EEE2 /* DeviceControllerDelegateBridge.mm in Sources */,
 				039547012992D461006D42A8 /* generic-callback-stubs.cpp in Sources */,
 				B45373D52A9FEB0C00807602 /* logs.c in Sources */,
+				514C79F12B62ADDA00DD6D7B /* descriptor.cpp in Sources */,
 				0382FA312992FD6E00247BBB /* MTRLogging.mm in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -1833,6 +1877,7 @@
 			files = (
 				2C8C8FC2253E0C2100797F05 /* MTRPersistentStorageDelegateBridge.mm in Sources */,
 				99AECC802798A57F00B6355B /* MTRCommissioningParameters.mm in Sources */,
+				514C79F92B62F60100DD6D7B /* message.cpp in Sources */,
 				2CB7163C252E8A7C0026E2BB /* MTRDeviceControllerDelegateBridge.mm in Sources */,
 				997DED162695343400975E97 /* MTRThreadOperationalDataset.mm in Sources */,
 				515C1C6F284F9FFB00A48F0C /* MTRFramework.mm in Sources */,
@@ -1840,14 +1885,18 @@
 				27A53C1827FBC6920053F131 /* MTRAttestationTrustStoreBridge.mm in Sources */,
 				93B2CF9A2B56E45C00E4D187 /* MTRClusterNames.mm in Sources */,
 				998F287126D56940001846C6 /* MTRP256KeypairBridge.mm in Sources */,
+				516416012B6B483C00D5CE11 /* MTRIMDispatch.mm in Sources */,
 				51D0B1412B61B3A4006E3511 /* MTRServerCluster.mm in Sources */,
+				514C79FC2B62F94C00DD6D7B /* ota-provider.cpp in Sources */,
 				5136661428067D550025EDAE /* MTRDeviceControllerFactory.mm in Sources */,
 				51D0B1392B618CC6006E3511 /* MTRServerAttribute.mm in Sources */,
 				51B22C2A2740CB47008D5055 /* MTRCommandPayloadsObjc.mm in Sources */,
 				51F522682AE70734000C4050 /* MTRDeviceTypeMetadata.mm in Sources */,
 				75B765C32A1D82D30014719B /* MTRAttributeSpecifiedCheck.mm in Sources */,
 				AF5F90FF2878D351005503FA /* MTROTAProviderDelegateBridge.mm in Sources */,
+				516415FF2B6B132200D5CE11 /* DataModelHandler.cpp in Sources */,
 				51E95DFC2A78443C00A434F0 /* MTRSessionResumptionStorageBridge.mm in Sources */,
+				514C79ED2B62ADCD00DD6D7B /* ember-compatibility-functions.cpp in Sources */,
 				7534F12828BFF20300390851 /* MTRDeviceAttestationDelegate.mm in Sources */,
 				B4C8E6B72B3453AD00FCD54D /* MTRDiagnosticLogsDownloader.mm in Sources */,
 				2C5EEEF7268A85C400CAE3D3 /* MTRDeviceConnectionBridge.mm in Sources */,
@@ -1857,10 +1906,12 @@
 				3CF134A9289D8D800017A19E /* MTRCSRInfo.mm in Sources */,
 				991DC0892475F47D00C13860 /* MTRDeviceController.mm in Sources */,
 				B2E0D7B7245B0B5C003C5B48 /* MTRQRCodeSetupPayloadParser.mm in Sources */,
+				514C79F32B62ED5500DD6D7B /* attribute-storage.cpp in Sources */,
 				514304202914CED9004DC7FE /* generic-callback-stubs.cpp in Sources */,
 				1EDCE546289049A100E41EC9 /* MTROTAHeader.mm in Sources */,
 				51D0B13D2B61B2F2006E3511 /* MTRDeviceTypeRevision.mm in Sources */,
 				1EC4CE5D25CC26E900D7304F /* MTRBaseClusters.mm in Sources */,
+				514C79F62B62F0B900DD6D7B /* util.cpp in Sources */,
 				51565CB22A7AD77600469F18 /* MTRDeviceControllerDataStore.mm in Sources */,
 				51D0B12F2B617800006E3511 /* MTRAccessGrant.mm in Sources */,
 				88E6C9482B6334ED001A1FE0 /* MTRMetrics.mm in Sources */,
@@ -1876,11 +1927,12 @@
 				5ACDDD7D27CD16D200EFD68A /* MTRClusterStateCacheContainer.mm in Sources */,
 				513DDB8A2761F6F900DAA01A /* MTRAttributeTLVValueDecoder.mm in Sources */,
 				5117DD3829A931AE00FFA1AA /* MTROperationalBrowser.mm in Sources */,
+				514C79F02B62ADDA00DD6D7B /* descriptor.cpp in Sources */,
 				3D843757294AD25A0070D20A /* MTRCertificateInfo.mm in Sources */,
 				5A7947E427C0129600434CF2 /* MTRDeviceController+XPC.mm in Sources */,
 				5A6FEC9027B563D900F25F42 /* MTRDeviceControllerOverXPC.mm in Sources */,
+				516415FC2B6ACA8300D5CE11 /* MTRServerAccessControl.mm in Sources */,
 				B289D4222639C0D300D4E314 /* MTROnboardingPayloadParser.mm in Sources */,
-				51CFDDB12AC5F78F00DA7CA5 /* EmptyDataModelHandler.cpp in Sources */,
 				3CF134AD289D8E570017A19E /* MTRDeviceAttestationInfo.mm in Sources */,
 				2C1B027A2641DB4E00780EF1 /* MTROperationalCredentialsDelegate.mm in Sources */,
 				7560FD1C27FBBD3F005E85B3 /* MTREventTLVValueDecoder.mm in Sources */,
@@ -2022,6 +2074,7 @@
 				PROVISIONING_PROFILE_SPECIFIER = "";
 				SDKROOT = macosx;
 				STRIP_INSTALLED_PRODUCT = NO;
+				SYSTEM_HEADER_SEARCH_PATHS = "$(CHIP_ROOT)/src/darwin/Framework/CHIP/";
 				USER_HEADER_SEARCH_PATHS = "";
 				WARNING_CFLAGS = (
 					"-Wformat",
@@ -2100,6 +2153,7 @@
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
 				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = "$(CHIP_ROOT)/src/darwin/Framework/CHIP/";
 				USER_HEADER_SEARCH_PATHS = "";
 				WARNING_CFLAGS = (
 					"-Wformat",
@@ -2248,9 +2302,6 @@
 					"$(CHIP_ROOT)/config/ios",
 					"$(CHIP_ROOT)/src",
 					"$(CHIP_ROOT)/src/include",
-					"$(CHIP_ROOT)/src/lib",
-					"$(CHIP_ROOT)/src/app",
-					"$(CHIP_ROOT)/src/app/util",
 					"$(CHIP_ROOT)/zzz_generated/",
 					"$(CHIP_ROOT)/zzz_generated/app-common",
 					"$(CHIP_ROOT)/third_party/nlassert/repo/include",
@@ -2419,9 +2470,6 @@
 					"$(CHIP_ROOT)/config/ios",
 					"$(CHIP_ROOT)/src",
 					"$(CHIP_ROOT)/src/include",
-					"$(CHIP_ROOT)/src/lib",
-					"$(CHIP_ROOT)/src/app",
-					"$(CHIP_ROOT)/src/app/util",
 					"$(CHIP_ROOT)/zzz_generated/",
 					"$(CHIP_ROOT)/zzz_generated/app-common",
 					"$(CHIP_ROOT)/third_party/nlassert/repo/include",
diff --git a/src/darwin/Framework/chip_xcode_build_connector.sh b/src/darwin/Framework/chip_xcode_build_connector.sh
index 54e3594..3ab0464 100755
--- a/src/darwin/Framework/chip_xcode_build_connector.sh
+++ b/src/darwin/Framework/chip_xcode_build_connector.sh
@@ -93,7 +93,7 @@
 declare -a args=(
     'default_configs_cosmetic=[]' # suppress colorization
     'chip_crypto="boringssl"'
-    'chip_build_controller_dynamic_server=true'
+    'chip_build_controller_dynamic_server=false'
     'chip_build_tools=false'
     'chip_build_tests=false'
     'chip_enable_wifi=false'
diff --git a/src/platform/Darwin/CHIPDevicePlatformConfig.h b/src/platform/Darwin/CHIPDevicePlatformConfig.h
index 8cb7797..15fdaa4 100644
--- a/src/platform/Darwin/CHIPDevicePlatformConfig.h
+++ b/src/platform/Darwin/CHIPDevicePlatformConfig.h
@@ -58,9 +58,8 @@
 #define CHIP_DEVICE_CONFIG_EVENT_LOGGING_UTC_TIMESTAMPS 1
 #endif // CHIP_DEVICE_CONFIG_EVENT_LOGGING_UTC_TIMESTAMPS
 
-// Reserve a single dynamic endpoint that we can use to host things like OTA
-// Provider server.
+// Default to as many dynamic endpoints as we can manage.
 #if !defined(CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT) || CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT == 0
 #undef CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT
-#define CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT 1
+#define CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT 254
 #endif // CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT