mcp-kit 0.1.4

Ergonomic, type-safe Rust library for building MCP servers with authentication support
Documentation

mcp-kit

Crates.io Documentation License: MIT CI MSRV

An ergonomic, type-safe Rust library for building Model Context Protocol (MCP) servers.

MCP enables AI assistants to securely access tools, data sources, and prompts through a standardized protocol. This library provides a modern, async-first implementation with powerful procedural macros for rapid development.


Features

  • 🚀 Async-first — Built on Tokio for high-performance concurrent operations
  • 🛡️ Type-safe — Leverage Rust's type system with automatic JSON Schema generation
  • 🎯 Ergonomic macros#[tool], #[resource], #[prompt] attributes for minimal boilerplate
  • 🔌 Multiple transports — stdio (default), SSE/HTTP, and HTTPS/TLS support
  • 🔐 Authentication — Bearer, API Key, Basic, OAuth 2.0, and mTLS support
  • 📝 Completion — Auto-complete argument values for prompts and resources
  • 📊 Progress tracking — Report progress for long-running operations
  • 📢 Notifications — Push updates to clients (resource changes, log messages)
  • 🧩 Modular — Feature-gated architecture, WASM-compatible core
  • 📦 Batteries included — State management, error handling, tracing integration
  • 🎨 Flexible APIs — Choose between macro-based or manual builder patterns

Installation

Add to your Cargo.toml:

[dependencies]
mcp-kit = "0.1"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
schemars = "0.8"
anyhow = "1"  # For error handling

Minimum Supported Rust Version (MSRV): 1.85


Quick Start

Using Macros (Recommended)

The fastest way to build an MCP server with automatic schema generation:

use mcp_kit::prelude::*;

/// Add two numbers
#[tool(description = "Add two numbers and return the sum")]
async fn add(a: f64, b: f64) -> String {
    format!("{}", a + b)
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    McpServer::builder()
        .name("calculator")
        .version("1.0.0")
        .tool_def(add_tool_def())  // Generated by #[tool] macro
        .build()
        .serve_stdio()
        .await?;
    Ok(())
}

Manual API

For more control over schema and behavior:

use mcp_kit::prelude::*;
use schemars::JsonSchema;
use serde::Deserialize;

#[derive(Deserialize, JsonSchema)]
struct AddInput {
    /// First operand
    a: f64,
    /// Second operand  
    b: f64,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let schema = serde_json::to_value(schemars::schema_for!(AddInput))?;

    McpServer::builder()
        .name("calculator")
        .version("1.0.0")
        .tool(
            Tool::new("add", "Add two numbers", schema),
            |params: AddInput| async move {
                CallToolResult::text(format!("{}", params.a + params.b))
            },
        )
        .build()
        .serve_stdio()
        .await?;
    Ok(())
}

Core Concepts

Tools

Tools are functions that AI models can invoke. Define them with the #[tool] macro or manually:

// Macro approach
#[tool(description = "Multiply two numbers")]
async fn multiply(x: f64, y: f64) -> String {
    format!("{}", x * y)
}

// Manual approach
let schema = serde_json::to_value(schemars::schema_for!(MultiplyInput))?;
builder.tool(
    Tool::new("multiply", "Multiply two numbers", schema),
    |params: MultiplyInput| async move {
        CallToolResult::text(format!("{}", params.x * params.y))
    }
);

Error Handling:

#[tool(description = "Divide two numbers")]
async fn divide(a: f64, b: f64) -> Result<String, String> {
    if b == 0.0 {
        return Err("Cannot divide by zero".to_string());
    }
    Ok(format!("{}", a / b))
}

Resources

Resources expose data (files, APIs, databases) to AI models:

// Static resource
#[resource(
    uri = "config://app",
    name = "Application Config",
    mime_type = "application/json"
)]
async fn get_config(_req: ReadResourceRequest) -> McpResult<ReadResourceResult> {
    let config = serde_json::json!({"version": "1.0", "debug": false});
    Ok(ReadResourceResult::text(
        "config://app",
        serde_json::to_string_pretty(&config)?
    ))
}

