Description
examples/transport/src/tcp.rs's main() spawns the server as a background task and immediately calls client().await with no synchronization between the two:
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tokio::spawn(server());
client().await?;
Ok(())
}
async fn server() -> anyhow::Result<()> {
let tcp_listener = tokio::net::TcpListener::bind("127.0.0.1:8001").await?;
while let Ok((stream, _)) = tcp_listener.accept().await { ... }
Ok(())
}
async fn client() -> anyhow::Result<()> {
let stream = tokio::net::TcpSocket::new_v4()?
.connect("127.0.0.1:8001".parse()?)
.await?;
...
}
tokio::spawn(server()) only schedules the task; it returns before TcpListener::bind has necessarily run. There is no oneshot channel, retry/backoff, or any other readiness signal before client() connects. In this environment the client task always wins the scheduling race, so the example fails deterministically.
Sibling examples in the same crate use a safer structure: unix_socket.rs calls the synchronous UnixListener::bind directly in main() before spawning the accept loop, guaranteeing the listener exists before the client runs — and it (plus http_upgrade.rs) ran successfully in the same session this was found in.
Reproduction Steps
- On
main @ 02c62ae (v3.1.2, no drift):
cargo run --manifest-path examples/transport/Cargo.toml --example tcp
- Observe:
Error: Connection refused (os error 61)
- Repeat 3 more times — same failure every time (4/4 failures,
lsof -i :8001 confirmed no other process was holding the port).
Expected Behavior
The tcp example demonstrates a working TCP transport round-trip (connect, list tools, call a tool) reliably, the way unix_socket and http_upgrade do.
Actual Behavior
100% failure rate — the client always connects before the server finishes binding its listener.
Environment
- Version: v3.1.2, commit
02c62ae
- Platform: macOS (Darwin), local run, no other process on port 8001
Upstream
No exact duplicate found (searched issues and PRs, open and closed, in both bug-ops/rust-sdk and modelcontextprotocol/rust-sdk, with tcp/connection-refused/race keywords).
Spec
.local/specs/004-tcp-example-startup-race/spec.md
Description
examples/transport/src/tcp.rs'smain()spawns the server as a background task and immediately callsclient().awaitwith no synchronization between the two:tokio::spawn(server())only schedules the task; it returns beforeTcpListener::bindhas necessarily run. There is no oneshot channel, retry/backoff, or any other readiness signal beforeclient()connects. In this environment the client task always wins the scheduling race, so the example fails deterministically.Sibling examples in the same crate use a safer structure:
unix_socket.rscalls the synchronousUnixListener::binddirectly inmain()before spawning the accept loop, guaranteeing the listener exists before the client runs — and it (plushttp_upgrade.rs) ran successfully in the same session this was found in.Reproduction Steps
main@02c62ae(v3.1.2, no drift):Error: Connection refused (os error 61)lsof -i :8001confirmed no other process was holding the port).Expected Behavior
The
tcpexample demonstrates a working TCP transport round-trip (connect, list tools, call a tool) reliably, the wayunix_socketandhttp_upgradedo.Actual Behavior
100% failure rate — the client always connects before the server finishes binding its listener.
Environment
02c62aeUpstream
No exact duplicate found (searched issues and PRs, open and closed, in both
bug-ops/rust-sdkandmodelcontextprotocol/rust-sdk, with tcp/connection-refused/race keywords).Spec
.local/specs/004-tcp-example-startup-race/spec.md