Skip to content

MemoryCo/sml_mcps

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

sml_mcps

CI codecov License: MIT

Small MCP Server - A minimal, sync MCP server implementation. No tokio, no async, just works.

Why?

The official rmcp SDK is async/tokio-based. That's fine for some use cases, but:

  1. Tokio is viral - once you're async, everything wants to be async
  2. MCP is sequential - request → response → request → response
  3. 53% test coverage - rmcp is young and under-tested
  4. Apache 2 licensed - rmcp switched from MIT; we prefer MIT
  5. We want control - our core crates are sync

sml_mcps gives us a clean, sync MCP server that we control.

Features

[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 auth

Usage (Stdio)

Define 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)
}

HTTP Transport (Streamable HTTP with SSE)

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.

JWT Authentication

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.

Tool Environment

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")?;

Low-Level HTTP (Advanced)

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();
}

Protocol Version

Implements MCP protocol version 2025-03-26 (Streamable HTTP).

What's NOT Included

  • Client implementation - this is a server SDK
  • Sampling/LLM callbacks - not needed for tool servers
  • Async anything - by design

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •