hopeful0 commited on
Commit
f64fa17
·
unverified ·
1 Parent(s): 6eb724d

Refactor `get_http_request` and `context.session_id` (#1242)

Browse files
src/fastmcp/server/context.py CHANGED
@@ -130,7 +130,7 @@ class Context:
130
  _current_context.reset(token)
131
 
132
  @property
133
- def request_context(self) -> RequestContext:
134
  """Access to the underlying request context.
135
 
136
  If called outside of a request context, this will raise a ValueError.
@@ -217,35 +217,48 @@ class Context:
217
  return str(self.request_context.request_id)
218
 
219
  @property
220
- def session_id(self) -> str | None:
221
- """Get the MCP session ID for HTTP transports.
222
 
223
  Returns the session ID that can be used as a key for session-based
224
  data storage (e.g., Redis) to share data between tool calls within
225
  the same client session.
226
 
227
  Returns:
228
- The session ID for HTTP transports (SSE, StreamableHTTP), or None
229
- for stdio and in-memory transports which don't use session IDs.
230
 
231
  Example:
232
  ```python
233
  @server.tool
234
  def store_data(data: dict, ctx: Context) -> str:
235
- if session_id := ctx.session_id:
236
- redis_client.set(f"session:{session_id}:data", json.dumps(data))
237
- return f"Data stored for session {session_id}"
238
- return "No session ID available (stdio/memory transport)"
239
  ```
240
  """
241
- try:
242
- from fastmcp.server.dependencies import get_http_headers
243
 
244
- headers = get_http_headers(include_all=True)
245
- return headers.get("mcp-session-id")
246
- except RuntimeError:
247
- # No HTTP context available (stdio/in-memory transport)
248
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
  @property
251
  def session(self) -> ServerSession:
 
130
  _current_context.reset(token)
131
 
132
  @property
133
+ def request_context(self) -> RequestContext[ServerSession, Any, Request]:
134
  """Access to the underlying request context.
135
 
136
  If called outside of a request context, this will raise a ValueError.
 
217
  return str(self.request_context.request_id)
218
 
219
  @property
220
+ def session_id(self) -> str:
221
+ """Get the MCP session ID for ALL transports.
222
 
223
  Returns the session ID that can be used as a key for session-based
224
  data storage (e.g., Redis) to share data between tool calls within
225
  the same client session.
226
 
227
  Returns:
228
+ The session ID for StreamableHTTP transports, or a generated ID
229
+ for other transports.
230
 
231
  Example:
232
  ```python
233
  @server.tool
234
  def store_data(data: dict, ctx: Context) -> str:
235
+ session_id = ctx.session_id
236
+ redis_client.set(f"session:{session_id}:data", json.dumps(data))
237
+ return f"Data stored for session {session_id}"
 
238
  ```
239
  """
240
+ request_ctx = self.request_context
241
+ session = request_ctx.session
242
 
243
+ # Try to get the session ID from the session attributes
244
+ session_id = getattr(session, "_fastmcp_id", None)
245
+ if session_id is not None:
246
+ return session_id
247
+
248
+ # Try to get the session ID from the http request headers
249
+ request = request_ctx.request
250
+ if request:
251
+ session_id = request.headers.get("mcp-session-id")
252
+
253
+ # Generate a session ID if it doesn't exist.
254
+ if session_id is None:
255
+ from uuid import uuid4
256
+
257
+ session_id = str(uuid4())
258
+
259
+ # Save the session id to the session attributes
260
+ setattr(session, "_fastmcp_id", session_id)
261
+ return session_id
262
 
263
  @property
264
  def session(self) -> ServerSession:
src/fastmcp/server/dependencies.py CHANGED
@@ -37,9 +37,14 @@ def get_context() -> Context:
37
 
38
 
39
  def get_http_request() -> Request:
40
- from fastmcp.server.http import _current_http_request
 
 
 
 
 
 
41
 
42
- request = _current_http_request.get()
43
  if request is None:
44
  raise RuntimeError("No active HTTP request found.")
45
  return request
@@ -72,6 +77,8 @@ def get_http_headers(include_all: bool = False) -> dict[str, str]:
72
  "proxy-authenticate",
73
  "proxy-authorization",
74
  "proxy-connection",
 
 
75
  }
