rust/bt-mcs: Add public methods for updating server state

Add high-level public methods on McsServer to update track metadata,
media playback state, position, identity, and optional control
characteristics.

Issue GATT notifications for state updates according to MCS v1.0.1:
 - Order Track Changed notification after track property updates
 - Resolve FromEnd track position offsets into non-negative values
 - Notify TrackPosition as Unavailable on transition to Inactive
 - Treat redundant Media State transitions as no-ops
 - Reject write notifications when state validation fails

Update track position calculation to factor in playback speed and
seeking speed without integer overflow.

Add FakeServerEventsExt assertion helpers in bt-gatt test_utils and
update the MCS server test suite to explicitly assert GATT handles
and notification payloads.

Bug: 540400364
Test: cargo test -p bt-gatt -p bt-mcs, ./presubmit.sh
Change-Id: Ib0d9e20de601d19bce339e332d5256a3f998eb10
Reviewed-on: https://bluetooth-review.googlesource.com/c/bluetooth/+/3900
diff --git a/rust/bt-gatt/src/test_utils.rs b/rust/bt-gatt/src/test_utils.rs
index def7f62..ed05af5 100644
--- a/rust/bt-gatt/src/test_utils.rs
+++ b/rust/bt-gatt/src/test_utils.rs
@@ -6,10 +6,11 @@
 use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
 use futures::future::{ready, Ready};
 use futures::stream::{FusedStream, Stream};
+use futures::StreamExt;
 use parking_lot::Mutex;
 use std::collections::{HashMap, HashSet, VecDeque};
 use std::sync::Arc;
-use std::task::{Poll, Waker};
+use std::task::{Context, Poll, Waker};
 
 use bt_common::{PeerId, Uuid};
 
@@ -802,3 +803,94 @@
             .unwrap();
     }
 }
+
+/// Extension trait providing assertion helpers on a `FakeServerEvent` event
+/// stream.
+pub trait FakeServerEventsExt {
+    /// Polls the receiver and expects a single event to be ready, panicking if
+    /// none is ready.
+    fn expect_event(&mut self) -> FakeServerEvent;
+
+    /// Asserts that no event is currently pending on the receiver.
+    fn expect_no_event(&mut self);
+
+    /// Expects a `FakeServerEvent::Notified` event for `expected_handle`.
+    /// Returns the notification payload and the list of notified peers.
+    fn expect_notification(&mut self, expected_handle: Handle) -> (Vec<u8>, Vec<PeerId>);
+
+    /// Expects a `FakeServerEvent::WriteResponded` event for `expected_handle`.
+    /// Returns the result of the write response.
+    fn expect_write_response(&mut self, expected_handle: Handle) -> Result<()>;
+
+    /// Expects a `FakeServerEvent::ReadResponded` event for `expected_handle`.
+    /// Returns the result of the read response.
+    fn expect_read_response(&mut self, expected_handle: Handle) -> Result<Vec<u8>>;
+}
+
+impl FakeServerEventsExt for UnboundedReceiver<FakeServerEvent> {
+    #[track_caller]
+    fn expect_event(&mut self) -> FakeServerEvent {
+        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
+        match self.poll_next_unpin(&mut cx) {
+            Poll::Ready(Some(event)) => event,
+            x => panic!("Expected fake server event, got {x:?}"),
+        }
+    }
+
+    #[track_caller]
+    fn expect_no_event(&mut self) {
+        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
+        match self.poll_next_unpin(&mut cx) {
+            Poll::Pending => {}
+            Poll::Ready(other) => panic!("Expected no fake server event, got {other:?}"),
+        }
+    }
+
+    #[track_caller]
+    fn expect_notification(&mut self, expected_handle: Handle) -> (Vec<u8>, Vec<PeerId>) {
+        match self.expect_event() {
+            FakeServerEvent::Notified { handle, value, peers, .. } => {
+                assert_eq!(
+                    handle, expected_handle,
+                    "Expected notification for handle {expected_handle:?}, but got notification for handle {handle:?}"
+                );
+                (value, peers)
+            }
+            other => panic!(
+                "Expected FakeServerEvent::Notified for handle {expected_handle:?}, got {other:?}"
+            ),
+        }
+    }
+
+    #[track_caller]
+    fn expect_write_response(&mut self, expected_handle: Handle) -> Result<()> {
+        match self.expect_event() {
+            FakeServerEvent::WriteResponded { handle, value, .. } => {
+                assert_eq!(
+                    handle, expected_handle,
+                    "Expected write response for handle {expected_handle:?}, but got response for handle {handle:?}"
+                );
+                value
+            }
+            other => panic!(
+                "Expected FakeServerEvent::WriteResponded for handle {expected_handle:?}, got {other:?}"
+            ),
+        }
+    }
+
+    #[track_caller]
+    fn expect_read_response(&mut self, expected_handle: Handle) -> Result<Vec<u8>> {
+        match self.expect_event() {
+            FakeServerEvent::ReadResponded { handle, value, .. } => {
+                assert_eq!(
+                    handle, expected_handle,
+                    "Expected read response for handle {expected_handle:?}, but got response for handle {handle:?}"
+                );
+                value
+            }
+            other => panic!(
+                "Expected FakeServerEvent::ReadResponded for handle {expected_handle:?}, got {other:?}"
+            ),
+        }
+    }
+}
diff --git a/rust/bt-gatt/src/tests.rs b/rust/bt-gatt/src/tests.rs
index 9f0bbd7..2a263bd 100644
--- a/rust/bt-gatt/src/tests.rs
+++ b/rust/bt-gatt/src/tests.rs
@@ -433,3 +433,64 @@
     assert_eq!(Handle(1), handle);
     assert_eq!(&[0, 1, 2, 3, 4, 5], value.expect("should be ok").as_slice());
 }
+
+#[test]
+fn fake_server_events_ext_helpers() {
+    use server::{
+        LocalService, NotificationType, ReadResponder, Server, ServiceId, WriteResponder,
+    };
+    let mut noop_cx = futures::task::Context::from_waker(futures::task::noop_waker_ref());
+
+    let (server, mut events) = FakeServer::new();
+    events.expect_no_event();
+
+    let mut local_service_fut = server.prepare(example_service_definition());
+    let Poll::Ready(Ok(local_service)) = local_service_fut.poll_unpin(&mut noop_cx) else {
+        panic!("prepare failed");
+    };
+    let mut service_event_stream = local_service.publish();
+
+    // Published event
+    let FakeServerEvent::Published { id, .. } = events.expect_event() else {
+        panic!("expected published");
+    };
+    assert_eq!(id, ServiceId::new(1));
+    events.expect_no_event();
+
+    // Read response
+    server.incoming_read(PeerId(1), ServiceId::new(1), Handle(1), 0);
+    let Poll::Ready(Some(Ok(ServiceEvent::Read { responder, .. }))) =
+        service_event_stream.poll_next_unpin(&mut noop_cx)
+    else {
+        panic!("expected read");
+    };
+    responder.respond(&[10, 20, 30]);
+    let value = events.expect_read_response(Handle(1)).expect("read response ok");
+    assert_eq!(value, vec![10, 20, 30]);
+    events.expect_no_event();
+
+    // Write response
+    server.incoming_write(PeerId(1), ServiceId::new(1), Handle(2), 0, vec![1, 2]);
+    let Poll::Ready(Some(Ok(ServiceEvent::Write { responder, .. }))) =
+        service_event_stream.poll_next_unpin(&mut noop_cx)
+    else {
+        panic!("expected write");
+    };
+    responder.acknowledge();
+    events.expect_write_response(Handle(2)).expect("write response ok");
+    events.expect_no_event();
+
+    // Notification
+    server.incoming_client_configuration(
+        PeerId(1),
+        ServiceId::new(1),
+        Handle(3),
+        NotificationType::Notify,
+    );
+    let _ = service_event_stream.poll_next_unpin(&mut noop_cx);
+    local_service.notify(&Handle(3), &[42], &[]);
+    let (data, peers) = events.expect_notification(Handle(3));
+    assert_eq!(data, vec![42]);
+    assert_eq!(peers, vec![PeerId(1)]);
+    events.expect_no_event();
+}
diff --git a/rust/bt-mcs/src/error.rs b/rust/bt-mcs/src/error.rs
index d57a6f7..6565d15 100644
--- a/rust/bt-mcs/src/error.rs
+++ b/rust/bt-mcs/src/error.rs
@@ -4,11 +4,19 @@
 
 use thiserror::Error;
 
