feat: add Pigweed platform integration for MCTP service (Phase 6)

- MCTP server main.rs with IPC dispatch loop (NoopSender for bring-up)
- Wire IpcMctpClient::send_recv to syscall::channel_transact (pigweed feature)
- Bazel BUILD files for all MCTP crates (api, server, client, transport-i2c)
- system.json5 with I2C server + MCTP server + echo app
- Echo binary as Pigweed userspace process (target/ast1060-evb/mctp/)

diff --git a/port-mctp-echo.md b/port-mctp-echo.md
index bba4a31..8124f7e 100644
--- a/port-mctp-echo.md
+++ b/port-mctp-echo.md
@@ -81,36 +81,37 @@
 - [x] IPC client (`services/mctp/client/`) — `IpcMctpClient` implementing `MctpClient` via wire protocol + IPC
 - [x] Server-side IPC dispatch (`services/mctp/server/src/dispatch.rs`) — decodes wire requests, calls `Server`
 - [x] Wire-protocol dispatch integration test (`tests/dispatch.rs`) — full round-trip through wire encoding
-- [ ] Echo binary as Pigweed userspace process (needs Phase 6 platform wiring)
-- [ ] Wire up with server + I2C transport for an end-to-end demo
+- [x] Echo binary as Pigweed userspace process (`target/ast1060-evb/mctp/mctp_echo.rs`)
+- [ ] Wire up with server + I2C transport for an end-to-end demo (needs real I2C sender in server `main.rs`)
 
-## Phase 6: Pigweed Platform Integration — NOT STARTED
+## Phase 6: Pigweed Platform Integration — MOSTLY DONE
 
 Wire up the MCTP server as a Pigweed userspace process on the AST1060-EVB (`target/ast1060-evb/`).
 
-- [ ] MCTP server `main.rs`: event loop driven by pw_kernel IPC/channels
+- [x] MCTP server `main.rs`: event loop driven by pw_kernel IPC/channels
   - Replaces hubris `sys_recv_open` / notifications / `idol` IPC dispatch
   - Uses `dispatch_mctp_op` for IPC request handling
   - Follows the pattern established by `services/i2c/server/`
-- [ ] Connect `IpcMctpClient::send_recv` to `syscall::channel_transact`
-- [ ] Bazel BUILD files for each new crate (following `services/i2c/` pattern)
-- [ ] `system.json5` entry for MCTP server + echo processes
-- [ ] Integration with `target/ast1060-evb/` platform definition
+  - Uses NoopSender for initial bring-up; real I2cSender wiring is TODO
+- [x] Connect `IpcMctpClient::send_recv` to `syscall::channel_transact` (gated behind `pigweed` feature)
+- [x] Bazel BUILD files for each new crate (following `services/i2c/` pattern)
+- [x] `system.json5` entry for MCTP server + echo processes (`target/ast1060-evb/mctp/`)
+- [x] Integration with `target/ast1060-evb/` platform definition (`target.rs`, `BUILD.bazel`)
 
 ## Phase 7: Testing & Documentation — PARTIALLY DONE
 
 - [x] Wire protocol unit tests (7 tests in `api/src/wire.rs`)
 - [x] Echo integration tests with client/server partition (2 tests in `server/tests/echo.rs`)
 - [x] Wire-protocol dispatch integration tests (2 tests in `server/tests/dispatch.rs`)
+- [x] README for each MCTP crate (`api/`, `server/`, `client/`, `transport-i2c/`)
 - [ ] QEMU-based end-to-end test (following `services/i2c/` test pattern)
 - [ ] Update `docs/src/specification/middleware/mctp.md` with implementation status
-- [ ] README for `services/mctp/`
 
 ---
 
 ## Current Status
 
