A full-featured Model Context Protocol (MCP) server implementation in Rust, providing tools, resources, and prompts for LLM integration.
- Overview
- Features
- Project Structure
- Prerequisites
- Installation
- Building the Project
- Running the Server
- Testing the Server
- API Reference
- Adding Custom Tools
- Troubleshooting
- Security Considerations
This Rust MCP Server implements the Model Context Protocol (MCP) specification, enabling seamless integration between LLM applications and external tools, resources, and knowledge bases.
The Model Context Protocol is an open standard that allows:
- LLM Applications to connect to external data sources and tools
- Servers to expose capabilities (tools, resources, prompts) in a standardized way
- Secure Integration without modifying client applications
✅ Type-Safe: Leverages Rust's type system for safety
✅ Performant: Async/await with Tokio for high concurrency
✅ Modular: Easy to add custom tools and resources
✅ Secure: Built-in validation and error handling
✅ Stdio-Based: Simple deployment as a subprocess
-
Greeting Tool (
greet)- Greets users with personalized messages
- Input:
name(string) - Output: Greeting message
-
BMI Calculator Tool (
calculate-bmi)- Calculates Body Mass Index
- Inputs:
weightKg(number),heightM(number) - Output: Calculated BMI value
-
Weather Tool (
fetch-weather)- Fetches weather information (simulated)
- Input:
city(string) - Output: Weather data (temperature, condition, humidity, etc.)
- Application Configuration (
config://app)- Provides application metadata and settings
- Returns JSON configuration
- Code Review (
review-code)- Generates prompts for LLM to review code
- Arguments:
code(required),focus(optional: performance, security, style, general)
- ✅ JSON-RPC 2.0 compliant
- ✅ Stdio transport (newline-delimited JSON)
- ✅ Proper error handling with standard error codes
- ✅ Logging to stderr
- ✅ Protocol versioning (2024-11-05)
mcp-server-rust/
├── Cargo.toml # Project manifest
├── README.md # This file
├── src/
│ ├── main.rs # Entry point
│ ├── lib.rs # Library exports
│ ├── server.rs # MCP server implementation
│ ├── tools/
│ │ ├── mod.rs # Tool definitions
│ │ ├── greeting_tool.rs # Greeting tool implementation
│ │ ├── calculator_tool.rs # BMI calculator tool
│ │ └── weather_tool.rs # Weather tool (simulated)
│ ├── resources/
│ │ ├── mod.rs # Resource definitions
│ │ ├── config_resource.rs # App config resource
│ │ └── file_resource.rs # File-based resource
│ ├── prompts/
│ │ ├── mod.rs # Prompt definitions
│ │ └── code_review_prompt.rs # Code review prompt
│ ├── transport/
│ │ ├── mod.rs # Transport trait
│ │ └── stdio.rs # Stdio implementation
│ └── utils/
│ ├── mod.rs # Utility modules
│ ├── logger.rs # Logging utilities
│ └── error.rs # Error types
└── data/
└── (sample data files)
- OS: macOS, Linux, or Windows (with WSL2)
- RAM: 512 MB minimum (2 GB recommended)
- Disk Space: 1 GB (mostly for dependencies)
-
Rust (1.70 or later)
# Install from https://rustup.rs/ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
Cargo (comes with Rust)
cargo --version
-
Git
git --version
rustc --version
cargo --versiongit clone https://github.com/gitsudhir/mcp-server-rust.git
cd mcp-server-rustOr create a new project from scratch:
cargo new mcp-server-rust
cd mcp-server-rustCopy all provided source files into the src/ directory as shown in the Project Structure section.
Ensure your Cargo.toml matches the provided configuration with all required dependencies.
cargo checkThis downloads and verifies all dependencies without building.
# Build with debug information (slower, larger binary)
cargo build
# Output: target/debug/mcp-server-rust# Build optimized binary (faster execution)
cargo build --release
# Output: target/release/mcp-server-rust# Check without building
cargo check
# Run tests
cargo test
# View warnings
cargo clippy# Using debug binary
./target/debug/mcp-server-rust
# Using release binary
./target/release/mcp-server-rust# Enable debug logging
RUST_LOG=debug ./target/release/mcp-server-rust
# Enable specific module logging
RUST_LOG=rust_mcp_server::server=debug,rust_mcp_server::transport=debug ./target/release/mcp-server-rust
# All logs
RUST_LOG=trace ./target/release/mcp-server-rustFor easier invocation, create a shell script:
#!/bin/bash
exec /full/path/to/mcp-server-rust "$@"Make it executable:
chmod +x mcp-server.shTest in another terminal while the server is running:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"TestClient","version":"1.0.0"}}}' | ./target/release/mcp-server-rustecho '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"greet","arguments":{"name":"Alice"}}}' | ./target/release/mcp-server-rustecho '{"jsonrpc":"2.0","id":3,"method":"tools/list"}' | ./target/release/mcp-server-rustecho '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"calculate-bmi","arguments":{"weightKg":70,"heightM":1.75}}}' | ./target/release/mcp-server-rustecho '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"fetch-weather","arguments":{"city":"San Francisco"}}}' | ./target/release/mcp-server-rustecho '{"jsonrpc":"2.0","id":6,"method":"prompts/get","params":{"name":"review-code","arguments":{"code":"fn main() { println!(\"Hello!\"); }","focus":"performance"}}}' | ./target/release/mcp-server-rustThe official MCP Inspector tool allows interactive testing:
-
Install Inspector:
npm install -g @modelcontextprotocol/inspector
-
Run Server:
./target/release/mcp-server-rust
-
Run Inspector (in another terminal):
mcp-inspector stdio ./target/release/mcp-server-rust
-
Open the Inspector UI in your browser and test tools interactively.
# Run built-in tests
cargo test
# Run with output
cargo test -- --nocapture
# Run specific test
cargo test test_name -- --nocapturePurpose: Initialize the MCP connection
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "ClientName",
"version": "1.0.0"
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"serverInfo": {
"name": "RustMcpServer",
"version": "1.0.0"
}
}
}Purpose: List available tools
Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "greet",
"description": "Greets a person with a friendly message",
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the person to greet"
}
},
"required": ["name"]
},
"annotations": {
"title": "Greet Tool",
"readOnlyHint": true
}
}
]
}
}Purpose: Call a tool
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "greet",
"arguments": {
"name": "Alice"
}
}
}Response (Success):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Hello, Alice! Welcome to MCP."
}
],
"isError": false
}
}Response (Error):
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Invalid params: Missing 'name' parameter"
}
}Purpose: List available resources
Request:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/list"
}Response:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resources": [
{
"uri": "config://app",
"name": "Application Configuration",
"description": "Current application configuration",
"mimeType": "application/json"
}
]
}
}Purpose: Read a resource
Request:
{
"jsonrpc": "2.0",
"id": 5,
"method": "resources/read",
"params": {
"uri": "config://app"
}
}Response:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"contents": [
{
"uri": "config://app",
"mimeType": "application/json",
"text": "{\"appName\": \"Rust MCP Server\", \"version\": \"1.0.0\", ...}"
}
]
}
}Purpose: List available prompts
Request:
{
"jsonrpc": "2.0",
"id": 6,
"method": "prompts/list"
}Response:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"prompts": [
{
"name": "review-code",
"description": "Generates a prompt to ask the LLM to review code",
"arguments": [
{
"name": "code",
"description": "The code snippet to review",
"required": true
},
{
"name": "focus",
"description": "Optional area of focus",
"required": false
}
]
}
]
}
}Purpose: Get a prompt
Request:
{
"jsonrpc": "2.0",
"id": 7,
"method": "prompts/get",
"params": {
"name": "review-code",
"arguments": {
"code": "fn main() { println!(\"Hello!\"); }",
"focus": "performance"
}
}
}Response:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"description": "Requesting performance review for code snippet",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Please review the following code for potential issues and suggest improvements, focusing specifically on performance:\n\n```\nfn main() { println!(\"Hello!\"); }\n```"
}
]
}
]
}
}use super::{Tool, CallToolResult, TextContent, ToolHandler};
use serde_json::{json, Value};
use async_trait::async_trait;
use crate::utils::{Result, Error, Logger};
pub struct CustomTool {
logger: Logger,
}
impl CustomTool {
pub fn new() -> Self {
Self {
logger: Logger::new("CustomTool"),
}
}
pub fn tool_definition() -> Tool {
Tool {
name: "custom-tool".to_string(),
description: "Description of your custom tool".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "First parameter"
},
"param2": {
"type": "number",
"description": "Second parameter"
}
},
"required": ["param1"]
}),
annotations: Some(json!({
"title": "Custom Tool",
"readOnlyHint": true
})),
}
}
}
impl Default for CustomTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl ToolHandler for CustomTool {
async fn call(&self, arguments: Value) -> Result<CallToolResult> {
let param1 = arguments
.get("param1")
.and_then(|v| v.as_str())
.ok_or_else(|| Error::InvalidParams("Missing 'param1'".to_string()))?;
let param2 = arguments
.get("param2")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
self.logger.debug_with_context(
"Tool called",
&format!("param1={}, param2={}", param1, param2),
);
// Your custom logic here
let result = format!("Processed: {} and {}", param1, param2);
Ok(CallToolResult::success(vec![TextContent::new(result)]))
}
}Add to src/tools/mod.rs:
pub mod custom_tool;
pub use custom_tool::CustomTool;In src/server.rs, update handle_tools_list:
async fn handle_tools_list(&self, _message: &Value) -> Result<Value> {
self.logger.debug("Listing tools");
let tools = vec![
GreetingTool::tool_definition(),
CalculatorTool::tool_definition(),
WeatherTool::tool_definition(),
CustomTool::tool_definition(), // Add this
];
Ok(json!({
"tools": tools
}))
}And in handle_tools_call, add the match arm:
"custom-tool" => {
let handler = CustomTool::new();
handler.call(arguments).await?
}cargo build --release
# Test
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"custom-tool","arguments":{"param1":"test","param2":42}}}' | ./target/release/mcp-server-rust# Check if executable exists
ls -la target/release/mcp-server-rust
# Try building again
cargo clean
cargo build --release
# Check for errors
cargo check- Verify the server is running
- Check if another process is using the port
- View logs:
RUST_LOG=trace ./target/release/mcp-server-rust
- Verify tool is registered in
handle_tools_list - Check tool name matches exactly
- Verify match arm in
handle_tools_call
- Check argument names in JSON-RPC request
- Verify types (string vs number)
- Look at tool's
inputSchema
- Check if server is waiting for input
- Verify stdin/stdout not blocked
- Enable logging:
RUST_LOG=trace ./target/release/mcp-server-rust
# All modules
RUST_LOG=debug ./target/release/mcp-server-rust
# Specific module
RUST_LOG=rust_mcp_server::server=debug ./target/release/mcp-server-rust
# Very detailed
RUST_LOG=trace ./target/release/mcp-server-rust# Terminal 1: Start server
./target/release/mcp-server-rust
# Terminal 2: Run inspector
npx @modelcontextprotocol/inspector stdio ./target/release/mcp-server-rust✅ All tool arguments are validated using JSON Schema
✅ File paths are checked for traversal attempts
✅ Tool parameters are type-checked
✅ Tool results are serialized safely as JSON
✅ No raw command execution
✅ Error messages don't expose internal details
-
Never trust client input
// BAD let path = arguments.get("file").unwrap(); // GOOD let path = arguments .get("file") .and_then(|v| v.as_str()) .ok_or_else(|| Error::InvalidParams("..."))?;
-
Validate file paths
fn validate_path(&self, filename: &str) -> Result<PathBuf> { let requested = self.base_dir.join(filename); let resolved = std::fs::canonicalize(&requested)?; if !resolved.starts_with(&self.base_dir) { return Err(Error::ResourceError("Path traversal".into())); } Ok(resolved) }
-
Handle errors safely
match operation() { Ok(result) => Ok(CallToolResult::success(...)), Err(e) => { self.logger.error(&e.to_string()); // Log internally Ok(CallToolResult::error("Operation failed")) // Generic response } }
-
Set resource limits
const MAX_RESULT_SIZE: usize = 10_000_000; // 10MB if result.len() > MAX_RESULT_SIZE { return Err(Error::InternalError("Result too large".into())); }
While the current implementation uses Stdio, you can add HTTP support:
use crate::utils::Result;
use async_trait::async_trait;
use axum::{extract::Json, http::StatusCode, routing::post, Router};
use serde_json::Value;
#[async_trait]
pub async fn http_handler(
Json(payload): Json<Value>,
) -> (StatusCode, Json<Value>) {
// Handle JSON-RPC request
(StatusCode::OK, Json(payload))
}
pub fn create_router() -> Router {
Router::new()
.route("/mcp", post(http_handler))
}use std::sync::Arc;
use tokio::sync::RwLock;
pub struct ServerState {
data: Arc<RwLock<HashMap<String, Value>>>,
}
impl ServerState {
pub async fn get(&self, key: &str) -> Option<Value> {
self.data.read().await.get(key).cloned()
}
pub async fn set(&self, key: String, value: Value) {
self.data.write().await.insert(key, value);
}
}#[async_trait]
impl ToolHandler for AsyncTool {
async fn call(&self, arguments: Value) -> Result<CallToolResult> {
// Can use tokio::time::sleep, reqwest, etc.
let result = tokio::time::timeout(
std::time::Duration::from_secs(30),
self.perform_async_work()
).await??;
Ok(CallToolResult::success(vec![TextContent::new(result)]))
}
}Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Submit a pull request
MIT License - see LICENSE file for details
- MCP Specification: modelcontextprotocol.io
- Rust Guide: doc.rust-lang.org
- Tokio Docs: tokio.rs
Q: Can I run multiple MCP servers?
A: Yes! Each application can manage multiple client-server connections.
Q: Is this production-ready?
A: The core is stable, but review security considerations before production use.
Q: Can I add database integration?
A: Absolutely! Use sqlx, tokio-postgres, etc. in your tool handlers.
Q: How do I debug tools?
A: Enable logging with RUST_LOG=debug and use the MCP Inspector tool.
For issues and questions:
- Check this README and examples
- Enable debug logging
- Review MCP specification
- Open a GitHub issue
Last Updated: 2026-02-13
Version: 1.0.0
Status: Stable