// Template resource (dynamic URIs)
#[resource(uri = "file://{path}", name = "File System")]
async fn read_file(req: ReadResourceRequest) -> McpResult<ReadResourceResult> {
    let path = req.uri.trim_start_matches("file://");
    let content = tokio::fs::read_to_string(path).await
        .map_err(|e| McpError::ResourceNotFound(e.to_string()))?;
    Ok(ReadResourceResult::text(req.uri.clone(), content))
}

Prompts

Prompts provide reusable templates for AI interactions:

#[prompt(
    name = "code-review",
    description = "Generate a code review prompt",
    arguments = ["code:required", "language:optional"]
)]
async fn code_review(req: GetPromptRequest) -> McpResult<GetPromptResult> {
    let code = req.arguments.get("code").cloned().unwrap_or_default();
    let lang = req.arguments.get("language").cloned().unwrap_or("".into());
    
    Ok(GetPromptResult::new(vec![
        PromptMessage::user_text(format!(
            "Review this {lang} code:\n\n```{lang}\n{code}\n```"
        ))
    ]))
}

Transports

Stdio (Default)

Standard input/output transport for local process communication:

server.serve_stdio().await?;

SSE (Server-Sent Events)

HTTP-based transport for web clients:

// Requires the "sse" feature
server.serve_sse(([0, 0, 0, 0], 3000)).await?;

Enable in Cargo.toml:

[dependencies]
mcp-kit = { version = "0.1", features = ["sse"] }

TLS/HTTPS

Secure HTTPS transport with optional mTLS:

use mcp_kit::transport::tls::{TlsConfig, ServeSseTlsExt};

let tls = TlsConfig::builder()
    .cert_pem("server.crt")
    .key_pem("server.key")
    .client_auth_ca_pem("ca.crt")  // Enable mTLS
    .build()?;

server.serve_tls("0.0.0.0:8443".parse()?, tls).await?;

Authentication

Protect your MCP server with various authentication methods. All auth features are composable and can be combined.

Bearer Token Authentication

use mcp_kit::prelude::*;
use mcp_kit::auth::{BearerTokenProvider, IntoDynProvider};
use mcp_kit::Auth;
use std::sync::Arc;

// Protected tool - requires auth parameter
#[tool(description = "Say hello to the authenticated user")]
async fn greet(message: String, auth: Auth) -> McpResult<CallToolResult> {
    Ok(CallToolResult::text(format!(
        "Hello, {}! Message: {}", auth.subject, message
    )))
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let provider = Arc::new(BearerTokenProvider::new(["my-secret-token"]));

    McpServer::builder()
        .name("secure-server")
        .version("1.0.0")
        .auth(provider.into_dyn())
        .tool_def(greet_tool_def())
        .build()
        .serve_sse("0.0.0.0:3000".parse()?)
        .await?;
    Ok(())
}

Test with: curl -H "Authorization: Bearer my-secret-token" http://localhost:3000/sse

API Key Authentication

use mcp_kit::auth::{ApiKeyProvider, IntoDynProvider};

// Supports both header and query param
let provider = Arc::new(ApiKeyProvider::new(["api-key-123", "api-key-456"]));

McpServer::builder()
    .auth(provider.into_dyn())
    // ...

Test with:

  • Header: curl -H "X-Api-Key: api-key-123" http://localhost:3000/sse
  • Query: curl "http://localhost:3000/sse?api_key=api-key-123"

Basic Authentication

use mcp_kit::auth::{AuthenticatedIdentity, BasicAuthProvider, IntoDynProvider};

let provider = Arc::new(BasicAuthProvider::new(|username, password| {
    Box::pin(async move {
        if username == "admin" && password == "secret" {
            Ok(AuthenticatedIdentity::new("admin")
                .with_scopes(["read", "write", "admin"]))
        } else {
            Err(McpError::Unauthorized("invalid credentials".into()))
        }
    })
}));

Test with: curl -u admin:secret http://localhost:3000/sse

OAuth 2.0 (JWT/JWKS)