-**Phases 1–3 complete, Phase 5 mostly done.** All library code is written: wire protocol, IPC client, server dispatch, and transport bindings. 11 tests pass. The remaining work is Phase 6 (Pigweed platform integration): server `main.rs`, Bazel BUILD files, and `system.json5`.
+**Phases 1–3, 5, and 6 mostly complete.** All library code, IPC dispatch, Bazel BUILD files, system configuration, and echo binary are written. 11 tests pass. Remaining work: wire up real I2cSender in server main.rs (replace NoopSender), QEMU e2e test, and docs update.
 
 ## Key Dependencies
 
diff --git a/services/mctp/api/BUILD.bazel b/services/mctp/api/BUILD.bazel
new file mode 100644
index 0000000..39b1944
--- /dev/null
+++ b/services/mctp/api/BUILD.bazel
@@ -0,0 +1,20 @@
+# Licensed under the Apache-2.0 license
+
+load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")
+
+rust_library(
+    name = "mctp_api",
+    srcs = glob(["src/**/*.rs"]),
+    crate_name = "openprot_mctp_api",
+    edition = "2024",
+    visibility = ["//visibility:public"],
+    deps = [
+        "@rust_crates//:heapless",
+        "@rust_crates//:zerocopy",
+    ],
+)
+
+rust_test(
+    name = "mctp_api_test",
+    crate = ":mctp_api",
+)
diff --git a/services/mctp/client/BUILD.bazel b/services/mctp/client/BUILD.bazel
new file mode 100644
index 0000000..fb4cd8d
--- /dev/null
+++ b/services/mctp/client/BUILD.bazel
@@ -0,0 +1,15 @@
+# Licensed under the Apache-2.0 license
+
+load("@rules_rust//rust:defs.bzl", "rust_library")
+
+rust_library(
+    name = "mctp_client",
+    srcs = ["src/lib.rs"],
+    crate_name = "openprot_mctp_client",
+    edition = "2024",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//services/mctp/api:mctp_api",
+        "@pigweed//pw_kernel/userspace",
+    ],
+)
diff --git a/services/mctp/client/Cargo.toml b/services/mctp/client/Cargo.toml
index 7de8400..edaad9e 100644
--- a/services/mctp/client/Cargo.toml
+++ b/services/mctp/client/Cargo.toml
@@ -7,5 +7,10 @@
 description = "MCTP IPC client for OpenPRoT (Pigweed userspace)"
 license = "Apache-2.0"
 
+[features]
+default = []
+# Enable Pigweed IPC transport (Bazel builds only; requires `userspace` crate).
+pigweed = []
+
 [dependencies]
 openprot-mctp-api = { path = "../api" }
diff --git a/services/mctp/client/src/lib.rs b/services/mctp/client/src/lib.rs
index 4eeaa11..fa0af29 100644
--- a/services/mctp/client/src/lib.rs
+++ b/services/mctp/client/src/lib.rs
@@ -85,19 +85,25 @@
         Ok((header, resp_len))
     }
 
-    /// Platform-specific send/receive.
+    /// Platform-specific send/receive via Pigweed IPC.
     ///
-    /// In a Pigweed build, this calls `syscall::channel_transact`.
-    /// For now, this is a stub that returns an error.
+    /// Uses `syscall::channel_transact` when built under Pigweed (Bazel).
+    /// Returns a stub error when built under Cargo (no `userspace` crate).
+    #[cfg(feature = "pigweed")]
+    fn send_recv(&self, req_len: usize) -> Result<usize, MctpError> {
+        let mut inner = self.inner.borrow_mut();
+        userspace::syscall::channel_transact(
+            self.handle,
+            &inner.request_buf[..req_len],
+            &mut inner.response_buf,
+            userspace::time::Instant::MAX,
+        )
+        .map_err(|_| MctpError::from_code(ResponseCode::InternalError))
+    }
+
+    /// Stub for non-Pigweed builds (Cargo workspace).
+    #[cfg(not(feature = "pigweed"))]
     fn send_recv(&self, _req_len: usize) -> Result<usize, MctpError> {
-        // TODO(Phase 6): Replace with:
-        //   let mut inner = self.inner.borrow_mut();
-        //   syscall::channel_transact(
-        //       self.handle,
-        //       &inner.request_buf[..req_len],
-        //       &mut inner.response_buf,
-        //       Instant::MAX,
-        //   ).map_err(|_| MctpError::from_code(ResponseCode::InternalError))
         Err(MctpError::from_code(ResponseCode::InternalError))
     }
 }
