DEV Community

zhoutk
zhoutk

Posted on

There is no official C++ SDK for MCP. So we wrote one — and ran it through the official conformance suite

Body

C++ is where the world's data infrastructure lives — databases, game engines,
embedded devices, trading systems, HPC. Agents increasingly need to reach into
those systems, and MCP is the protocol everyone is standardizing on. The
official SDK roster lists ten languages: TypeScript, Python, C#, Go, Rust,
Ruby, Java, Swift, PHP, Kotlin. C++ — the language those systems are written
in — is not one of them.

So we wrote mcp-cpp: a C++17 SDK targeting
the 2026-07-28 specification. No async runtime. One bundled JSON header,
one optional HTTP library. find_package(mcp CONFIG) and it's in your build.

This post is about what it is, what the official conformance suite taught us —
and one bug we found in the harness itself.

What implementing the 2026-07-28 spec actually means

The current revision is a big departure from the 2025-era model: no
initialize handshake, no session ids. Every request carries its protocol
version and client capabilities in params._meta; servers advertise
themselves via server/discover; the server never initiates a request —
when it needs more input it answers with an input_required result and the
client retries (MRTR); live updates flow through subscriptions/listen.

mcp-cpp implements all of it:

  • Server: Builder with tools/resources/prompts (including RFC 6570-style URI templates), capability gating, MRTR with a signed requestState codec (HMAC-SHA256, context-bound, TTL — in-tree SHA-256, no crypto dependency), progress notifications, cancellation, CacheableResult.
  • Client: blocking and value-semantic, MRTR auto-fulfil (callToolWithInput), and a full OAuth 2.1 client — protected-resource metadata discovery (RFC 9728), AS metadata (RFC 8414 + OIDC), DCR (RFC 7591), Client ID Metadata Documents (SEP-991), PKCE (S256) from a CSPRNG, RFC 8707 resource, RFC 9207 iss validation, scope step-up, SEP-2352 migration.
  • Transports: stdio and Streamable HTTP on both sides — mirrored headers (MCP-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Param-*) validated with -32020 enforcement, SSE responses, a GET listening stream for legacy servers, Last-Event-ID reconnect honoring the retry: field.
  • Validation: a JSON Schema 2020-12 validator (local $ref, resource bounds) enforcing inputSchema/outputSchema on every call.

A complete server in 15 lines:

#include <mcp/mcp.hpp>
#include <string>

int main() {
  mcp::server::Builder b;
  b.name("my-server").version("1.0");
  b.tool(mcp::Json(R"({"name":"add","description":"adds two integers",
    "inputSchema":{"type":"object","properties":{"a":{"type":"integer"},
      "b":{"type":"integer"}},"required":["a","b"]}})"),
    [](mcp::server::Ctx& c) -> mcp::McpResult<mcp::Json> {
      const mcp::Json args = mcp::member(c.params, "arguments");
      const int64_t sum = mcp::member(args, "a").toInt64() +
                          mcp::member(args, "b").toInt64();
      mcp::Json out(R"({})");
      out.add("sum", mcp::Json(sum));
      return mcp::Ok(mcp::server::structuredResult(out,
          "sum = " + std::to_string(sum)));
    });
  mcp::StdioTransport t;
  return b.build()->run(t).has_value() ? 0 : 1;
}
Enter fullscreen mode Exit fullscreen mode

The proof: the official conformance suite

We didn't self-certify parity — we ran it against
modelcontextprotocol/conformance
(v0.2.0-alpha.12), the same suite the official SDKs use, for both spec
revisions and both roles
:

Role 2026-07-28 2025-11-25
Server 42/42 checks · 20/20 scenarios (100%) 26/30 (see below)
Client 32/32 scenarios · 399/399 checks (100%), including all 25 auth/* OAuth 2.1 + CIMD scenarios 17/18 (see below)

Every check that doesn't pass is baselined with an explanation:

  • The four 2025 server failures (tools-call-sampling, tools-call-elicitation, elicitation-sep1034-defaults, elicitation-sep1330-enums) all require the server to send a request to the client mid-call — the mechanism 2026-07-28 replaced with MRTR. Implementing a removed mechanism for a 2025-only scenario is a deliberate non-goal.
  • The one 2025 client failure is a harness bug — see next section.

What the suite caught (the honest part)

Running the official harness against a C++ SDK found five real bugs in days.
Each is in the commit history
with a named spec clause:

  1. Notifications emitted during a request were silently dropped over HTTP — the transport kept only the last line of the response. Progress and logging on Streamable HTTP didn't exist. Fix: stream multi-message responses as SSE.
  2. resultType was injected on legacy responses — pre-2026 schemas reject unknown fields. A 2026-correct response was a 2025 violation.
  3. JSON-RPC 2.0 allows "id": null on error responses — 2025-era peers send it; our strict parser rejected it and the client hung until timeout instead of surfacing the error.
  4. The Mcp-Session-Id from initialize raced the next request — stateful 2025 servers answered 400 Invalid or missing session ID. The response is now held until the id is captured.
  5. An empty string is a valid mirrored header value — Mcp-Param-X: must be sent, not deleted. Our setHeader(name, "") semantics were wrong, and the suite's http-custom-headers scenario (18 checks) proved it.

Plus one we found in their mock: the elicitation-sep1034-client-defaults
scenario's mock never delivers the server→client elicitation — its GET stream
only answers keepalives, and the tool call never completes. We didn't shrug:
we built the official rust-sdk reference client in the same environment and
ran the same scenario — it fails identically (0/5, and crashes with
0xC0000005). A mock defect, not an SDK defect. We filed it upstream
and baseline it with that evidence
until it's fixed.

Design notes (C++ flavor)

  • C++17, not 20/23. The audience is existing C++ infrastructure, and GCC 9 and MSVC v142 are everywhere. Conditional std::expected aliasing gives modern compilers the standard types without splitting the build matrix.
  • Value-semantic, exception-free protocol layer. McpResult<T>, [[nodiscard]], nothing throws between transport and handler.
  • Zero crypto dependency. HMAC-SHA256 and the CSPRNG (PKCE verifiers, OAuth state) are in-tree — RtlGenRandom on Windows, /dev/urandom on POSIX.
  • Single-threaded, stateless dispatch — one connection, one server, no locks except the subscription fan-out. For an ORM/database MCP server this is the right shape: handlers are the slow part, not the loop.
  • Docs bilingual by default — English primary, full zh-CN siblings.

Who's using it

This SDK is the protocol layer behind
zorm-mcp — a production MCP server
exposing 13 structured database tools over sqlite3/jsonfile/mysql/postgres/dm8
through one dialect-free surface — and its ORM,
zorm. Real users, five backends, one bug
tracker.

Roadmap and the ask

Next up: OAuth server-side enforcement, the logging helper, a zero-copy parse
pass (raw parse is currently slower than serde_json and we say so in the
README), vcpkg/Conan ports, Doxygen.

  • Try it: git clone + cmake --preset clang-rlz — or find_package(mcp) after cmake --install. The CI matrix covers Linux/macOS/Windows × gcc/clang/MSVC plus ASan/UBSan and conformance.
  • File issues. Even "the docs are wrong about X".
  • If you agree C++ belongs on the official MCP SDK roster, say so where it counts — the MCP SDK Working Group decides the roster, and a proposal is in our future. Conformance scores, a CI-green repo, real users and a dependency policy are ready.

Repo: https://github.com/zhoutk/mcp-cpp · MIT · conformance reports in-tree.

Top comments (0)