use mcp_kit::auth::oauth2::{OAuth2Config, OAuth2Provider};

// JWT validation with JWKS endpoint
let provider = Arc::new(OAuth2Provider::new(OAuth2Config::Jwt {
    jwks_url: "https://auth.example.com/.well-known/jwks.json".to_owned(),
    required_audience: Some("https://my-api.example.com".to_owned()),
    required_issuer: Some("https://auth.example.com/".to_owned()),
    jwks_refresh_secs: 3600,
}));

// Or token introspection (RFC 7662)
let provider = Arc::new(OAuth2Provider::new(OAuth2Config::Introspection {
    introspection_url: "https://auth.example.com/introspect".to_owned(),
    client_id: "my-client".to_owned(),
    client_secret: "my-secret".to_owned(),
    cache_ttl_secs: 60,
}));

mTLS (Mutual TLS)

use mcp_kit::auth::mtls::MtlsProvider;
use mcp_kit::transport::tls::{TlsConfig, ServeSseTlsExt};

let mtls = MtlsProvider::new(|cert_der: &[u8]| {
    // Validate client certificate, extract subject
    Ok(AuthenticatedIdentity::new("client-cn"))
});

let tls = TlsConfig::builder()
    .cert_pem("server.crt")
    .key_pem("server.key")
    .client_auth_ca_pem("ca.crt")
    .build()?;

McpServer::builder()
    .auth(Arc::new(mtls))
    .build()
    .serve_tls("0.0.0.0:8443".parse()?, tls)
    .await?;

Composite Authentication

Combine multiple auth methods:

use mcp_kit::auth::{
    BearerTokenProvider, ApiKeyProvider, BasicAuthProvider,
    CompositeAuthProvider, IntoDynProvider,
};

let composite = CompositeAuthProvider::new(vec![
    BearerTokenProvider::new(["service-token"]).into_dyn(),
    ApiKeyProvider::new(["api-key"]).into_dyn(),
    BasicAuthProvider::new(/* validator */).into_dyn(),
]);

McpServer::builder()
    .auth(Arc::new(composite))
    // ...

Auth Extractor in Tools

Access authentication info in tool handlers:

use mcp_kit::Auth;

#[tool(description = "Protected operation")]
async fn secure_op(data: String, auth: Auth) -> McpResult<CallToolResult> {
    // Access authenticated identity
    println!("User: {}", auth.subject);
    println!("Scopes: {:?}", auth.scopes);
    println!("Metadata: {:?}", auth.metadata);
    
    // Check scopes
    if !auth.has_scope("write") {
        return Err(McpError::Unauthorized("write scope required".into()));
    }
    
    Ok(CallToolResult::text("Success!"))
}

Completion

Provide auto-complete suggestions for prompt and resource arguments:

use mcp_kit::prelude::*;
use mcp_kit::types::messages::{CompleteRequest, CompletionReference};

McpServer::builder()
    .name("completion-demo")
    .version("1.0.0")
    // Prompt with completion handler
    .prompt_with_completion(
        Prompt::new("search")
            .with_description("Search with auto-complete")
            .with_arguments(vec![
                PromptArgument::required("query"),
                PromptArgument::optional("category"),
            ]),
        // Prompt handler
        |req: mcp_kit::types::messages::GetPromptRequest| async move {
            Ok(GetPromptResult::new(vec![
                PromptMessage::user_text(format!("Search: {}", req.arguments.get("query").unwrap()))
            ]))
        },
        // Completion handler
        |req: CompleteRequest| async move {
            let values = match req.argument.name.as_str() {
                "category" => vec!["books", "movies", "music", "games"],
                _ => vec![],
            };
            Ok(CompleteResult::new(values))
        },
    )
    // Global completion for resources
    .completion(|req: CompleteRequest| async move {
        match &req.reference {
            CompletionReference::Resource { uri } if uri.starts_with("file://") => {
                Ok(CompleteResult::new(vec!["file:///src/", "file:///docs/"]))
            }
            _ => Ok(CompleteResult::empty()),
        }
    })
    .build();