76
  # (just in case)
77
  if not all(h.lower() == h for h in exclude_headers):
 
37
 
38
 
39
  def get_http_request() -> Request:
40
+ from mcp.server.lowlevel.server import request_ctx
41
+
42
+ request = None
43
+ try:
44
+ request = request_ctx.get().request
45
+ except LookupError:
46
+ pass
47
 
 
48
  if request is None:
49
  raise RuntimeError("No active HTTP request found.")
50
  return request
 
77
  "proxy-authenticate",
78
  "proxy-authorization",
79
  "proxy-connection",
80
+ # MCP-related headers
81
+ "mcp-session-id",
82
  }
83
  # (just in case)
84
  if not all(h.lower() == h for h in exclude_headers):
tests/server/test_context.py CHANGED
@@ -91,38 +91,44 @@ class TestParseModelPreferences:
91
  class TestSessionId:
92
  def test_session_id_with_http_headers(self, context):
93
  """Test that session_id returns the value from mcp-session-id header."""
 
 
 
94
  mock_headers = {"mcp-session-id": "test-session-123"}
95
 
96
- with patch(
97
- "fastmcp.server.dependencies.get_http_headers", return_value=mock_headers
98
- ):
99
- assert context.session_id == "test-session-123"
 
 
 
 
 
 
 
 
 
100
 
101
  def test_session_id_without_http_headers(self, context):
102
- """Test that session_id returns None when no HTTP headers are available."""
103
- with patch(
104
- "fastmcp.server.dependencies.get_http_headers",
105
- side_effect=RuntimeError("No active HTTP request found."),
106
- ):
107
- assert context.session_id is None
108
 
109
- def test_session_id_with_missing_header(self, context):
110
- """Test that session_id returns None when mcp-session-id header is missing."""
111
- mock_headers = {"other-header": "value"}
112
 
113
- with patch(
114
- "fastmcp.server.dependencies.get_http_headers", return_value=mock_headers
115
- ):
116
- assert context.session_id is None
 
 
 
 
117
 
118
- def test_session_id_with_empty_header(self, context):
119
- """Test that session_id returns None when mcp-session-id header is empty."""
120
- mock_headers = {"mcp-session-id": ""}
121
 
122
- with patch(
123
- "fastmcp.server.dependencies.get_http_headers", return_value=mock_headers
124
- ):
125
- assert context.session_id == "" # Empty string is still returned as-is
126
 
127
 
128
  class TestContextState:
 
91
  class TestSessionId:
92
  def test_session_id_with_http_headers(self, context):
93
  """Test that session_id returns the value from mcp-session-id header."""
94
+ from mcp.server.lowlevel.server import request_ctx
95
+ from mcp.shared.context import RequestContext
96
+
97
  mock_headers = {"mcp-session-id": "test-session-123"}
98
 
99
+ token = request_ctx.set(
100
+ RequestContext(
101
+ request_id=0,
102
+ meta=None,
103
+ session=MagicMock(wraps={}),
104
+ lifespan_context=MagicMock(),
105
+ request=MagicMock(headers=mock_headers),
106
+ )
107
+ )
108
+
109
+ assert context.session_id == "test-session-123"
110
+
111
+ request_ctx.reset(token)
112
 
113
  def test_session_id_without_http_headers(self, context):
114
+ """Test that session_id returns a UUID string when no HTTP headers are available."""
115
+ import uuid
 
 
 
 
116
 
117
+ from mcp.server.lowlevel.server import request_ctx
118
+ from mcp.shared.context import RequestContext
 
119
 
120
+ token = request_ctx.set(
121
+ RequestContext(
122
+ request_id=0,
123
+ meta=None,
124
+ session=MagicMock(wraps={}),
125
+ lifespan_context=MagicMock(),
126
+ )
127
+ )
128
 
129
+ assert uuid.UUID(context.session_id)
 
 
130
 
131
+ request_ctx.reset(token)
 
 
 
132
 
133
 
134
  class TestContextState: