← Back to list

Building an MCP Server in Rust: Lessons From a Production-Ready Implementation

Architecture decisions, protocol handling, and performance considerations beyond the tutorial stage

Michael Preston in Rustaceans · 2026-06-21 11:21 · 50 claps · 6.2 min read paywalled
#rust #mcp-server #coding #programming #architecture
Open on Medium ↗
Wiki topics: AGT · AI Agents 💻 · Programming 🏛️ · Architecture

Building an MCP Server in Rust: Lessons From a Production-Ready Implementation

Architecture decisions, protocol handling, and performance considerations beyond the tutorial stage

Google AI studio by Author

Google AI studio by Author

1. The Tutorial Stage Ends Faster Than You Think

I stopped thinking about MCP as a neat integration exercise once I looked at the protocol as it is actually defined today. MCP is built around a JSON-RPC data layer and a separate transport layer, and the transport is not an implementation detail you can shrug off once the server starts handling real traffic. The spec also treats tools, resources, prompts, notifications, and lifecycle negotiation as first-class parts of the protocol, not add-ons. That changes the engineering problem immediately.

The official Rust SDK reflects that shape. The repo describes rmcp as the official Rust SDK, built on Tokio, with separate crates for the core protocol and macro support, and its README now lists server, client, macros, auth, elicitation, and other capabilities in the same project tree. That is a strong signal that the production question is not “Can I expose a tool?” It is “How much protocol surface am I willing to own well?”

2. The Protocol Is Smaller Than the Product Around It

The first wrong assumption I made was that the server would mostly be about request handlers. It is not. MCP’s core data layer is JSON-RPC 2.0, but the real work starts once you decide how to expose context, how to negotiate capabilities, and how to keep the lifecycle predictable from initialization through shutdown. The spec calls out lifecycle management explicitly, along with server features like tools, resources, and prompts.

That framing matters because production failures rarely happen in the neat center of the protocol. They happen at the edges: capability mismatches, incomplete metadata, transport assumptions, and the quiet places where a client expects one shape of response while your server is returning another. I learned to treat the spec as an operating contract, not a feature checklist.

3. Transport Choice Decides the Shape of the Server

The biggest architectural fork is transport. MCP currently defines stdio and Streamable HTTP as the two standard transports, and the docs are explicit that stdio is the preferred local-process path when possible. stdio is simple and fast because the client launches the server as a subprocess and exchanges newline-delimited JSON-RPC messages over standard input and output. Streamable HTTP, by contrast, is designed for independent server processes and uses HTTP POST and GET, with optional SSE streaming.

That difference is not cosmetic. A stdio server feels like a child process with a protocol attached. A Streamable HTTP server feels like a network service with all the usual responsibilities: authentication, host validation, headers, load balancers, and connection lifetime concerns. The spec also notes that Streamable HTTP replaced the older HTTP+SSE transport from the 2024-11-05 version, which is a reminder that transport semantics in MCP are still maturing.

// Conceptual shape, not exact SDK API:
//
// 1) protocol layer: tools/resources/prompts
// 2) transport layer: stdio or Streamable HTTP
// 3) application layer: your business logic and state
//
// The mistake is letting these blur together.
//
// enum Transport {
//     Stdio,
//     StreamableHttp { addr: SocketAddr },
// }
//
// struct AppState {
//     db: DatabasePool,
//     cache: Cache,
//     config: Config,
// }
//
// async fn run_server(transport: Transport, state: AppState) -> anyhow::Result<()> {
//     // wire protocol handlers to business logic
//     // then bind them to the selected transport
//     Ok(())
// }

4. Tools Were Easy. Resources Took Real Thought.

Tools are the part everyone wants first because they are the most immediately useful. The spec says tools let servers expose callable actions that models can invoke, and the official Rust SDK README treats tools as a core supported feature. That part is straightforward enough that it can lull you into thinking the rest of the implementation will be equally direct.

Resources were where I had to slow down. MCP resources are meant to expose context data such as files, database schemas, or application-specific information, and each resource is identified by a URI. The capability can also include subscribe and listChanged, which means resources are not just static blobs; they can participate in change notification and longer-lived interaction patterns. That creates a different design pressure than tools do. You are no longer just answering a call. You are managing context over time.

That was the first place I had a failed implementation. I initially treated resources as a thin wrapper around a read function. That worked until the first client wanted discovery, then change notifications, then a stable URI scheme that did not turn into a maintenance problem six weeks later. The interface was small, but the lifecycle was not.