diff --git a/services/mctp/server/BUILD.bazel b/services/mctp/server/BUILD.bazel
new file mode 100644
index 0000000..2374fe8
--- /dev/null
+++ b/services/mctp/server/BUILD.bazel
@@ -0,0 +1,54 @@
+# Licensed under the Apache-2.0 license
+
+load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
+load("@pigweed//pw_kernel/tooling:app_package.bzl", "app_package")
+
+rust_library(
+    name = "mctp_server_lib",
+    srcs = [
+        "src/dispatch.rs",
+        "src/lib.rs",
+        "src/server.rs",
+    ],
+    crate_name = "openprot_mctp_server",
+    edition = "2024",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//services/mctp/api:mctp_api",
+        "@rust_crates//:heapless",
+        "@rust_crates//:mctp",
+        "@rust_crates//:mctp-lib",
+    ],
+)
+
+rust_test(
+    name = "mctp_server_test",
+    crate = ":mctp_server_lib",
+)
+
+rust_binary(
+    name = "mctp_server",
+    srcs = ["src/main.rs"],
+    edition = "2024",
+    tags = ["kernel"],
+    visibility = ["//visibility:public"],
+    deps = [
+        ":app_mctp_server",
+        ":mctp_server_lib",
+        "//services/mctp/api:mctp_api",
+        "@rust_crates//:mctp",
+        "@rust_crates//:mctp-lib",
+        "@pigweed//pw_kernel/syscall:syscall_user",
+        "@pigweed//pw_kernel/userspace",
+        "@pigweed//pw_log/rust:pw_log",
+        "@pigweed//pw_status/rust:pw_status",
+    ],
+)
+
+app_package(
+    name = "app_mctp_server",
+    app_name = "mctp_server",
+    edition = "2024",
+    system_config = "//target/ast1060-evb/mctp:system_config",
+    tags = ["kernel"],
+)
diff --git a/services/mctp/server/Cargo.toml b/services/mctp/server/Cargo.toml
index 73a7acf..60e7209 100644
--- a/services/mctp/server/Cargo.toml
+++ b/services/mctp/server/Cargo.toml
@@ -6,6 +6,8 @@
 edition = "2021"
 description = "MCTP server implementation for OpenPRoT"
 license = "Apache-2.0"
+# main.rs is a Pigweed binary built by Bazel only.
+autobins = false
 
 [dependencies]
 openprot-mctp-api = { path = "../api" }
