Skip to content

Commit 7cf3cc9

Browse files
feat: add SessionState model for serializable session state
This adds a Pydantic BaseModel that can be serialized to JSON for storing session state in external storage (Redis, databases, etc.) Related: #2111
1 parent 0fe16dd commit 7cf3cc9

File tree

2 files changed

+141
-0
lines changed

2 files changed

+141
-0
lines changed

src/mcp/shared/session_state.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Serializable session state for distributed deployments.
2+
3+
This module provides a SessionState dataclass that can be serialized to JSON
4+
and stored in external storage (Redis, database, etc.) for distributed deployments.
5+
6+
This enables session state to be shared across multiple server instances,
7+
allowing MCP services to run behind load balancers or in horizontally-scaled
8+
deployments.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
from typing import Any
14+
15+
from pydantic import BaseModel, Field
16+
17+
18+
class SessionState(BaseModel):
19+
"""A serializable snapshot of MCP session state.
20+
21+
This contains the minimal state needed to reconstruct a session context
22+
across process boundaries. Runtime objects (streams, callbacks) are NOT
23+
included as they cannot be serialized and must be recreated.
24+
25+
Attributes:
26+
session_id: Unique identifier for this session
27+
protocol_version: MCP protocol version being used
28+
next_request_id: The next request ID to use (continues sequence)
29+
server_capabilities: Server capabilities from initialization (as dict)
30+
server_info: Server metadata from initialization (as dict)
31+
initialized_sent: Whether the initialized notification was sent
32+
"""
33+
34+
session_id: str = Field(description="Unique identifier for this session")
35+
protocol_version: str = Field(description="MCP protocol version being used")
36+
next_request_id: int = Field(
37+
description="Next request ID to use",
38+
ge=0,
39+
)
40+
server_capabilities: dict[str, Any] | None = Field(
41+
default=None,
42+
description="Server capabilities received during initialization",
43+
)
44+
server_info: dict[str, Any] | None = Field(
45+
default=None,
46+
description="Server information metadata",
47+
)
48+
initialized_sent: bool = Field(
49+
default=False,
50+
description="Whether the initialized notification was sent",
51+
)

tests/shared/test_session_state.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""Tests for SessionState serialization."""
2+
3+
from mcp.shared.session_state import SessionState
4+
import pytest
5+
6+
7+
def test_session_state_creation():
8+
"""Test that SessionState can be created with all fields."""
9+
state = SessionState(
10+
session_id="test-session-123",
11+
protocol_version="2025-11-25",
12+
next_request_id=5,
13+
server_capabilities={"tools": {}, "resources": {}},
14+
server_info={"name": "test-server", "version": "1.0.0"},
15+
initialized_sent=True,
16+
)
17+
18+
assert state.session_id == "test-session-123"
19+
assert state.protocol_version == "2025-11-25"
20+
assert state.next_request_id == 5
21+
assert state.server_capabilities is not None
22+
assert state.server_info is not None
23+
assert state.initialized_sent is True
24+
25+
26+
def test_session_state_defaults():
27+
"""Test that SessionState works with minimal required fields."""
28+
state = SessionState(
29+
session_id="test-session-456",
30+
protocol_version="2025-11-25",
31+
next_request_id=0,
32+
)
33+
34+
assert state.server_capabilities is None
35+
assert state.server_info is None
36+
assert state.initialized_sent is False
37+
38+
39+
def test_session_state_json_serialization():
40+
"""Test that SessionState can be serialized to JSON and back."""
41+
original = SessionState(
42+
session_id="test-session-789",
43+
protocol_version="2025-11-25",
44+
next_request_id=10,
45+
server_capabilities={"tools": {"listChanged": True}},
46+
server_info={"name": "test-server", "version": "2.0.0"},
47+
initialized_sent=True,
48+
)
49+
50+
# Serialize to JSON
51+
json_str = original.model_dump_json()
52+
53+
# Deserialize from JSON
54+
restored = SessionState.model_validate_json(json_str)
55+
56+
# Verify all fields match
57+
assert restored.session_id == original.session_id
58+
assert restored.protocol_version == original.protocol_version
59+
assert restored.next_request_id == original.next_request_id
60+
assert restored.server_capabilities == original.server_capabilities
61+
assert restored.server_info == original.server_info
62+
assert restored.initialized_sent == original.initialized_sent
63+
64+
65+
def test_session_state_dict_serialization():
66+
"""Test that SessionState can be serialized to dict and back."""
67+
original = SessionState(
68+
session_id="test-session-dict",
69+
protocol_version="2025-11-25",
70+
next_request_id=3,
71+
)
72+
73+
# Serialize to dict
74+
data_dict = original.model_dump()
75+
76+
# Deserialize from dict
77+
restored = SessionState.model_validate(data_dict)
78+
79+
assert restored.session_id == original.session_id
80+
assert restored.next_request_id == original.next_request_id
81+
82+
83+
def test_session_state_validation():
84+
"""Test that SessionState validates input data."""
85+
with pytest.raises(ValueError): # Pydantic validation error
86+
SessionState(
87+
session_id="test",
88+
protocol_version="2025-11-25",
89+
next_request_id=-1, # Invalid: must be >= 0
90+
)

0 commit comments

Comments
 (0)