+use crate::types::PlayingOrder;
+
 #[derive(Debug, Error)]
 pub enum Error {
     #[error("Service is already published")]
     AlreadyPublished,
 
+    #[error("Feature not supported or configured on this server: {0}")]
+    FeatureNotSupported(&'static str),
+
+    #[error("Playing order {0:?} is not supported by this server")]
+    UnsupportedPlayingOrder(PlayingOrder),
+
     #[error("GATT operation error: {0}")]
     Gatt(#[from] bt_gatt::types::Error),
 
diff --git a/rust/bt-mcs/src/server.rs b/rust/bt-mcs/src/server.rs
index 658631c..98e6df0 100644
--- a/rust/bt-mcs/src/server.rs
+++ b/rust/bt-mcs/src/server.rs
@@ -285,18 +285,143 @@
         Ok(())
     }
 
-    #[cfg(test)]
-    pub(crate) fn set_media_state(&mut self, state: MediaState) {
-        self.state.set_media_state(state);
+    fn notify_track_changed(&mut self) {
+        self.local_service.notify(&TRACK_TITLE_HANDLE, self.state.track_title.as_bytes(), &[]);
+        self.local_service.notify(
+            &TRACK_DURATION_HANDLE,
+            &self.state.track_duration.to_notify_value(),
+            &[],
+        );
+        self.local_service.notify(
+            &TRACK_POSITION_HANDLE,
+            &self.state.track_position.to_notify_value(),
+            &[],
+        );
+        // Characteristic updates must precede Track Changed notification (MCS
+        // v1.0.1 Section 3.4.1).
+        self.local_service.notify(&TRACK_CHANGED_HANDLE, &[], &[]);
     }
 
-    #[cfg(test)]
-    pub(crate) fn set_supported_opcodes(&mut self, opcodes: SupportedOpcodes) {
-        self.state.set_supported_opcodes(opcodes);
+    fn notify_current_track_position(&mut self) {
+        let raw_pos = self.state.current_track_position().to_notify_value();
+        self.local_service.notify(&TRACK_POSITION_HANDLE, &raw_pos, &[]);
+    }
+
+    /// Sets the current track metadata and dispatches track update
+    /// notifications.
+    pub fn set_track(
+        &mut self,
+        title: impl Into<String>,
+        duration: TrackDuration,
+        position: TrackPosition,
+    ) {
+        self.state.set_track(title.into(), duration, position);
+        self.notify_track_changed();
+    }
+
+    /// Clears the current track metadata and dispatches track update
+    /// notifications.
+    pub fn clear_track(&mut self) {
+        self.state.clear_track();
+        self.notify_track_changed();
+    }
+
+    /// Sets the current playback state and dispatches state update
+    /// notifications.
+    pub fn set_media_state(&mut self, state: MediaState) {
+        if self.state.media_state == state {
+            return;
+        }
+        let previous_state = self.state.media_state;
+        self.state.set_media_state(state);
+        self.local_service.notify(&MEDIA_STATE_HANDLE, &[state.into()], &[]);
+        // Notify track position when paused from active playback or seeking,
+        // when entering seeking, or when transitioning to inactive (MCS
+        // v1.0.1 Section 3.7.1).
+        if (state == MediaState::Paused
+            && (previous_state == MediaState::Playing || previous_state == MediaState::Seeking))
+            || state == MediaState::Seeking
+            || (state == MediaState::Inactive && previous_state != MediaState::Inactive)
+        {
+            self.notify_current_track_position();
+        }
+    }
+
+    /// Sets the playback position within the current track and dispatches
+    /// position notifications.
+    pub fn set_track_position(&mut self, position: TrackPosition) {
+        self.state.set_track_position(position);
+        self.local_service.notify(
+            &TRACK_POSITION_HANDLE,
+            &self.state.track_position.to_notify_value(),
+            &[],
+        );
+    }
+
+    /// Sets the media player name and dispatches name update notifications.
+    pub fn set_player_name(&mut self, name: impl Into<String>) {
+        let name_str = name.into();
+        self.state.set_player_name(name_str.clone());
+        self.local_service.notify(&MEDIA_PLAYER_NAME_HANDLE, name_str.as_bytes(), &[]);
+    }
+
+    /// Sets the media player icon URL.
+    pub fn set_icon_url(&mut self, url: impl Into<String>) -> Result<(), Error> {
+        self.state.set_icon_url(url.into())
+    }
+
+    /// Sets the playback speed and dispatches speed update notifications.
+    pub fn set_playback_speed(&mut self, speed: PlaybackSpeed) -> Result<(), Error> {
+        self.state.set_playback_speed(speed)?;
+        self.local_service.notify(&PLAYBACK_SPEED_HANDLE, &[speed.into()], &[]);
+        // Notify track position when playback speed changes during active
+        // playback (MCS v1.0.1 Section 3.7.1).
+        if self.state.media_state == MediaState::Playing {
+            self.notify_current_track_position();
+        }
+        Ok(())
+    }
+
+    /// Sets the seeking speed and dispatches seeking speed update
+    /// notifications.
+    pub fn set_seeking_speed(&mut self, speed: SeekingSpeed) -> Result<(), Error> {
+        self.state.set_seeking_speed(speed)?;
+        self.local_service.notify(&SEEKING_SPEED_HANDLE, &[speed.into()], &[]);
+        // Notify track position if seeking speed is updated while seeking (MCS
+        // v1.0.1 Section 3.7.1, 3.9).
+        if self.state.media_state == MediaState::Seeking {
+            self.notify_current_track_position();
+        }
+        Ok(())
+    }
+
+    /// Sets the playing order and dispatches playing order update
+    /// notifications.
+    pub fn set_playing_order(&mut self, order: PlayingOrder) -> Result<(), Error> {
+        self.state.set_playing_order(order)?;
+        self.local_service.notify(&PLAYING_ORDER_HANDLE, &[order.into()], &[]);
+        Ok(())
+    }
+
+    /// Sets the supported media control opcodes and dispatches opcode update
+    /// notifications.
+    pub fn set_supported_opcodes(&mut self, opcodes: SupportedOpcodes) -> Result<(), Error> {
+        self.state.set_supported_opcodes(opcodes)?;
+        self.local_service.notify(
+            &MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE,
+            &opcodes.bits().to_le_bytes(),
+            &[],
+        );
+        Ok(())
     }
 }
 
 impl<'a, T: bt_gatt::ServerTypes> McsServerProj<'a, T> {
+    fn notify_current_track_position(&mut self) {
+        let raw_pos = self.state.current_track_position().to_notify_value();
+        self.local_service.notify(&TRACK_POSITION_HANDLE, &raw_pos, &[]);
+    }
+
     fn handle_read<R: ReadResponder>(&self, handle: Handle, offset: usize, responder: R) {
         match self.state.handle_read(handle, offset) {
             Ok(bytes) => responder.respond(&bytes),
@@ -470,17 +595,25 @@
                 self.state.set_track_position(position);
                 self.local_service.notify(
                     &TRACK_POSITION_HANDLE,
-                    &position.raw_10ms().to_le_bytes(),
+                    &self.state.track_position.to_notify_value(),
                     &[],
                 );
             }
             PendingWriteResponse::PlaybackSpeed(speed) => {
-                self.state.set_playback_speed(speed);
-                self.local_service.notify(&PLAYBACK_SPEED_HANDLE, &[speed.into()], &[]);
+                if self.state.set_playback_speed(speed).is_ok() {
+                    self.local_service.notify(&PLAYBACK_SPEED_HANDLE, &[speed.into()], &[]);
+                    // Notify track position when playback speed changes during
+                    // active playback (MCS v1.0.1 Section
+                    // 3.7.1).
+                    if self.state.media_state == MediaState::Playing {
+                        self.notify_current_track_position();
+                    }
+                }
             }
             PendingWriteResponse::PlayingOrder(order) => {
-                self.state.set_playing_order(order);
-                self.local_service.notify(&PLAYING_ORDER_HANDLE, &[order.into()], &[]);
+                if self.state.set_playing_order(order).is_ok() {
+                    self.local_service.notify(&PLAYING_ORDER_HANDLE, &[order.into()], &[]);
+                }
             }
         }
     }
@@ -544,7 +677,7 @@
     use super::*;
 
     use assert_matches::assert_matches;
-    use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeTypes};
+    use bt_gatt::test_utils::{FakeServer, FakeServerEvent, FakeServerEventsExt, FakeTypes};
     use bt_gatt::Characteristic;
     use futures::{FutureExt, StreamExt};
 
@@ -808,25 +941,6 @@
         assert_matches!(poll_result, Poll::Ready(None));
     }
 
-    #[track_caller]
-    fn expect_service_event(
-        events: &mut futures::channel::mpsc::UnboundedReceiver<FakeServerEvent>,
-    ) -> FakeServerEvent {
-        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
-        match events.poll_next_unpin(&mut cx) {
-            Poll::Ready(Some(event)) => event,
-            x => panic!("Expected fake server event, got {x:?}"),
-        }
-    }
-
-    #[track_caller]
-    fn expect_no_service_event(
-        events: &mut futures::channel::mpsc::UnboundedReceiver<FakeServerEvent>,
-    ) {
-        let mut cx = Context::from_waker(futures::task::noop_waker_ref());
-        assert_matches!(events.poll_next_unpin(&mut cx), Poll::Pending);
-    }
-
     fn setup_test_server(
         builder: McsServerBuilder,
     ) -> (
@@ -839,10 +953,7 @@
         let mut server: McsServer<FakeTypes> = builder.build().expect("server builds successfully");
         server.publish(fake_gatt_server.clone()).expect("publish succeeds");
         let _ = server.next().poll_unpin(&mut noop_cx);
-        assert!(matches!(
-            expect_service_event(&mut event_receiver),
-            FakeServerEvent::Published { .. }
-        ));
+        assert!(matches!(event_receiver.expect_event(), FakeServerEvent::Published { .. }));
         (server, fake_gatt_server, event_receiver)
     }
 
@@ -866,7 +977,7 @@
             bt_gatt::server::NotificationType::Notify,
         );
         let _ = server.next().poll_unpin(&mut noop_cx);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
         (server, fake_gatt_server, event_receiver, noop_cx)
     }
 
@@ -890,11 +1001,7 @@
             let poll_result = server.next().poll_unpin(&mut noop_cx);
             // Must NOT emit any McsServerEvent.
             assert_matches!(poll_result, Poll::Pending);
