diff --git a/zircon/system/core/devmgr/devcoordinator/coordinator.cpp b/zircon/system/core/devmgr/devcoordinator/coordinator.cpp
index ebc7a9bc42711a9afb95c41604d785887b8c7fd8..24ddf1861c854dc77fd7681675a15c26ab1acbc0 100644
--- a/zircon/system/core/devmgr/devcoordinator/coordinator.cpp
+++ b/zircon/system/core/devmgr/devcoordinator/coordinator.cpp
@@ -1183,6 +1183,15 @@ static zx_status_t fidl_PublishMetadata(void* ctx, const char* device_path_data,
     return fuchsia_device_manager_CoordinatorPublishMetadata_reply(txn, status);
 }
 
+static zx_status_t fidl_AddCompositeDevice(
+        void* ctx, const char* name_data, size_t name_size, const uint64_t* props,
+        size_t props_count, const fuchsia_device_manager_DeviceComponent components[16],
+        uint32_t components_count, uint32_t coresident_device_index, fidl_txn_t* txn) {
+
+    //auto dev = static_cast<Device*>(ctx);
+    return fuchsia_device_manager_CoordinatorAddCompositeDevice_reply(txn, ZX_ERR_NOT_SUPPORTED);
+}
+
 static zx_status_t fidl_DmCommand(void* ctx, zx_handle_t raw_log_socket, const char* command_data,
                                   size_t command_size, fidl_txn_t* txn) {
     zx::socket log_socket(raw_log_socket);
@@ -1299,6 +1308,8 @@ static fuchsia_device_manager_Coordinator_ops_t fidl_ops = {
     .GetMetadataSize = fidl_GetMetadataSize,
     .AddMetadata = fidl_AddMetadata,
     .PublishMetadata = fidl_PublishMetadata,
+    .AddCompositeDevice = fidl_AddCompositeDevice,
+
     .DmCommand = fidl_DmCommand,
     .DmOpenVirtcon = fidl_DmOpenVirtcon,
     .DmMexec = fidl_DmMexec,
diff --git a/zircon/system/core/devmgr/devhost/api.cpp b/zircon/system/core/devmgr/devhost/api.cpp
index 3d2a2915c5f7b84b1bf1a39581259b8e3e688dff..ad882b83769020d9befa96cf72537b79476d3b73 100644
--- a/zircon/system/core/devmgr/devhost/api.cpp
+++ b/zircon/system/core/devmgr/devhost/api.cpp
@@ -263,6 +263,8 @@ __EXPORT zx_status_t device_add_composite(
         zx_device_t* dev, const char* name, const zx_device_prop_t* props, size_t props_count,
         const device_component_t* components, size_t components_count,
         uint32_t coresident_device_index) {
-    // TODO(ZX-2648): Implement
-    return ZX_ERR_NOT_SUPPORTED;
+    ApiAutoLock lock;
+    auto dev_ref = fbl::WrapRefPtr(dev);
+    return devhost_device_add_composite(dev_ref, name, props, props_count, components,
+                                        components_count, coresident_device_index);
 }
diff --git a/zircon/system/core/devmgr/devhost/devhost.cpp b/zircon/system/core/devmgr/devhost/devhost.cpp
index bf8773a32f0825ff26e05fb1a5d5f3f173d35a11..48dbaf260082f31bcd810cb136d376c049783bb1 100644
--- a/zircon/system/core/devmgr/devhost/devhost.cpp
+++ b/zircon/system/core/devmgr/devhost/devhost.cpp
@@ -1185,6 +1185,64 @@ zx_status_t devhost_publish_metadata(const fbl::RefPtr<zx_device_t>& dev, const
     return call_status;
 }
 
+zx_status_t devhost_device_add_composite(const fbl::RefPtr<zx_device_t>& dev,
+                                         const char* name, const zx_device_prop_t* props,
+                                         size_t props_count, const device_component_t* components,
+                                         size_t components_count,
+                                         uint32_t coresident_device_index) {
+    if ((props == nullptr && props_count > 0) || components == nullptr || name == nullptr) {
+        return ZX_ERR_INVALID_ARGS;
+    }
+    if (components_count > fuchsia_device_manager_COMPONENTS_MAX) {
+        return ZX_ERR_INVALID_ARGS;
+    }
+    const zx::channel& rpc = *dev->rpc;
+    if (!rpc.is_valid()) {
+        return ZX_ERR_IO_REFUSED;
+    }
+
+    // Ideally we could perform the entire serialization with a single
+    // allocation, but for now we allocate this (potentially large) array on
+    // the heap.  The array is extra-large because of the use of FIDL array
+    // types instead of vector types, to get around the SimpleLayout
+    // restrictions.
+    std::unique_ptr<fuchsia_device_manager_DeviceComponent[]> fidl_components(
+            new fuchsia_device_manager_DeviceComponent[fuchsia_device_manager_COMPONENTS_MAX]());
+    for (size_t i = 0; i < components_count; ++i) {
+        auto& component = fidl_components[i];
+        component.parts_count = components[i].parts_count;
+        if (component.parts_count > fuchsia_device_manager_DEVICE_COMPONENT_PARTS_MAX) {
+            return ZX_ERR_INVALID_ARGS;
+        }
+        for (size_t j = 0; j < component.parts_count; ++j) {
+            auto& part = fidl_components[i].parts[j];
+            part.match_program_count = components[i].parts[j].instruction_count;
+            if (part.match_program_count >
+                fuchsia_device_manager_DEVICE_COMPONENT_PART_INSTRUCTIONS_MAX) {
+                return ZX_ERR_INVALID_ARGS;
+            }
+
+            static_assert(sizeof(components[i].parts[j].match_program[0]) ==
+                          sizeof(part.match_program[0]));
+            memcpy(part.match_program, components[i].parts[j].match_program,
+                   sizeof(part.match_program[0]) * part.match_program_count);
+        }
+    }
+
+    log_rpc(dev, "create-composite");
+    zx_status_t call_status;
+    static_assert(sizeof(props[0]) == sizeof(uint64_t));
+    zx_status_t status = fuchsia_device_manager_CoordinatorAddCompositeDevice(
+        rpc.get(), name, strlen(name), reinterpret_cast<const uint64_t*>(props), props_count,
+        fidl_components.get(), static_cast<uint32_t>(components_count),
+        coresident_device_index, &call_status);
+    log_rpc_result("create-composite", status, call_status);
+    if (status != ZX_OK) {
+        return status;
+    }
+    return call_status;
+}
+
 zx_handle_t root_resource_handle;
 
 zx_status_t devhost_start_connection(fbl::unique_ptr<DevfsConnection> conn, zx::channel h) {
diff --git a/zircon/system/core/devmgr/devhost/devhost.h b/zircon/system/core/devmgr/devhost/devhost.h
index ff08a9e9eb02b2363ef51cbfd9dc63835a5c9b1c..751c4d1449621c7cbb02cf33169496fec1a3e28e 100644
--- a/zircon/system/core/devmgr/devhost/devhost.h
+++ b/zircon/system/core/devmgr/devhost/devhost.h
@@ -163,6 +163,12 @@ zx_status_t devhost_add_metadata(const fbl::RefPtr<zx_device_t>& dev, uint32_t t
 zx_status_t devhost_publish_metadata(const fbl::RefPtr<zx_device_t>& dev, const char* path,
                                      uint32_t type, const void* data, size_t length) REQ_DM_LOCK;
 
+zx_status_t devhost_device_add_composite(const fbl::RefPtr<zx_device_t>& dev,
+                                         const char* name, const zx_device_prop_t* props,
+                                         size_t props_count, const device_component_t* components,
+                                         size_t components_count,
+                                         uint32_t coresident_device_index) REQ_DM_LOCK;
+
 // shared between devhost.c and rpc-device.c
 struct DevcoordinatorConnection : AsyncLoopOwnedRpcHandler<DevcoordinatorConnection> {
     DevcoordinatorConnection() = default;
diff --git a/zircon/system/fidl/fuchsia-device-manager/coordinator.fidl b/zircon/system/fidl/fuchsia-device-manager/coordinator.fidl
index 3ff3f97479a6a14aaab7d195fc7c8be59913e632..e7823ef59b5bb5cc1d4073d5d4b9d37c2465ba9b 100644
--- a/zircon/system/fidl/fuchsia-device-manager/coordinator.fidl
+++ b/zircon/system/fidl/fuchsia-device-manager/coordinator.fidl
@@ -14,6 +14,9 @@ using zx;
 // we can make this reflect the actual structure.
 using DeviceProperty = uint64;
 
+// Same as above, but for bind zx_bind_inst_t.
+using BindInstruction = uint64;
+
 /// This definition must match ZX_DEVICE_NAME_MAX and is checked by a static assert.
 const uint32 DEVICE_NAME_MAX = 31;
 
@@ -33,6 +36,33 @@ const uint32 COMMAND_MAX = 1024;
 /// Maximum number of properties that can be attached to a device
 const uint32 PROPERTIES_MAX = 256;
 
+/// Maximum number of components that a composite device can have
+const uint32 COMPONENTS_MAX = 8;
+
+/// Maximum number of parts that a composite device component can have
+const uint32 DEVICE_COMPONENT_PARTS_MAX = 16;
+
+/// Maximum instructions in a match program
+const uint32 DEVICE_COMPONENT_PART_INSTRUCTIONS_MAX = 32;
+
+/// A part of a description of a DeviceComponent
+struct DeviceComponentPart {
+    // This is an awful hack around the LLCPP bindings not being ready yet.
+    // Since we're using the C ones for now, we can only embed these structures as
+    // arrays instead of vectors.
+    uint32 match_program_count;
+    array<BindInstruction>:DEVICE_COMPONENT_PART_INSTRUCTIONS_MAX match_program;
+};
+
+/// A piece of a composite device
+struct DeviceComponent {
+    // This is an awful hack around the LLCPP bindings not being ready yet.
+    // Since we're using the C ones for now, we can only embed these structures as
+    // arrays instead of vectors.
+    uint32 parts_count;
+    array<DeviceComponentPart>:DEVICE_COMPONENT_PARTS_MAX parts;
+};
+
 // All functions in these interfaces have the most-significant-byte set to 0x01 to allow
 // multiplexing these interfaces and parts of the fuchsia.io interfaces.
 // TODO(ZX-2922): I believe this shouldn't be necessary anymore.
@@ -148,6 +178,16 @@ protocol Coordinator {
     PublishMetadata(string:DEVICE_PATH_MAX device_path, uint32 key,
                     vector<uint8>:METADATA_MAX? data) -> (zx.status status);
 
+    /// Adds the given composite device.  This causes the devcoordinator to try to match the
+    /// components against the existing device tree, and to monitor all new device additions
+    /// in order to find the components as they are created.
+    // The treatment of |components| is an awful hack around the LLCPP bindings not being ready
+    // yet.  Since we're using the C ones for now, we can only embed these structures as arrays
+    // instead of vectors.
+    AddCompositeDevice(string:DEVICE_NAME_MAX name, vector<DeviceProperty>:PROPERTIES_MAX props,
+                       array<DeviceComponent>:COMPONENTS_MAX components, uint32 components_count,
+                       uint32 coresident_device_index) -> (zx.status status);
+
     /// Special commands for implementing the dmctl device.
     // TODO(teisenbe): We should revisit how these are carried over.