5. The Server Got Better When I Stopped Hiding State

I underestimated how much state the server would need to carry. Once you support real tools and resources, the server usually ends up owning more than a stateless handler. It carries cached context, auth boundaries, session metadata, and the operational hints needed to keep long-running requests intelligible. The MCP spec’s lifecycle, capability negotiation, and utility features like notifications and progress tracking are all clues that the protocol expects state to exist somewhere, even if the surface API looks tidy.

The production lesson for me was to make the state explicit rather than scattering it across handlers. The moment I did that, the code became easier to reason about under failure. A tool call could be traced back to the state it depended on. A resource read could be validated against the context it was allowed to see. A session could be resumed without pretending the server was stateless when it clearly was not. That honesty paid off quickly.

6. Security Was Not a Late-Stage Concern

I would not ship a Streamable HTTP MCP server without taking header validation seriously. The transport docs say Streamable HTTP is designed for remote operation and supports standard HTTP authentication methods, including bearer tokens, API keys, and custom headers, with OAuth recommended for token acquisition. That immediately raises the bar beyond “local tool server.”

The Rust SDK’s own security advisory for DNS rebinding made that concrete. The advisory says a malicious page could cause a local rmcp-based server to expose tools, resources, prompts, and other session state unless protections are in place. The SDK issue tracker also discusses host validation and the need for defense in depth around Origin. After reading that, I stopped treating “local” as a synonym for “safe.”

That changed the implementation style. I became much less interested in convenience-first defaults and much more interested in making authorization and transport boundaries visible in the code. In production, that is not paranoia. It is the part where the architecture stops being theoretical.

7. Performance Came From Simpler Boundaries, Not Clever Tricks

I expected performance tuning to be about micro-optimizing serialization or squeezing out a few extra allocations. That was not where the meaningful gains came from. The real wins came from reducing unnecessary transitions between protocol logic, application logic, and transport logic. MCP already separates those layers conceptually, and the best Rust implementation I built followed that separation in the code as well.

Tokio helped here because the SDK is built around it, and the transport model itself gives you clean places to batch, stream, or defer work. The key was not to make every tool call “faster” in isolation. It was to keep the server from doing extra work that did not change the outcome. Fewer copies. Fewer ad hoc conversions. Fewer places where the transport had to know about business rules. The system got quicker mostly because it got less tangled.

8. The Rust SDK Was Helpful Exactly Where I Wanted It to Be Opinionated

The official Rust SDK made the implementation easier in the places where I wanted a strong foundation: protocol handling, server/client support, macros for tools and prompts, and transport compatibility. Its README explicitly lists feature flags for server functionality, macros, OAuth support, and elicitation support, which is exactly the kind of surface area that makes a production integration feel less improvised.

What I appreciated more over time was the balance. The SDK gives structure without pretending the architecture is solved for you. That is important, because MCP is not just a “write a handler” problem. It is a context-serving system, and the way you organize tools, resources, and prompts determines whether the server becomes maintainable or turns into a pile of protocol-shaped shortcuts. The examples in the official repo also make it clear that the ecosystem is broad enough to cover stdio, Streamable HTTP, OAuth, and multi-client patterns.

9. The Real Lesson Was About Respecting the Protocol Boundary

By the time the server felt production-ready, I had stopped thinking of MCP as a wrapper around model calls. It is a protocol for exposing context and actions in a constrained, negotiated way, and the details matter more than they do in a tutorial. The transport choice changes deployment. The capability declarations change client behavior. The resource model changes how you think about data freshness. The security model changes how much trust you are allowed to give the network.

That is what I trust now: fewer magical abstractions, more explicit boundaries, and a server that admits what it is responsible for. The version I would ship is not the one with the shortest code path. It is the one that makes the next failure legible before it makes the next incident expensive.


메타데이터
post_id
0a3c190922e6
slug
building-an-mcp-server-in-rust-lessons-from-a-production-ready-implementation-0a3c190922e6
url
https://medium.com/rustaceans/building-an-mcp-server-in-rust-lessons-from-a-production-ready-implementation-0a3c190922e6
canonical_url
https://medium.com/rustaceans/building-an-mcp-server-in-rust-lessons-from-a-production-ready-implementation-0a3c190922e6
author_url
https://medium.com/@michaelpreston515
status
ok
fetched_at
2026-06-22 17:31:34