diff --git a/services/mctp/server/src/main.rs b/services/mctp/server/src/main.rs
new file mode 100644
index 0000000..80242dc
--- /dev/null
+++ b/services/mctp/server/src/main.rs
@@ -0,0 +1,153 @@
+// Licensed under the Apache-2.0 license
+
+//! MCTP Server — IPC Dispatch Loop
+//!
+//! Userspace service that receives MCTP requests over a Pigweed IPC channel,
+//! dispatches them to the MCTP server core, and responds with results.
+//!
+//! # Architecture
+//!
+//! ```text
+//! ┌─ Client ──────────────────────────┐
+//! │ channel_transact(request)         │
+//! └──────────────┬────────────────────┘
+//!                │ IPC channel
+//!                ▼
+//! ┌─ This Server ─────────────────────┐
+//! │ object_wait(READABLE)             │
+//! │ channel_read → MctpRequestHeader  │
+//! │ dispatch_mctp_op(op, server)      │
+//! │ channel_respond ← MctpRespHeader  │
+//! └──────────────┬────────────────────┘
+//!                │ mctp-stack Router
+//!                ▼
+//! ┌─ I2C Transport ──────────────────┐
+//! │ I2cSender → I2C Server IPC      │
+//! └──────────────────────────────────┘
+//! ```
+//!
+//! # IPC Pattern
+//!
+//! Follows the same loop as `services/i2c/server/src/main.rs`:
+//!
+//! 1. `object_wait(handle, READABLE)` — block until a client sends a request
+//! 2. `channel_read(handle)` — read the raw request bytes
+//! 3. Parse `MctpRequestHeader`, dispatch via `dispatch_mctp_op`
+//! 4. `channel_respond(handle)` — send response header + data
+//!
+//! # Handle Binding
+//!
+//! The IPC handle is provided by the `app_package` Bazel rule, which generates
+//! `app_mctp_server::handle::MCTP` from the system configuration.
+
+#![no_main]
+#![no_std]
+
+use openprot_mctp_api::wire::{MctpRequestHeader, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, MAX_PAYLOAD_SIZE};
+use openprot_mctp_api::ResponseCode;
+use openprot_mctp_server::dispatch;
+
+use pw_status::Result;
+use userspace::entry;
+use userspace::syscall::{self, Signals};
+use userspace::time::Instant;
+
+use app_mctp_server::handle;
+
+// ---------------------------------------------------------------------------
+// Server loop
+// ---------------------------------------------------------------------------
+
+fn mctp_server_loop() -> Result<()> {
+    pw_log::info!("MCTP server starting");
+
+    // TODO(Phase 6): Initialize transport binding.
+    // The I2C sender needs an IpcI2cClient bound to the I2C server channel.
+    // For now, we create a stub sender that will be replaced with a real
+    // I2cSender<IpcI2cClient> once the I2C channel handle is wired up.
+    //
+    // let i2c_client = i2c_client::IpcI2cClient::new(handle::I2C);
+    // let sender = I2cSender::new(i2c_client, BusIndex::BUS_0, OWN_I2C_ADDR);
+    // let mut server = Server::new(Eid(OWN_EID), 0, sender);
+
+    // For initial bring-up, use a no-op sender so the IPC dispatch loop
+    // can be tested without I2C hardware.
+    let mut server = openprot_mctp_server::Server::<NoopSender, 16>::new(
+        mctp::Eid(8), // default EID
+        0,
+        NoopSender,
+    );
+
+    let mut request_buf = [0u8; MAX_REQUEST_SIZE];
+    let mut response_buf = [0u8; MAX_RESPONSE_SIZE];
+    let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE];
+
+    loop {
+        // Block until a client sends a request
+        syscall::object_wait(handle::MCTP, Signals::READABLE, Instant::MAX)?;
+
+        // Read the request
+        let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?;
+
+        if len < MctpRequestHeader::SIZE {
+            // Truncated request — respond with error
+            let resp = openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument);
+            response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE]
+                .copy_from_slice(&resp.to_bytes());
+            syscall::channel_respond(
+                handle::MCTP,
+                &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE],
+            )?;
+            continue;
+        }
+
+        // Dispatch and respond
+        let response_len = dispatch::dispatch_mctp_op(
+            &request_buf[..len],
+            &mut response_buf,
+            &mut server,
+            &mut recv_buf,
+        );
+        syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?;
+    }
+}
+
+// ---------------------------------------------------------------------------
+// No-op sender for initial bring-up
+// ---------------------------------------------------------------------------
+
+/// Stub sender that does nothing. Used for IPC dispatch testing before
+/// the I2C transport is wired up.
+struct NoopSender;
+
+impl mctp_stack::Sender for NoopSender {
+    fn send_vectored(
+        &mut self,
+        _fragmenter: mctp_stack::fragment::Fragmenter,
+        _payload: &[&[u8]],
+    ) -> mctp::Result<mctp::Tag> {
+        Err(mctp::Error::TxFailure)
+    }
+
+    fn get_mtu(&self) -> usize {
+        64
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Entry point
+// ---------------------------------------------------------------------------
+
+#[entry]
+fn entry() -> ! {
+    if let Err(e) = mctp_server_loop() {
+        pw_log::error!("MCTP server error: {}", e as u32);
+        let _ = syscall::debug_shutdown(Err(e));
+    }
+    loop {}
+}
+
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
diff --git a/services/mctp/transport-i2c/BUILD.bazel b/services/mctp/transport-i2c/BUILD.bazel
new file mode 100644
index 0000000..dc7d780
--- /dev/null
+++ b/services/mctp/transport-i2c/BUILD.bazel
@@ -0,0 +1,19 @@
+# Licensed under the Apache-2.0 license
+
+load("@rules_rust//rust:defs.bzl", "rust_library")
+
+rust_library(
+    name = "mctp_transport_i2c",
+    srcs = glob(["src/**/*.rs"]),
+    crate_name = "openprot_mctp_transport_i2c",
+    edition = "2024",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//services/i2c/api:i2c_api",
+        "//services/mctp/api:mctp_api",
+        "@rust_crates//:embedded-hal",
+        "@rust_crates//:heapless",
+        "@rust_crates//:mctp",
+        "@rust_crates//:mctp-lib",
+    ],
+)
diff --git a/target/ast1060-evb/mctp/BUILD.bazel b/target/ast1060-evb/mctp/BUILD.bazel
new file mode 100644
index 0000000..f813178
--- /dev/null
+++ b/target/ast1060-evb/mctp/BUILD.bazel
@@ -0,0 +1,112 @@
+# Licensed under the Apache-2.0 license
+
+load("@pigweed//pw_kernel/tooling:app_package.bzl", "app_package")
+load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test")
+load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen")
+load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script")
+load("@rules_rust//rust:defs.bzl", "rust_binary")
+load("//target/ast1060-evb:defs.bzl", "TARGET_COMPATIBLE_WITH")
+load("//target:uart_boot_image.bzl", "uart_boot_image")
+
+# ---------------------------------------------------------------------------
+# MCTP echo binary (userspace process)
+# ---------------------------------------------------------------------------
+
+rust_binary(
+    name = "mctp_echo",
+    srcs = ["mctp_echo.rs"],
+    edition = "2024",
+    tags = ["kernel"],
+    deps = [
+        ":app_mctp_echo",
+        "//services/mctp/api:mctp_api",
+        "//services/mctp/client:mctp_client",
+        "@pigweed//pw_kernel/syscall:syscall_user",
+        "@pigweed//pw_kernel/userspace",
+        "@pigweed//pw_log/rust:pw_log",
+        "@pigweed//pw_status/rust:pw_status",
+    ],
+)
+
+app_package(
+    name = "app_mctp_echo",
+    app_name = "mctp_echo",
+    edition = "2024",
+    system_config = ":system_config",
+    tags = ["kernel"],
+)
+
+# ---------------------------------------------------------------------------
+# System image: I2C server + MCTP server + echo client
+# ---------------------------------------------------------------------------
+
+system_image(
+    name = "mctp",
+    apps = [
+        ":mctp_echo",
+        "//services/i2c/server:i2c_server",
+        "//services/mctp/server:mctp_server",
+    ],
+    kernel = ":target",
+    platform = "//target/ast1060-evb",
+    system_config = ":system_config",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    visibility = ["//visibility:public"],
+)
+
+system_image_test(
+    name = "mctp_test",
+    image = ":mctp",
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+)
+
+# Generate UART bootable image
+uart_boot_image(
+    name = "mctp_uart",
+    src = ":mctp",
+    out = "mctp_uart.bin",
+)
+
+filegroup(
+    name = "system_config",
+    srcs = ["system.json5"],
+    visibility = ["//visibility:public"],
+)
+
+target_codegen(
+    name = "codegen",
+    arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
+    system_config = ":system_config",
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+)
+
+target_linker_script(
+    name = "linker_script",
+    system_config = ":system_config",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    template = "//target/ast1060-evb:linker_script_template",
+)
+
+rust_binary(
+    name = "target",
+    srcs = [
+        "target.rs",
+    ],
+    edition = "2024",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    deps = [
+        ":codegen",
+        ":linker_script",
+        "//target/ast1060-evb:entry",
+        "//target/ast1060-evb:console_backend_uart",
+        "@rust_crates//:cortex-m-semihosting",
+        "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
+        "@pigweed//pw_kernel/kernel",
+        "@pigweed//pw_kernel/target:target_common",
+        "@pigweed//pw_kernel/userspace",
+        "@pigweed//pw_log/rust:pw_log",
+    ],
+)
diff --git a/target/ast1060-evb/mctp/mctp_echo.rs b/target/ast1060-evb/mctp/mctp_echo.rs
new file mode 100644
index 0000000..ff08843
--- /dev/null
+++ b/target/ast1060-evb/mctp/mctp_echo.rs
@@ -0,0 +1,83 @@
+// Licensed under the Apache-2.0 license
+
+//! MCTP Echo Application
+//!
+//! Listens for MCTP type-1 (vendor-defined) messages and echoes the
+//! payload back to the sender. This is a direct port of the Hubris
+//! `task/mctp-echo/` task.
+//!
+//! # Architecture
+//!
+//! The echo app is a simple loop:
+//! 1. Register a listener for MCTP message type 1
+//! 2. Receive a message (blocking)
+//! 3. Send the payload back to the sender
+//! 4. Repeat
+
+#![no_main]
+#![no_std]
+
+use openprot_mctp_api::MctpClient;
+use openprot_mctp_client::IpcMctpClient;
+
+use pw_status::Result;
+use userspace::entry;
+use userspace::syscall;
+
+use app_mctp_echo::handle;
+
+/// MCTP message type for echo (vendor-defined type 1).
+const ECHO_MSG_TYPE: u8 = 1;
+
+fn mctp_echo_loop() -> Result<()> {
+    pw_log::info!("MCTP echo starting");
+
+    let client = IpcMctpClient::new(handle::MCTP);
+
+    // Register a listener for type-1 messages
+    let listener = client
+        .listener(ECHO_MSG_TYPE)
+        .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)
+            .map_err(|_| pw_status::Error::Internal)?;
+
+        pw_log::info!(
+            "Echo: received {} bytes from EID {}",
+            meta.payload_size as u32,
+            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)?;
+    }
+}
+
+#[entry]
+fn entry() -> ! {
+    if let Err(e) = mctp_echo_loop() {
+        pw_log::error!("MCTP echo error: {}", e as u32);
+        let _ = syscall::debug_shutdown(Err(e));
+    }
+    loop {}
+}
+
+#[panic_handler]
+fn panic(_info: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
diff --git a/target/ast1060-evb/mctp/system.json5 b/target/ast1060-evb/mctp/system.json5
new file mode 100644
index 0000000..b0a3b86
--- /dev/null
+++ b/target/ast1060-evb/mctp/system.json5
@@ -0,0 +1,105 @@
+// Licensed under the Apache-2.0 license
+
+// AST1060-EVB MCTP Echo Server + Client Configuration
+// ARM Cortex-M4 @ 200 MHz
+// 768KB SRAM (640KB usable), executes from RAM
+//
+// Memory Layout (PMSAv7-friendly, power-of-2 aligned regions):
+//   0x00000000 - 0x000004A0: Vector table + kernel annotations (1184 bytes)
+//   0x000004A0 - 0x00020000: Kernel code (~126KB)
+//   0x00020000 - 0x00080000: App flash (384KB: 3 apps × 128KB)
+//   0x00080000 - 0x000B0000: App RAM (192KB: 3 apps × 64KB)
+//   0x000B0000 - 0x000C0000: Kernel RAM (64KB)
+//
+// Total: ~768KB, fits within AST1060's 768KB SRAM.
+{
+    arch: {
+        type: "armv7m",
+        vector_table_start_address: 0x00000000,
+        vector_table_size_bytes: 1184,  // 0x4A0 (272 vectors + thread/stack annotations)
+    },
+    kernel: {
+        flash_start_address: 0x000004A0,  // After vector table + annotations
+        flash_size_bytes: 129888,         // ~126KB (ends at 0x00020000, power-of-2 boundary)
+        ram_start_address: 0x000B0000,    // After all app RAM
+        ram_size_bytes: 65536,            // 64KB
+    },
+    apps: [
+        // ──── I2C Server ────
+        // Owns the AST1060 I2C controllers.
+        // The MCTP server uses I2C as its transport.
+        {
+            name: "i2c_server",
+            flash_size_bytes: 131072,     // 128KB for server code
+            ram_size_bytes: 65536,        // 64KB RAM (register maps + buffers)
+            process: {
+                name: "i2c server process",
+                objects: [
+                    {
+                        name: "I2C",
+                        type: "channel_handler",  // Server side of client→server channel
+                    },
+                ],
+                threads: [
+                    {
+                        name: "i2c server thread",
+                        stack_size_bytes: 4096,   // 4KB stack
+                    },
+                ],
+            },
+        },
+        // ──── MCTP Server ────
+        // Runs the MCTP stack. Receives MCTP operations via IPC,
+        // uses I2C transport for on-the-wire communication.
+        {
+            name: "mctp_server",
+            flash_size_bytes: 131072,     // 128KB for MCTP server code
+            ram_size_bytes: 65536,        // 64KB RAM (Router state, buffers)
+            process: {
+                name: "mctp server process",
+                objects: [
+                    {
+                        name: "MCTP",
+                        type: "channel_handler",  // Server side: echo client → MCTP server
+                    },
+                    {
+                        name: "I2C",
+                        type: "channel_initiator",  // Client side: MCTP server → I2C server
+                        handler_app: "i2c_server",
+                        handler_object_name: "I2C",
+                    },
+                ],
+                threads: [
+                    {
+                        name: "mctp server thread",
+                        stack_size_bytes: 8192,   // 8KB stack (Router + fragmentation buffers)
+                    },
+                ],
+            },
+        },
+        // ──── MCTP Echo Client ────
+        // Listens for MCTP type-1 messages and echoes the payload back.
+        {
+            name: "mctp_echo",
+            flash_size_bytes: 131072,     // 128KB for echo app
+            ram_size_bytes: 65536,        // 64KB RAM
+            process: {
+                name: "mctp echo process",
+                objects: [
+                    {
+                        name: "MCTP",
+                        type: "channel_initiator",  // Client side: echo → MCTP server
+                        handler_app: "mctp_server",
+                        handler_object_name: "MCTP",
+                    },
+                ],
+                threads: [
+                    {
+                        name: "mctp echo thread",
+                        stack_size_bytes: 4096,   // 4KB stack
+                    },
+                ],
+            },
+        },
+    ],
+}
diff --git a/target/ast1060-evb/mctp/target.rs b/target/ast1060-evb/mctp/target.rs
new file mode 100644
index 0000000..3cb0a2e
--- /dev/null
+++ b/target/ast1060-evb/mctp/target.rs
@@ -0,0 +1,38 @@
+// Licensed under the Apache-2.0 license
+
+//! AST1060-EVB MCTP Echo Target
+//!
+//! This target runs the I2C server, MCTP server, and MCTP echo application
+//! as separate userspace processes communicating over IPC channels.
+
+#![no_std]
+#![no_main]
+
+use cortex_m_semihosting::debug::{EXIT_FAILURE, EXIT_SUCCESS, exit};
+use target_common::{TargetInterface, declare_target};
+use {console_backend as _, entry as _};
+
+pub struct Target {}
+
+impl TargetInterface for Target {
+    const NAME: &'static str = "AST1060-EVB MCTP Echo";
+
+    fn main() -> ! {
+        codegen::start();
+        #[expect(clippy::empty_loop)]
+        loop {}
+    }
+
+    fn shutdown(code: u32) -> ! {
+        pw_log::info!("Shutting down with code {}", code as u32);
+        let status = match code {
+            0 => EXIT_SUCCESS,
+            _ => EXIT_FAILURE,
+        };
+        exit(status);
+        #[expect(clippy::empty_loop)]
+        loop {}
+    }
+}
+
+declare_target!(Target);