Jeremiah Lowin commited on
Commit
69d9f94
·
1 Parent(s): 2d2e845

Update mcp pin to 1.8

Browse files
pyproject.toml CHANGED
@@ -7,9 +7,7 @@ dependencies = [
7
  "python-dotenv>=1.1.0",
8
  "exceptiongroup>=1.2.2",
9
  "httpx>=0.28.1",
10
- # "mcp>=1.7.1,<2.0.0",
11
- # use git commit until 1.7.2 is released
12
- "mcp",
13
  "openapi-pydantic>=0.5.1",
14
  "rich>=13.9.4",
15
  "typer>=0.15.2",
@@ -70,11 +68,6 @@ Documentation = "https://gofastmcp.com"
70
  requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
71
  build-backend = "hatchling.build"
72
 
73
- [tool.uv]
74
-
75
- [tool.uv.sources]
76
- mcp = { git = "https://github.com/modelcontextprotocol/python-sdk", rev = "a027d75f609000378522c5873c2a16aa1963d487" }
77
-
78
  [tool.hatch.version]
79
  source = "uv-dynamic-versioning"
80
 
 
7
  "python-dotenv>=1.1.0",
8
  "exceptiongroup>=1.2.2",
9
  "httpx>=0.28.1",
10
+ "mcp>=1.8.0,<2.0.0",
 
 
11
  "openapi-pydantic>=0.5.1",
12
  "rich>=13.9.4",
13
  "typer>=0.15.2",
 
68
  requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
69
  build-backend = "hatchling.build"
70
 
 
 
 
 
 
71
  [tool.hatch.version]
72
  source = "uv-dynamic-versioning"
73
 
src/fastmcp/server/http.py CHANGED
@@ -14,6 +14,7 @@ from mcp.server.auth.provider import OAuthAuthorizationServerProvider
14
  from mcp.server.auth.routes import create_auth_routes
15
  from mcp.server.auth.settings import AuthSettings
16
  from mcp.server.sse import SseServerTransport
 
17
  from starlette.applications import Starlette
18
  from starlette.middleware import Middleware
19
  from starlette.middleware.authentication import AuthenticationMiddleware
@@ -24,9 +25,6 @@ from starlette.types import Receive, Scope, Send
24
 
25
  from fastmcp.utilities.logging import get_logger
26
 
27
- # This import is vendored until it is finalized in the upstream SDK
28
- from fastmcp.vendor.streamable_http_manager import StreamableHTTPSessionManager
29
-
30
  if TYPE_CHECKING:
31
  from fastmcp.server.server import FastMCP
32
 
 
14
  from mcp.server.auth.routes import create_auth_routes
15
  from mcp.server.auth.settings import AuthSettings
16
  from mcp.server.sse import SseServerTransport
17
+ from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
18
  from starlette.applications import Starlette
19
  from starlette.middleware import Middleware
20
  from starlette.middleware.authentication import AuthenticationMiddleware
 
25
 
26
  from fastmcp.utilities.logging import get_logger
27
 
 
 
 
28
  if TYPE_CHECKING:
29
  from fastmcp.server.server import FastMCP
30
 
src/fastmcp/vendor/streamable_http_manager.py DELETED
@@ -1,241 +0,0 @@
1
- """StreamableHTTP Session Manager for MCP servers."""
2
-
3
- # follows https://github.com/modelcontextprotocol/python-sdk/blob/ihrpr/shttp/src/mcp/server/streamable_http_manager.py
4
- # and can be removed once that spec is finalized
5
-
6
- from __future__ import annotations
7
-
8
- import contextlib
9
- import logging
10
- from collections.abc import AsyncIterator
11
- from http import HTTPStatus
12
- from typing import Any
13
- from uuid import uuid4
14
-
15
- import anyio
16
- from anyio.abc import TaskStatus
17
- from mcp.server.lowlevel.server import Server as MCPServer
18
- from mcp.server.streamable_http import (
19
- MCP_SESSION_ID_HEADER,
20
- EventStore,
21
- StreamableHTTPServerTransport,
22
- )
23
- from starlette.requests import Request
24
- from starlette.responses import Response
25
- from starlette.types import Receive, Scope, Send
26
-
27
- logger = logging.getLogger(__name__)
28
-
29
-
30
- class StreamableHTTPSessionManager:
31
- """
32
- Manages StreamableHTTP sessions with optional resumability via event store.
33
-
34
- This class abstracts away the complexity of session management, event storage,
35
- and request handling for StreamableHTTP transports. It handles:
36
-
37
- 1. Session tracking for clients
38
- 2. Resumability via an optional event store
39
- 3. Connection management and lifecycle
40
- 4. Request handling and transport setup
41
-
42
- Args:
43
- app: The MCP server instance
44
- event_store: Optional event store for resumability support.
45
- If provided, enables resumable connections where clients
46
- can reconnect and receive missed events.
47
- If None, sessions are still tracked but not resumable.
48
- json_response: Whether to use JSON responses instead of SSE streams
49
- stateless: If True, creates a completely fresh transport for each request
50
- with no session tracking or state persistence between requests.
51
-
52
- """
53
-
54
- def __init__(
55
- self,
56
- app: MCPServer[Any],
57
- event_store: EventStore | None = None,
58
- json_response: bool = False,
59
- stateless: bool = False,
60
- ):
61
- self.app = app
62
- self.event_store = event_store
63
- self.json_response = json_response
64
- self.stateless = stateless
65
-
66
- # Session tracking (only used if not stateless)
67
- self._session_creation_lock = anyio.Lock()
68
- self._server_instances: dict[str, StreamableHTTPServerTransport] = {}
69
-
70
- # The task group will be set during lifespan
71
- self._task_group = None
72
-
73
- @contextlib.asynccontextmanager
74
- async def run(self) -> AsyncIterator[None]:
75
- """
76
- Run the session manager with proper lifecycle management.
77
-
78
- This creates and manages the task group for all session operations.
79
-
80
- Use this in the lifespan context manager of your Starlette app:
81
-
82
- @contextlib.asynccontextmanager
83
- async def lifespan(app: Starlette) -> AsyncIterator[None]:
84
- async with session_manager.run():
85
- yield
86
- """
87
- async with anyio.create_task_group() as tg:
88
- # Store the task group for later use
89
- self._task_group = tg
90
- logger.info("StreamableHTTP session manager started")
91
- try:
92
- yield # Let the application run
93
- finally:
94
- logger.info("StreamableHTTP session manager shutting down")
95
- # Cancel task group to stop all spawned tasks
96
- tg.cancel_scope.cancel()
97
- self._task_group = None
98
- # Clear any remaining server instances
99
- self._server_instances.clear()
100
-
101
- async def handle_request(
102
- self,
103
- scope: Scope,
104
- receive: Receive,
105
- send: Send,
106
- ) -> None:
107
- """
108
- Process ASGI request with proper session handling and transport setup.
109
-
110
- Dispatches to the appropriate handler based on stateless mode.
111
-
112
- Args:
113
- scope: ASGI scope
114
- receive: ASGI receive function
115
- send: ASGI send function
116
- """
117
- if self._task_group is None:
118
- raise RuntimeError(
119
- "Task group is not initialized. Make sure to use the run()."
120
- )
121
-
122
- # Dispatch to the appropriate handler
123
- if self.stateless:
124
- await self._handle_stateless_request(scope, receive, send)
125
- else:
126
- await self._handle_stateful_request(scope, receive, send)
127
-
128
- async def _handle_stateless_request(
129
- self,
130
- scope: Scope,
131
- receive: Receive,
132
- send: Send,
133
- ) -> None:
134
- """
135
- Process request in stateless mode - creating a new transport for each request.
136
-
137
- Args:
138
- scope: ASGI scope
139
- receive: ASGI receive function
140
- send: ASGI send function
141
- """
142
- logger.debug("Stateless mode: Creating new transport for this request")
143
- # No session ID needed in stateless mode
144
- http_transport = StreamableHTTPServerTransport(
145
- mcp_session_id=None, # No session tracking in stateless mode
146
- is_json_response_enabled=self.json_response,
147
- event_store=None, # No event store in stateless mode
148
- )
149
-
150
- # Start server in a new task
151
- async def run_stateless_server(
152
- *, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED
153
- ):
154
- async with http_transport.connect() as streams:
155
- read_stream, write_stream = streams
156
- task_status.started()
157
- await self.app.run(
158
- read_stream,
159
- write_stream,
160
- self.app.create_initialization_options(),
161
- stateless=True,
162
- )
163
-
164
- # Assert task group is not None for type checking
165
- assert self._task_group is not None
166
- # Start the server task
167
- await self._task_group.start(run_stateless_server)
168
-
169
- # Handle the HTTP request and return the response
170
- await http_transport.handle_request(scope, receive, send)
171
-
172
- async def _handle_stateful_request(
173
- self,
174
- scope: Scope,
175
- receive: Receive,
176
- send: Send,
177
- ) -> None:
178
- """
179
- Process request in stateful mode - maintaining session state between requests.
180
-
181
- Args:
182
- scope: ASGI scope
183
- receive: ASGI receive function
184
- send: ASGI send function
185
- """
186
- request = Request(scope, receive)
187
- request_mcp_session_id = request.headers.get(MCP_SESSION_ID_HEADER)
188
-
189
- # Existing session case
190
- if (
191
- request_mcp_session_id is not None
192
- and request_mcp_session_id in self._server_instances
193
- ):
194
- transport = self._server_instances[request_mcp_session_id]
195
- logger.debug("Session already exists, handling request directly")
196
- await transport.handle_request(scope, receive, send)
197
- return
198
-
199
- if request_mcp_session_id is None:
200
- # New session case
201
- logger.debug("Creating new transport")
202
- async with self._session_creation_lock:
203
- new_session_id = uuid4().hex
204
- http_transport = StreamableHTTPServerTransport(
205
- mcp_session_id=new_session_id,
206
- is_json_response_enabled=self.json_response,
207
- event_store=self.event_store, # May be None (no resumability)
208
- )
209
-
210
- assert http_transport.mcp_session_id is not None
211
- self._server_instances[http_transport.mcp_session_id] = http_transport
212
- logger.info(f"Created new transport with session ID: {new_session_id}")
213
-
214
- # Define the server runner
215
- async def run_server(
216
- *, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED
217
- ) -> None:
218
- async with http_transport.connect() as streams:
219
- read_stream, write_stream = streams
220
- task_status.started()
221
- await self.app.run(
222
- read_stream,
223
- write_stream,
224
- self.app.create_initialization_options(),
225
- stateless=False, # Stateful mode
226
- )
227
-
228
- # Assert task group is not None for type checking
229
- assert self._task_group is not None
230
- # Start the server task
231
- await self._task_group.start(run_server)
232
-
233
- # Handle the HTTP request and return the response
234
- await http_transport.handle_request(scope, receive, send)
235
- else:
236
- # Invalid session ID
237
- response = Response(
238
- "Bad Request: No valid session ID provided",
239
- status_code=HTTPStatus.BAD_REQUEST,
240
- )
241
- await response(scope, receive, send)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
uv.lock CHANGED
@@ -331,6 +331,7 @@ dev = [
331
  { name = "pytest-cov" },
332
  { name = "pytest-flakefinder" },
333
  { name = "pytest-report" },
 
334
  { name = "pytest-xdist" },
335
  { name = "ruff" },
336
  ]
@@ -339,7 +340,7 @@ dev = [
339
  requires-dist = [
340
  { name = "exceptiongroup", specifier = ">=1.2.2" },
341
  { name = "httpx", specifier = ">=0.28.1" },
342
- { name = "mcp", git = "https://github.com/modelcontextprotocol/python-sdk.git?rev=a027d75f609000378522c5873c2a16aa1963d487" },
343
  { name = "openapi-pydantic", specifier = ">=0.5.1" },
344
  { name = "python-dotenv", specifier = ">=1.1.0" },
345
  { name = "rich", specifier = ">=13.9.4" },
@@ -361,6 +362,7 @@ dev = [
361
  { name = "pytest-cov", specifier = ">=6.1.1" },
362
  { name = "pytest-flakefinder" },
363
  { name = "pytest-report", specifier = ">=0.2.1" },
 
364
  { name = "pytest-xdist", specifier = ">=3.6.1" },
365
  { name = "ruff" },
366
  ]
@@ -571,8 +573,8 @@ wheels = [
571
 
572
  [[package]]
573
  name = "mcp"
574
- version = "1.7.1.dev17+a027d75"
575
- source = { git = "https://github.com/modelcontextprotocol/python-sdk.git?rev=a027d75f609000378522c5873c2a16aa1963d487#a027d75f609000378522c5873c2a16aa1963d487" }
576
  dependencies = [
577
  { name = "anyio" },
578
  { name = "httpx" },
@@ -584,6 +586,10 @@ dependencies = [
584
  { name = "starlette" },
585
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
586
  ]
 
 
 
 
587
 
588
  [[package]]
589
  name = "mdurl"
@@ -956,6 +962,18 @@ dependencies = [
956
  ]
957
  sdist = { url = "https://files.pythonhosted.org/packages/3b/82/e141da085de0b6dac3f047ae009e136bcedbcfca4ada082a55359d6f735e/pytest-report-0.2.1.tar.gz", hash = "sha256:d382e8db4c52a815d39dae5f21ee5edc0da3ae8ec19a22e55e9be5c60714a39d", size = 3517, upload-time = "2016-05-11T02:08:04.665Z" }
958
 
 
 
 
 
 
 
 
 
 
 
 
 
959
  [[package]]
960
  name = "pytest-xdist"
961
  version = "3.6.1"
 
331
  { name = "pytest-cov" },
332
  { name = "pytest-flakefinder" },
333
  { name = "pytest-report" },
334
+ { name = "pytest-timeout" },
335
  { name = "pytest-xdist" },
336
  { name = "ruff" },
337
  ]
 
340
  requires-dist = [
341
  { name = "exceptiongroup", specifier = ">=1.2.2" },
342
  { name = "httpx", specifier = ">=0.28.1" },
343
+ { name = "mcp", specifier = ">=1.8.0,<2.0.0" },
344
  { name = "openapi-pydantic", specifier = ">=0.5.1" },
345
  { name = "python-dotenv", specifier = ">=1.1.0" },
346
  { name = "rich", specifier = ">=13.9.4" },
 
362
  { name = "pytest-cov", specifier = ">=6.1.1" },
363
  { name = "pytest-flakefinder" },
364
  { name = "pytest-report", specifier = ">=0.2.1" },
365
+ { name = "pytest-timeout", specifier = ">=2.4.0" },
366
  { name = "pytest-xdist", specifier = ">=3.6.1" },
367
  { name = "ruff" },
368
  ]
 
573
 
574
  [[package]]
575
  name = "mcp"
576
+ version = "1.8.0"
577
+ source = { registry = "https://pypi.org/simple" }
578
  dependencies = [
579
  { name = "anyio" },
580
  { name = "httpx" },
 
586
  { name = "starlette" },
587
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
588
  ]
589
+ sdist = { url = "https://files.pythonhosted.org/packages/ff/97/0a3e08559557b0ac5799f9fb535fbe5a4e4dcdd66ce9d32e7a74b4d0534d/mcp-1.8.0.tar.gz", hash = "sha256:263dfb700540b726c093f0c3e043f66aded0730d0b51f04eb0a3eb90055fe49b", size = 264641, upload-time = "2025-05-08T20:09:06.255Z" }
590
+ wheels = [
591
+ { url = "https://files.pythonhosted.org/packages/b2/b2/4ac3bd17b1fdd65658f18de4eb0c703517ee0b483dc5f56467802a9197e0/mcp-1.8.0-py3-none-any.whl", hash = "sha256:889d9d3b4f12b7da59e7a3933a0acadae1fce498bfcd220defb590aa291a1334", size = 119544, upload-time = "2025-05-08T20:09:04.458Z" },
592
+ ]
593
 
594
  [[package]]
595
  name = "mdurl"
 
962
  ]
963
  sdist = { url = "https://files.pythonhosted.org/packages/3b/82/e141da085de0b6dac3f047ae009e136bcedbcfca4ada082a55359d6f735e/pytest-report-0.2.1.tar.gz", hash = "sha256:d382e8db4c52a815d39dae5f21ee5edc0da3ae8ec19a22e55e9be5c60714a39d", size = 3517, upload-time = "2016-05-11T02:08:04.665Z" }
964
 
965
+ [[package]]
966
+ name = "pytest-timeout"
967
+ version = "2.4.0"
968
+ source = { registry = "https://pypi.org/simple" }
969
+ dependencies = [
970
+ { name = "pytest" },
971
+ ]
972
+ sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" }
973
+ wheels = [
974
+ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
975
+ ]
976
+
977
  [[package]]
978
  name = "pytest-xdist"
979
  version = "3.6.1"