mctp-api: add Stack facade and update mctp_echo to use high-level traits

- Add services/mctp/api/src/stack.rs: Stack<C: MctpClient> facade that
  wraps any MctpClient and returns StackListener, StackReqChannel and
  StackRespChannel implementing the MctpListener / MctpReqChannel /
  MctpRespChannel traits. All channel types release server handles on Drop.

- Update services/mctp/api/src/lib.rs: expose stack module and re-export
  Stack, StackListener, StackReqChannel, StackRespChannel.

- Update target/ast1060-evb/mctp/mctp_echo.rs: replace direct MctpClient
  calls with Stack::new(IpcMctpClient) + MctpListener / MctpRespChannel
  traits, matching the Hubris mctp-echo task structure.

- Update services/mctp/server/tests/integration.rs: add Stack facade tests
  exercising echo-via-Stack and req/resp roundtrip using DirectClient as
  the MctpClient strategy.

- Update services/mctp/api/README.md: document the two-layer abstraction,
  Stack API, Strategy pattern, and wire protocol.
diff --git a/services/mctp/api/README.md b/services/mctp/api/README.md
index b034290..de26023 100644
--- a/services/mctp/api/README.md
+++ b/services/mctp/api/README.md
@@ -1,27 +1,99 @@
 # openprot-mctp-api
 
-Platform-independent MCTP types and traits crate.
+Platform-independent MCTP types, traits, and stack facade.
 
 ## Overview
 
-This crate defines the core API contract between MCTP clients and the MCTP server. It provides traits for client operations, listener management, and request/response channels, as well as the binary IPC wire protocol used for inter-process communication.
+This crate defines the API contract between MCTP applications and the MCTP server.
+It provides two layers:
+
+1. **`MctpClient` trait** — low-level interface mirroring the IPC wire operations
+   (req, listener, recv, send, drop_handle). Platform-specific crates such as
+   `openprot-mctp-client` implement this trait using the OS transport (e.g. Pigweed IPC).
+
+2. **`Stack` facade** (`stack` module) — high-level entry point that wraps any `MctpClient`
+   and returns typed channel objects (`StackListener`, `StackReqChannel`, `StackRespChannel`)
+   that implement the `MctpListener` / `MctpReqChannel` / `MctpRespChannel` traits.
+
+This two-layer design hides both the **concrete MCTP stack implementation** (which lives
+inside the server process) and the **OS / IPC transport** from application code.
+Applications depend only on the high-level traits; swapping the transport or stack
+requires no application changes.
+
+```text
+┌─────────────────────┐
+│   Application       │  uses MctpListener / MctpReqChannel / MctpRespChannel traits
+└─────────┬───────────┘
+          │
+          ▼
+┌─────────────────────┐
+│   Stack (this crate)│  wraps any MctpClient, returns typed channel handles
+└─────────┬───────────┘
+          │ MctpClient trait
+          ▼
+┌─────────────────────┐
+│  IpcMctpClient      │  encodes wire protocol, calls OS IPC (e.g. Pigweed channel_transact)
+│  (mctp-client crate)│
+└─────────┬───────────┘
+          │ IPC
+          ▼
+┌─────────────────────┐
+│   MCTP Server       │  owns the concrete MCTP stack (mctp-lib, etc.)
+└─────────────────────┘
+```
 
 ## Key Types
 
-- `Handle` — opaque handle for listeners, requests, or response channels
-- `RecvMetadata` — metadata from a successful receive (msg_type, tag, remote_eid, payload_size, etc.)
-- `MctpError` / `ResponseCode` — error types (Success, InternalError, NoSpace, AddrInUse, TimedOut, BadArgument, ServerRestarted)
+- `Handle` — opaque handle for listeners, request, or response channels
+- `RecvMetadata` — metadata from a successful receive (msg_type, tag, remote_eid, payload_size)
+- `MctpError` / `ResponseCode` — error types (InternalError, NoSpace, AddrInUse, TimedOut, BadArgument, ServerRestarted)
 
-## Traits
+## High-level API (`stack` module)
 
-- `MctpClient` — main client interface (req, listener, get/set EID, recv, send, drop_handle)
-- `MctpListener` — receiving incoming MCTP messages of a specific type
-- `MctpReqChannel` — request/response channels
-- `MctpRespChannel` — response channels
+| Type | Trait | Obtained via |
+|------|-------|--------------|
+| `Stack<C>` | — | `Stack::new(client)` |
+| `StackListener<'_, C>` | `MctpListener` | `stack.listener(msg_type, timeout)` |
+| `StackReqChannel<'_, C>` | `MctpReqChannel` | `stack.req(eid, timeout)` |
+| `StackRespChannel<'_, C>` | `MctpRespChannel` | returned by `StackListener::recv` |
 
-## Wire Protocol
+All channel types release their server-side handle automatically on `Drop`.
 
-The `wire` module implements binary request/response encoding for IPC communication between userspace processes and the MCTP server.
+## Low-level API (`MctpClient` trait)
+
+| Method | Description |
+|--------|-------------|
+| `req(eid)` | Allocate a request handle for a remote EID |
+| `listener(msg_type)` | Register to receive messages of a given type |
+| `get_eid() / set_eid(eid)` | Read/write the local endpoint ID |
+| `recv(handle, timeout, buf)` | Receive a message on a handle |
+| `send(handle, msg_type, eid, tag, ic, buf)` | Send a message (request or response) |
+| `drop_handle(handle)` | Release a handle |
+
+## Design: Strategy Pattern
+
+`Stack<C: MctpClient>` applies the **Strategy pattern**:
+
+- **Context** → `Stack<C>` holds the strategy and exposes the high-level API
+- **Strategy trait** → `MctpClient` defines the IPC operations (req, listener, recv, send, drop_handle)
+- **Concrete strategies** → `IpcMctpClient` (Pigweed IPC), test `DirectClient`, future Linux socket client
+
+Applications code against `MctpListener` / `MctpReqChannel` / `MctpRespChannel` traits and never
+see the strategy. The concrete `MctpClient` implementation is injected via `Stack::new(client)`.
+
+This gives two independent axes of variation:
+
+| Concern | How to swap |
+|---------|-------------|
+| MCTP stack implementation (mctp-lib, etc.) | Replace the server process — no API change |
+| OS / IPC transport | Provide a different `MctpClient` impl to `Stack::new` |
+| Application logic | Written against the high-level traits — unchanged across both |
+
+
+
+The `wire` module implements binary request/response encoding for IPC communication.
+It is used internally by `openprot-mctp-client` and the server; applications do not
+use it directly.
 
 ## Dependencies
 
diff --git a/services/mctp/api/STACK-DESIGN.md b/services/mctp/api/STACK-DESIGN.md
new file mode 100644
index 0000000..2200e2c
--- /dev/null
+++ b/services/mctp/api/STACK-DESIGN.md
@@ -0,0 +1,179 @@
+# MCTP Stack Design
+
+## Pattern: Facade + Factory Method + Strategy
+
+`Stack<C>` is a **Facade** over any `MctpClient` implementation. It exposes three
+factory methods (`req`, `listener`, via listener's `recv` for resp) that produce
+typed channel handles. The `C: MctpClient` bound is a compile-time **Strategy**,
+making the transport completely swappable without changing call-site code.
+
+---
+
+## Type Hierarchy
+
+```
+                       «trait»
+                      MctpClient
+                    ┌────────────┐
+                    │ req()      │
+                    │ listener() │
+                    │ send()     │
+                    │ recv()     │
+                    │ drop_handle│
+                    │ get/set_eid│
+                    └─────┬──────┘
+                          │ implemented by
+              ┌───────────┼───────────┐
+              │           │           │
+       IpcMctpClient  LinuxClient  (future)
+        (Hubris IPC)  (sockets)
+```
+
+---
+
+## Stack Facade
+
+```
+ Application code
+      │
+      │  only sees traits:
+      │  MctpReqChannel / MctpListener / MctpRespChannel
+      │
+      ▼
+┌─────────────────────────────────────────────────┐
+│                  Stack<C: MctpClient>            │
+│                                                  │
+│  ┌──────────────────────────────────────────┐   │
+│  │  + new(client: C) → Stack<C>             │   │
+│  │  + get_eid() → u8                        │   │
+│  │  + set_eid(eid) → Result                 │   │
+│  │                                          │   │  ◄── Facade
+│  │  + req(eid, timeout)                     │   │
+│  │      → StackReqChannel<'_, C>            │   │
+│  │                                          │   │
+│  │  + listener(msg_type, timeout)           │   │
+│  │      → StackListener<'_, C>              │   │
+│  └──────────────────────────────────────────┘   │
+│                                                  │
+│  client: C  ◄── Strategy (hidden from callers)  │
+└─────────────────────────────────────────────────┘
+```
+
+---
+
+## Channel Products (Factory Method)
+
+```
+Stack::req()                           Stack::listener()
+     │                                       │
+     ▼                                       ▼
+┌──────────────────────┐        ┌───────────────────────┐
+│  StackReqChannel<C>  │        │   StackListener<C>    │
+│                      │        │                       │
+│  handle: Handle      │        │  handle: Handle       │
+│  eid: u8             │        │  timeout: u32         │
+│  sent_tag: Option<u8>│        │  stack: &Stack<C>     │
+│  timeout: u32        │        └────────────┬──────────┘
+│  stack: &Stack<C>    │                     │
+└──────────┬───────────┘                     │ recv() returns
+           │                                 ▼
+           │ implements           ┌──────────────────────┐
+           ▼                      │  StackRespChannel<C> │
+    «trait»                       │                      │
+  MctpReqChannel                  │  stack: &Stack<C>    │
+  ┌──────────────┐                │  eid: u8             │
+  │ send()       │                │  msg_type: u8        │
+  │ recv()       │                │  tag: u8             │
+  │ remote_eid() │                └──────────┬───────────┘
+  └──────────────┘                           │ implements
+                                             ▼
+                                      «trait»
+                                    MctpRespChannel
+                                    ┌─────────────┐
+                                    │ send()      │
+                                    │ remote_eid()│
+                                    └─────────────┘
+```
+
+---
+
+## Full Call-Flow: Server (Listener) Path
+
+```
+  App                  Stack<C>           StackListener<C>       MctpClient (C)
+   │                      │                     │                      │
+   │  listener(type, t)   │                     │                      │
+   │─────────────────────►│                     │                      │
+   │                      │── listener(type) ──►│                      │
+   │                      │                     │─── client.listener() ►│
+   │                      │                     │◄── Handle ────────────│
+   │◄── StackListener ────│                     │                      │
+   │                      │                     │                      │
+   │  recv(&mut buf)       │                     │                      │
+   │────────────────────────────────────────────►│                      │
+   │                      │                     │── client.recv() ─────►│
+   │                      │                     │◄── RecvMetadata ──────│
+   │                      │                     │  builds StackRespChannel
+   │◄── (meta, payload, StackRespChannel) ───────│                      │
+   │                      │                     │                      │
+   │  resp.send(&reply)   │                     │                      │
+   │─────────────────────────────────────────────────────────────────► │
+   │                      │                     │  client.send(None,..) │
+   │◄── Ok(()) ────────────────────────────────────────────────────────│
+   │                      │                     │                      │
+   │  [drop listener]     │                     │                      │
+   │────────────────────────────────────────────►│                      │
+   │                      │                     │── client.drop_handle()►│
+```
+
+---
+
+## Full Call-Flow: Client (Request) Path
+
+```
+  App                  Stack<C>         StackReqChannel<C>      MctpClient (C)
+   │                      │                    │                      │
+   │  req(eid, timeout)   │                    │                      │
+   │─────────────────────►│                    │                      │
+   │                      │── client.req(eid) ──────────────────────► │
+   │                      │◄── Handle ─────────────────────────────── │
+   │◄── StackReqChannel ──│                    │                      │
+   │                      │                    │                      │
+   │  send(msg_type, buf) │                    │                      │
+   │────────────────────────────────────────── ►│                      │
+   │                      │                    │── client.send(..) ───►│
+   │                      │                    │◄── tag ───────────────│
+   │                      │                    │ sent_tag = Some(tag)  │
+   │◄── Ok(()) ───────────────────────────── ──│                      │
+   │                      │                    │                      │
+   │  recv(&mut buf)      │                    │                      │
+   │────────────────────────────────────────── ►│                      │
+   │                      │                    │── client.recv(..) ───►│
+   │                      │                    │◄── RecvMetadata ──────│
+   │◄── (meta, payload) ──────────────────── ──│                      │
+   │                      │                    │                      │
+   │  [drop channel]      │                    │                      │
+   │────────────────────────────────────────── ►│                      │
+   │                      │                    │── client.drop_handle()►│
+```
+
+---
+
+## Design Patterns Summary
+
+| Pattern | Where | Effect |
+|---------|-------|--------|
+| **Facade** | `Stack<C>` | Single entry point; hides `MctpClient` complexity and handle lifecycle |
+| **Factory Method** | `Stack::req()`, `Stack::listener()` | Produces typed channel structs with lifetime-bound borrows |
+| **Strategy** | `C: MctpClient` generic | Transport swapped at compile time — IPC, sockets, mock, etc. |
+| **RAII / Handle Guard** | `Drop` on `StackReqChannel` & `StackListener` | Handles are released automatically; no explicit cleanup needed |
+
+### Why Facade fits perfectly here
+
+The classic Facade pattern calls for:
+
+1. A complex subsystem with many low-level operations — ✓ (`MctpClient`: `req`, `listener`, `send`, `recv`, `drop_handle`, `set_eid`)
+2. A simplified, cohesive interface for clients — ✓ (`Stack`: `req()` / `listener()` / `get_eid()` / `set_eid()`)
+3. The subsystem remaining accessible directly if needed — ✓ (callers can use `MctpClient` directly; `Stack` does not prevent it)
+
+The Strategy (generic `C`) is a natural complement: the Facade is stable, the strategy behind it changes per platform.
diff --git a/services/mctp/api/src/lib.rs b/services/mctp/api/src/lib.rs
index ffe5b7c..9b1178c 100644
--- a/services/mctp/api/src/lib.rs
+++ b/services/mctp/api/src/lib.rs
@@ -36,10 +36,12 @@
 #![warn(missing_docs)]
 
 mod error;
+pub mod stack;
 mod traits;
 pub mod wire;
 
 pub use error::{MctpError, ResponseCode};
+pub use stack::{Stack, StackListener, StackReqChannel, StackRespChannel};
 pub use traits::{MctpClient, MctpListener, MctpReqChannel, MctpRespChannel};
 
 /// An opaque handle for a listener, request, or response channel.
diff --git a/services/mctp/api/src/stack.rs b/services/mctp/api/src/stack.rs
new file mode 100644
index 0000000..58ff418
--- /dev/null
+++ b/services/mctp/api/src/stack.rs
@@ -0,0 +1,223 @@
+// Licensed under the Apache-2.0 license
+
+//! High-level MCTP stack facade
+//!
+//! Bridges any [`MctpClient`] implementation to the abstract
+//! [`MctpListener`], [`MctpReqChannel`], and [`MctpRespChannel`] traits,
+//! hiding both the concrete MCTP stack implementation and the underlying
+//! OS / transport mechanism.
+//!
+//! ## Usage
+//!
+//! ```rust,ignore
+//! use openprot_mctp_client::IpcMctpClient;
+//! use openprot_mctp_api::stack::Stack;
+//! use openprot_mctp_api::{MctpListener, MctpReqChannel, MctpRespChannel};
+//!
+//! let stack = Stack::new(IpcMctpClient::new(handle::MCTP));
+//! stack.set_eid(8).unwrap();
+//!
+//! // Server side: receive a request and reply
+//! let mut listener = stack.listener(MSG_TYPE_SPDM, 0).unwrap();
+//! let (meta, payload, mut resp) = listener.recv(&mut buf).unwrap();
+//! resp.send(&reply).unwrap();
+//!
+//! // Client side: send a request and receive the response
+//! let mut req = stack.req(remote_eid, 0).unwrap();
+//! req.send(MSG_TYPE_SPDM, &msg).unwrap();
+//! let (meta, response) = req.recv(&mut buf).unwrap();
+//! ```
+
+use crate::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode};
+use crate::traits::{MctpListener, MctpReqChannel, MctpRespChannel};
+
+// ============================================================================
+// Stack
+// ============================================================================
+
+/// An MCTP stack facade backed by any [`MctpClient`] implementation.
+///
+/// `Stack` is the entry point for application code. It wraps a concrete
+/// `MctpClient` and returns typed channel handles whose methods implement
+/// the standard MCTP traits. Applications only depend on those traits;
+/// the underlying stack implementation and OS transport are invisible.
+pub struct Stack<C: MctpClient> {
+    client: C,
+}
+
+impl<C: MctpClient> Stack<C> {
+    /// Create a new stack backed by the given `MctpClient`.
+    pub fn new(client: C) -> Self {
+        Stack { client }
+    }
+
+    /// Get the local endpoint ID.
+    pub fn get_eid(&self) -> u8 {
+        self.client.get_eid()
+    }
+
+    /// Set the local endpoint ID.
+    pub fn set_eid(&self, eid: u8) -> Result<(), MctpError> {
+        self.client.set_eid(eid)
+    }
+
+    /// Open an outbound request channel to `eid`.
+    ///
+    /// `timeout_millis` of 0 means no timeout (block indefinitely).
+    pub fn req(
+        &self,
+        eid: u8,
+        timeout_millis: u32,
+    ) -> Result<StackReqChannel<'_, C>, MctpError> {
+        let handle = self.client.req(eid)?;
+        Ok(StackReqChannel {
+            stack: self,
+            handle,
+            eid,
+            sent_tag: None,
+            timeout: timeout_millis,
+        })
+    }
+
+    /// Register a listener for incoming messages of the given MCTP type.
+    ///
+    /// `timeout_millis` of 0 means no timeout (block indefinitely).
+    pub fn listener(
+        &self,
+        msg_type: u8,
+        timeout_millis: u32,
+    ) -> Result<StackListener<'_, C>, MctpError> {
+        let handle = self.client.listener(msg_type)?;
+        Ok(StackListener {
+            stack: self,
+            handle,
+            timeout: timeout_millis,
+        })
+    }
+}
+
+// ============================================================================
+// Request channel
+// ============================================================================
+
+/// A request channel for sending MCTP requests and receiving responses.
+///
+/// Obtained via [`Stack::req`]. Implements [`MctpReqChannel`].
+pub struct StackReqChannel<'s, C: MctpClient> {
+    stack: &'s Stack<C>,
+    handle: Handle,
+    eid: u8,
+    /// Tag captured after the first `send`; required before `recv` may be called.
+    sent_tag: Option<u8>,
+    timeout: u32,
+}
+
+impl<C: MctpClient> MctpReqChannel for StackReqChannel<'_, C> {
+    fn send(&mut self, msg_type: u8, buf: &[u8]) -> Result<(), MctpError> {
+        if self.sent_tag.is_some() {
+            return Err(MctpError::from_code(ResponseCode::BadArgument));
+        }
+        let tag = self.stack.client.send(
+            Some(self.handle),
+            msg_type,
+            None,
+            None,
+            false,
+            buf,
+        )?;
+        self.sent_tag = Some(tag);
+        Ok(())
+    }
+
+    fn recv<'f>(
+        &mut self,
+        buf: &'f mut [u8],
+    ) -> Result<(RecvMetadata, &'f mut [u8]), MctpError> {
+        if self.sent_tag.is_none() {
+            return Err(MctpError::from_code(ResponseCode::BadArgument));
+        }
+        let meta = self.stack.client.recv(self.handle, self.timeout, buf)?;
+        let len = meta.payload_size;
+        Ok((meta, &mut buf[..len]))
+    }
+
+    fn remote_eid(&self) -> u8 {
+        self.eid
+    }
+}
+
+impl<C: MctpClient> Drop for StackReqChannel<'_, C> {
+    fn drop(&mut self) {
+        self.stack.client.drop_handle(self.handle);
+    }
+}
+
+// ============================================================================
+// Listener
+// ============================================================================
+
+/// A listener that receives incoming MCTP messages of a specific type.
+///
+/// Obtained via [`Stack::listener`]. Implements [`MctpListener`].
+pub struct StackListener<'s, C: MctpClient> {
+    stack: &'s Stack<C>,
+    handle: Handle,
+    timeout: u32,
+}
+
+impl<'s, C: MctpClient> MctpListener for StackListener<'s, C> {
+    type RespChannel<'a>
+        = StackRespChannel<'s, C>
+    where
+        Self: 'a;
+
+    fn recv<'f>(
+        &mut self,
+        buf: &'f mut [u8],
+    ) -> Result<(RecvMetadata, &'f mut [u8], Self::RespChannel<'_>), MctpError> {
+        let meta = self.stack.client.recv(self.handle, self.timeout, buf)?;
+        let len = meta.payload_size;
+        let resp = StackRespChannel {
+            stack: self.stack,
+            eid: meta.remote_eid,
+            msg_type: meta.msg_type,
+            tag: meta.msg_tag,
+        };
+        Ok((meta, &mut buf[..len], resp))
+    }
+}
+
+impl<C: MctpClient> Drop for StackListener<'_, C> {
+    fn drop(&mut self) {
+        self.stack.client.drop_handle(self.handle);
+    }
+}
+
+// ============================================================================
+// Response channel
+// ============================================================================
+
+/// A response channel for replying to an incoming MCTP request.
+///
+/// Returned by [`StackListener::recv`]. Implements [`MctpRespChannel`].
+pub struct StackRespChannel<'s, C: MctpClient> {
+    stack: &'s Stack<C>,
+    eid: u8,
+    msg_type: u8,
+    tag: u8,
+}
+
+impl<C: MctpClient> MctpRespChannel for StackRespChannel<'_, C> {
+    fn send(&mut self, buf: &[u8]) -> Result<(), MctpError> {
+        // Responses pass handle=None; the server distinguishes requests from
+        // responses by the presence or absence of a handle.
+        self.stack
+            .client
+            .send(None, self.msg_type, Some(self.eid), Some(self.tag), false, buf)
+            .map(|_| ())
+    }
+
+    fn remote_eid(&self) -> u8 {
+        self.eid
+    }
+}
diff --git a/services/mctp/server/tests/integration.rs b/services/mctp/server/tests/integration.rs
index 3f1b006..7c35e7a 100644
--- a/services/mctp/server/tests/integration.rs
+++ b/services/mctp/server/tests/integration.rs
@@ -16,6 +16,7 @@
 use std::cell::RefCell;
 
 use mctp::Eid;
