Every piece of the protocol has been on the table on its own. This post puts them in one server: a small knowledge base that exposes tools, resources, a prompt, and completion, calls back to the client with sampling and elicitation, runs over both transports, sits behind OAuth, and is tested end to end. One server, one session trace, the whole protocol in one place. Reading it should now feel like reading a sentence, because every word has its own post behind it.
This is the finale of MCP on the Wire, a series that takes the Model Context Protocol apart message by message, in Go. It comes out of building and running MCP servers in production, including the open-source
txn2/mcp-data-platform, an Apache-2.0 platform in Go that connects AI assistants to Trino, DataHub, and S3 through one MCP endpoint, enriching every result with semantic context (ownership, lineage, PII, data quality) behind OAuth 2.1 auth, personas, and an audit trail. Everything here is read straight off the wire against spec revision2025-11-25, with the official Go SDK atv1.6.1.
§One Server, Every Primitive
The server is a NewServer function that wires each primitive in, and the registrations read like a table of contents for the series:
func NewServer() *mcp.Server {
s := mcp.NewServer(&mcp.Implementation{Name: "kb", Title: "Knowledge Base", Version: "v1.0.0"},
&mcp.ServerOptions{CompletionHandler: complete})
mcp.AddTool(s, &mcp.Tool{Name: "summarize_topic", Description: "Summarize a topic with the host model.",
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true}}, summarizeTopic) // tool + sampling
mcp.AddTool(s, &mcp.Tool{Name: "delete_topic", Description: "Delete a topic after confirming."}, deleteTopic) // tool + elicitation
s.AddResource(&mcp.Resource{URI: "kb://index", Name: "index", MIMEType: "text/plain"}, readIndex) // resource
s.AddResourceTemplate(&mcp.ResourceTemplate{URITemplate: "kb:///{topic}", Name: "topic"}, readTopic) // resource template
s.AddPrompt(&mcp.Prompt{Name: "explain", Arguments: []*mcp.PromptArgument{{Name: "topic", Required: true}, {Name: "level"}}}, explainPrompt) // prompt
return s
}
The tools, resources, prompts, and completion posts each covered one of those lines. Here they are one server’s surface. Two of the registrations are worth a second look, because they are where the protocol turns around.
§Two Tools That Call Back
summarize_topic does not summarize anything itself. It reads the topic document and asks the client’s model to summarize it, the sampling inversion, inside a tool handler:
func summarizeTopic(ctx context.Context, req *mcp.CallToolRequest, in topicArg) (*mcp.CallToolResult, any, error) {
doc := docs[in.Topic]
res, _ := req.Session.CreateMessage(ctx, &mcp.CreateMessageParams{
MaxTokens: 100, SystemPrompt: "Summarize in one short sentence.",
Messages: []*mcp.SamplingMessage{{Role: "user", Content: &mcp.TextContent{Text: doc}}},
})
return &mcp.CallToolResult{Content: []mcp.Content{res.Content.(*mcp.TextContent)}}, nil, nil
}
delete_topic does the same trick with elicitation: before it deletes anything, it asks the user to confirm with a form, and acts on the answer. Both tools are short, and both reach back through the connection to something only the host has: its model, and its user.
§Runnable Two Ways
The same NewServer() runs over either transport, chosen by a flag:
server := kb.NewServer()
if !*httpFlag {
server.Run(context.Background(), &mcp.StdioTransport{}) // stdio: a local subprocess
return
}
handler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { return server }, nil)
mux.Handle("/mcp", guard(handler)) // Streamable HTTP, optionally behind a bearer gate
http.ListenAndServe(*addr, mux)
Over stdio it is a subprocess; over Streamable HTTP it is a network service, and adding -auth wraps it in the OAuth bearer gate from the authorization post. The gate works as captured:
no token -> HTTP 401
valid-token -> HTTP 200
The server logic never changes. The transport and the gate are layers around it, which is the whole point of the protocol being transport-agnostic.
§Tested With an In-Memory Transport
You do not need a subprocess or a port to test an MCP server. The SDK’s InMemoryTransports connects a client and server in one process, speaking the real protocol over a pipe:
func connect(t *testing.T) *mcp.ClientSession {
server := NewServer()
client := mcp.NewClient(&mcp.Implementation{Name: "test", Version: "v0"}, &mcp.ClientOptions{
CreateMessageHandler: func(ctx context.Context, r *mcp.CreateMessageRequest) (*mcp.CreateMessageResult, error) {
return &mcp.CreateMessageResult{Role: "assistant", Content: &mcp.TextContent{Text: "A short summary."}}, nil
},
ElicitationHandler: func(ctx context.Context, r *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
return &mcp.ElicitResult{Action: "accept", Content: map[string]any{"confirm": true}}, nil
},
})
sc, cc := mcp.NewInMemoryTransports()
go server.Run(context.Background(), sc)
session, _ := client.Connect(context.Background(), cc, nil)
return session
}
The test client supplies the sampling and elicitation handlers a real host would, so the inverted calls resolve. The tests exercise every primitive and pass:
=== RUN TestTools --- PASS
=== RUN TestResources --- PASS
=== RUN TestPromptAndCompletion --- PASS
PASS
ok example.com/wiredemo/kb 0.009s
TestTools calls summarize_topic and asserts the sampled summary comes back, then calls delete_topic and asserts the confirmed deletion. TestResources reads the index, reads a templated document, and confirms a missing topic errors. TestPromptAndCompletion renders the prompt and completes a topic argument. The whole protocol surface, covered without a process boundary in sight.
§One Session, Annotated
Driving the server through a logging transport produces the trace the series has been building toward, one session, every kind of message, including the inversion happening mid-call:
write: initialize / notifications/initialized
read: result { capabilities: completions, logging, prompts, resources, tools }
write: tools/list -> read: [summarize_topic, delete_topic]
write: resources/list -> read: [kb://index]
write: prompts/list -> read: [explain]
write: tools/call summarize_topic { topic: "mcp" }
read: sampling/createMessage { the doc to summarize } <- the server calls back
write: result { "MCP is a JSON-RPC session protocol for tools." }
read: result { content: ["MCP is a JSON-RPC session protocol for tools."] }
write: resources/read kb:///mcp -> read: contents { the document }
Read the middle of the tool call. The client sent tools/call, and before the result came back, a sampling/createMessage arrived from the server, was answered, and then the tool result followed. A request inside a request, the server borrowing the model while the client waited. Nothing about that frame is mysterious now: it is a server-initiated request, legal because the client declared the sampling capability in the handshake, matched by its own id.
§The Spine, Filled In
The first post promised a table: for every message, which direction it runs, who starts it, and what crosses the boundary. Here it is, complete.
Two directions, a handful of methods, three shapes of message underneath all of it. That is the entire protocol.
§This Is the Shape Behind Production
The knowledge base is a toy, but the shape is not. A real data platform is the same picture at scale: a server fronting actual systems, a warehouse, a lake, a catalog, exposing them as tools and resources, calling back for the model and for consent, behind auth, tested in memory before it ever sees a network. That is what txn2/mcp-data-platform is, and what Plexara runs on. The kb server has three documents in a map; swap the map for a query engine and the protocol does not change. The skeleton you just read is the production skeleton.
§Reading the Wire, From Here
The first post made a claim: that the SDK is a convenience, and conveniences leak, and the person who can read the wire ships the fix. Twenty posts later, that floor is yours. You can take any MCP frame, name its shape, say which direction it runs and whether its id belongs there, and tell whether what crossed the boundary is what the model should have seen. The SDK is still a typist that saves you keystrokes, and you should still use it. The difference is that it is no longer magic. It is JSON-RPC 2.0 over a transport, three message shapes, two directions, a short list of methods, and you have read every one of them off the wire.
That is the series. Build the thing, read what it sends, and trust nothing you have not seen on the wire.
The production data platform behind this series is txn2/mcp-data-platform, available hosted as Plexara.