turul-mcp-session-storage

Pluggable session storage backends for the turul-mcp-framework, supporting everything from in-memory development to distributed production deployments.
Overview
turul-mcp-session-storage provides the SessionStorage trait and multiple implementations for persisting MCP session data, state, and SSE events across different storage backends.
Features
- ✅ Pluggable Architecture - Swap backends without code changes
- ✅ Production Ready - Multiple production-grade backends
- ✅ Session Persistence - Sessions survive server restarts
- ✅ State Management - Type-safe session state storage
- ✅ SSE Event Storage - Event replay for SSE resumability
- ✅ Automatic Cleanup - TTL-based session expiry
- ✅ Multi-Instance Support - Distributed session sharing
Storage Backends
| Backend |
Use Case |
Features |
Production Ready |
| InMemory |
Development/Testing |
Fast, simple |
✅ Dev only |
| SQLite |
Single-instance production |
File-based, ACID |
✅ Yes |
| PostgreSQL |
Multi-instance production |
Distributed, scalable |
✅ Yes |
| DynamoDB |
Serverless/AWS Lambda |
Auto-scaling, managed |
✅ Yes |
Quick Start
Add this to your Cargo.toml:
[dependencies]
turul-mcp-session-storage = { version = "0.4", features = ["sqlite"] }
turul-mcp-server = "0.4"
In-Memory (Development)
use turul_mcp_server::McpServer;
use turul_mcp_session_storage::InMemorySessionStorage;
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let storage = Arc::new(InMemorySessionStorage::new());
let server = McpServer::builder()
.with_session_storage(storage)
.tool()
.build()?;
server.run().await?;
Ok(())
}
SQLite (Single Instance)
use turul_mcp_session_storage::SqliteSessionStorage;
use std::sync::Arc;
let storage = Arc::new(SqliteSessionStorage::new().await?);
let server = McpServer::builder()
.with_session_storage(storage)
.build()?;
PostgreSQL (Multi-Instance)
use turul_mcp_session_storage::PostgresSessionStorage;
use std::sync::Arc;
let storage = Arc::new(PostgresSessionStorage::new().await?);
let server = McpServer::builder()
.with_session_storage(storage)
.build()?;
DynamoDB (Serverless)
use turul_mcp_session_storage::DynamoDbSessionStorage;
use std::sync::Arc;
let storage = Arc::new(
DynamoDbSessionStorage::new().await? );
let server = McpServer::builder()
.with_session_storage(storage)
.build()?;
Session Management
Session Lifecycle
Sessions follow this lifecycle:
- Creation - Server assigns UUID v7 session ID
- Usage - Tools read/write session state
- Persistence - State automatically saved to storage
- Expiry - TTL-based cleanup (default 30 minutes)
- Cleanup - Automatic background cleanup
Session State API
use turul_mcp_server::SessionContext;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct UserPreferences {
theme: String,
language: String,
notifications: bool,
}
async fn handle_user_preferences(session: SessionContext) -> Result<(), Box<dyn std::error::Error>> {
let prefs: Option<UserPreferences> = session.get_typed_state("user_prefs").await;
let mut preferences = prefs.unwrap_or(UserPreferences {
theme: "light".to_string(),
language: "en".to_string(),
notifications: true,
});
preferences.theme = "dark".to_string();
session.set_typed_state("user_prefs", preferences).await?;
session.remove_state("user_prefs");
Ok(())
}
Session Information
async fn session_info(session: SessionContext) -> Result<(), Box<dyn std::error::Error>> {
println!("Session ID: {}", session.session_id());
println!("Created: {:?}", session.created_at());
println!("Last accessed: {:?}", session.last_accessed());
let keys = session.list_state_keys().await?;
println!("State keys: {:?}", keys);
let raw_state = session.get_state("some_key").await?;
if let Some(value) = raw_state {
println!("Raw value: {}", value);
}
Ok(())
}
SSE Event Storage
Event Persistence
All session backends support SSE event storage for resumability:
use turul_mcp_server::SessionContext;
async fn send_progress_with_persistence(session: SessionContext) -> Result<(), Box<dyn std::error::Error>> {
session.notify_progress("long-task", 50).await;
Ok(())
}
Event Replay
SSE clients can resume from any point using Last-Event-ID:
GET /mcp HTTP/1.1
Accept: text/event-stream
Last-Event-ID: event-123
Mcp-Session-Id: sess-456
The storage backend will replay all events after event-123.
Backend Configuration
SQLite Configuration
use turul_mcp_session_storage::{SqliteSessionStorage, SqliteConfig};
let config = SqliteConfig {
database_path: "sessions.db".into(),
session_timeout_minutes: 60, cleanup_interval_minutes: 5, max_events_per_session: 1000,
verify_tables: true,
create_tables: true,
..Default::default()
};
let storage = SqliteSessionStorage::with_config(config).await?;
PostgreSQL Configuration
use turul_mcp_session_storage::{PostgresSessionStorage, PostgresConfig};
let config = PostgresConfig {
database_url: "postgresql://user:pass@localhost/mcpdb".to_string(),
session_timeout_minutes: 30, cleanup_interval_minutes: 10, max_connections: 10,
verify_tables: true,
create_tables: true,
..Default::default()
};
let storage = PostgresSessionStorage::with_config(config).await?;
DynamoDB Configuration
use turul_mcp_session_storage::{DynamoDbSessionStorage, DynamoDbConfig};
let config = DynamoDbConfig {
table_name: "mcp-sessions".to_string(),
region: "us-east-1".to_string(),
session_ttl_minutes: 30,
event_ttl_minutes: 60,
verify_tables: true,
create_tables: true,
..Default::default()
};
let storage = DynamoDbSessionStorage::with_config(config).await?;
Production Deployment
Single-Instance with SQLite
use turul_mcp_session_storage::{SqliteSessionStorage, SqliteConfig};
let storage = SqliteSessionStorage::with_config(SqliteConfig {
database_path: "/var/lib/mcp/sessions.db".into(),
session_timeout_minutes: 120, cleanup_interval_minutes: 10, max_events_per_session: 5000,
verify_tables: true,
create_tables: true,
..Default::default()
}).await?;
Multi-Instance with PostgreSQL
use turul_mcp_session_storage::{PostgresSessionStorage, PostgresConfig};
use std::sync::Arc;
let database_url = std::env::var("DATABASE_URL")?;
let config = PostgresConfig {
connection_string: database_url,
..Default::default()
};
let storage = PostgresSessionStorage::with_config(config).await?;
let server = McpServer::builder()
.bind("0.0.0.0:3000")
.with_session_storage(Arc::new(storage))
.build()?;
Serverless with DynamoDB
For AWS Lambda and serverless:
use turul_mcp_session_storage::DynamoDbSessionStorage;
let storage = DynamoDbSessionStorage::new().await?;
let lambda_server = turul_mcp_aws_lambda::LambdaMcpServerBuilder::new()
.storage(Arc::new(storage))
.build()
.await?;
Custom Storage Backend
Implementing SessionStorage
use turul_mcp_session_storage::{SessionStorage, SessionData, SessionEvent};
use async_trait::async_trait;
use uuid::Uuid;
use std::collections::HashMap;
pub struct RedisSessionStorage {
client: redis::Client,
}
#[async_trait]
impl SessionStorage for RedisSessionStorage {
type Error = redis::RedisError;
async fn create_session(&self, session_id: Uuid) -> Result<(), Self::Error> {
let mut conn = self.client.get_async_connection().await?;
let session_data = SessionData::new(session_id);
let serialized = serde_json::to_string(&session_data)?;
redis::cmd("SETEX")
.arg(format!("session:{}", session_id))
.arg(1800) .arg(serialized)
.query_async(&mut conn)
.await
}
async fn get_session(&self, session_id: Uuid) -> Result<Option<SessionData>, Self::Error> {
let mut conn = self.client.get_async_connection().await?;
let result: Option<String> = redis::cmd("GET")
.arg(format!("session:{}", session_id))
.query_async(&mut conn)
.await?;
match result {
Some(data) => Ok(Some(serde_json::from_str(&data)?)),
None => Ok(None),
}
}
async fn update_session(&self, session_data: &SessionData) -> Result<(), Self::Error> {
todo!()
}
async fn delete_session(&self, session_id: Uuid) -> Result<(), Self::Error> {
todo!()
}
}
Error Handling
Storage Errors
Each backend defines its own error type:
use turul_mcp_session_storage::{SqliteSessionStorage, SqliteError};
match storage.get_session(session_id).await {
Ok(Some(session)) => {
}
Ok(None) => {
}
Err(SqliteError::Database(e)) => {
}
Err(SqliteError::Serialization(e)) => {
}
}
Graceful Degradation
The framework provides graceful degradation when storage fails:
if let Err(e) = session.set_typed_state("key", value).await {
tracing::warn!("Failed to persist session state: {}", e);
}
Performance & Monitoring
Connection Pooling
Production backends use connection pooling:
let storage = PostgresSessionStorage::with_config(PostgresConfig {
max_connections: 20, ..Default::default()
}).await?;
Metrics Collection
async fn collect_session_metrics(storage: &dyn SessionStorage) {
let active_sessions = storage.count_sessions().await?;
let cleanup_stats = storage.cleanup_expired_sessions().await?;
metrics::gauge!("mcp.sessions.active", active_sessions as f64);
metrics::counter!("mcp.sessions.cleaned_up", cleanup_stats.removed as u64);
}
Testing
Test Utilities
use turul_mcp_session_storage::test_utils::*;
#[tokio::test]
async fn test_session_storage() {
let storage = InMemorySessionStorage::new();
test_session_lifecycle(&storage).await;
test_state_operations(&storage).await;
test_event_operations(&storage).await;
}
Integration Tests
cargo test --package turul-mcp-session-storage --all-features
cargo test --package turul-mcp-session-storage --features sqlite
cargo test --package turul-mcp-session-storage --features postgres -- --ignored
Feature Flags
[dependencies]
turul-mcp-session-storage = { version = "0.4", features = ["sqlite", "postgres"] }
default - Only InMemory backend
sqlite - SQLite backend
postgres - PostgreSQL backend
dynamodb - DynamoDB backend
Migration Guide
Upgrading Storage Backends
When upgrading from InMemory to persistent storage:
let storage = Arc::new(InMemorySessionStorage::new());
let storage = Arc::new(SqliteSessionStorage::new("sessions.db").await?);
let server = McpServer::builder()
.with_session_storage(storage) .build()?;
License
Licensed under the MIT License. See LICENSE for details.