A minimal MCP (Model Context Protocol) server template built with Rust, Axum, and Tower. This template provides authentication, tool registration, and a clean architecture for building custom MCP servers.
Warning
You connect this server to your MCP client at your own responsibility. Language models can make mistakes, misinterpret instructions, or perform unintended actions. Always verify commands before execution, especially for database operations (INSERT, UPDATE, DELETE), Python script execution, or operations using external API keys from your credentials.
The current authentication system stores API keys in plain text and is suitable for development and internal use. For production deployments, implement additional security measures: API key hashing (bcrypt/argon2), TLS/HTTPS termination, rate limiting, audit logging, secure credential storage, and response caching to prevent excessive API calls.
- Authentication: Bearer token authentication with per-user credentials
- MCP Protocol: JSON-RPC 2.0 compliant implementation
- Tool System: Trait-based tool registration with JSON Schema validation
- Type Safety: Leverages Rust's type system for compile-time guarantees
- Performance: Arc-wrapped state, O(1) API key lookups
- Security: Middleware-based authentication, per-user external credentials
- Extensibility: Simple trait implementation to add new tools
mcp-server/
├── src/
│ ├── main.rs # Server entry point
│ ├── lib.rs # Core MCP protocol implementation
│ ├── auth/ # Authentication module
│ │ ├── mod.rs # Module exports
│ │ ├── types.rs # Credential data structures
│ │ ├── middleware.rs # Tower authentication layer
│ │ ├── error.rs # Auth-specific errors
│ │ └── loader.rs # TOML credentials loading
│ └── tools/ # Tool implementations
│ ├── mod.rs # McpTool trait and registration
│ └── get_time.rs # Example tool
├── config/
│ └── credentials.toml # User credentials (not in git)
└── Cargo.toml # Dependencies
- MCP Protocol Handler (
lib.rs): Handlesdiscoverandinvokerequests - Authentication Layer (
auth/): Tower middleware for Bearer token validation - Tool Registry (
tools/): Trait-based system for registering and executing tools - Credentials Store (
auth/types.rs): HashMap indexed by API key for O(1) lookups
git clone <repository-url> mcp-server
cd mcp-servercp config/credentials.toml.example config/credentials.tomlEdit config/credentials.toml and add your API keys:
[alice]
api_key = "your-secure-api-key-here"
[alice.external_keys]
# Add external service credentials if neededImportant: Add config/credentials.toml to .gitignore to avoid committing secrets!
cargo build --release
cargo runThe server will start on http://0.0.0.0:3000.
Health Check:
curl http://localhost:3000/healthDiscover Tools:
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"method": "discover"}'Invoke a Tool:
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"method": "invoke", "params": {"tool_name": "get_current_time"}}'MCP_CREDENTIALS_PATH: Path to credentials file (default:config/credentials.toml)
[username]
api_key = "bearer-token-for-authentication"
[username.external_keys]
# Optional: External service credentials for this user
postgres_url = "postgresql://localhost/dbname"
stripe_key = "sk_test_..."Health check endpoint (no authentication required).
Response:
OK
Main MCP endpoint (requires Bearer authentication).
Request Format:
{
"method": "discover" | "invoke",
"params": { ... }
}Returns a list of all available tools.
Request:
{
"method": "discover"
}Response:
{
"jsonrpc": "2.0",
"result": {
"tools": [
{
"name": "get_current_time",
"description": "Returns the current server time as an ISO 8601 string.",
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": false,
"required": []
}
}
]
}
}Executes a specific tool.
Request:
{
"method": "invoke",
"params": {
"tool_name": "get_current_time",
"arguments": {}
}
}Response:
{
"jsonrpc": "2.0",
"result": {
"current_time": "2025-12-15T10:30:00.123456789Z"
}
}MCP Server uses JSON-RPC 2.0 error codes:
| Code | Name | Description |
|---|---|---|
| -32001 | ERROR_AUTH | Authentication failure |
| -32002 | ERROR_INVALID_PARAMS | Invalid or missing parameters |
| -32003 | ERROR_TOOL_EXECUTION | Tool execution error |
| -32600 | ERROR_INVALID_REQUEST | Malformed request |
| -32601 | ERROR_METHOD_NOT_FOUND | Tool not found |
Error Response Example:
{
"jsonrpc": "2.0",
"error": {
"code": -32601,
"message": "Tool 'unknown_tool' not found",
"data": {
"available_tools": ["get_current_time"]
}
}
}Tools are automatically registered using the #[mcp_tool] attribute macro. No manual registration needed!
Create a new file in src/tools/, e.g., src/tools/my_tool.rs:
use super::{mcp_tool, validate_tool_args, McpTool, PinBoxedFuture};
use crate::auth::AuthenticatedUser;
use anyhow::{Error, Result};
use serde_json::{json, Value};
#[mcp_tool] // <-- This attribute auto-registers the tool!
pub struct MyTool;
impl McpTool for MyTool {
fn name(&self) -> &'static str {
"my_tool"
}
fn description(&self) -> &'static str {
"Description of what my tool does"
}
fn parameters_schema(&self) -> Value {
json!({
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "Input parameter",
"minLength": 1
}
},
"required": ["input"],
"additionalProperties": false
})
}
fn execute(
&self,
args: Option<Value>,
user: AuthenticatedUser,
) -> PinBoxedFuture<Result<Value, Error>> {
let schema = self.parameters_schema();
Box::pin(async move {
// Validate arguments
validate_tool_args(&schema, &args)?;
// Extract arguments
let args_obj = args.unwrap();
let input = args_obj["input"].as_str().unwrap();
// Access user's external credentials if needed
if let Some(api_key) = user.get_external_key("some_service_key") {
// Use the API key
}
// Execute tool logic
let result = format!("Processed: {}", input);
Ok(json!({ "output": result }))
})
}
}In src/tools/mod.rs, simply add the module declaration:
pub mod my_tool;That's it! The #[mcp_tool] attribute automatically:
- Validates the struct is public (compile-time check)
- Generates the registration code
- Submits the tool to the inventory for automatic discovery
- Ensures no duplicate tool names at startup
Note: Tool structs must be:
pub(public visibility required)- Unit structs (no generics allowed)
- Implementing the
McpTooltrait
cargo build
cargo runcurl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{
"method": "invoke",
"params": {
"tool_name": "my_tool",
"arguments": {
"input": "test"
}
}
}'cargo runcargo build --release
./target/release/mcp-serverThe project includes a comprehensive test suite with 160+ tests achieving industry-standard coverage targets.
Run all tests:
# Run tests sequentially (recommended for env var tests)
cargo test -- --test-threads=1
# Run tests in parallel (faster, but may have race conditions)
cargo testRun specific test suites:
cargo test --test auth_tests # Auth module tests
cargo test --test integration_tests # Full HTTP integration tests
cargo test --test tools_proptest # Property-based testsCheck code coverage:
# Install tarpaulin (one-time)
cargo install cargo-tarpaulin
# Generate coverage report
cargo tarpaulin --out Html --output-dir coverage
# Open coverage/index.html in your browserWhen adding new code, maintain these coverage targets:
| Module | Target | Reason |
|---|---|---|
auth/ |
100% | Security-critical authentication logic |
tools/ |
90%+ | Complex validation and execution paths |
lib.rs |
80%+ | Core protocol handlers and routing |
| Overall | 85%+ | Industry standard for production code |
Adding tests for new tools:
When you create a new tool in src/tools/, add corresponding tests in tests/tools_tests.rs:
#[test]
fn test_my_tool_validates_required_params() {
let schema = MyTool.parameters_schema();
let args = None; // Missing required params
let result = validate_tool_args(&schema, &args);
assert!(result.is_err());
}
#[test]
fn test_my_tool_executes_successfully() {
// Test happy path execution
}
#[test]
fn test_my_tool_handles_invalid_input() {
// Test error cases
}Integration test pattern:
Add integration tests in tests/integration_tests.rs for new endpoints:
#[tokio::test]
async fn test_my_tool_via_http() {
let credentials = create_test_credentials_store();
let app = create_app(credentials);
let server = TestServer::new(app).unwrap();
let response = server
.post("/mcp")
.add_header(
http::HeaderName::from_static("authorization"),
http::HeaderValue::from_str(&format!("Bearer {}", TEST_API_KEY)).unwrap()
)
.json(&json!({
"method": "invoke",
"params": {
"tool_name": "my_tool",
"arguments": {"input": "test"}
}
}))
.await;
response.assert_status_ok();
}src/
└── main.rs # Server setup tests (2 tests)
tests/
├── common/mod.rs # Shared test utilities
├── auth_tests.rs # Auth module unit tests (18 tests)
├── auth_loader_tests.rs # Credential loading tests (12 tests)
├── auth_middleware_tests.rs # Middleware tests (14 tests)
├── tools_tests.rs # Tool validation tests (51 tests)
├── tools_proptest.rs # Property-based tests (13 tests)
├── lib_tests.rs # Handler unit tests (34 tests)
└── integration_tests.rs # Full HTTP tests (18 tests)
Total: 162 tests covering authentication, tool validation, protocol handling, and server initialization.
- Modularity: Keep auth, tools, and protocol logic separate
- Error Handling: Use
anyhow::Resultfor flexible error propagation - Validation: Always validate tool arguments using
validate_tool_args - Security: Never log API keys or sensitive credentials
- Performance: Use
Arcfor shared state, avoid unnecessary clones
- API Keys: Store credentials in
config/credentials.toml(not in git) - HTTPS: Use a reverse proxy (nginx, Caddy) for TLS in production
- Rate Limiting: Consider adding rate limiting middleware
- Input Validation: Always validate tool parameters using JSON Schema
- External Credentials: Store per-user secrets in
external_keys
- Check that
config/credentials.tomlexists and is valid TOML - Verify port 3000 is not already in use
- Check file permissions on config directory
- Verify the
Authorization: Bearer <token>header is correct - Check that the API key exists in
credentials.toml - Ensure there are no whitespace issues in the API key
- Check tool parameter validation in the schema
- Review error messages for specific validation failures
- Verify external credentials are configured if needed
MIT License - see LICENSE file for details.
-
Fork the repository
-
Create a feature branch
-
Make your changes
-
Add tests - Required for all new features:
- Unit tests for new functions/modules
- Integration tests for new endpoints
- Property tests for complex validation logic
-
Verify coverage meets targets:
cargo test -- --test-threads=1 # All tests must pass cargo tarpaulin --out Html --output-dir coverage
-
Submit a pull request with:
- Clear description of changes
- Test coverage report summary
- Any new dependencies justified
- Add more example tools
- WebSocket support for streaming responses
- procedural macro crate
#[mcp_tool] - Rate limiting middleware
- Prometheus metrics
- Docker Compose setup
- Kubernetes deployment manifests