+use openprot_mctp_api::stack::Stack;
 use openprot_mctp_api::{MctpClient, MctpListener, MctpReqChannel, MctpRespChannel};
 use openprot_mctp_server::Server;
 
@@ -321,3 +322,162 @@
     assert_eq!(resp.remote_eid, 8);
     assert_eq!(resp.msg_type, 5);
 }
+
+// ---------------------------------------------------------------------------
+// Stack facade (openprot-mctp-api::stack)
+// ---------------------------------------------------------------------------
+//
+// These tests exercise `Stack<DirectClient>` — the same code path used by the
+// real application (`mctp_echo.rs` with `Stack<IpcMctpClient>`), but running
+// entirely on the host with no IPC or embedded target.
+
+/// Echo via `Stack::listener` → `StackListener::recv` → `StackRespChannel::send`.
+///
+/// This is the exact sequence used by `mctp_echo.rs`:
+/// ```ignore
+/// let mut listener = stack.listener(ECHO_MSG_TYPE, 0).unwrap();
+/// let (meta, msg, mut resp) = listener.recv(&mut buf).unwrap();
+/// resp.send(msg).unwrap();
+/// ```
+#[test]
+fn stack_listener_echo() {
+    let buf_a = RefCell::new(Vec::new());
+    let buf_b = RefCell::new(Vec::new());
+
+    let server_a: RefCell<Server<_, 16>> =
+        RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a }));
+    let server_b: RefCell<Server<_, 16>> =
+        RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+    // Application A uses Stack — the same API as in production.
+    let stack_a = Stack::new(DirectClient::new(&server_a));
+    // Application B uses a raw client to send the request and check the reply.
+    let client_b = DirectClient::new(&server_b);
+
+    let mut listener = stack_a.listener(1, 0).expect("listener alloc");
+    let req_handle = client_b.req(8).unwrap();
+
+    // B sends a request
+    client_b
+        .send(Some(req_handle), 1, None, None, false, b"hello from B")
+        .unwrap();
+    transfer(&buf_b, &mut server_a.borrow_mut());
+
+    // A echoes back via Stack facade
+    let mut recv_buf = [0u8; 255];
+    let (meta, payload, mut resp) = listener
+        .recv(&mut recv_buf)
+        .expect("stack listener should receive the message");
+
+    assert_eq!(payload, b"hello from B");
+    assert_eq!(meta.remote_eid, 42);
+
+    resp.send(payload).expect("stack resp send");
+
+    // Deliver A → B and verify
+    transfer(&buf_a, &mut server_b.borrow_mut());
+
+    let mut resp_buf = [0u8; 255];
+    let resp_meta = client_b
+        .recv(req_handle, 0, &mut resp_buf)
+        .expect("B should receive the echo");
+
+    assert_eq!(&resp_buf[..resp_meta.payload_size], b"hello from B");
+    assert_eq!(resp_meta.remote_eid, 8);
+    assert_eq!(resp_meta.msg_type, 1);
+}
+
+/// Echo via `Stack::req` → `StackReqChannel::send` + `StackReqChannel::recv`.
+#[test]
+fn stack_req_channel_roundtrip() {
+    let buf_a = RefCell::new(Vec::new());
+    let buf_b = RefCell::new(Vec::new());
+
+    let server_a: RefCell<Server<_, 16>> =
+        RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a }));
+    let server_b: RefCell<Server<_, 16>> =
+        RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+    let client_a = DirectClient::new(&server_a);
+    // B uses Stack for the request side.
+    let stack_b = Stack::new(DirectClient::new(&server_b));
+
+    let listener_handle = client_a.listener(1).unwrap();
+
+    let mut req = stack_b.req(8, 0).expect("req channel alloc");
+    req.send(1, b"stack req test").expect("req send");
+    assert_eq!(req.remote_eid(), 8);
+
+    transfer(&buf_b, &mut server_a.borrow_mut());
+
+    // A echoes back manually
+    let mut echo_buf = [0u8; 255];
+    let meta = client_a.recv(listener_handle, 0, &mut echo_buf).unwrap();
+    client_a
+        .send(
+            None,
+            meta.msg_type,
+            Some(meta.remote_eid),
+            Some(meta.msg_tag),
+            false,
+            &echo_buf[..meta.payload_size],
+        )
+        .unwrap();
+
+    transfer(&buf_a, &mut server_b.borrow_mut());
+
+    let mut resp_buf = [0u8; 255];
+    let (resp_meta, resp_payload) = req.recv(&mut resp_buf).expect("req channel recv");
+
+    assert_eq!(resp_payload, b"stack req test");
+    assert_eq!(resp_meta.remote_eid, 8);
+    assert_eq!(resp_meta.msg_type, 1);
+}
+
+/// Both sides use `Stack` — listener on A, request channel on B.
+#[test]
+fn stack_both_sides_echo() {
+    let buf_a = RefCell::new(Vec::new());
+    let buf_b = RefCell::new(Vec::new());
+
+    let server_a: RefCell<Server<_, 16>> =
+        RefCell::new(Server::new(Eid(8), 0, BufferSender { packets: &buf_a }));
+    let server_b: RefCell<Server<_, 16>> =
+        RefCell::new(Server::new(Eid(42), 0, BufferSender { packets: &buf_b }));
+
+    let stack_a = Stack::new(DirectClient::new(&server_a));
+    let stack_b = Stack::new(DirectClient::new(&server_b));
+
+    let mut listener = stack_a.listener(1, 0).expect("listener alloc");
+    let mut req = stack_b.req(8, 0).expect("req alloc");
+
+    // B sends
+    req.send(1, b"both sides").expect("req send");
+    transfer(&buf_b, &mut server_a.borrow_mut());
+
+    // A receives and replies via Stack
+    let mut recv_buf = [0u8; 255];
+    let (_, payload, mut resp) = listener.recv(&mut recv_buf).expect("listener recv");
+    assert_eq!(payload, b"both sides");
+    resp.send(payload).expect("resp send");
+    transfer(&buf_a, &mut server_b.borrow_mut());
+
+    // B receives via Stack req channel
+    let mut resp_buf = [0u8; 255];
+    let (meta, data) = req.recv(&mut resp_buf).expect("req recv");
+    assert_eq!(data, b"both sides");
+    assert_eq!(meta.remote_eid, 8);
+}
+
+/// `Stack::get_eid` and `Stack::set_eid` delegate correctly.
+#[test]
+fn stack_eid_accessors() {
+    let server: RefCell<Server<_, 16>> =
+        RefCell::new(Server::new(Eid(8), 0, common::DroppingBufferSender));
+    let stack = Stack::new(DirectClient::new(&server));
+
+    assert_eq!(stack.get_eid(), 8);
+    stack.set_eid(99).expect("set_eid should succeed");
+    assert_eq!(stack.get_eid(), 99);
+}
+
diff --git a/target/ast1060-evb/mctp/mctp_echo.rs b/target/ast1060-evb/mctp/mctp_echo.rs
index ff08843..4ca26be 100644
--- a/target/ast1060-evb/mctp/mctp_echo.rs
+++ b/target/ast1060-evb/mctp/mctp_echo.rs
@@ -17,7 +17,8 @@
 #![no_main]
 #![no_std]
 