-            let bt_gatt::test_utils::FakeServerEvent::WriteResponded { value, .. } =
-                expect_service_event(&mut event_receiver)
-            else {
-                panic!("expected WriteResponded for handle {:?}", handle);
-            };
+            let value = event_receiver.expect_write_response(handle);
             assert_matches!(
                 value.unwrap_err(),
                 bt_gatt::types::Error::Gatt(GattError::InvalidHandle),
@@ -921,7 +1028,7 @@
             bt_gatt::server::NotificationType::Notify,
         );
         let _ = server.next().poll_unpin(&mut noop_cx);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         fake_gatt_server.incoming_write(
             peer,
@@ -943,12 +1050,7 @@
         assert_eq!(peer_id, peer);
         assert_eq!(position, TrackPosition::from_raw_10ms(4200));
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(TRACK_POSITION_HANDLE).unwrap();
 
         // Upper layer confirms the set track position.
         responder.send(position);
@@ -957,24 +1059,15 @@
         // notification.
         let _ = server.next().poll_unpin(&mut noop_cx);
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, TRACK_POSITION_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, 4200i32.to_le_bytes().to_vec());
 
         // Subsequent read reflects the updated track position
         fake_gatt_server.incoming_read(peer, service_id, TRACK_POSITION_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), 4200i32.to_le_bytes());
+        let value = event_receiver.expect_read_response(TRACK_POSITION_HANDLE).unwrap();
+        assert_eq!(value, 4200i32.to_le_bytes());
     }
 
     #[test]
@@ -996,11 +1089,7 @@
         );
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
+        let value = event_receiver.expect_write_response(TRACK_POSITION_HANDLE);
         assert_matches!(value.unwrap_err(), bt_gatt::types::Error::Gatt(GattError::InvalidOffset));
 
         // Invalid value length (3 bytes instead of 4)
@@ -1013,11 +1102,7 @@
         );
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
+        let value = event_receiver.expect_write_response(TRACK_POSITION_HANDLE);
         assert_matches!(
             value.unwrap_err(),
             bt_gatt::types::Error::Gatt(GattError::InvalidAttributeValueLength)
@@ -1040,7 +1125,7 @@
             bt_gatt::server::NotificationType::Notify,
         );
         let _ = server.next().poll_unpin(&mut noop_cx);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         fake_gatt_server.incoming_write(
             peer,
@@ -1059,35 +1144,21 @@
         assert_eq!(peer_id, peer);
         assert_eq!(speed, PlaybackSpeed::HALF);
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(PLAYBACK_SPEED_HANDLE).unwrap();
 
         // Upper layer confirms playback speed
         responder.send(speed);
         let _ = server.next().poll_unpin(&mut noop_cx);
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, PLAYBACK_SPEED_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(PLAYBACK_SPEED_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![-64i8 as u8]);
 
         // Subsequent read reflects the updated playback speed
         fake_gatt_server.incoming_read(peer, service_id, PLAYBACK_SPEED_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), vec![-64i8 as u8]);
+        let value = event_receiver.expect_read_response(PLAYBACK_SPEED_HANDLE).unwrap();
+        assert_eq!(value, vec![-64i8 as u8]);
     }
 
     #[test]
@@ -1103,11 +1174,7 @@
         fake_gatt_server.incoming_write(peer, service_id, PLAYBACK_SPEED_HANDLE, 1, vec![0x00]);
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
+        let value = event_receiver.expect_write_response(PLAYBACK_SPEED_HANDLE);
         assert_matches!(value.unwrap_err(), bt_gatt::types::Error::Gatt(GattError::InvalidOffset));
 
         // Invalid value length (2 bytes instead of 1)
@@ -1120,11 +1187,7 @@
         );
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
+        let value = event_receiver.expect_write_response(PLAYBACK_SPEED_HANDLE);
         assert_matches!(
             value.unwrap_err(),
             bt_gatt::types::Error::Gatt(GattError::InvalidAttributeValueLength)
@@ -1149,7 +1212,7 @@
             bt_gatt::server::NotificationType::Notify,
         );
         let _ = server.next().poll_unpin(&mut noop_cx);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         fake_gatt_server.incoming_write(
             peer,
@@ -1168,35 +1231,21 @@
         assert_eq!(peer_id, peer);
         assert_eq!(order, PlayingOrder::ShuffleOnce);
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(PLAYING_ORDER_HANDLE).unwrap();
 
         // Upper layer confirms playing order
         responder.send(order);
         let _ = server.next().poll_unpin(&mut noop_cx);
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, PLAYING_ORDER_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(PLAYING_ORDER_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![PlayingOrder::ShuffleOnce.into()]);
 
         // Subsequent read reflects the updated playing order
         fake_gatt_server.incoming_read(peer, service_id, PLAYING_ORDER_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), vec![PlayingOrder::ShuffleOnce.into()]);
+        let value = event_receiver.expect_read_response(PLAYING_ORDER_HANDLE).unwrap();
+        assert_eq!(value, vec![PlayingOrder::ShuffleOnce.into()]);
     }
 
     #[test]
@@ -1223,22 +1272,13 @@
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(PLAYING_ORDER_HANDLE).unwrap();
 
         // Value remains unchanged (InOrderOnce)
         fake_gatt_server.incoming_read(peer, service_id, PLAYING_ORDER_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), vec![PlayingOrder::InOrderOnce.into()]);
+        let value = event_receiver.expect_read_response(PLAYING_ORDER_HANDLE).unwrap();
+        assert_eq!(value, vec![PlayingOrder::InOrderOnce.into()]);
     }
 
     #[test]
@@ -1267,11 +1307,7 @@
             fake_gatt_server.incoming_write(peer, service_id, handle, 0, value);
             let poll_result = server.next().poll_unpin(&mut noop_cx);
             assert_matches!(poll_result, Poll::Pending);
-            let FakeServerEvent::WriteResponded { value, .. } =
-                expect_service_event(&mut event_receiver)
-            else {
-                panic!("expected WriteResponded for handle {:?}", handle);
-            };
+            let value = event_receiver.expect_write_response(handle);
             assert_matches!(
                 value.unwrap_err(),
                 bt_gatt::types::Error::Gatt(GattError::WriteNotPermitted),
@@ -1301,21 +1337,11 @@
         assert_matches!(poll_result, Poll::Pending);
 
         // ATT write is acknowledged
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
         // Immediate Media Control Point Notification sent with
         // OPCODE_NOT_SUPPORTED (0x02)
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![0xEE, ControlPointResultCode::OpcodeNotSupported.into()]);
     }
@@ -1340,21 +1366,11 @@
         assert_matches!(poll_result, Poll::Pending);
 
         // ATT write is acknowledged
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
         // Immediate Media Control Point Notification sent with
         // MEDIA_PLAYER_INACTIVE (0x03)
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![0x01, ControlPointResultCode::MediaPlayerInactive.into()]);
     }
@@ -1366,6 +1382,8 @@
         let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) =
             setup_test_server_with_control_point(peer);
         server.set_media_state(MediaState::Playing);
+        let (val, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(val, vec![MediaState::Playing.into()]);
 
         // Write Pause opcode (0x02)
         fake_gatt_server.incoming_write(
@@ -1385,12 +1403,7 @@
         assert_eq!(opcode, MediaControlOpcode::Pause);
 
         // ATT write is acknowledged
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
         // Upper layer executes command and responds with Success
         responder.send(ControlPointResultCode::Success);
@@ -1398,12 +1411,7 @@
         // Polling the server dispatches the notification
         let _ = server.next().poll_unpin(&mut noop_cx);
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![0x02, ControlPointResultCode::Success.into()]);
     }
@@ -1415,6 +1423,8 @@
         let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) =
             setup_test_server_with_control_point(peer);
         server.set_media_state(MediaState::Playing);
+        let (val, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(val, vec![MediaState::Playing.into()]);
 
         // MoveRelative opcode with 4-byte offset parameter
         let opcode = MediaControlOpcode::MoveRelative(1500);
@@ -1433,22 +1443,12 @@
         };
         assert_eq!(rx_opcode, opcode);
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
         responder.send(ControlPointResultCode::Success);
         let _ = server.next().poll_unpin(&mut noop_cx);
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![opcode.raw_opcode(), ControlPointResultCode::Success.into()]);
     }
@@ -1460,6 +1460,8 @@
         let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) =
             setup_test_server_with_control_point(peer);
         server.set_media_state(MediaState::Playing);
+        let (val, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(val, vec![MediaState::Playing.into()]);
 
         // MoveRelative requires 5 octets; provide only 2 octets
         let raw_op = MediaControlOpcode::MoveRelative(0).raw_opcode();
@@ -1474,19 +1476,9 @@
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![raw_op, ControlPointResultCode::OpcodeNotSupported.into()]);
     }
@@ -1508,22 +1500,14 @@
         );
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
+        let value = event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE);
         assert_matches!(value.unwrap_err(), bt_gatt::types::Error::Gatt(GattError::InvalidOffset));
 
         // Empty value returns InvalidAttributeValueLength
         fake_gatt_server.incoming_write(peer, service_id, MEDIA_CONTROL_POINT_HANDLE, 0, vec![]);
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
+        let value = event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE);
         assert_matches!(
             value.unwrap_err(),
             bt_gatt::types::Error::Gatt(GattError::InvalidAttributeValueLength)
@@ -1537,7 +1521,15 @@
         let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) =
             setup_test_server_with_control_point(peer);
         server.set_media_state(MediaState::Playing);
