plugins/turul-mcp-skills/skills/session-storage-backends/SKILL.md
This skill should be used when the user asks about "session storage", "SessionStorage trait", "SqliteSessionStorage", "PostgresSessionStorage", "DynamoDbSessionStorage", "InMemorySessionStorage", "session backend", "session persistence", "session events", "SSE reconnection storage", "which storage backend", "session TTL", "session cleanup", "session event management", "SseEvent", or "SessionStorageError". Covers the SessionStorage trait, backend selection, event management for SSE resumability, error types, and background cleanup in the Turul MCP Framework (Rust). Do NOT use for TaskStorage — task persistence is a separate trait; see task-patterns.
npx skillsauth add aussierobots/turul-mcp-framework session-storage-backendsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Session storage persists MCP sessions across requests and manages SSE events for reconnection resumability. Four backends serve different deployment scenarios — choose based on persistence needs and scaling requirements.
Need session persistence across restarts?
├─ No ──────────────────────────────────────→ InMemory (default, zero config)
└─ Yes
├─ Single server instance? ─────────────→ SQLite (file-based, no external deps)
└─ Multiple instances / horizontal scaling?
├─ AWS / serverless? ───────────────→ DynamoDB (managed, auto-scaling)
└─ Traditional infrastructure? ─────→ PostgreSQL (shared state, optimistic locking)
Config details (feature flags, Cargo.toml patterns, env vars, config struct fields) are in the storage-backend-matrix reference. This skill covers architecture, the trait API, event management, and decision-making.
The SessionStorage trait has three method groups:
| Method | Signature | Purpose |
|---|---|---|
| create_session | (capabilities) → SessionInfo | Create session with auto-generated UUID v7 |
| create_session_with_id | (id, capabilities) → SessionInfo | Create with specific ID (testing only) |
| get_session | (id) → Option<SessionInfo> | Look up session by ID |
| update_session | (SessionInfo) → () | Update entire session info |
| set_session_state | (id, key, value) → () | Set a state key-value pair |
| get_session_state | (id, key) → Option<Value> | Get a state value by key |
| remove_session_state | (id, key) → Option<Value> | Remove and return a state value |
| delete_session | (id) → bool | Delete session completely |
| list_sessions | () → Vec<String> | List all session IDs |
| Method | Signature | Purpose |
|---|---|---|
| store_event | (id, SseEvent) → SseEvent | Store event, assigns monotonic ID |
| get_events_after | (id, after_event_id) → Vec<SseEvent> | Get events since last seen (for reconnection) |
| get_recent_events | (id, limit) → Vec<SseEvent> | Get latest N events (for initial connection) |
| delete_events_before | (id, before_event_id) → u64 | Delete old events (cleanup) |
| Method | Signature | Purpose |
|---|---|---|
| expire_sessions | (older_than) → Vec<String> | Remove expired sessions, return their IDs |
| session_count | () → usize | Active session count (monitoring) |
| event_count | () → usize | Total event count across all sessions |
| maintenance | () → () | Backend-specific maintenance (compaction, etc.) |
SSE reconnection relies on event storage. When a client disconnects and reconnects with Last-Event-ID, the server replays missed events.
Client connects → GET /mcp (Accept: text/event-stream)
Server sends events → id: 1, id: 2, id: 3 (stored via store_event)
Client disconnects → network interruption
Client reconnects → GET /mcp (Last-Event-ID: 2)
Server replays → get_events_after(session_id, 2) → [event 3, ...]
Key behaviors:
store_event() assigns monotonic IDs — each event gets an ID higher than the previousget_events_after() is exclusive — after_event_id: 2 returns events 3, 4, 5, ...max_events_per_session caps storage (default: 1000) — oldest events are evicted: keepalive\n\n) to preserve Last-Event-IDMcpServer::builder() uses it by default.db file, no external servermax_connections default: 10tokio::time::sleep loop (NOT interval — first-tick race):memory: with pools gives each connection its own DB. Use file:{uuid}?mode=memory&cache=shared for shared in-memory test databasesversion column prevents lost updates; ConcurrentModification error on conflictmin_connections (default: 2), max_connections (default: 20), statement_timeout_secs (default: 30)tokio::time::sleep pattern as SQLitesession_ttl_minutes: 5 and event_ttl_minutes: 5 — override for production (e.g., 1440 for 24 hours)verify_tables: true, create_tables: true verifies and creates tables + GSIs on first use. Default is verify_tables: false (skip verification for Lambda deployments)All backends share SessionStorageError as a unified error type, with From conversions from backend-specific errors.
| Variant | When |
|---|---|
| SessionNotFound(id) | Session ID doesn't exist |
| MaxSessionsReached(limit) | InMemory session limit exceeded |
| MaxEventsReached(limit) | Event storage limit exceeded |
| DatabaseError(msg) | SQLite/PostgreSQL query failure |
| SerializationError(msg) | JSON serialization/deserialization failure |
| ConnectionError(msg) | Database connection failure |
| MigrationError(msg) | Schema migration failure |
| AwsError(msg) | DynamoDB SDK error |
| AwsConfigurationError(msg) | AWS credential/region configuration issue |
| TableNotFound(table) | DynamoDB table doesn't exist |
| InvalidData(msg) | Corrupt or unexpected session data |
| ConcurrentModification(msg) | PostgreSQL optimistic lock conflict |
| Generic(msg) | Catch-all |
SQLite and PostgreSQL run background cleanup tasks. DynamoDB uses native TTL. InMemory is synchronous.
// Internal pattern (for reference — you don't write this code)
// SQLite/PostgreSQL cleanup loop:
loop {
tokio::time::sleep(Duration::from_secs(cleanup_interval_secs)).await;
storage.expire_sessions(cutoff_time).await?;
storage.maintenance().await?; // compaction, vacuum, etc.
}
Why sleep not interval: tokio::time::interval fires immediately on the first tick, which can race with newly-created sessions in tests. sleep defers the first cleanup by the full interval.
When a session expires (TTL cleanup) or is terminated (client DELETE), subsequent requests with that session ID receive HTTP 404 Not Found (MCP 2025-11-25 Streamable HTTP). This tells the client to start a fresh initialize handshake — it is not an authentication error (401).
| Session State | HTTP Status | Client Action | |---|---|---| | Active | 200 | Normal operation | | Terminated (DELETE) | 404 | Re-initialize | | Expired (TTL cleanup) | 404 | Re-initialize | | Never existed | 404 | Re-initialize |
This applies to all storage backends equally — the HTTP transport layer checks session existence before dispatching.
DynamoDB 5-minute TTL in production — Default session_ttl_minutes: 5 is for testing. Production deployments should set 60–1440+ minutes via DynamoDbConfig { session_ttl_minutes: 1440, event_ttl_minutes: 1440, ..Default::default() }.
SQLite :memory: with connection pools — Each pool connection gets its own isolated database. For shared test databases, use file:{uuid}?mode=memory&cache=shared as the database path.
Forgetting verify_tables: true for first-time setup — All backends default to verify_tables: false (optimized for Lambda). For first-time setup or development, use verify_tables: true, create_tables: true to auto-create tables.
Using InMemory in Lambda — Lambda containers recycle unpredictably. Sessions stored in memory are lost. Use DynamoDbSessionStorage for persistence across invocations.
Feature flags, Cargo.toml patterns, config fields? → See the storage-backend-matrix reference for the complete config reference.
Lambda deployment with DynamoDB? → See the lambda-deployment skill for LambdaMcpServerBuilder, cold-start caching, and DynamoDB setup.
Task storage backends? → See the task-patterns skill for TaskStorage trait and backend configuration (mirrors session storage patterns).
Server builder setup? Use McpServer::builder().with_session_storage(storage). See: CLAUDE.md — Basic Server
tools
This skill should be used when the user asks to "create a tool", "add a tool", "new tool", "which tool pattern", "compare tool patterns", "function macro vs derive", "mcp_tool macro", "#[mcp_tool]", "derive McpTool", "#[derive(McpTool)]", "ToolBuilder", "tool creation", "function macro tool", "server icon", "server branding", ".icons()", "Icon::data_uri", "server identity", "dynamic tools", "ToolChangeMode", "activate_tool", "deactivate_tool", "ToolRegistry", "tool_change_mode", or "notifications/tools/list_changed". Covers choosing between function macro (#[mcp_tool]), derive macro (#[derive(McpTool)]), and runtime builder (ToolBuilder) patterns, plus server identity (icons), in the Turul MCP Framework (Rust).
tools
This skill should be used when the user asks about "testing", "test patterns", "write tests", "unit test", "e2e test", "integration test", "McpTestClient", "TestServerManager", "compliance test", "test server", "test fixture", "doctest", "cargo test", "test organization", "SSE testing", or "test consolidation". Covers unit testing, E2E testing, compliance testing, SSE testing, and test organization in the Turul MCP Framework (Rust). McpTestClient is the in-process test harness; for the production client API see mcp-client-patterns.
tools
This skill should be used when the user asks about "task support", "TaskRuntime", "TaskStorage", "task_support attribute", "long-running tool", "CancellationHandle", "tasks/get", "tasks/list", "tasks/cancel", "tasks/result", "TaskStatus", "TaskRecord", "TaskOutcome", "InMemoryTaskStorage", "with_task_storage", "task state machine", "TaskExecutor", "TokioTaskExecutor", "task_support = optional", "task_support = required", "task_support = forbidden", "with_task_runtime", or "task storage backend". Covers MCP task support for long-running tools, state machine, storage backends, cancellation, and capability truthfulness in the Turul MCP Framework (Rust). TaskStorage is distinct from SessionStorage; for session persistence see session-storage-backends.
tools
This skill should be used when the user asks to "create a resource", "add a resource", "MCP resource", "McpResource", "mcp_resource macro", "#[derive(McpResource)]", "ResourceBuilder", "resource URI", "URI template", "ResourceContent", "dynamic resource", "resource!", "create a prompt", "add a prompt", "MCP prompt", "McpPrompt", "#[derive(McpPrompt)]", "PromptBuilder", "prompt arguments", "PromptMessage", "prompt template", "GetPromptResult", "prompt!", "resources/read", "prompts/get", or "resource vs prompt". Covers creating MCP resources (function macro, derive, resource!{}, builder) and MCP prompts (derive, prompt!{}, builder) in the Turul MCP Framework (Rust).