TL;DR
Rust MCP servers use 5-10x less memory than Node.js and handle true parallel execution via Tokio's async runtime.
Define tool inputs as strongly-typed structs with serde, and use Arc plus RwLock for safe shared state across concurrent handlers.
Start with TypeScript for prototyping, then rewrite in Rust only when you have measured performance bottlenecks or need memory safety guarantees.
Custom MCP server development in Rust delivers performance and memory safety that TypeScript cannot match. For teams building MCP servers that handle high-throughput data, interact with blockchain nodes, or run compute-intensive operations, Rust is the right choice. The tradeoff is a steeper learning curve and more verbose code, but the result is a server that uses less memory, handles more concurrent connections, and eliminates entire categories of runtime errors.
This guide walks through building a production MCP server in Rust using the community SDK, Tokio for async runtime, and serde for serialization.
Why Rust for MCP Servers?
Most MCP servers are written in TypeScript or Python. Rust makes sense in specific scenarios:
High-throughput data processing. If your server needs to parse large datasets, process blockchain transaction streams, or handle hundreds of concurrent tool calls, Rust's zero-cost abstractions and efficient memory model matter.
Integration with existing Rust codebases. If your team already has Rust libraries for blockchain interaction (like Anchor, Solana SDK, or custom protocol clients), wrapping them in a Rust MCP server avoids FFI overhead and keeps the dependency tree clean.
Memory-constrained environments. Rust MCP servers typically use 5-10x less memory than equivalent Node.js servers. For edge deployments or containerized environments with tight resource limits, this matters.
Security-critical applications. Rust's type system and ownership model prevent buffer overflows, use-after-free bugs, and data races at compile time. For MCP servers that handle sensitive data or execute privileged operations, these guarantees are valuable.
If none of these apply, TypeScript is faster to develop in and has a more mature MCP ecosystem. Choose Rust when the performance or safety requirements justify the investment.
Project Setup
Create a new Rust project and add the required dependencies:
cargo new mcp-server-rust
cd mcp-server-rust
Add to your Cargo.toml:
[dependencies]
mcp-server = "0.2" # Community Rust MCP SDK
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
The mcp-server crate provides the protocol implementation. Tokio handles async I/O. Serde handles JSON serialization. Anyhow provides ergonomic error handling. Tracing gives structured logging (to stderr, never stdout).
The Async Architecture
Rust MCP servers are async by nature. The Tokio runtime manages concurrent connections, tool executions, and I/O operations. Every tool handler is an async function, and the server processes multiple requests concurrently on the Tokio thread pool.
This is where Rust shines compared to Node.js. Node runs on a single thread with an event loop, meaning CPU-intensive tool handlers block all other requests. Rust with Tokio uses a multi-threaded work-stealing scheduler. CPU-intensive work runs on dedicated threads without blocking the I/O reactor. For MCP servers that parse blockchain data, run ML inference, or process large datasets, this difference is significant.
Defining Tools with Strongly-Typed Inputs
In Rust, tool inputs are defined as structs with serde derive macros. The struct fields map directly to the JSON Schema that the AI model sees. Here is a tool that queries a blockchain node for account balance:
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct GetBalanceInput {
/// The base58-encoded public key of the account
address: String,
/// Optional commitment level: finalized, confirmed, or processed
#[serde(default = "default_commitment")]
commitment: String,
}
fn default_commitment() -> String {
"confirmed".to_string()
}
The doc comments (///) on struct fields become the descriptions in the JSON Schema. This is the Rust equivalent of Zod's .describe() in TypeScript. Write clear, specific descriptions that tell the model what format to use and what constraints apply.
Register the tool with the server by implementing the Tool trait or using the SDK's builder pattern. The exact API depends on which Rust MCP crate you use, but the pattern is consistent: define a name, description, input type, and async handler function.
The handler receives the validated input struct and returns a Result containing content blocks. Rust's type system ensures you handle all error cases at compile time, which is a significant advantage over TypeScript where runtime errors in tool handlers are common.
Error Handling the Rust Way
Rust's Result type forces explicit error handling at every step. This eliminates the class of bugs where a tool handler throws an unexpected exception and crashes the server. Use the ? operator for propagation and map errors to user-friendly messages at the boundary:
async fn handle_get_balance(input: GetBalanceInput) -> Result<ToolResponse> {
let pubkey = Pubkey::from_str(&input.address)
.map_err(|e| anyhow!("Invalid address format: {}", e))?;
let balance = rpc_client
.get_balance_with_commitment(&pubkey, commitment_from_str(&input.commitment))
.await
.map_err(|e| anyhow!("RPC call failed: {}", e))?;
Ok(ToolResponse::text(format!(
"{{\\"address\\": \\"{}\\", \\"balance_sol\\": {}, \\"balance_lamports\\": {}}}",
input.address,
balance.value as f64 / 1_000_000_000.0,
balance.value
)))
}
Every error path is handled. The compiler enforces it. In TypeScript, you have to remember to add try/catch blocks. In Rust, the type system makes unhandled errors a compilation failure.
Shared State and Concurrency
Many MCP servers need shared state: database connection pools, RPC client instances, cached configuration. In Rust, shared state across async tasks requires explicit synchronization. The most common pattern is wrapping shared data in Arc (atomic reference counting) and using Mutex or RwLock for interior mutability.
use std::sync::Arc;
use tokio::sync::RwLock;
struct ServerState {
rpc_client: RpcClient,
cache: RwLock<HashMap<String, CachedBalance>>,
}
let state = Arc::new(ServerState {
rpc_client: RpcClient::new("<https://api.mainnet-beta.solana.com>"),
cache: RwLock::new(HashMap::new()),
});
Pass Arc<ServerState> into each tool handler. Use RwLock::read() for concurrent reads and RwLock::write() for exclusive writes. Tokio's async-aware locks ensure you never block the runtime while waiting for a lock.
Performance Comparison: Rust vs TypeScript MCP Servers
Based on our experience building MCP servers in both languages, here are typical performance characteristics:
Memory usage: Rust servers typically use 10-30MB at idle. Node.js servers start at 50-100MB due to the V8 heap. Under load, the gap widens further.
Startup time: Rust binaries start in milliseconds. Node.js servers take 200-500ms for module loading and JIT compilation warmup.
Throughput: For CPU-bound tool handlers (data parsing, cryptographic operations, serialization), Rust is 5-20x faster. For I/O-bound handlers (HTTP calls, database queries), the difference narrows to 1.5-3x.
Development speed: TypeScript is 2-3x faster to develop. The MCP SDK is more mature, the ecosystem has more examples, and the edit-compile-test cycle is shorter.
The practical takeaway: build in TypeScript first. Rewrite in Rust when you have measured performance bottlenecks that justify the investment. The MCP protocol is the same regardless of implementation language, so switching later is straightforward.
When to Choose Rust from the Start
Start with Rust if: your team already writes Rust daily, the server integrates with Rust blockchain libraries (Solana SDK, Anchor), the deployment environment has strict memory limits (under 50MB), or the tool handlers involve heavy computation (transaction parsing, proof verification, data aggregation at scale).
Start with TypeScript if: you are prototyping, the integration is primarily I/O-bound (API calls, database queries), or you need to ship fast and iterate. You can always rewrite individual servers in Rust later without changing the protocol interface.
Exo builds production MCP servers in both Rust and TypeScript. Our team has deep expertise in Rust (26 Solana protocols shipped, all in Rust) and we choose the right tool for each integration. Whether you need a high-performance blockchain data server or a rapid-iteration internal tooling server, we build it. Ready to build? Reach out at founders@exotechnologies.xyz
