Zero-dependency MCP (Model Context Protocol) implementation, written twice: Go and C++. Every layer annotated.
中文说明 | English
MCP is the protocol that lets a language model call tools, read resources and use prompt templates. Most people use it through an SDK and never read the wire format. This repository is the wire format, in full, in two languages, small enough to read in an afternoon.
It is not a production SDK. It is the thing you read to understand the one you use in production, and the thing you copy when you need MCP in a codebase that cannot take a dependency.
Measured on the reference machine in docs/BENCHMARKS.md.
Reproduce with make bench.
| Go implementation | 2,692 lines |
| Go tests | 911 lines, 32 tests, -race clean |
| C++ implementation | 2,334 lines (1,721 source + 613 headers) |
| C++ tests | 70 assertions, ASan/UBSan clean |
Round-trip latency, one tools/call over stdio |
p50 10.7 µs |
| of which JSON encode + decode | 1.5 µs |
| Throughput, stdio | ~67,000 msg/s |
tools/list response size |
352 bytes |
# Go
cd go && go build ./... && go test ./...
# C++
cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release
cmake --build cpp/build -j
cd cpp/build && ctest --output-on-failureNo dependencies. Go uses the standard library; C++ uses the standard library and POSIX sockets.
cd go
go build -o bin/mcpserver ./cmd/mcpserver
go build -o bin/mcpclient ./cmd/mcpclient
./bin/mcpclient -server ./bin/mcpserverThe client spawns the server as a child process and speaks JSON-RPC over its stdin and stdout, which is how a real agent host does it. Expected output:
connected to simple-mcp-demo 1.0.0 (protocol 2025-06-18)
instructions: Demo server. Use echo to repeat text, add to sum two integers, and get_weather for a fixed mock forecast.
3 tools:
echo Repeat the given text back verbatim. Use to verify connectivity.
add Add two integers and return the sum. Use for arithmetic, not for string concatenation.
get_weather Return a mock forecast for a city. The data is fixed and synthetic.
calling tools:
echo hello from the client
add 42
get_weather Beijing: 22 C (mock, clear)
error taxonomy:
wrong argument type -> protocol error: rpc error -32602: invalid arguments: city: expected string, got number
The same demo exists in C++:
cmake --build cpp/build -j
./cpp/build/mcp_client ./cpp/build/mcp_serverThe server speaks stdio, so anything that launches a subprocess works:
{
"mcpServers": {
"simple-mcp": {
"command": "/path/to/bin/mcpserver"
}
}
}Or run it over HTTP: ./bin/mcpserver -http :8080, then connect to
http://localhost:8080/mcp.
| Layer | Responsibility | Go | C++ |
|---|---|---|---|
| 1. JSON-RPC 2.0 | request / response / notification framing, error codes | go/protocol/ |
cpp/src/jsonrpc.cpp |
| 2. Transport | newline-delimited JSON over stdio; POST + SSE over HTTP | go/transport/ |
cpp/src/transport_stdio.cpp, transport_http.cpp |
| 3. Handshake | initialize, capability negotiation, notifications/initialized |
go/server/server.go |
cpp/src/server.cpp |
| 4. Tools | tools/list, tools/call, JSON Schema validation |
go/server/handlers.go |
cpp/src/server.cpp |
| 5. Resources & prompts | resources/list, resources/read, prompts/list, prompts/get |
go/server/handlers.go |
cpp/src/server.cpp |
| 6. Client SDK | request/response matching, reader goroutine, typed methods | go/client/ |
cpp/src/client.cpp |
Reading order: start at go/protocol/jsonrpc.go, then go/server/server.go,
then go/client/client.go. The C++ versions mirror them file for file.
MCP has two kinds of failure, and they are not interchangeable.
malformed call -> JSON-RPC error (-32602, -32601, -32002 ...)
tool ran and failed -> result with isError (content explains what went wrong)
A client that receives a JSON-RPC error treats the call as not having happened.
A client that receives isError knows the tool executed. If a tool that cannot
find a city reports a protocol error, a well-behaved agent retries it three
times, gives up, and reports that the tool is broken, when in fact the tool
worked exactly as intended.
go/server/handlers.go is where this decision is made, and
go/server/integration_test.go asserts both halves of it from the client's
side. The C++ tests do the same.
Each of these has a test that fails if the code regresses.
bufio.Scannercaps a message at 64 KiB. A tool returning a source file or a base64 image exceeds that routinely, and the failure message mentions nothing about MCP.go/transport/transport_test.goproves a 300 KiB message crosses intact, and proves that a defaultScannerrefuses it.- stdout belongs to the protocol. One log line on stdout breaks every client that parses the stream. Diagnostics go to stderr.
- Notifications get no response. Answering
notifications/initializedmakes a client read a response while waiting for the next real one. - The id must round-trip byte for byte. Decoding
idintointerface{}turns1into the float641, and a client matching responses by id hangs. - A client needs a reader goroutine. Write-then-read works until a notification arrives first.
- An unknown resource is
-32002, not-32602. One means "malformed", the other means "does not exist", and they need different recovery. - JSON Schema validation is the server's job. MCP does not specify who validates tool arguments, so if you do not, nobody does.
Full write-ups with reproductions in docs/PITFALLS.md.
- Architecture · 架构(中文)
- Protocol notes · 协议笔记(中文)
- Pitfalls · 踩坑(中文)
- Benchmarks · 基准测试(中文)
- Publishing to GitHub · 发布步骤(中文)
- Getting stars · 涨星策略(中文)
- Article, in Chinese
Stated up front, because a teaching implementation that hides its edges teaches the wrong lesson.
- Linux and macOS. The C++ HTTP transport uses POSIX sockets.
- Not a production SDK. No retry, no pooling, no pagination, no cancellation, no progress notifications, no sampling.
- Streamable HTTP is simplified. Single-response form only; a server holding a POST open to stream is not implemented. See docs/PROTOCOL.md.
- The C++ build has no HTTP server. Writing one by hand would add a few hundred lines to teach a lesson the Go version already teaches.
get_weatherreturns fixed mock data. A demo that calls a real API needs a key, breaks offline, and turns rate limits into a teaching problem.- Protocol version 2025-06-18 is the default; older revisions are accepted in the handshake but their removed fields are not implemented.
go/
protocol/ JSON-RPC 2.0: framing, ids, error codes
transport/ stdio and Streamable HTTP
mcptypes/ wire types, including the Content tagged union
server/ registries, handshake, dispatch, schema validation
client/ typed methods, reader goroutine, pending-request table
cmd/ mcpserver, mcpclient, mcpbench
cpp/
include/mcp/ headers, one per layer
src/ implementations, one per layer
tests/ 70 assertions
examples/ server_main, client_main, http_client_main
docs/ architecture, protocol, pitfalls, benchmarks
http_client_main exists to do one thing no unit test can: it points the C++
client at the running Go server and speaks to it over a real socket. That is
how the socket-close trap in PITFALLS was found — both
implementations were green while the client deadlocked on exit. To run it:
cd go && go build -o mcpserver ./cmd/mcpserver && ./mcpserver -http 127.0.0.1:8080 &
cmake -S cpp -B cpp/build -DCMAKE_BUILD_TYPE=Release && cmake --build cpp/build -j
./cpp/build/mcp_http_client 127.0.0.1 8080Corrections are the most valuable contribution this repository can receive. If a comment is wrong or a protocol detail does not match the spec, please file an issue pointing at the spec section. See CONTRIBUTING.md.
Apache-2.0. See LICENSE.