Uh oh!
There was an error while loading. Please reload this page.
M2 upstream: HTTP-split + perf, HttpServerHost fixes, Windows/MSVC, MCP_HTTP_NO_TLS, session-close + client-capabilities hooks - #4
Merged
Conversation
…arget Two changes that were developed together and share the CMake/target reshaping, landed as one slice. Performance: request methods no longer spawn a std::thread per call. Client::call_tool et al. (and Server::sample/list_roots/elicit) wrapped each call in std::async(std::launch::async) purely to convert the result JSON to a typed value. That now runs as a typed continuation on the session read thread via the new Session::send_request_for<T>(), eliminating the per-call thread while preserving std::future semantics (including .wait_for()). Measured in-process tools/call throughput +~25% and p50 latency -~21% on Apple Silicon. HTTP split: the Streamable-HTTP transport (HttpServerHost / HttpClientTransport) and its cpp-httplib -> OpenSSL dependency move to a separate mcp-http / mcp::http target. The core mcp library now depends only on nlohmann_json, so a stdio-only server links no OpenSSL/brotli/ Security at all. httplib's optional brotli/zlib auto-detection is pinned off so builds are deterministic regardless of what is installed on the host. A shared mcp_target_warnings() function applies the warning set to both first-party targets. Breaking (build only): consumers of the HTTP transport must link mcp::http instead of mcp::mcp; stdio-only consumers are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two HttpServerHost bugs surfaced by real-client interop testing. Options::port was silently ignored: start() always called bind_to_any_port, so every consumer asking for a fixed port got an OS-assigned one. start() now binds the requested port via bind_to_port and throws on failure instead of falling back to a random port; port 0 keeps the OS-assigned behavior. POSTing to a terminated or unknown Mcp-Session-Id returned 400; the 2025-11-25 spec requires 404 so the client knows to re-initialize with a fresh InitializeRequest. A session-less non-initialize POST still returns 400 (malformed usage, not a stale session). Regression tests: HttpEndToEnd.FixedPortIsHonored and HttpEndToEnd.PostToTerminatedSessionIs404. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The POSIX-only StdioTransport (poll(2) + file descriptors) is compiled out on _WIN32: its header carries a matching #error guard, and the CMake source lists gate it behind if(NOT WIN32) in src/, tests/, and examples/. mcp.hpp only includes stdio_transport.hpp off Windows, and audit_regression_test.cpp guards its stdio/pipe(2) test and its <unistd.h> include the same way. Everything else — protocol, session, client/server, and the Streamable HTTP transport — builds and tests under MSVC. New windows-latest CI jobs (Debug + RelWithDebInfo) make this a supported configuration. MCP_WARNINGS_AS_ERRORS is off on the Windows job until the first MSVC warning surface has been triaged — this is the SDK's first MSVC compile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New CMake option MCP_HTTP_NO_TLS (default OFF) drops the OpenSSL dependency from the HTTP transport by setting HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF before FetchContent_MakeAvailable. The resulting binaries link no crypto at all — https:// URLs then fail at runtime, which plaintext-loopback deployments and hosts that terminate TLS elsewhere don't care about. The motivating consumer is an embedder (a game engine) that already ships its own OpenSSL: a second copy in the same process is a symbol-collision lottery. The full test suite passes in this configuration. README options table updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Minimal Streamable-HTTP server exposing a single `echo` tool, for exercising real MCP clients (Claude Code, VS Code, Cursor, MCP Inspector) against HttpServerHost: initialize handshake, Mcp-Session-Id round-trip, tools/list, tools/call, long-lived sessions, DELETE teardown, and optional MCP_TOKEN bearer enforcement. Links mcp::http. Verified end-to-end against Claude Code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bench/mcp_bench.cpp measures the SDK's own per-call cost in isolation (tools/call and ping round-trips over the in-process paired transport, plus the JSON-RPC codec), and bench/README.md records the results and the native-vs-interpreted footprint/startup rationale. Standalone: built ad-hoc against the prebuilt static lib per the README, not wired into the CMake build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HttpServerHost destroys each session's Server on teardown, but exposed no hook for an embedder to drop a raw Server* it holds before that destruction. An embedder with a cross-thread Server* registry (e.g. a logging fan-out calling Server::log() from an arbitrary thread) thus has no safe deregistration point: ~Server is too late, because member destruction order lets a concurrent log() lock an already-destroyed session mutex — a genuine use-after-free. Options::on_session_closed(Server&) is invoked exactly once per session on each teardown path (client DELETE, and stop() for every surviving session), while the Server is still fully alive — before the transport is closed, the run thread is joined, or the Server is destroyed. The sessions map lock is not held during the call, and the hook is exception-contained. The header documents the full threading contract. This also carries session identity (the Server&), which is what a session-lifetime consumer such as a sampling provider needs to know a capable session exists and to be torn down with it — so no separate session-lifetime API is required. Tests: HttpEndToEnd.OnSessionClosedFiresOnceOnDelete (fires once on the DELETE path, not re-fired by a later stop()) and HttpEndToEnd.OnSessionClosedFiresOnStop (fires once per surviving session, idempotent across repeated stop()). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Capture the client's negotiated capabilities during initialize handling and expose them via client_capabilities(), returning std::optional<ClientCapabilities> (nullopt before initialize). Guarded by its own mutex and readable from any thread — the narrow-surface mutex style already used for the other handler-registration state. Only the initialize that wins the initialized_ compare-exchange records the capabilities, so the store is race-free. This lets an embedder gate optional server-initiated flows (only sample() when the client advertised sampling; short-circuit an elicitation-driven tool with an actionable message when elicitation is absent) instead of discovering the gap through a round-trip failure. Test: HttpEndToEnd.ClientCapabilitiesCapturedAtInitialize — a fresh Server returns nullopt; after an initialize advertising sampling + elicitation, the accessor returns exactly those (read cross-thread from the on_session_closed hook). CHANGELOG updated for both new SDK hooks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first MSVC compile of the HTTP host failed: http_server_host.cpp uses std::deque (buffered_/get_queue_), std::condition_variable (get_cv_), std::optional, std::vector, std::system_error facilities (std::errc/error_code/make_error_code), and std::stdexcept types (invalid_argument/runtime_error) but relied on them being pulled in transitively — which libstdc++/libc++ do and MSVC's STL does not. All 36 Windows errors cascaded from these missing includes; the core protocol/session/server/client TUs compiled clean under MSVC. Add the explicit includes. No behavior change on any platform. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ignore list covered build/, build-asan/, build-tsan/ but not the build-rel/ dir the HTTP examples/release config uses, nor macOS .DS_Store. An untracked build-rel/ makes `git status --porcelain` non-empty, which trips the strict dirty check in mcp-unreal's Tools/vendor_mcp_cpp.py (it refuses to vendor from a dirty tree, by design — untracked files at vendored paths could perturb the header glob). Collapse the build dirs to a single `build*/` glob so the vendor script sees a clean tree without weakening its guard or resorting to --allow-dirty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Options::idle_timeout documented that idle sessions are torn down after the interval, but no enforcement exists: last_seen is stamped on each request (src/http_server_host.cpp) and never read, and there is no reaper. Sessions live until the client sends DELETE or the host is stop()ed. Rewrite the comment to say so plainly, and record that when an idle reaper is eventually added it MUST invoke on_session_closed for the reaped session before destroying its Server, matching the DELETE/stop() teardown contract (otherwise embedders holding a raw Server* get a UAF). Documentation-only; no behavior change. A reaper is deliberately out of scope here — it needs on_session_closed integration and dedicated tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sanitizers/tsan job (ubuntu, default libstdc++) intermittently reported a data race whose entire stack sits in UNINSTRUMENTED libstdc++.so.6 — std::_Sp_counted_base::_M_release / std::__exception_ptr::exception_ptr::_M_release reached from the plain ~promise / shared_ptr<Session> teardown in Session::send_request_for<T>'s resolver path (session.cpp). TSan cannot see the atomic refcount / happens-before edges inside precompiled libstdc++, so it flags a false positive on standard-conformant refcount teardown. That is what flaked run 28716770512. Building the tsan job with -stdlib=libc++ (libc++'s std headers are inlined into the TU and therefore TSan-instrumented) removes the blind spot without a suppression file. The macOS runs, which already use header-inlined instrumented libc++, were clean 20+ times, and a bounded worker4 repro under default libstdc++ reproduced the false positive in 5/40 runs — all in the same Session-teardown family — confirming the race is environment-dependent, not a real bug. Install libc++-dev/libc++abi-dev in the tsan job only, and pass -stdlib=libc++ at both compile and link. No suppressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands the post-0.1.0 SDK work as reviewable slices, plus three additive,
non-breaking public-API hooks. All eight commits are on this branch; the tree
builds and passes the full test suite (225/225) in both the default
(
MCP_ENABLE_HTTP=ON) andMCP_HTTP_NO_TLS=ONconfigurations.Changed / Performance
mcp::httptarget. The coremcplibrary now depends only on
nlohmann_json; the Streamable-HTTP transport(
HttpServerHost/HttpClientTransport) and itscpp-httplib → OpenSSLdependency move to a separate
mcp::httptarget (still gated byMCP_ENABLE_HTTP, default ON). A stdio-only server linked againstmcp::mcpnow loads no OpenSSL/brotli/Security at all.
mcp::httpinstead ofmcp::mcp. Stdio-only consumers are unaffected.brotli/zlibauto-detection is pinned off soHTTP builds are deterministic regardless of what's installed on the host.
std::threadper call. The per-callstd::async(std::launch::async)used only to type-convert the result now runsas a typed continuation on the session read thread via
Session::send_request_for<T>(). Measured in-processtools/callthroughput+~25% and p50 latency −~21% on an Apple Silicon dev machine.
Fixed
HttpServerHostnow honorsOptions::port(previously alwaysbind_to_any_port, silently ignoring a requested fixed port); a bind failureon the requested port throws instead of falling back.
Mcp-Session-Idnow returns 404 as thespec requires (the client's cue to re-initialize), instead of 400.
Added
StdioTransportis compiled out on_WIN32(its header carries a matching#errorguard); everything else builds and tests under MSVC.MCP_HTTP_NO_TLSCMake option — build the HTTP transport without OpenSSL(plaintext only), for loopback deployments and for embedding in processes that
already ship their own TLS/OpenSSL (game engines, editors). The full test suite
passes in this configuration and the resulting binaries link no crypto at all.
examples/http_echo_server— a minimal Streamable-HTTP interop server, verifiedend-to-end against a real MCP client.
bench/— in-process micro-benchmarks plus footprint/startup measurements.HttpServerHost::Options::on_session_closed— astd::function<void(Server&)>invoked exactly once per session on each teardown path (client
DELETE, andstop()for every surviving session) while theServeris still fully alive,before the transport is closed or the
Serveris destroyed. Lets an embedderholding a raw
Server*(e.g. a cross-thread logging fan-out) deregister itwhile the pointer is valid, closing a use-after-free window that
~Servermember-destruction order would otherwise leave open.
Server::client_capabilities()—std::optional<ClientCapabilities>capturedat initialize, readable from any thread. Lets an embedder gate optional
server-initiated flows (sampling, elicitation) on what the connected client
negotiated, instead of discovering the gap through a round-trip failure.
Windows CI
The
windows-latestmatrix jobs (Debug + RelWithDebInfo) are the SDK'sfirst-ever MSVC compile — this PR is how they get proven.
MCP_WARNINGS_AS_ERRORSis off on the Windows job until the MSVC warning surface has been triaged once.
Test plan
MCP_ENABLE_HTTP=ON): 225/225 green locally.MCP_HTTP_NO_TLS=ON: 225/225 green locally; binaries link no crypto.🤖 Generated with Claude Code