use async_trait::async_trait;
use pmcp::server::streamable_http_server::{StreamableHttpServer, StreamableHttpServerConfig};
use pmcp::types::capabilities::ServerCapabilities;
use pmcp::{Server, ToolHandler};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::info;
struct EchoTool;
#[async_trait]
impl ToolHandler for EchoTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let message = args
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("(no message provided)");
Ok(json!({
"echo": message,
"timestamp": chrono::Utc::now().to_rfc3339()
}))
}
}
#[derive(Debug, Deserialize)]
struct CalculatorArgs {
operation: String,
a: f64,
b: f64,
}
#[derive(Debug, Serialize)]
struct CalculatorResult {
result: f64,
expression: String,
}
struct CalculatorTool;
#[async_trait]
impl ToolHandler for CalculatorTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let params: CalculatorArgs = serde_json::from_value(args)
.map_err(|e| pmcp::Error::validation(format!("Invalid arguments: {}", e)))?;
let result = match params.operation.as_str() {
"add" => params.a + params.b,
"subtract" => params.a - params.b,
"multiply" => params.a * params.b,
"divide" => {
if params.b == 0.0 {
return Err(pmcp::Error::validation("Division by zero"));
}
params.a / params.b
},
op => {
return Err(pmcp::Error::validation(format!(
"Unknown operation: {}",
op
)));
},
};
let expression = format!(
"{} {} {} = {}",
params.a, params.operation, params.b, result
);
Ok(serde_json::to_value(CalculatorResult {
result,
expression,
})?)
}
}
struct RandomNumberTool;
#[async_trait]
impl ToolHandler for RandomNumberTool {
async fn handle(&self, args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
let min = args.get("min").and_then(|v| v.as_i64()).unwrap_or(0);
let max = args.get("max").and_then(|v| v.as_i64()).unwrap_or(100);
if min >= max {
return Err(pmcp::Error::validation("min must be less than max"));
}
let now = chrono::Utc::now();
let seed = now.timestamp_nanos_opt().unwrap_or(0);
let range = (max - min) as u64;
let random_number = min + ((seed as u64 % range) as i64);
Ok(json!({
"number": random_number,
"range": format!("[{}, {})", min, max),
"timestamp": now.to_rfc3339(),
"note": "Using timestamp-based pseudo-random for demo purposes"
}))
}
}
struct ServerInfoTool;
#[async_trait]
impl ToolHandler for ServerInfoTool {
async fn handle(&self, _args: Value, _extra: pmcp::RequestHandlerExtra) -> pmcp::Result<Value> {
Ok(json!({
"message": "This is a stateless server - no session management",
"server_mode": "stateless",
"benefits": [
"No session overhead",
"Perfect for serverless",
"Simplified operation",
"Horizontal scaling friendly",
"No state to manage"
],
"timestamp": chrono::Utc::now().to_rfc3339()
}))
}
}
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter("pmcp=info")
.init();
info!("Starting Stateless Streamable HTTP Server Example");
let server = Server::builder()
.name("stateless-http-example")
.version("1.0.0")
.capabilities(ServerCapabilities::tools_only())
.tool("echo", EchoTool)
.tool("calculate", CalculatorTool)
.tool("random", RandomNumberTool)
.tool("server_info", ServerInfoTool)
.build()
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
let server = Arc::new(Mutex::new(server));
let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8081);
info!("Creating stateless HTTP server on {}", addr);
let config = StreamableHttpServerConfig {
session_id_generator: None, enable_json_response: true, event_store: None, on_session_initialized: None, on_session_closed: None,
http_middleware: None, allowed_origins: None,
max_request_bytes: pmcp::server::limits::DEFAULT_MAX_REQUEST_BYTES,
};
let http_server = StreamableHttpServer::with_config(addr, server, config);
let (bound_addr, server_handle) = http_server
.start()
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
println!("╔════════════════════════════════════════════════════════════╗");
println!("║ STATELESS STREAMABLE HTTP SERVER RUNNING ║");
println!("╠════════════════════════════════════════════════════════════╣");
println!("║ Address: http://{:43} ║", bound_addr);
println!("║ Mode: Stateless (no session management) ║");
println!("╠════════════════════════════════════════════════════════════╣");
println!("║ Features: ║");
println!("║ • No session IDs generated or required ║");
println!("║ • Each request is independent ║");
println!("║ • Perfect for serverless deployments ║");
println!("║ • Re-initialization allowed ║");
println!("║ • Simplified operation with less overhead ║");
println!("╠════════════════════════════════════════════════════════════╣");
println!("║ Ideal for: ║");
println!("║ • AWS Lambda / Azure Functions ║");
println!("║ • Kubernetes pods with horizontal scaling ║");
println!("║ • Simple request/response workflows ║");
println!("║ • Development and testing ║");
println!("╠════════════════════════════════════════════════════════════╣");
println!("║ Available Tools: ║");
println!("║ • echo - Echo back messages ║");
println!("║ • calculate - Perform arithmetic ║");
println!("║ • random - Generate random numbers ║");
println!("║ • server_info - Get server mode information ║");
println!("╠════════════════════════════════════════════════════════════╣");
println!("║ Connect with: ║");
println!("║ cargo run --example 24_streamable_http_client -- stateless║");
println!("╚════════════════════════════════════════════════════════════╝");
println!();
println!("Press Ctrl+C to stop the server");
server_handle.await.map_err(|e| {
Box::new(pmcp::Error::Internal(e.to_string())) as Box<dyn std::error::Error>
})?;
Ok(())
}