-        server.set_supported_opcodes(SupportedOpcodes::PLAY);
+        let (val, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(val, vec![MediaState::Playing.into()]);
+        server
+            .set_supported_opcodes(SupportedOpcodes::PLAY)
+            .expect("set_supported_opcodes succeeds");
+        let (val, peers) =
+            event_receiver.expect_notification(MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE);
+        assert_eq!(val, SupportedOpcodes::PLAY.bits().to_le_bytes().to_vec());
+        assert_eq!(peers, vec![peer]);
 
         // Pause is valid in spec, but not in supported_opcodes for this server
         // instance
@@ -1552,19 +1544,9 @@
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(
             value,
@@ -1582,6 +1564,8 @@
         let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) =
             setup_test_server_with_control_point(peer);
         server.set_media_state(MediaState::Playing);
+        let (val, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(val, vec![MediaState::Playing.into()]);
 
         // Write Stop opcode (0x05)
         fake_gatt_server.incoming_write(
@@ -1601,12 +1585,7 @@
         assert_eq!(opcode, MediaControlOpcode::Stop);
 
         // ATT write is acknowledged
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
         // Responder is dropped without calling send
         drop(responder);
@@ -1614,12 +1593,7 @@
         // Polling the server dispatches the fallback notification
         let _ = server.next().poll_unpin(&mut noop_cx);
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![0x05, ControlPointResultCode::CommandCannotBeCompleted.into()]);
     }
@@ -1632,6 +1606,8 @@
         let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) =
             setup_test_server_with_control_point(peer1);
         server.set_media_state(MediaState::Playing);
+        let (val, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(val, vec![MediaState::Playing.into()]);
 
         // Also configure notifications for peer2
         fake_gatt_server.incoming_client_configuration(
@@ -1641,7 +1617,7 @@
             bt_gatt::server::NotificationType::Notify,
         );
         let _ = server.next().poll_unpin(&mut noop_cx);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         // Issue 1st command (Play from peer1)
         fake_gatt_server.incoming_write(
@@ -1682,19 +1658,8 @@
         assert_eq!(op2, MediaControlOpcode::NextTrack);
 
         // Drain write acknowledgements
-        let FakeServerEvent::WriteResponded { value: val1, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected 1st WriteResponded");
-        };
-        assert!(val1.is_ok());
-
-        let FakeServerEvent::WriteResponded { value: val2, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected 2nd WriteResponded");
-        };
-        assert!(val2.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
         // Respond to both commands in a row before polling the stream
         resp1.send(ControlPointResultCode::Success);
@@ -1706,17 +1671,10 @@
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
 
-        let n1 = expect_service_event(&mut event_receiver);
-        let n2 = expect_service_event(&mut event_receiver);
+        let (val1, peers1) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
+        let (val2, peers2) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
 
-        let mut received = Vec::new();
-        for event in [n1, n2] {
-            let FakeServerEvent::Notified { handle, value, peers, .. } = event else {
-                panic!("expected Notified event, got {event:?}");
-            };
-            assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
-            received.push((peers, value));
-        }
+        let received = vec![(peers1, val1), (peers2, val2)];
 
         assert!(received.contains(&(
             vec![peer1],
@@ -1731,7 +1689,7 @@
         )));
 
         // No further events pending
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
     }
 
     #[test]
@@ -1741,6 +1699,8 @@
         let (mut server, fake_gatt_server, mut event_receiver, mut noop_cx) =
             setup_test_server_with_control_point(peer);
         server.set_media_state(MediaState::Playing);
+        let (val, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(val, vec![MediaState::Playing.into()]);
 
         // Write Pause opcode (0x02)
         fake_gatt_server.incoming_write(
@@ -1760,12 +1720,7 @@
         assert_eq!(opcode, MediaControlOpcode::Pause);
 
         // ATT write is acknowledged
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        event_receiver.expect_write_response(MEDIA_CONTROL_POINT_HANDLE).unwrap();
 
         // Replace local_service events stream with a custom channel and close
         // it.
@@ -1792,12 +1747,7 @@
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Ready(None));
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, MEDIA_CONTROL_POINT_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(MEDIA_CONTROL_POINT_HANDLE);
         assert_eq!(peers, vec![peer]);
         assert_eq!(value, vec![0x02, ControlPointResultCode::Success.into()]);
     }
@@ -1807,6 +1757,7 @@
         let (mut server, fake_gatt_server, mut event_receiver) =
             setup_test_server(McsServerBuilder::generic(0x42, "Test Player"));
         server.state.media_state = MediaState::Paused;
+        server.state.track_duration = TrackDuration::from_raw_10ms(3000); // 30s
         let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
 
         let peer = PeerId(1);
@@ -1819,7 +1770,7 @@
             bt_gatt::server::NotificationType::Notify,
         );
         let _ = server.next().poll_unpin(&mut noop_cx);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         // Negative value represents offset from end of track per MCS v1.0.1
         // Section 3.7.1
@@ -1843,35 +1794,26 @@
         assert_eq!(peer_id, peer);
         assert_eq!(position, TrackPosition::from_raw_10ms(-500));
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        assert_matches!(event_receiver.expect_write_response(TRACK_POSITION_HANDLE), Ok(()));
 
         // Upper layer confirms the updated track position
         responder.send(position);
         let _ = server.next().poll_unpin(&mut noop_cx);
 
-        let FakeServerEvent::Notified { handle, value, peers, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected Notified");
-        };
-        assert_eq!(handle, TRACK_POSITION_HANDLE);
+        let (value, peers) = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
         assert_eq!(peers, vec![peer]);
-        assert_eq!(value, (-500i32).to_le_bytes().to_vec());
+        // MCS Section 3.7.1: The characteristic value shall be set to the
+        // offset from the start of the track to the new playing
+        // position (3000 - 500 = 2500).
+        assert_eq!(value, (2500i32).to_le_bytes().to_vec());
 
         // Subsequent read reflects the updated track position
         fake_gatt_server.incoming_read(peer, service_id, TRACK_POSITION_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), (-500i32).to_le_bytes());
+        let value = event_receiver
+            .expect_read_response(TRACK_POSITION_HANDLE)
+            .expect("read response succeeds");
+        assert_eq!(value, (2500i32).to_le_bytes());
     }
 
     #[test]
@@ -1894,29 +1836,22 @@
             panic!("expected SetPlaybackSpeed event, got {poll_result:?}");
         };
         // ATT write is acknowledged
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        assert_matches!(event_receiver.expect_write_response(PLAYBACK_SPEED_HANDLE), Ok(()));
 
         // Drop responder without calling send
         drop(responder);
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
         // No notification should be emitted
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         // Playback speed remains unchanged
         fake_gatt_server.incoming_read(peer, service_id, PLAYBACK_SPEED_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), vec![PlaybackSpeed::NORMAL.into()]);
+        let value = event_receiver
+            .expect_read_response(PLAYBACK_SPEED_HANDLE)
+            .expect("read response succeeds");
+        assert_eq!(value, vec![PlaybackSpeed::NORMAL.into()]);
 
         // 2. Track position write
         fake_gatt_server.incoming_write(
@@ -1931,27 +1866,20 @@
         else {
             panic!("expected SetTrackPosition event, got {poll_result:?}");
         };
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        assert_matches!(event_receiver.expect_write_response(TRACK_POSITION_HANDLE), Ok(()));
 
         drop(responder);
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         // Track position remains unavailable
         fake_gatt_server.incoming_read(peer, service_id, TRACK_POSITION_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), TrackPosition::Unavailable.raw_10ms().to_le_bytes());
+        let value = event_receiver
+            .expect_read_response(TRACK_POSITION_HANDLE)
+            .expect("read response succeeds");
+        assert_eq!(value, TrackPosition::Unavailable.raw_10ms().to_le_bytes());
 
         // 3. Playing order write
         fake_gatt_server.incoming_write(
@@ -1966,27 +1894,20 @@
         else {
             panic!("expected SetPlayingOrder event, got {poll_result:?}");
         };
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        assert_matches!(event_receiver.expect_write_response(PLAYING_ORDER_HANDLE), Ok(()));
 
         drop(responder);
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
-        expect_no_service_event(&mut event_receiver);
+        event_receiver.expect_no_event();
 
         // Playing order remains default InOrderOnce
         fake_gatt_server.incoming_read(peer, service_id, PLAYING_ORDER_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected ReadResponded");
-        };
-        assert_eq!(value.unwrap(), vec![PlayingOrder::InOrderOnce.into()]);
+        let value = event_receiver
+            .expect_read_response(PLAYING_ORDER_HANDLE)
+            .expect("read response succeeds");
+        assert_eq!(value, vec![PlayingOrder::InOrderOnce.into()]);
     }
 
     #[test]
@@ -2006,21 +1927,393 @@
         let poll_result = server.next().poll_unpin(&mut noop_cx);
         assert_matches!(poll_result, Poll::Pending);
 
-        let FakeServerEvent::WriteResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
-        else {
-            panic!("expected WriteResponded");
-        };
-        assert!(value.is_ok());
+        assert_matches!(event_receiver.expect_write_response(PLAYING_ORDER_HANDLE), Ok(()));
 
         // Value remains unchanged (default InOrderOnce)
         fake_gatt_server.incoming_read(peer, service_id, PLAYING_ORDER_HANDLE, 0);
         let _ = server.next().poll_unpin(&mut noop_cx);