Notifications

Push updates from server to clients:

use mcp_kit::prelude::*;

// Create notification channel
let (notifier, mut receiver) = NotificationSender::channel(100);

// In a tool handler - notify about resource changes
async fn update_data(notifier: NotificationSender) {
    // ... update data ...
    
    // Notify clients the resource changed
    notifier.resource_updated("data://config").await.ok();
    
    // Notify about list changes
    notifier.resources_list_changed().await.ok();
    notifier.tools_list_changed().await.ok();
    notifier.prompts_list_changed().await.ok();
    
    // Send log messages
    notifier.log_info("update", "Data updated successfully").await.ok();
    notifier.log_warning("update", "Some items skipped").await.ok();
}

Available Notifications:

  • resource_updated(uri) — A specific resource's content changed
  • resources_list_changed() — Available resources list changed
  • tools_list_changed() — Available tools list changed
  • prompts_list_changed() — Available prompts list changed
  • log_debug/info/warning/error() — Log messages

Progress Tracking

Report progress for long-running operations:

use mcp_kit::prelude::*;

async fn process_files(notifier: NotificationSender, files: Vec<String>) {
    let tracker = ProgressTracker::new(notifier, Some("token-123".into()));
    
    for (i, file) in files.iter().enumerate() {
        // Process file...
        
        // Report progress
        tracker.update_with_message(
            i as f64 + 1.0,
            files.len() as f64,
            format!("Processing {}", file),
        ).await;
    }
    
    tracker.complete("All files processed").await;
}

ProgressTracker Methods:

  • update(progress, total, message) — Send progress update
  • update_percent(0.0..1.0, message) — Progress as percentage
  • complete(message) — Mark operation complete
  • is_tracking() — Check if progress token was provided

---

## Advanced Features

### State Management

Share state across tool invocations:

