Small MCP Server - A minimal, sync MCP server implementation. No tokio, no async, just works.
The official rmcp SDK is async/tokio-based. That's fine for some use cases, but:
- Tokio is viral - once you're async, everything wants to be async
- MCP is sequential - request → response → request → response
- 53% test coverage - rmcp is young and under-tested
- Apache 2 licensed - rmcp switched from MIT; we prefer MIT
- We want control - our core crates are sync
sml_mcps gives us a clean, sync MCP server that we control.
[features]
default = ["schema"]
schema = ["dep:schemars"] # JSON Schema generation for tools
http = ["dep:tiny_http"] # Streamable HTTP transport (with SSE)
auth = ["dep:jsonwebtoken"] # JWT validation for hosted
hosted = ["http", "auth"] # Both HTTP and authDefine your context and tools, then wire them up:
use sml_mcps::{Server, ServerConfig, StdioTransport, Tool, ToolEnv, CallToolResult, Result, LogLevel};
use serde_json::Value;
// Your shared context
struct AppContext {
counter: i64,
}
// Define a tool
struct IncrementTool;
impl Tool<AppContext> for IncrementTool {
fn name(&self) -> &str { "increment" }
fn description(&self) -> &str { "Increment the counter" }
fn schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"amount": { "type": "integer", "description": "Amount to increment by" }
}
})
}
fn execute(&self, args: Value, ctx: &mut AppContext, env: &ToolEnv) -> Result<CallToolResult> {
let amount = args.get("amount").and_then(|a| a.as_i64()).unwrap_or(1);
ctx.counter += amount;
// Send notification to client
env.log(LogLevel::Info, format!("Counter is now {}", ctx.counter))?;
Ok(CallToolResult::text(format!("Counter: {}", ctx.counter)))
}
}
fn main() -> Result<()> {
let config = ServerConfig {
name: "my-server".to_string(),
version: "1.0.0".to_string(),
instructions: Some("A counter server".to_string()),
};
let mut server = Server::new(config);
server.add_tool(IncrementTool)?;
let context = AppContext { counter: 0 };
let transport = StdioTransport::new();
server.start(transport, context)
}With the http feature, HttpServer handles all the HTTP boilerplate for you:
use sml_mcps::{HttpServer, ServerConfig, Tool, ToolEnv, CallToolResult, Result};
use serde_json::Value;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
struct CounterTool;
impl Tool<AppContext> for CounterTool {
fn name(&self) -> &str { "counter" }
fn description(&self) -> &str { "Increment counter" }
fn schema(&self) -> Value { serde_json::json!({ "type": "object" }) }
fn execute(&self, _args: Value, ctx: &mut AppContext, _env: &ToolEnv) -> Result<CallToolResult> {
let val = ctx.counter.fetch_add(1, Ordering::SeqCst) + 1;
Ok(CallToolResult::text(format!("Counter: {}", val)))
}
}
struct AppContext {
counter: Arc<AtomicI64>,
}
fn main() -> Result<()> {
let shared_counter = Arc::new(AtomicI64::new(0));
let config = ServerConfig {
name: "my-http-server".to_string(),
version: "1.0.0".to_string(),
instructions: None,
};
HttpServer::new(config)
.endpoint("/mcp") // optional, this is the default
.with_tools(|server| {
server.add_tool(CounterTool)?;
Ok(())
})
.serve("127.0.0.1:3000", {
let counter = shared_counter.clone();
move || AppContext { counter: counter.clone() }
})
}Key feature: When tools send notifications (via env.log() or env.send_progress()),
the response is automatically formatted as SSE. For requests without notifications, plain JSON is returned.
See examples/http_server.rs for a complete example.
With the hosted feature (enables both http and auth), add JWT validation:
use sml_mcps::{HttpServer, ServerConfig, auth::JwtValidator};
struct AuthContext {
user_id: String,
tenant_id: String,
}
fn main() -> Result<()> {
let config = ServerConfig {
name: "authenticated-server".to_string(),
version: "1.0.0".to_string(),
instructions: None,
};
HttpServer::new(config)
.with_tools(|server| {
server.add_tool(WhoamiTool)?;
Ok(())
})
.serve_with_auth(
"127.0.0.1:3001",
JwtValidator::hs256(b"your-secret-key"),
|claims| AuthContext {
user_id: claims.user_id().to_string(),
tenant_id: claims.tenant_id().to_string(),
},
)
}The validator supports both HS256 (symmetric) and RS256 (asymmetric) algorithms:
// HS256 (symmetric)
let validator = JwtValidator::hs256(b"your-secret-key");
// RS256 (asymmetric)
let validator = JwtValidator::rs256(&public_key_pem)?;See examples/http_auth.rs for a complete authenticated server.
During tool execution, ToolEnv provides:
// Send log notification
env.log(LogLevel::Info, "Processing...")?;
// Send progress update
env.send_progress("token", 0.5, Some(1.0))?;
// Access resources
let uris = env.list_resources();
let resource = env.get_resource("my://resource")?;If you need custom HTTP handling, you can use HttpTransport directly:
use sml_mcps::{Server, ServerConfig, HttpTransport};
use std::sync::{Arc, Mutex};
// In your HTTP handler:
let transport = Arc::new(Mutex::new(HttpTransport::new(request_body)));
server.process_one(transport.clone(), &mut context)?;
let mut t = transport.lock().unwrap();
if t.has_notifications() {
// Return as SSE (Content-Type: text/event-stream)
let sse_body = t.take_sse_response();
} else {
// Return plain JSON (Content-Type: application/json)
let json_body = t.take_response().unwrap_or_default();
}Implements MCP protocol version 2025-03-26 (Streamable HTTP).
- Client implementation - this is a server SDK
- Sampling/LLM callbacks - not needed for tool servers
- Async anything - by design
MIT