-        let FakeServerEvent::ReadResponded { value, .. } =
-            expect_service_event(&mut event_receiver)
+        let value = event_receiver
+            .expect_read_response(PLAYING_ORDER_HANDLE)
+            .expect("read response succeeds");
+        assert_eq!(value, vec![PlayingOrder::InOrderOnce.into()]);
+    }
+
+    #[test]
+    fn pending_write_response_rejected_by_state_does_not_notify() {
+        let (mut server, fake_gatt_server, mut event_receiver) = setup_test_server(
+            McsServerBuilder::generic(0x42, "Test Player")
+                .with_playing_orders(SupportedPlayingOrders::SINGLE_ONCE),
+        );
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+
+        let peer = PeerId(1);
+        let service_id = ServiceId::new(0x42);
+
+        fake_gatt_server.incoming_client_configuration(
+            peer,
+            service_id,
+            PLAYING_ORDER_HANDLE,
+            bt_gatt::server::NotificationType::Notify,
+        );
+        let _ = server.next().poll_unpin(&mut noop_cx);
+        event_receiver.expect_no_event();
+
+        fake_gatt_server.incoming_write(
+            peer,
+            service_id,
+            PLAYING_ORDER_HANDLE,
+            0,
+            vec![PlayingOrder::SingleOnce.into()],
+        );
+        let poll_result = server.next().poll_unpin(&mut noop_cx);
+        let Poll::Ready(Some(Ok(McsServerEvent::SetPlayingOrder { responder, .. }))) = poll_result
         else {
-            panic!("expected ReadResponded");
+            panic!("expected SetPlayingOrder event, got {poll_result:?}");
         };
-        assert_eq!(value.unwrap(), vec![PlayingOrder::InOrderOnce.into()]);
+        assert_matches!(event_receiver.expect_write_response(PLAYING_ORDER_HANDLE), Ok(()));
+
+        // Upper layer attempts to set an unsupported playing order.
+        responder.send(PlayingOrder::ShuffleOnce);
+        let poll_result = server.next().poll_unpin(&mut noop_cx);
+        assert_matches!(poll_result, Poll::Pending);
+        // State rejected the update, so no notification should be emitted.
+        event_receiver.expect_no_event();
+
+        fake_gatt_server.incoming_read(peer, service_id, PLAYING_ORDER_HANDLE, 0);
+        let _ = server.next().poll_unpin(&mut noop_cx);
+        let value = event_receiver
+            .expect_read_response(PLAYING_ORDER_HANDLE)
+            .expect("read response succeeds");
+        assert_eq!(value, vec![PlayingOrder::InOrderOnce.into()]);
+    }
+
+    #[test]
+    fn public_api_set_and_clear_track() {
+        let (mut server, fake_gatt_server, mut event_receiver) =
+            setup_test_server(McsServerBuilder::generic(0x42, "Test Player"));
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+
+        let peer = PeerId(1);
+        let service_id = ServiceId::new(0x42);
+
+        for handle in
+            [TRACK_CHANGED_HANDLE, TRACK_TITLE_HANDLE, TRACK_DURATION_HANDLE, TRACK_POSITION_HANDLE]
+        {
+            fake_gatt_server.incoming_client_configuration(
+                peer,
+                service_id,
+                handle,
+                bt_gatt::server::NotificationType::Notify,
+            );
+            let _ = server.next().poll_unpin(&mut noop_cx);
+            event_receiver.expect_no_event();
+        }
+
+        // Set track
+        server.set_track(
+            "Song Title",
+            TrackDuration::from_raw_10ms(3000),
+            TrackPosition::from_raw_10ms(0),
+        );
+
+        // Verify notifications per MCS Section 3.4.1 / 3.7.1
+        let (val, _) = event_receiver.expect_notification(TRACK_TITLE_HANDLE);
+        assert_eq!(val, b"Song Title");
+        let (val, _) = event_receiver.expect_notification(TRACK_DURATION_HANDLE);
+        assert_eq!(val, 3000i32.to_le_bytes());
+        let (val, _) = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+        assert_eq!(val, 0i32.to_le_bytes());
+        let (val, _) = event_receiver.expect_notification(TRACK_CHANGED_HANDLE);
+        assert_eq!(val, vec![]);
+        event_receiver.expect_no_event();
+
+        // Clear track
+        server.clear_track();
+
+        let (val, _) = event_receiver.expect_notification(TRACK_TITLE_HANDLE);
+        assert_eq!(val, vec![]);
+        let (val, _) = event_receiver.expect_notification(TRACK_DURATION_HANDLE);
+        assert_eq!(val, TrackDuration::Unknown.raw_10ms().to_le_bytes());
+        let (val, _) = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+        assert_eq!(val, TrackPosition::Unavailable.raw_10ms().to_le_bytes());
+        let (val, _) = event_receiver.expect_notification(TRACK_CHANGED_HANDLE);
+        assert_eq!(val, vec![]);
+        event_receiver.expect_no_event();
+    }
+
+    #[test]
+    fn public_api_set_media_state_and_track_position() {
+        let (mut server, fake_gatt_server, mut event_receiver) =
+            setup_test_server(McsServerBuilder::generic(0x42, "Test Player"));
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+
+        let peer = PeerId(1);
+        let service_id = ServiceId::new(0x42);
+
+        for handle in [MEDIA_STATE_HANDLE, TRACK_POSITION_HANDLE] {
+            fake_gatt_server.incoming_client_configuration(
+                peer,
+                service_id,
+                handle,
+                bt_gatt::server::NotificationType::Notify,
+            );
+            let _ = server.next().poll_unpin(&mut noop_cx);
+            event_receiver.expect_no_event();
+        }
+
+        server.set_media_state(MediaState::Playing);
+        let (value, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(value, vec![MediaState::Playing.into()]);
+
+        server.set_track_position(TrackPosition::from_raw_10ms(1500));
+        let (value, _) = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+        assert_eq!(value, 1500i32.to_le_bytes().to_vec());
+
+        // Pausing from Playing emits both MediaState and TrackPosition
+        // notifications per MCS §3.7.1
+        server.set_media_state(MediaState::Paused);
+        let (value, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(value, vec![MediaState::Paused.into()]);
+
+        let _ = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+
+        // Setting same MediaState is a no-op and produces no notifications per
+        // MCS v1.0.1 Section 3.17.1.
+        server.set_media_state(MediaState::Paused);
+        event_receiver.expect_no_event();
+
+        // Transitioning to Inactive emits MediaState and
+        // TrackPosition::Unavailable per MCS Section 3.7.1.
+        server.set_media_state(MediaState::Inactive);
+        let (value, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(value, vec![MediaState::Inactive.into()]);
+
+        let (value, _) = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+        assert_eq!(value, TrackPosition::Unavailable.raw_10ms().to_le_bytes().to_vec());
+
+        // Repeated transition to Inactive produces no notifications.
+        server.set_media_state(MediaState::Inactive);
+        event_receiver.expect_no_event();
+
+        // Transition back to Paused with a track to test FromEnd position
+        // resolving (MCS Section 3.7.1).
+        server.set_media_state(MediaState::Paused);
+        let (value, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(value, vec![MediaState::Paused.into()]);
+
+        server.set_track(
+            "Track",
+            TrackDuration::from_raw_10ms(3000),
+            TrackPosition::from_raw_10ms(0),
+        );
+        let _ = event_receiver.expect_notification(TRACK_TITLE_HANDLE);
+        let _ = event_receiver.expect_notification(TRACK_DURATION_HANDLE);
+        let _ = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+        let _ = event_receiver.expect_notification(TRACK_CHANGED_HANDLE);
+
+        server.set_track_position(TrackPosition::from_end(std::time::Duration::from_millis(5000)));
+        let (value, _) = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+        // Duration is 3000 (30.0s), 5.0s from end is 25.0s = 2500 raw units,
+        // not -500.
+        assert_eq!(value, 2500i32.to_le_bytes().to_vec());
+    }
+
+    #[test]
+    fn public_api_media_identity() {
+        let (mut server, fake_gatt_server, mut event_receiver) = setup_test_server(
+            McsServerBuilder::generic(0x42, "Old Name")
+                .with_icon_url("https://example.com/old.png"),
+        );
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+
+        let peer = PeerId(1);
+        let service_id = ServiceId::new(0x42);
+
+        fake_gatt_server.incoming_client_configuration(
+            peer,
+            service_id,
+            MEDIA_PLAYER_NAME_HANDLE,
+            bt_gatt::server::NotificationType::Notify,
+        );
+        let _ = server.next().poll_unpin(&mut noop_cx);
+        event_receiver.expect_no_event();
+
+        server.set_player_name("New Name");
+        let (value, _) = event_receiver.expect_notification(MEDIA_PLAYER_NAME_HANDLE);
+        assert_eq!(value, b"New Name");
+
+        server.set_icon_url("https://example.com/new.png").expect("set_icon_url succeeds");
+        // Icon URL is Read-only, so verify value via GATT read response
+        fake_gatt_server.incoming_read(peer, service_id, MEDIA_PLAYER_ICON_URL_HANDLE, 0);
+        let _ = server.next().poll_unpin(&mut noop_cx);
+        let value = event_receiver
+            .expect_read_response(MEDIA_PLAYER_ICON_URL_HANDLE)
+            .expect("read response succeeds");
+        assert_eq!(value, b"https://example.com/new.png");
+
+        // Test server without icon URL returns FeatureNotSupported
+        let (mut server_no_icon, _, _) =
+            setup_test_server(McsServerBuilder::generic(0x43, "Player"));
+        assert_matches!(
+            server_no_icon.set_icon_url("https://example.com"),
+            Err(Error::FeatureNotSupported("icon_url"))
+        );
+    }
+
+    #[test]
+    fn public_api_playback_speed_and_seeking_speed() {
+        let (mut server, fake_gatt_server, mut event_receiver) =
+            setup_test_server(McsServerBuilder::generic(0x42, "Test Player").with_player_speeds());
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+
+        let peer = PeerId(1);
+        let service_id = ServiceId::new(0x42);
+
+        for handle in
+            [MEDIA_STATE_HANDLE, TRACK_POSITION_HANDLE, PLAYBACK_SPEED_HANDLE, SEEKING_SPEED_HANDLE]
+        {
+            fake_gatt_server.incoming_client_configuration(
+                peer,
+                service_id,
+                handle,
+                bt_gatt::server::NotificationType::Notify,
+            );
+            let _ = server.next().poll_unpin(&mut noop_cx);
+            event_receiver.expect_no_event();
+        }
+
+        server.set_playback_speed(PlaybackSpeed::DOUBLE).expect("set_playback_speed succeeds");
+        let (value, _) = event_receiver.expect_notification(PLAYBACK_SPEED_HANDLE);
+        assert_eq!(value, vec![PlaybackSpeed::DOUBLE.into()]);
+
+        // During active playback, setting playback speed emits both
+        // PlaybackSpeed and TrackPosition notifications (MCS v1.0.1
+        // Section 3.7.1).
+        server.set_media_state(MediaState::Playing);
+        let (value, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(value, vec![MediaState::Playing.into()]);
+        server.set_playback_speed(PlaybackSpeed::HALF).expect("set_playback_speed succeeds");
+        let (value, _) = event_receiver.expect_notification(PLAYBACK_SPEED_HANDLE);
+        assert_eq!(value, vec![PlaybackSpeed::HALF.into()]);
+
+        let _ = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+
+        // Transition to Seeking emits MediaState and TrackPosition
+        // notifications
+        server.set_media_state(MediaState::Seeking);
+        let (value, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(value, vec![MediaState::Seeking.into()]);
+        let _ = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+
+        // During seeking, setting seeking speed emits both SeekingSpeed and
+        // TrackPosition
+        server.set_seeking_speed(SeekingSpeed::new(2)).expect("set_seeking_speed succeeds");
+        let (value, _) = event_receiver.expect_notification(SEEKING_SPEED_HANDLE);
+        assert_eq!(value, vec![SeekingSpeed::new(2).into()]);
+
+        let _ = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+
+        // Pausing from Seeking emits both MediaState and TrackPosition
+        // notifications
+        server.set_media_state(MediaState::Paused);
+        let (value, _) = event_receiver.expect_notification(MEDIA_STATE_HANDLE);
+        assert_eq!(value, vec![MediaState::Paused.into()]);
+        let _ = event_receiver.expect_notification(TRACK_POSITION_HANDLE);
+
+        // Test server without player speeds returns FeatureNotSupported
+        let (mut server_no_speeds, _, _) =
+            setup_test_server(McsServerBuilder::generic(0x43, "Player"));
+        assert_matches!(
+            server_no_speeds.set_playback_speed(PlaybackSpeed::DOUBLE),
+            Err(Error::FeatureNotSupported("playback_speed"))
+        );
+        assert_matches!(
+            server_no_speeds.set_seeking_speed(SeekingSpeed::new(2)),
+            Err(Error::FeatureNotSupported("seeking_speed"))
+        );
+    }
+
+    #[test]
+    fn public_api_playing_order_and_supported_opcodes() {
+        let (mut server, fake_gatt_server, mut event_receiver) = setup_test_server(
+            McsServerBuilder::generic(0x42, "Test Player")
+                .with_playing_orders(
+                    SupportedPlayingOrders::IN_ORDER_ONCE | SupportedPlayingOrders::SHUFFLE_ONCE,
+                )
+                .with_supported_operations(SupportedOpcodes::PLAY),
+        );
+        let mut noop_cx = Context::from_waker(futures::task::noop_waker_ref());
+
+        let peer = PeerId(1);
+        let service_id = ServiceId::new(0x42);
+
+        for handle in [PLAYING_ORDER_HANDLE, MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE] {
+            fake_gatt_server.incoming_client_configuration(
+                peer,
+                service_id,
+                handle,
+                bt_gatt::server::NotificationType::Notify,
+            );
+            let _ = server.next().poll_unpin(&mut noop_cx);
+            event_receiver.expect_no_event();
+        }
+
+        // Supported order succeeds and notifies
+        server.set_playing_order(PlayingOrder::ShuffleOnce).expect("set_playing_order succeeds");
+        let (value, _) = event_receiver.expect_notification(PLAYING_ORDER_HANDLE);
+        assert_eq!(value, vec![PlayingOrder::ShuffleOnce.into()]);
+
+        // Unsupported order returns Error and does not notify
+        assert_matches!(
+            server.set_playing_order(PlayingOrder::SingleRepeat),
+            Err(Error::UnsupportedPlayingOrder(PlayingOrder::SingleRepeat))
+        );
+        event_receiver.expect_no_event();
+
+        // Update supported opcodes
+        let new_opcodes = SupportedOpcodes::PLAY | SupportedOpcodes::PAUSE;
+        server.set_supported_opcodes(new_opcodes).expect("set_supported_opcodes succeeds");
+        let (value, _) =
+            event_receiver.expect_notification(MEDIA_CONTROL_POINT_OPCODES_SUPPORTED_HANDLE);
+        assert_eq!(value, new_opcodes.bits().to_le_bytes().to_vec());
+
+        // Test server without playing orders or control point returns
+        // FeatureNotSupported
+        let (mut server_no_optional, _, _) =
+            setup_test_server(McsServerBuilder::generic(0x43, "Player"));
+        assert_matches!(
+            server_no_optional.set_playing_order(PlayingOrder::InOrderOnce),
+            Err(Error::FeatureNotSupported("playing_orders"))
+        );
+        assert_matches!(
+            server_no_optional.set_supported_opcodes(SupportedOpcodes::PLAY),
+            Err(Error::FeatureNotSupported("media_control_point"))
+        );
+    }
+
+    #[test]
+    fn public_api_unpublished_server_updates_state() {
+        let mut server = McsServerBuilder::generic(0x42, "Test Player")
+            .with_icon_url("https://example.com/old.png")
+            .with_player_speeds()
+            .with_playing_orders(SupportedPlayingOrders::all())
+            .with_supported_operations(SupportedOpcodes::all())
+            .build::<FakeTypes>()
+            .unwrap();
+
+        server.set_media_state(MediaState::Playing);
+        server.set_track(
+            "Title",
+            TrackDuration::from_raw_10ms(100),
+            TrackPosition::from_raw_10ms(0),
+        );
+        server.clear_track();
+        server.set_track_position(TrackPosition::from_raw_10ms(0));
+        server.set_player_name("Name");
+        assert_matches!(server.set_icon_url("https://example.com"), Ok(()));
+        assert_matches!(server.set_playback_speed(PlaybackSpeed::NORMAL), Ok(()));
+        assert_matches!(server.set_seeking_speed(SeekingSpeed::NOT_SEEKING), Ok(()));
+        assert_matches!(server.set_playing_order(PlayingOrder::InOrderOnce), Ok(()));
+        assert_matches!(server.set_supported_opcodes(SupportedOpcodes::PLAY), Ok(()));
     }
 }
diff --git a/rust/bt-mcs/src/server/state.rs b/rust/bt-mcs/src/server/state.rs
index f802d19..46d3e2a 100644
--- a/rust/bt-mcs/src/server/state.rs
+++ b/rust/bt-mcs/src/server/state.rs
@@ -10,6 +10,7 @@
 use bt_gatt::Characteristic;
 
 use crate::types::*;
+use crate::Error;
 
 // ============================================================================
 // Characteristic Definitions & Handles (MCS v1.0.1 Section 3)
@@ -170,62 +171,185 @@
             return TrackPosition::Unavailable;
         }
 
-        // Playback timing has not started since the track was initialized or
-        // loaded.
-        let Some(updated_at) = self.position_updated_at else {
-            return self.track_position;
-        };
-
-        // Playback is paused or seeking, so the position remains fixed at the
-        // current offset.
-        if self.media_state != MediaState::Playing {
-            return self.track_position;
-        }
-
         let base = match (self.track_position, self.track_duration) {
             (TrackPosition::FromStart(base), _) => base,
             (TrackPosition::FromEnd(end), TrackDuration::Duration(total)) => {
                 total.saturating_sub(end)
             }
+            (TrackPosition::FromEnd(_), TrackDuration::Unknown) => {
+                return TrackPosition::Unavailable;
+            }
             _ => return self.track_position,
         };
 
-        // TODO(b/540400364): Factor in optional playback speed when supported
-        let mut current = base + updated_at.elapsed();
-        if let TrackDuration::Duration(total) = self.track_duration {
-            current = current.min(total);
+        // Playback is paused, so position remains fixed at current offset.
+        if self.media_state == MediaState::Paused {
+            return TrackPosition::FromStart(base);
         }
-        TrackPosition::FromStart(current)
+
+        // Playback timing has not started since the track was initialized or
+        // loaded.
+        let Some(updated_at) = self.position_updated_at else {
+            return TrackPosition::FromStart(base);
+        };
+
+        let elapsed = updated_at.elapsed();
+        let current = match self.media_state {
+            MediaState::Playing => {
+                let speed_multiplier = self.playback_speed.map_or(1.0, |s| s.as_multiplier());
+                base + elapsed.mul_f64(speed_multiplier)
+            }
+            MediaState::Seeking => {
+                let factor = self.seeking_speed.map_or(0, |s| s.factor());
+                if factor > 0 {
+                    base + elapsed.mul_f64(factor as f64)
+                } else if factor < 0 {
+                    base.saturating_sub(elapsed.mul_f64(factor.unsigned_abs() as f64))
+                } else {
+                    base
+                }
+            }
+            _ => base,
+        };
+
+        let bounded = if let TrackDuration::Duration(total) = self.track_duration {
+            current.min(total)
+        } else {
+            current
+        };
+
+        TrackPosition::FromStart(bounded)
+    }
+
+    /// Updates the track position and synchronizes the timestamp anchor.
+    fn update_track_position(&mut self, position: TrackPosition) {
+        let resolved = match (position, self.track_duration) {
+            (TrackPosition::FromStart(base), _) => TrackPosition::FromStart(base),
+            (TrackPosition::FromEnd(end), TrackDuration::Duration(total)) => {
+                TrackPosition::FromStart(total.saturating_sub(end))
+            }
+            (TrackPosition::FromEnd(_), TrackDuration::Unknown) => TrackPosition::Unavailable,
+            (TrackPosition::Unavailable, _) => TrackPosition::Unavailable,
+        };
+        self.track_position = resolved;
+        self.position_updated_at = match resolved {
+            TrackPosition::Unavailable => None,
+            _ => Some(std::time::Instant::now()),
+        };
+    }
+
+    /// Sets the media playback state and updates the playback position clock
+    /// anchor accordingly.
+    pub(crate) fn set_media_state(&mut self, state: MediaState) {
+        if self.media_state == state {
+            return;
+        }
+
+        match state {
+            MediaState::Playing | MediaState::Paused | MediaState::Seeking => {
+                let position = if self.media_state == MediaState::Playing
+                    || self.media_state == MediaState::Seeking
+                {
+                    self.current_track_position()
+                } else {
+                    self.track_position
+                };
+                self.update_track_position(position);
+            }
+            MediaState::Inactive => {
+                self.update_track_position(TrackPosition::Unavailable);
+            }
+        }
+        self.media_state = state;
     }
 
     /// Sets the track position and records the update timestamp.
     pub(crate) fn set_track_position(&mut self, position: TrackPosition) {
-        self.track_position = position;
-        self.position_updated_at = Some(std::time::Instant::now());
+        self.update_track_position(position);
+    }
+
+    /// Sets the current track title, duration, and position.
+    pub(crate) fn set_track(
+        &mut self,
+        title: String,
+        duration: TrackDuration,
+        position: TrackPosition,
+    ) {
+        self.track_title = title;
+        self.track_duration = duration;
+        self.update_track_position(position);
+    }
+
+    /// Clears the active track metadata and position.
+    pub(crate) fn clear_track(&mut self) {
+        self.track_title.clear();
+        self.track_duration = TrackDuration::Unknown;
+        self.update_track_position(TrackPosition::Unavailable);
+    }
+
+    /// Sets the human-readable media player name.
+    pub(crate) fn set_player_name(&mut self, name: String) {
+        self.player_name = name;
+    }
+
+    /// Sets the media player icon URL if supported.
+    pub(crate) fn set_icon_url(&mut self, url: String) -> Result<(), Error> {
+        if self.icon_url.is_none() {
+            return Err(Error::FeatureNotSupported("icon_url"));
+        }
+        self.icon_url = Some(url);
+        Ok(())
     }
 
     /// Sets the playback speed.
-    pub(crate) fn set_playback_speed(&mut self, speed: PlaybackSpeed) {
-        self.playback_speed = Some(speed);
-    }
-
-    /// Sets the playing order if playing order is supported.
-    pub(crate) fn set_playing_order(&mut self, order: PlayingOrder) {
-        if let Some(ref mut orders) = self.playing_orders {
-            if orders.supported.contains(order.into()) {
-                orders.current = order;
-            }
+    pub(crate) fn set_playback_speed(&mut self, speed: PlaybackSpeed) -> Result<(), Error> {
+        if self.playback_speed.is_none() {
+            return Err(Error::FeatureNotSupported("playback_speed"));
         }
+        // Freeze current track position before adjusting speed so the new
+        // multiplier is not retroactively applied to prior playback
+        // time (MCS v1.0.1 Section 3.7.1).
+        if self.media_state == MediaState::Playing {
+            self.update_track_position(self.current_track_position());
+        }
+        self.playback_speed = Some(speed);
+        Ok(())
     }
 
-    #[cfg(test)]
-    pub(crate) fn set_media_state(&mut self, state: MediaState) {
-        self.media_state = state;
+    /// Sets the seeking speed if supported.
+    pub(crate) fn set_seeking_speed(&mut self, speed: SeekingSpeed) -> Result<(), Error> {
+        if self.seeking_speed.is_none() {
+            return Err(Error::FeatureNotSupported("seeking_speed"));
+        }
+        // Freeze current track position before entering or adjusting seeking
+        // speed so prior elapsed playback is preserved (MCS v1.0.1
+        // Section 3.7.1, 3.9).
+        if self.media_state == MediaState::Playing || self.media_state == MediaState::Seeking {
+            self.update_track_position(self.current_track_position());
+        }
+        self.seeking_speed = Some(speed);
+        Ok(())
     }
 
-    #[cfg(test)]
-    pub(crate) fn set_supported_opcodes(&mut self, opcodes: SupportedOpcodes) {
+    /// Sets the playing order if supported and enabled.
+    pub(crate) fn set_playing_order(&mut self, order: PlayingOrder) -> Result<(), Error> {
+        let Some(ref mut orders) = self.playing_orders else {
+            return Err(Error::FeatureNotSupported("playing_orders"));
+        };
+        if !orders.supported.contains(order.into()) {
+            return Err(Error::UnsupportedPlayingOrder(order));
+        }
+        orders.current = order;
+        Ok(())
+    }
+
+    /// Sets the supported opcodes if supported.
+    pub(crate) fn set_supported_opcodes(&mut self, opcodes: SupportedOpcodes) -> Result<(), Error> {
+        if self.supported_opcodes.is_none() {
+            return Err(Error::FeatureNotSupported("media_control_point"));
+        }
         self.supported_opcodes = Some(opcodes);
+        Ok(())
     }
 
     /// Reads the characteristic bytes for `handle` at `offset`.
@@ -286,11 +410,14 @@
         state.playing_orders = Some(PlayingOrderState::new(SupportedPlayingOrders::SINGLE_ONCE));
         assert_eq!(state.playing_orders.as_ref().unwrap().current, PlayingOrder::InOrderOnce);
 
-        state.set_playing_order(PlayingOrder::SingleOnce);
+        assert_matches!(state.set_playing_order(PlayingOrder::SingleOnce), Ok(()));
         assert_eq!(state.playing_orders.as_ref().unwrap().current, PlayingOrder::SingleOnce);
 
-        // Unsupported playing order is not applied
-        state.set_playing_order(PlayingOrder::ShuffleOnce);
+        // Unsupported playing order returns Err and is not applied
+        assert_matches!(
+            state.set_playing_order(PlayingOrder::ShuffleOnce),
+            Err(Error::UnsupportedPlayingOrder(PlayingOrder::ShuffleOnce))
+        );
         assert_eq!(state.playing_orders.as_ref().unwrap().current, PlayingOrder::SingleOnce);
     }
 
@@ -511,4 +638,204 @@
             assert!(chrc.permissions.update.map_or(true, |s| s.encryption));
         }
     }
+
+    #[test]
+    fn local_state_track_position_calculation_with_playback_speed() {
+        let mut state = McsLocalState::new(0x01, "Player");
+        state.track_duration = TrackDuration::from_raw_10ms(10000);
+        state.media_state = MediaState::Playing;
+        state.track_position = TrackPosition::from_raw_10ms(1000); // 10s base
+
+        // Double speed (2.0x): 2s elapsed should add ~4s -> ~14s
+        state.playback_speed = Some(PlaybackSpeed::DOUBLE);
+        state.position_updated_at = Some(std::time::Instant::now() - Duration::from_secs(2));
+        match state.current_track_position() {
+            TrackPosition::FromStart(dur) => {
+                assert!(dur >= Duration::from_millis(13800) && dur <= Duration::from_millis(14200));
+            }
+            other => panic!("Expected FromStart, got {other:?}"),
+        }
+
+        // Half speed (0.5x): 4s elapsed should add ~2s -> ~12s
+        state.playback_speed = Some(PlaybackSpeed::HALF);
+        state.position_updated_at = Some(std::time::Instant::now() - Duration::from_secs(4));
+        match state.current_track_position() {
+            TrackPosition::FromStart(dur) => {
+                assert!(dur >= Duration::from_millis(11800) && dur <= Duration::from_millis(12200));
+            }
+            other => panic!("Expected FromStart, got {other:?}"),
+        }
+    }
+
+    #[test]
+    fn local_state_track_position_calculation_with_seeking_speed() {
+        let mut state = McsLocalState::new(0x01, "Player");
+        state.track_duration = TrackDuration::from_raw_10ms(10000); // 100s
+        state.media_state = MediaState::Seeking;
+        state.track_position = TrackPosition::from_raw_10ms(2000); // 20s base
+
+        // Fast forward (+4x): 2s elapsed should add ~8s -> ~28s
+        state.seeking_speed = Some(SeekingSpeed::new(4));
+        state.position_updated_at = Some(std::time::Instant::now() - Duration::from_secs(2));
+        match state.current_track_position() {
+            TrackPosition::FromStart(dur) => {
+                assert!(dur >= Duration::from_millis(27800) && dur <= Duration::from_millis(28200));
+            }
+            other => panic!("Expected FromStart, got {other:?}"),
+        }
+
+        // Fast rewind (-4x): 2s elapsed should subtract ~8s -> ~12s
+        state.seeking_speed = Some(SeekingSpeed::new(-4));
+        state.position_updated_at = Some(std::time::Instant::now() - Duration::from_secs(2));
+        match state.current_track_position() {
+            TrackPosition::FromStart(dur) => {
+                assert!(dur >= Duration::from_millis(11800) && dur <= Duration::from_millis(12200));
+            }
+            other => panic!("Expected FromStart, got {other:?}"),
+        }
+
+        // Fast rewind saturated at start of track (0s)
+        state.position_updated_at = Some(std::time::Instant::now() - Duration::from_secs(10));
+        assert_eq!(state.current_track_position(), TrackPosition::from_raw_10ms(0));
+
+        // Fast forward clamped at track duration (100s)
+        state.seeking_speed = Some(SeekingSpeed::new(4));
+        state.position_updated_at = Some(std::time::Instant::now() - Duration::from_secs(50));
+        assert_eq!(state.current_track_position(), TrackPosition::from_raw_10ms(10000));
+    }
+
+    #[test]
+    fn local_state_state_transitions_timeline_capture() {
+        let mut state = McsLocalState::new(0x01, "Player");
+        state.track_duration = TrackDuration::from_raw_10ms(10000);
+        state.set_track(
+            "Test Track".to_string(),
+            TrackDuration::from_raw_10ms(5000),
+            TrackPosition::from_raw_10ms(100),
+        );
+        assert_eq!(state.track_title, "Test Track");
+        assert_eq!(state.track_duration, TrackDuration::from_raw_10ms(5000));
+        assert_eq!(state.track_position, TrackPosition::from_raw_10ms(100));
+
+        // Transition to Playing
+        state.set_media_state(MediaState::Playing);
+        assert_eq!(state.media_state, MediaState::Playing);
+        assert!(state.position_updated_at.is_some());
+
+        // Fast-forward time simulated: 1s elapsed
+        state.position_updated_at = Some(std::time::Instant::now() - Duration::from_secs(1));
+
+        // Transition to Paused: snaps dynamic position
+        state.set_media_state(MediaState::Paused);
+        assert_eq!(state.media_state, MediaState::Paused);
+        match state.track_position {
+            TrackPosition::FromStart(dur) => {
+                assert!(dur >= Duration::from_millis(1900) && dur <= Duration::from_millis(2200));
+            }
+            other => panic!("Expected FromStart, got {other:?}"),
+        }
+
+        // Transition to Inactive: position unavailable
+        state.set_media_state(MediaState::Inactive);
+        assert_eq!(state.media_state, MediaState::Inactive);
+        assert_eq!(state.track_position, TrackPosition::Unavailable);
+        assert!(state.position_updated_at.is_none());
+
+        // Clear track
+        state.clear_track();
+        assert_eq!(state.track_title, "");
+        assert_eq!(state.track_duration, TrackDuration::Unknown);
+        assert_eq!(state.track_position, TrackPosition::Unavailable);
+    }
+
+    #[test]
+    fn local_state_optional_setters_feature_validation() {
+        let mut state = McsLocalState::new(0x01, "Player");
+
+        // Unconfigured features return FeatureNotSupported
+        assert_matches!(
+            state.set_icon_url("https://example.com".to_string()),
+            Err(Error::FeatureNotSupported("icon_url"))
+        );
+        assert_matches!(
+            state.set_playback_speed(PlaybackSpeed::NORMAL),
+            Err(Error::FeatureNotSupported("playback_speed"))
+        );
+        assert_matches!(
+            state.set_seeking_speed(SeekingSpeed::new(1)),
+            Err(Error::FeatureNotSupported("seeking_speed"))
+        );
+        assert_matches!(
+            state.set_playing_order(PlayingOrder::SingleOnce),
+            Err(Error::FeatureNotSupported("playing_orders"))
+        );
+        assert_matches!(
+            state.set_supported_opcodes(SupportedOpcodes::all()),
+            Err(Error::FeatureNotSupported("media_control_point"))
+        );
+
+        // When configured, setters succeed and validate
+        state.icon_url = Some(String::new());
+        state.playback_speed = Some(PlaybackSpeed::NORMAL);
+        state.seeking_speed = Some(SeekingSpeed::NOT_SEEKING);
+        state.playing_orders = Some(PlayingOrderState::new(SupportedPlayingOrders::SINGLE_ONCE));
+        state.supported_opcodes = Some(SupportedOpcodes::PLAY);
+
+        assert_matches!(state.set_icon_url("https://example.com/icon.png".to_string()), Ok(()));
+        assert_eq!(state.icon_url.as_deref(), Some("https://example.com/icon.png"));
+
+        assert_matches!(state.set_playback_speed(PlaybackSpeed::DOUBLE), Ok(()));
+        assert_eq!(state.playback_speed, Some(PlaybackSpeed::DOUBLE));
+
+        assert_matches!(state.set_seeking_speed(SeekingSpeed::new(5)), Ok(()));
+        assert_eq!(state.seeking_speed, Some(SeekingSpeed::new(5)));
+
+        assert_matches!(state.set_playing_order(PlayingOrder::SingleOnce), Ok(()));
+        assert_matches!(
+            state.set_playing_order(PlayingOrder::ShuffleOnce),
+            Err(Error::UnsupportedPlayingOrder(PlayingOrder::ShuffleOnce))
+        );
+
+        assert_matches!(state.set_supported_opcodes(SupportedOpcodes::all()), Ok(()));
+    }
+
+    #[test]
+    fn seeking_speed_min_boundary_calculation() {
+        let mut state = McsLocalState::new(0x01, "Player");
+        state.set_track(
+            "Song".to_string(),
+            TrackDuration::from_raw_10ms(10000),
+            TrackPosition::from_raw_10ms(5000),
+        );
+        state.seeking_speed = Some(SeekingSpeed::new(-128));
+        state.set_media_state(MediaState::Seeking);
+
+        // Verify seeking with minimum factor (-128) does not overflow or panic.
+        let pos = state.current_track_position();
+        match pos {
+            TrackPosition::FromStart(dur) => {
+                assert!(dur <= Duration::from_millis(50000));
+            }
+            other => panic!("Expected FromStart, got {other:?}"),
+        }
+
+        // Verify FromEnd with Unknown duration resolves to Unavailable per MCS
+        // v1.0.1 Section 3.7.1.
+        state.set_track(
+            "Live Stream".to_string(),
+            TrackDuration::Unknown,
+            TrackPosition::from_end(Duration::from_secs(10)),
+        );
+        assert_eq!(state.current_track_position(), TrackPosition::Unavailable);
+
+        // Verify FromEnd with known duration resolves to FromStart(total -
+        // end).
+        state.set_media_state(MediaState::Paused);
+        state.set_track(
+            "Track".to_string(),
+            TrackDuration::from_raw_10ms(5000), // 50s
+            TrackPosition::from_end(Duration::from_secs(10)),
+        );
+        assert_eq!(state.current_track_position(), TrackPosition::from_raw_10ms(4000));
+    }
 }
diff --git a/rust/bt-mcs/src/types.rs b/rust/bt-mcs/src/types.rs
index 35594c4..76fec63 100644
--- a/rust/bt-mcs/src/types.rs
+++ b/rust/bt-mcs/src/types.rs
@@ -598,6 +598,11 @@
             Self::Duration(d) => (d.as_millis() / 10).min(i32::MAX as u128) as i32,
         }
     }
+
+    /// Returns the 4-byte little-endian notification payload for this duration.
+    pub fn to_notify_value(&self) -> [u8; 4] {
+        self.raw_10ms().to_le_bytes()
+    }
 }
 
 impl Encodable for TrackDuration {
@@ -676,6 +681,11 @@
             Self::FromEnd(d) => -((d.as_millis() / 10).min(i32::MAX as u128) as i32),
         }
     }
