Transports
Transports define how the MCP server and client communicate. The mcp-cpp-sdk provides multiple transport layers out of the box, with a consistent API. Every transport implements the ITransport interface, ensuring that your server or client logic remains independent of the underlying communication protocol.
This guide explores the various transport options available in the SDK, from standard local process communication to remote network-based protocols.
Standard Transports
The SDK includes several built-in transport types to cover different use cases:
StdioTransport: For local process communication via standard input/output. This is the most common choice for connecting to local agents like Claude Desktop.
HttpServerTransport / HttpClientTransport: Single-session Streamable HTTP building blocks.
StreamableHttpSessionManagerprovides a multi-session server endpoint.WebSocketServerTransport / WebSocketClientTransport: An optional persistent transport for integrations that explicitly agree on WebSocket; it is not the standard Streamable HTTP transport.
MemoryTransport: An in-memory transport for unit testing and in-process communication.
Stdio Transport (Local)
The Stdio transport is the default and recommended way to integrate with local tools. It leverages the standard input and output streams of the process, making it extremely easy to deploy as a subprocess.
Server Side
The simplest way to start a stdio-based server is using the high-level run_stdio() method, which blocks the main thread until the connection is closed.
#include <mcp/mcp.hpp>
int main() {
// Define server information and capabilities
mcp::Implementation info{"my-server", "1.0.0"};
mcp::ServerCapabilities caps;
caps.tools = mcp::ServerCapabilities::ToolsCapability{};
mcp::Server server(info, caps);
// Add your tools, resources, and prompts here
server.add_tool("echo", "Echoes input", schema, [](auto args) { ... });
// Run the server on stdin/stdout
server.run_stdio();
}
Client Side
Clients can connect to a stdio-based server by providing the executor to the StdioTransport.
#include <mcp/client/client.hpp>
#include <mcp/transport/stdio.hpp>
boost::asio::co_spawn(executor, [&]() -> mcp::Task<void> {
auto transport = std::make_shared<mcp::StdioTransport>(executor);
mcp::Client client(transport, executor);
co_await client.connect(client_info, {});
// Interact with the server...
}, boost::asio::detached);
HTTP Transport (Network)
The HTTP implementation accepts MCP POST requests and returns either JSON or a
finite SSE body. Managed sessions support session IDs, DELETE teardown, and
bounded event replay through GET with Last-Event-ID. Continuously open SSE
polling streams are not yet implemented.
After initialization, HttpClientTransport carries the server-selected
protocol version on subsequent session requests and best-effort DELETE
teardown, including when the initialize response arrived in an SSE event.
The built-in HTTP transports are plaintext. Bind local development servers to
loopback. For remote deployments, place them behind a TLS-terminating proxy or
provide a custom TLS transport. Requests carrying an Origin header are
denied by default; configure an exact allowlist (or explicitly opt into all
origins) before starting the listener. Bearer-token validators must likewise be
installed before listen() or run().
HTTP Server Convenience
For developers who want a quick way to host an MCP server over HTTP without worrying about the underlying networking boilerplate, the SDK provides run_http().
Server::run_http() owns its HttpServerTransport internally and does not
currently expose Origin allowlist or bearer-token validator configuration. Use
it only on a suitable trusted boundary, such as the loopback example below. For
a configurable deployment, construct HttpServerTransport or
StreamableHttpSessionManager directly, apply the security settings, and
then start the listener.
// - Creates io_context
// - Creates HttpServerTransport
// - Calls transport->listen()
// - Calls server.run(transport, executor)
// - Manages signal handling for graceful shutdown
The following example shows how to use the convenience method:
std::cout << "Starting HTTP server on port " << http_port << " (convenience method)\n";
std::cout << "Server will run in background thread\n";
// Spawn server in background thread
std::thread server_thread([&server, http_port]() {
try {
// This convenience method handles everything:
// - Creates io_context
// - Creates HttpServerTransport
// - Calls transport->listen()
// - Calls server.run(transport, executor)
// - Manages signal handling for graceful shutdown
server.run_http("127.0.0.1", http_port);
} catch (const std::exception& e) {
std::cerr << "Server error: " << e.what() << '\n';
}
Manual HTTP Configuration
If you need to integrate the MCP transport into an existing HTTP server (like one built with Boost.Beast), you can manage the HttpServerTransport lifecycle yourself.
using namespace mcp;
// ========== MANUAL SETUP (for comparison - NOT used in this example) ==========
// This is what run_http() does internally:
//
// asio::io_context io_ctx;
// auto transport = std::make_shared<HttpServerTransport>(
// io_ctx.get_executor(), "127.0.0.1", 18100);
//
// asio::co_spawn(io_ctx, transport->listen(), asio::detached);
// asio::co_spawn(io_ctx, server.run(transport, io_ctx.get_executor()),
WebSocket Transport
For integrations that explicitly choose full-duplex WebSocket communication,
WebSocketServerTransport and WebSocketClientTransport
provide a persistent connection. Interoperability with standard MCP clients is
not implied; use Streamable HTTP when protocol-standard remote transport is
required.
#include <mcp/transport/websocket.hpp>
// On the server, accept a TCP socket first and then wrap it.
mcp::WebSocketServerTransport server_transport(std::move(socket));
// On the client, the transport performs the TCP + WebSocket handshake.
mcp::WebSocketClientTransport client_transport(io_ctx.get_executor(), "127.0.0.1", "9001");
WebSocketServerTransport refuses an upgrade request that carries an
Origin header with 403 unless that origin was added with
set_allowed_origins() or set_allow_all_origins(true) was called, as
the HTTP transports do. An upgrade without an Origin header is accepted.
Configure origins before the first read_message() or write_message(),
which perform the handshake.
MemoryTransport for Testing
Testing is a critical part of developing MCP integrations. The MemoryTransport allows you to run both a server and a client in the same process, communicating entirely in memory. This eliminates the need for network configuration or local process management during testing.
Creating Transport Pairs
You should always use the create_memory_transport_pair helper to ensure that both ends of the transport are correctly linked.
asio::io_context io_ctx;
// Create bidirectional memory transport pair
auto [transport_a, transport_b] = create_memory_transport_pair(io_ctx.get_executor());
std::cout << "[Main] Created MemoryTransport pair\n";
std::cout << "[Main] Transport A: " << transport_a.get() << "\n";
Usage in Integration Tests
Because MemoryTransport implements the same ITransport interface as all other transports, your server and client code remain identical to production.
bool success = false;
asio::co_spawn(
io_ctx,
[transport_a, transport_b, &io_ctx, &success]() -> Task<void> {
try {
co_await transport_a->write_message("hello from transport A");
std::string received_on_b = co_await transport_b->read_message();
std::cout << "[Transport B] Received: " << received_on_b << "\n";
co_await transport_b->write_message("reply from transport B");
std::string received_on_a = co_await transport_a->read_message();
std::cout << "[Transport A] Received: " << received_on_a << "\n";
std::string json_rpc = R"({"jsonrpc":"2.0","method":"demo/ping"})";
co_await transport_a->write_message(json_rpc);
std::string json_received = co_await transport_b->read_message();
std::cout << "[Transport B] JSON-RPC frame: " << json_received << "\n";
success = true;
} catch (const std::exception& e) {
std::cerr << "[MemoryTransport] Fatal error: " << e.what() << '\n';
}
io_ctx.stop();
},
For more information on writing effective tests, see the Testing MCP Applications guide.
TransportFactory and Runtime
The Runtime class is a high-level wrapper around the Boost.Asio event loop, providing a simple run() and stop() interface. The TransportFactory further simplifies transport creation by binding them to a specific runtime.
try {
using namespace mcp;
Runtime runtime;
TransportFactory factory(runtime);
std::cout << "[Main] Runtime and TransportFactory created\n";
std::cout << "[Main] Factory methods available:\n";
std::cout << " - factory.create_stdio() -> stdio transport\n";
std::cout << " - factory.create_http_client(url) -> HTTP transport\n\n";
(void)factory;
Choosing the Right Transport
Selecting the appropriate transport is essential for the success of your MCP integration. Use the table below as a guide:
Transport |
Primary Use Case |
Key Advantages |
|---|---|---|
Stdio |
Local IDE/Agent Plugins |
|
HTTP |
MCP remote endpoints |
|
WebSocket |
Agreed custom integration |
|
Memory |
Unit & Integration Tests |
|
Custom Transports
If none of the built-in transports meet your needs, you can implement a custom one. This might be useful for IPC mechanisms like Unix Domain Sockets or proprietary messaging protocols.
All you need to do is inherit from mcp::ITransport and implement three methods:
#include <mcp/transport/transport.hpp>
class MyCustomTransport : public mcp::ITransport {
public:
// Read a single JSON-RPC message as a string
mcp::Task<std::string> read_message() override {
// Wait for data on your custom pipe/socket
std::string data = co_await my_socket.async_read(...);
co_return data;
}
// Write a single JSON-RPC message
mcp::Task<void> write_message(std::string_view message) override {
co_await my_socket.async_write(message);
}
// Clean up resources and signal closure
void close() override {
my_socket.close();
}
};