diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index e3bac97..e0c88cc 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -99,5 +99,56 @@ set_property(
PROPERTY INTERPROCEDURAL_OPTIMIZATION ${UMB_IPO_SUPPORTED}
)
+add_executable(
+ umb_echo_server
+ umb_echo_server.cpp
+)
+target_link_libraries(
+ umb_echo_server
+ PRIVATE
+ Boost::boost
+ umb
+ test_msg_library
+)
+
+set_property(
+ TARGET umb_echo_server
+ PROPERTY INTERPROCEDURAL_OPTIMIZATION ${UMB_IPO_SUPPORTED}
+)
+
+if (MSCV)
+ set(
+ UMB_ECHO_SERVER_COMPILE_OPTIONS
+ /await
+ )
+else ()
+ set(
+ UMB_ECHO_SERVER_COMPILE_OPTIONS
+ -fcoroutines
+ )
+endif ()
+
+target_compile_options(
+ umb_echo_server
+ PRIVATE
+ ${UMB_ECHO_SERVER_COMPILE_OPTIONS}
+)
+
+target_compile_features(
+ umb_echo_server
+ PRIVATE
+ cxx_std_23
+)
+
+# Avoid executable not being found in later targets due to
+# MSVC added /Debug /Release paths.
+if (MSVC)
+ set_target_properties(umb_echo_server
+ PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY_DEBUG ${CMAKE_BINARY_DIR}
+ RUNTIME_OUTPUT_DIRECTORY_RELEASE ${CMAKE_BINARY_DIR}
+ )
+endif ()
+
# TODO: clean up all the unneeded dependency links. The graph is a mess.
# TODO: make reusable CMake functions to be used in other projects.
diff --git a/tests/umb_echo_server.cpp b/tests/umb_echo_server.cpp
new file mode 100644
index 0000000..767aa8d
--- /dev/null
+++ b/tests/umb_echo_server.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 Tuomo Kriikkula
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ *
+ * ****************************************************************************
+ *
+ * Copyright (c) 2003-2023 Christopher M. Kohlhoff (chris at kohlhoff dot com)
+ *
+ * Distributed under the Boost Software License, Version 1.0. (See accompanying
+ * file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+ *
+ */
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "umb/umb.hpp"
+#include "TestMessages.umb.hpp"
+
+using boost::asio::ip::tcp;
+using boost::asio::awaitable;
+using boost::asio::co_spawn;
+using boost::asio::detached;
+using boost::asio::use_awaitable;
+namespace this_coro = boost::asio::this_coro;
+
+#if defined(BOOST_ASIO_ENABLE_HANDLER_TRACKING)
+# define use_awaitable \
+ boost::asio::use_awaitable_t(__FILE__, __LINE__, __PRETTY_FUNCTION__)
+#endif
+
+awaitable echo(tcp::socket socket)
+{
+ try
+ {
+ std::array data{};
+ std::array msg_buf{};
+
+ for (;;)
+ {
+ std::size_t n = co_await socket.async_read_some(boost::asio::buffer(data),
+ use_awaitable);
+
+ // Parse UMB message.
+ // Serialize it to bytes, send it back.
+
+ const auto size = static_cast(data[0]);
+ std::cout << std::format("size: {}\n", size);
+
+ if (n < size)
+ {
+ std::cout << std::format("got {} bytes, size indicates {}, waiting for more...\n",
+ n, size);
+ throw std::runtime_error("TODO: handle this");
+ }
+
+ const auto part = data[1];
+ std::cout << std::format("part: {}\n", part);
+
+ const auto type = static_cast(
+ data[2] | (data[3] << 8));
+ std::cout << std::format("type: {}\n", testmessages::umb::meta::to_string(type));
+
+ co_await async_write(socket, boost::asio::buffer(data, n), use_awaitable);
+ }
+ }
+ catch (const std::exception& e)
+ {
+ std::cout << std::format("echo Exception: {}\n", e.what());
+ }
+}
+
+awaitable listener()
+{
+ auto executor = co_await this_coro::executor;
+ tcp::acceptor acceptor(executor, {tcp::v4(), 55555});
+ for (;;)
+ {
+ tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
+ co_spawn(executor, echo(std::move(socket)), detached);
+ }
+}
+
+int main()
+{
+ try
+ {
+ boost::asio::io_context io_context(1);
+
+ boost::asio::signal_set signals(io_context, SIGINT, SIGTERM);
+ signals.async_wait([&](auto, auto)
+ { io_context.stop(); });
+
+ co_spawn(io_context, listener(), detached);
+
+ io_context.run();
+ }
+ catch (const std::exception& e)
+ {
+ std::cout << std::format("Exception: {}\n", e.what());
+ }
+}