+
+    /// Returns the 4-byte little-endian notification payload for this position.
+    pub fn to_notify_value(&self) -> [u8; 4] {
+        self.raw_10ms().to_le_bytes()
+    }
 }
 
 impl Encodable for TrackPosition {
@@ -1301,6 +1311,8 @@
 
         let dur = TrackDuration::from_duration(Duration::from_secs(180));
         assert_eq!(dur.raw_10ms(), 18000);
+        assert_eq!(dur.to_notify_value(), 18000i32.to_le_bytes());
+        assert_eq!(TrackDuration::Unknown.to_notify_value(), (-1i32).to_le_bytes());
         assert_eq!(TrackDuration::from_raw_10ms(18000), dur);
 
         let mut buf = [0u8; 4];
@@ -1321,14 +1333,17 @@
     #[test]
     fn track_position_encode_decode() {
         assert_eq!(TrackPosition::Unavailable.raw_10ms(), -1);
+        assert_eq!(TrackPosition::Unavailable.to_notify_value(), (-1i32).to_le_bytes());
         assert_eq!(TrackPosition::from_raw_10ms(-1), TrackPosition::Unavailable);
 
         let start_pos = TrackPosition::from_start(Duration::from_millis(1500));
         assert_eq!(start_pos.raw_10ms(), 150);
+        assert_eq!(start_pos.to_notify_value(), 150i32.to_le_bytes());
         assert_eq!(TrackPosition::from_raw_10ms(150), start_pos);
 
         let end_pos = TrackPosition::from_end(Duration::from_millis(2000));
         assert_eq!(end_pos.raw_10ms(), -200);
+        assert_eq!(end_pos.to_notify_value(), (-200i32).to_le_bytes());
         assert_eq!(TrackPosition::from_raw_10ms(-200), end_pos);
 
         let mut buf = [0u8; 4];