mctp: wire real I2C transport into server loop (Phase 6 Step 3)

Replace NoopSender with I2cSender<IpcI2cClient> and add inbound
slave-notification handling to the MCTP server main loop.

Changes:
- Add imports: IpcI2cClient, I2cTargetClient, I2cSender, MctpI2cReceiver,
  BusIndex, TargetMessage; add OWN_EID and OWN_I2C_ADDR constants
- Register slave notification via register_notification(BUS_0) at startup
- Replace single object_wait(MCTP) loop with a WaitGroup that multiplexes
  two sources:
    user_data=0: IPC from a client (MCTP channel READABLE) -> dispatch
    user_data=1: I2C slave notification (I2C channel USER) ->
                 get_pending_messages -> MctpI2cReceiver::decode ->
                 server.inbound()
- Remove NoopSender struct and its Sender impl
diff --git a/services/mctp/server/src/main.rs b/services/mctp/server/src/main.rs
index 63152cc..c3195d1 100644
--- a/services/mctp/server/src/main.rs
+++ b/services/mctp/server/src/main.rs
@@ -47,6 +47,10 @@
 use openprot_mctp_api::ResponseCode;
 use openprot_mctp_server::dispatch;
 
+use i2c_api::{BusIndex, I2cTargetClient, TargetMessage};
+use i2c_client::IpcI2cClient;
+use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver};
+
 use pw_status::Result;
 use userspace::entry;
 use userspace::syscall::{self, Signals};
@@ -54,6 +58,9 @@
 
 use app_mctp_server::handle;
 
+const OWN_EID: u8 = 8;
+const OWN_I2C_ADDR: u8 = 0x10;
+
 // ---------------------------------------------------------------------------
 // Server loop
 // ---------------------------------------------------------------------------
@@ -61,76 +68,71 @@
 fn mctp_server_loop() -> Result<()> {
     pw_log::info!("MCTP server starting");
 
-    // TODO(Phase 6): Initialize transport binding.
-    // The I2C sender needs an IpcI2cClient bound to the I2C server channel.
-    // For now, we create a stub sender that will be replaced with a real
-    // I2cSender<IpcI2cClient> once the I2C channel handle is wired up.
-    //
-    // let i2c_client = i2c_client::IpcI2cClient::new(handle::I2C);
-    // let sender = I2cSender::new(i2c_client, BusIndex::BUS_0, OWN_I2C_ADDR);
-    // let mut server = Server::new(Eid(OWN_EID), 0, sender);
+    // I2C notification client: receives slave-mode interrupts via Signals::USER.
+    let mut i2c_notify = IpcI2cClient::new(handle::I2C);
+    i2c_notify
+        .register_notification(BusIndex::BUS_0, 0)
+        .map_err(|_| pw_status::Error::Internal)?;
 
-    // For initial bring-up, use a no-op sender so the IPC dispatch loop
-    // can be tested without I2C hardware.
-    let mut server = openprot_mctp_server::Server::<NoopSender, 16>::new(
-        mctp::Eid(8), // default EID
+    // Separate handle for the sender — I2cSender takes ownership.
+    let sender = I2cSender::new(IpcI2cClient::new(handle::I2C), BusIndex::BUS_0, OWN_I2C_ADDR);
+    let receiver = MctpI2cReceiver::new(OWN_I2C_ADDR);
+    let mut server = openprot_mctp_server::Server::<_, 16>::new(
+        mctp::Eid(OWN_EID),
         0,
-        NoopSender,
+        sender,
     );
 
     let mut request_buf = [0u8; MAX_REQUEST_SIZE];
     let mut response_buf = [0u8; MAX_RESPONSE_SIZE];
     let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE];
 
+    // Register both event sources with the WaitGroup.
+    // user_data=0 → IPC from a client  (MCTP channel READABLE)
+    // user_data=1 → I2C slave notification (I2C channel USER)
+    syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?;
+    syscall::wait_group_add(handle::WG, handle::I2C,  Signals::USER,     1usize)?;
+
     loop {
-        // Block until a client sends a request
-        syscall::object_wait(handle::MCTP, Signals::READABLE, Instant::MAX)?;
+        let ev = syscall::object_wait(handle::WG, Signals::READABLE, Instant::MAX)?;
 
-        // Read the request
-        let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?;
+        if ev.user_data == 1 {
+            // Inbound I2C data: drain pending messages, decode I2C framing,
+            // feed raw MCTP packets into the router.
+            let mut msgs = [TargetMessage::default(); 1];
+            if let Ok(n) = i2c_notify.get_pending_messages(BusIndex::BUS_0, &mut msgs) {
+                for msg in &msgs[..n] {
+                    if let Ok((pkt, _src_addr)) = receiver.decode(msg) {
+                        let _ = server.inbound(pkt);
+                    }
+                }
+            }
+        } else {
+            // IPC from a client — channel_read is non-blocking here because
+            // the WaitGroup only fires after READABLE is set.
+            let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?;
 
-        if len < MctpRequestHeader::SIZE {
-            // Truncated request — respond with error
-            let resp = openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument);
-            response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE]
-                .copy_from_slice(&resp.to_bytes());
-            syscall::channel_respond(
-                handle::MCTP,
-                &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE],
-            )?;
-            continue;
+            if len < MctpRequestHeader::SIZE {
+                // Truncated request — respond with error
+                let resp = openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument);
+                response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE]
+                    .copy_from_slice(&resp.to_bytes());
+                syscall::channel_respond(
+                    handle::MCTP,
+                    &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE],
+                )?;
+                continue;
+            }
+
+            // Dispatch and respond
+            let response_len = dispatch::dispatch_mctp_op(
+                &request_buf[..len],
+                &mut response_buf,
+                &mut server,
+                &mut recv_buf,
+            );
+            syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?;
         }
-
-        // Dispatch and respond
-        let response_len = dispatch::dispatch_mctp_op(
-            &request_buf[..len],
-            &mut response_buf,
-            &mut server,
-            &mut recv_buf,
-        );
-        syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?;
-    }
-}
-
-// ---------------------------------------------------------------------------
-// No-op sender for initial bring-up
-// ---------------------------------------------------------------------------
-
-/// Stub sender that does nothing. Used for IPC dispatch testing before
-/// the I2C transport is wired up.
-struct NoopSender;
-
-impl mctp_lib::Sender for NoopSender {
-    fn send_vectored(
-        &mut self,
-        _fragmenter: mctp_lib::fragment::Fragmenter,
-        _payload: &[&[u8]],
-    ) -> mctp::Result<mctp::Tag> {
-        Err(mctp::Error::TxFailure)
-    }
-
-    fn get_mtu(&self) -> usize {
-        64
     }
 }