Building an MCP Server from Scratch Using Rust

Why Rust for MCP?

I've written MCP servers in Python and TypeScript before. They're productive and have great SDK support. But when I started thinking about production workloads — servers that get called thousands of times per minute by AI agents — Rust became the obvious choice. Zero-cost abstractions, no runtime overhead, memory safety without a GC, and async built on Tokio. The official Rust SDK (rmcp) is mature, well-maintained, and sits at over 3k stars on GitHub.

This article walks through building a real MCP server from scratch using rmcp, the official Rust MCP SDKarrow-up-right. I'll build a DNS lookup server — something practical that I've actually needed in my tooling.


What We're Building

An MCP server that exposes a single tool: dns_lookup. Given a hostname, it returns the resolved IP addresses. Simple, useful, and a good vehicle for learning all the moving parts.

The server will:

  • Run over stdio (suitable for use with Claude Desktop, VS Code Copilot, and any MCP host)

  • Expose one tool with typed input parameters

  • Return structured text results

  • Handle errors cleanly with proper McpError types


Prerequisites

# Rust stable (1.80+)
rustup update stable

# Check the toolchain
rustc --version
cargo --version

Project Setup

Add the dependencies to Cargo.toml:

Key dependencies explained:

  • rmcp — the official MCP Rust SDK with the server feature enabled

  • tokio — async runtime (rmcp requires it)

  • schemars — generates JSON Schema from Rust types (used for tool input schema)

  • serde / serde_json — serialization for JSON-RPC messages

  • tracing — structured logging (write to stderr, never stdout — stdout is the transport)


Core Concepts Before We Write Code

The rmcp SDK is built around three key traits:

Trait
What it does

ServerHandler

Defines server identity and capabilities via get_info()

#[tool_router] macro

Registers methods as MCP tools using #[tool(...)] attribute

#[tool_handler] macro

Wires the router into the ServerHandler implementation

ServiceExt::serve()

Starts the server over a transport (stdio, HTTP, etc.)

The macros generate the JSON Schema for your tool inputs from your Rust types automatically via schemars. This means your tool's inputSchema in the MCP protocol is always in sync with your actual function signature.


Implementation

Step 1: Define the Input Type

The #[derive(JsonSchema)] on DnsLookupInput is what allows rmcp to generate the correct inputSchema JSON automatically for the tool registration — no manual schema writing needed.


Step 2: Define the Server Struct

The ToolRouter<Self> field is a compile-time generated registry of all #[tool]-annotated methods on this struct. The #[tool_router] macro on the impl block populates it.


Step 3: Implement the Tool


Step 4: Implement ServerHandler

The #[tool_handler] macro wires the ToolRouter into the ServerHandler trait automatically. You only need to implement get_info() manually — this is where you declare your server's identity and capabilities.


Step 5: Wire Up the Entry Point


Full src/main.rs


Building and Testing

Test with MCP Inspector

The MCP Inspector is the fastest way to manually test any MCP server:

Then in the inspector UI, call dns_lookup with {"hostname": "github.com"}.

Test Manually via stdin

Since the protocol is JSON-RPC 2.0 over stdio, you can test directly:


Integrating with Claude Desktop

Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):

Restart Claude Desktop and ask: "What are the IP addresses for github.com?"


Integrating with VS Code Copilot

Add .vscode/mcp.json to your project:


Adding a Resource

To expose a read-only resource (e.g., a list of common DNS servers), implement read_resource on ServerHandler:


Key Patterns to Remember

  1. Always log to stderr — stdout is the JSON-RPC transport wire. Any stray println! will corrupt the protocol framing.

  2. #[tool_router] + #[tool_handler] go together — the router macro registers tools; the handler macro wires them into ServerHandler.

  3. Parameters<T> wraps input deserialization — your tool args are automatically deserialized from the JSON-RPC arguments field into your type.

  4. JsonSchema on input types = automatic inputSchema — the MCP client discovers what parameters your tool expects directly from your Rust type.

  5. service.waiting().await? — this blocks until the MCP host closes the connection. Don't exit early.


What's Next

  • Add multiple tools to the same server

  • Add Resources to expose static or dynamic data

  • Add Prompts for reusable LLM interaction templates

  • Switch to Streamable HTTP transport for a remote server deployment

The official repo has more examples: github.com/modelcontextprotocol/rust-sdk/tree/main/examplesarrow-up-right

Last updated