```rust
use std::sync::Arc;
use tokio::sync::Mutex;

#[derive(Clone)]
struct AppState {
    counter: Arc<Mutex<i32>>,
}

// In your tool handler
let state = AppState { counter: Arc::new(Mutex::new(0)) };

builder.tool(
    Tool::new("increment", "Increment counter", schema),
    {
        let state = state.clone();
        move |_: serde_json::Value| {
            let state = state.clone();
            async move {
                let mut counter = state.counter.lock().await;
                *counter += 1;
                CallToolResult::text(format!("Counter: {}", *counter))
            }
        }
    }
);

Logging

Integrate with tracing for structured logging:

tracing_subscriber::fmt()
    .with_writer(std::io::stderr)  // Log to stderr for stdio transport
    .with_env_filter("my_server=debug,mcp_kit=info")
    .init();

tracing::info!("Server starting");

Set log level:

RUST_LOG=my_server=debug cargo run

Error Handling

The library uses McpResult<T> and McpError:

use mcp_kit::{McpError, McpResult};

async fn my_tool() -> McpResult<CallToolResult> {
    // Automatic conversion from std::io::Error, serde_json::Error, etc.
    let data = tokio::fs::read_to_string("file.txt").await?;
    
    // Custom errors
    if data.is_empty() {
        return Err(McpError::InvalidParams("File is empty".into()));
    }
    
    Ok(CallToolResult::text(data))
}

Macro Reference

#[tool]

Generate tools from async functions:

#[tool(description = "Description here")]
async fn my_tool(param: Type) -> ReturnType {
    // Implementation
}

Attributes:

  • description = "..." — Tool description (required)
  • name = "..." — Tool name (optional, defaults to function name)

Supported return types:

  • String → Converted to CallToolResult::text
  • CallToolResult → Used directly
  • Result<T, E> → Error handling support

#[resource]

Generate resource handlers:

#[resource(
    uri = "scheme://path",
    name = "Resource Name",
    description = "Optional description",
    mime_type = "text/plain"
)]
async fn handler(req: ReadResourceRequest) -> McpResult<ReadResourceResult> {
    // Implementation
}

URI Templates: Use {variable} syntax for dynamic resources:

#[resource(uri = "file://{path}", name = "Files")]

#[prompt]

Generate prompt handlers:

#[prompt(
    name = "prompt-name",
    description = "Prompt description",
    arguments = ["arg1:required", "arg2:optional"]
)]
async fn handler(req: GetPromptRequest) -> McpResult<GetPromptResult> {
    // Implementation
}

Builder API Reference

McpServer::builder()
    // Server metadata
    .name("server-name")
    .version("1.0.0")
    .instructions("What this server does")

    // Register tools
    .tool(tool, handler)           // Manual API
    .tool_def(macro_generated_def) // From #[tool] macro

    // Register resources
    .resource(resource, handler)           // Static resource
    .resource_template(template, handler)  // URI template
    .resource_def(macro_generated_def)     // From #[resource] macro

    // Register prompts
    .prompt(prompt, handler)
    .prompt_def(macro_generated_def)  // From #[prompt] macro

    .build()

Examples

Run the included examples to see all features in action:

# Comprehensive showcase - all features
cargo run --example showcase

# Showcase with SSE transport on port 3000
cargo run --example showcase -- --sse

# Macro-specific examples
cargo run --example macros_demo

# Completion auto-complete example
cargo run --example completion

# Notifications and progress example
cargo run --example notifications

# Authentication examples
cargo run --example auth_bearer --features auth-full
cargo run --example auth_apikey --features auth-full
cargo run --example auth_basic --features auth-full
cargo run --example auth_composite --features auth-full
cargo run --example auth_oauth2 --features auth-oauth2
cargo run --example auth_mtls --features auth-mtls

Example Features:

  • ✅ Multiple tool types (math, async, state management)
  • ✅ Static and template resources
  • ✅ Prompts with arguments
  • ✅ Argument completion (auto-complete)
  • ✅ Notifications (resource updates, logging)
  • ✅ Progress tracking for long operations
  • ✅ Error handling patterns
  • ✅ State sharing between requests
  • ✅ JSON content types
  • ✅ Both stdio and SSE transports
  • ✅ Bearer, API Key, Basic, OAuth 2.0, mTLS authentication
  • ✅ Composite authentication (multiple methods)

Source code: examples/


Feature Flags

Control which features to compile:

[dependencies]
mcp-kit = { version = "0.1", default-features = false, features = ["server", "stdio"] }

Available features:

  • full (default) — All features enabled
  • server — Core server functionality
  • stdio — Standard I/O transport
  • sse — HTTP Server-Sent Events transport

Authentication features:

  • auth — Core auth types and traits
  • auth-bearer — Bearer token authentication
  • auth-apikey — API key authentication
  • auth-basic — HTTP Basic authentication
  • auth-oauth2 — OAuth 2.0 (JWT/JWKS + introspection)
  • auth-mtls — Mutual TLS / client certificates
  • auth-full — All auth features (bearer, apikey, basic)

WASM compatibility: Use default-features = false for WASM targets (only core protocol types).


Architecture

mcp-kit/
├── src/
│   ├── lib.rs           # Public API and re-exports
│   ├── error.rs         # Error types
│   ├── protocol.rs      # JSON-RPC 2.0 implementation
│   ├── types/           # MCP protocol types
│   ├── server/          # Server implementation [feature = "server"]
│   └── transport/       # Transport implementations
└── macros/              # Procedural macros crate

Crate structure:

  • mcp-kit — Main library
  • mcp-kit-kit-macros — Procedural macros (#[tool], etc.)

Testing

# Run all tests
cargo test --workspace --all-features

# Check formatting
cargo fmt --all -- --check

# Run lints
cargo clippy --workspace --all-features -- -D warnings

# Check MSRV
cargo check --workspace --all-features

Resources


Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes with tests
  4. Ensure cargo fmt and cargo clippy pass
  5. Submit a pull request

See AGENTS.md for development guidelines.


License

This project is licensed under the MIT License.


Changelog

See GitHub Releases for version history.


Built with ❤️ in Rust

⭐ Star on GitHub📦 View on crates.io📖 Read the docs