Spaces:
Running
Running
Jeremiah Lowin commited on
Commit ·
69d9f94
1
Parent(s): 2d2e845
Update mcp pin to 1.8
Browse files- pyproject.toml +1 -8
- src/fastmcp/server/http.py +1 -3
- src/fastmcp/vendor/streamable_http_manager.py +0 -241
- uv.lock +21 -3
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 |
-
|
| 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",
|
| 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.
|
| 575 |
-
source = {
|
| 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"
|