-use openprot_mctp_api::MctpClient;
+use openprot_mctp_api::stack::Stack;
+use openprot_mctp_api::{MctpListener, MctpRespChannel};
 use openprot_mctp_client::IpcMctpClient;
 
 use pw_status::Result;
@@ -32,19 +33,19 @@
 fn mctp_echo_loop() -> Result<()> {
     pw_log::info!("MCTP echo starting");
 
-    let client = IpcMctpClient::new(handle::MCTP);
+    let stack = Stack::new(IpcMctpClient::new(handle::MCTP));
 
-    // Register a listener for type-1 messages
-    let listener = client
-        .listener(ECHO_MSG_TYPE)
+    let mut listener = stack
+        .listener(ECHO_MSG_TYPE, 0)
         .map_err(|_| pw_status::Error::Internal)?;
 
     let mut buf = [0u8; 1024];
 
     loop {
-        // Block until a message arrives
-        let meta = client
-            .recv(listener, 0, &mut buf)
+        // Block until a message arrives; recv returns the payload slice and
+        // a response channel already bound to the sender's EID and tag.
+        let (meta, msg, mut resp) = listener
+            .recv(&mut buf)
             .map_err(|_| pw_status::Error::Internal)?;
 
         pw_log::info!(
@@ -53,18 +54,10 @@
             meta.remote_eid as u32,
         );
 
-        // Echo the payload back
-        let payload = &buf[..meta.payload_size];
-        client
-            .send(
-                None,                    // no request handle (this is a response)
-                meta.msg_type,           // same message type
-                Some(meta.remote_eid),   // back to sender
-                Some(meta.msg_tag),      // same tag
-                meta.msg_ic,             // preserve integrity check
-                payload,
-            )
-            .map_err(|_| pw_status::Error::Internal)?;
+        // Echo the payload back through the response channel.
+        if let Err(_) = resp.send(msg) {
+            pw_log::error!("Echo: failed to send response to EID {}", meta.remote_eid as u32);
+        }
     }
 }