fastmcp 2.2.10__py3-none-any.whl → 2.3.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
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)
fastmcp/settings.py CHANGED
@@ -61,6 +61,7 @@ class ServerSettings(BaseSettings):
61
61
  port: int = 8000
62
62
  sse_path: str = "/sse"
63
63
  message_path: str = "/messages/"
64
+ streamable_http_path: str = "/mcp"
64
65
  debug: bool = False
65
66
 
66
67
  # resource settings
@@ -82,6 +83,12 @@ class ServerSettings(BaseSettings):
82
83
 
83
84
  auth: AuthSettings | None = None
84
85
 
86
+ # StreamableHTTP settings
87
+ json_response: bool = False
88
+ stateless_http: bool = (
89
+ False # If True, uses true stateless mode (new transport per request)
90
+ )
91
+
85
92
 
86
93
  class ClientSettings(BaseSettings):
87
94
  """FastMCP client settings."""
fastmcp/tools/tool.py CHANGED
@@ -12,6 +12,7 @@ from pydantic import BaseModel, BeforeValidator, Field
12
12
 
13
13
  import fastmcp
14
14
  from fastmcp.exceptions import ToolError
15
+ from fastmcp.server.dependencies import get_context
15
16
  from fastmcp.utilities.json_schema import prune_params
16
17
  from fastmcp.utilities.logging import get_logger
17
18
  from fastmcp.utilities.types import (
@@ -22,10 +23,7 @@ from fastmcp.utilities.types import (
22
23
  )
23
24
 
24
25
  if TYPE_CHECKING:
25
- from mcp.server.session import ServerSessionT
26
- from mcp.shared.context import LifespanContextT
27
-
28
- from fastmcp.server import Context
26
+ pass
29
27
 
30
28
  logger = get_logger(__name__)
31
29
 
@@ -41,9 +39,6 @@ class Tool(BaseModel):
41
39
  name: str = Field(description="Name of the tool")
42
40
  description: str = Field(description="Description of what the tool does")
43
41
  parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
44
- context_kwarg: str | None = Field(
45
- None, description="Name of the kwarg that should receive context"
46
- )
47
42
  tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
48
43
  default_factory=set, description="Tags for the tool"
49
44
  )
@@ -60,13 +55,12 @@ class Tool(BaseModel):
60
55
  fn: Callable[..., Any],
61
56
  name: str | None = None,
62
57
  description: str | None = None,
63
- context_kwarg: str | None = None,
64
58
  tags: set[str] | None = None,
65
59
  annotations: ToolAnnotations | None = None,
66
60
  serializer: Callable[[Any], str] | None = None,
67
61
  ) -> Tool:
68
62
  """Create a Tool from a function."""
69
- from fastmcp import Context
63
+ from fastmcp.server.context import Context
70
64
 
71
65
  # Reject functions with *args or **kwargs
72
66
  sig = inspect.signature(fn)
@@ -86,8 +80,7 @@ class Tool(BaseModel):
86
80
  type_adapter = get_cached_typeadapter(fn)
87
81
  schema = type_adapter.json_schema()
88
82
 
89
- if context_kwarg is None:
90
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
83
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
91
84
  if context_kwarg:
92
85
  schema = prune_params(schema, params=[context_kwarg])
93
86
 
@@ -96,25 +89,23 @@ class Tool(BaseModel):
96
89
  name=func_name,
97
90
  description=func_doc,
98
91
  parameters=schema,
99
- context_kwarg=context_kwarg,
100
92
  tags=tags or set(),
101
93
  annotations=annotations,
102
94
  serializer=serializer,
103
95
  )
104
96
 
105
97
  async def run(
106
- self,
107
- arguments: dict[str, Any],
108
- context: Context[ServerSessionT, LifespanContextT] | None = None,
98
+ self, arguments: dict[str, Any]
109
99
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
110
100
  """Run the tool with arguments."""
101
+ from fastmcp.server.context import Context
111
102
 
112
- try:
113
- injected_args = (
114
- {self.context_kwarg: context} if self.context_kwarg is not None else {}
115
- )
103
+ arguments = arguments.copy()
116
104
 
117
- parsed_args = arguments.copy()
105
+ try:
106
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
107
+ if context_kwarg and context_kwarg not in arguments:
108
+ arguments[context_kwarg] = get_context()
118
109
 
119
110
  if fastmcp.settings.settings.tool_attempt_parse_json_args:
120
111
  # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
@@ -125,7 +116,7 @@ class Tool(BaseModel):
125
116
  # which can be pre-parsed here.
126
117
  signature = inspect.signature(self.fn)
127
118
  for param_name in self.parameters["properties"]:
128
- arg = parsed_args.get(param_name, None)
119
+ arg = arguments.get(param_name, None)
129
120
  # if not in signature, we won't have annotations, so skip logic
130
121
  if param_name not in signature.parameters:
131
122
  continue
@@ -140,13 +131,13 @@ class Tool(BaseModel):
140
131
  ):
141
132
  continue
142
133
  try:
143
- parsed_args[param_name] = json.loads(arg)
134
+ arguments[param_name] = json.loads(arg)
144
135
 
145
136
  except json.JSONDecodeError:
146
137
  pass
147
138
 
148
139
  type_adapter = get_cached_typeadapter(self.fn)
149
- result = type_adapter.validate_python(parsed_args | injected_args)
140
+ result = type_adapter.validate_python(arguments)
150
141
  if inspect.isawaitable(result):
151
142
  result = await result
152
143
 
@@ -3,7 +3,6 @@ from __future__ import annotations as _annotations
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from mcp.shared.context import LifespanContextT
7
6
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
8
7
 
9
8
  from fastmcp.exceptions import NotFoundError
@@ -12,9 +11,7 @@ from fastmcp.tools.tool import Tool
12
11
  from fastmcp.utilities.logging import get_logger
13
12
 
14
13
  if TYPE_CHECKING:
15
- from mcp.server.session import ServerSessionT
16
-
17
- from fastmcp.server import Context
14
+ pass
18
15
 
19
16
  logger = get_logger(__name__)
20
17
 
@@ -98,14 +95,11 @@ class ToolManager:
98
95
  return tool
99
96
 
100
97
  async def call_tool(
101
- self,
102
- key: str,
103
- arguments: dict[str, Any],
104
- context: Context[ServerSessionT, LifespanContextT] | None = None,
98
+ self, key: str, arguments: dict[str, Any]
105
99
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
106
100
  """Call a tool by name with arguments."""
107
101
  tool = self.get_tool(key)
108
102
  if not tool:
109
103
  raise NotFoundError(f"Unknown tool: {key}")
110
104
 
111
- return await tool.run(arguments, context=context)
105
+ return await tool.run(arguments)
@@ -0,0 +1,26 @@
1
+ import datetime
2
+ from typing import Any
3
+
4
+ UTC = datetime.timezone.utc
5
+
6
+
7
+ class TimedCache:
8
+ NOT_FOUND = object()
9
+
10
+ def __init__(self, expiration: datetime.timedelta):
11
+ self.expiration = expiration
12
+ self.cache: dict[Any, tuple[Any, datetime.datetime]] = {}
13
+
14
+ def set(self, key: Any, value: Any) -> None:
15
+ expires = datetime.datetime.now(UTC) + self.expiration
16
+ self.cache[key] = (value, expires)
17
+
18
+ def get(self, key: Any) -> Any:
19
+ value = self.cache.get(key)
20
+ if value is not None and value[1] > datetime.datetime.now(UTC):
21
+ return value[0]
22
+ else:
23
+ return self.NOT_FOUND
24
+
25
+ def clear(self) -> None:
26
+ self.cache.clear()
@@ -1,9 +1,20 @@
1
+ from __future__ import annotations
2
+
1
3
  import copy
4
+ import multiprocessing
5
+ import socket
6
+ import time
7
+ from collections.abc import Callable, Generator
2
8
  from contextlib import contextmanager
3
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any, Literal
10
+
11
+ import uvicorn
4
12
 
5
13
  from fastmcp.settings import settings
6
14
 
15
+ if TYPE_CHECKING:
16
+ from fastmcp.server.server import FastMCP
17
+
7
18
 
8
19
  @contextmanager
9
20
  def temporary_settings(**kwargs: Any):
@@ -39,3 +50,64 @@ def temporary_settings(**kwargs: Any):
39
50
  for attr in kwargs:
40
51
  if hasattr(settings, attr):
41
52
  setattr(settings, attr, old_settings[attr])
53
+
54
+
55
+ def _run_server(mcp_server: FastMCP, transport: Literal["sse"], port: int) -> None:
56
+ # Some Starlette apps are not pickleable, so we need to create them here based on the indicated transport
57
+ if transport == "sse":
58
+ app = mcp_server.sse_app()
59
+ else:
60
+ raise ValueError(f"Invalid transport: {transport}")
61
+ uvicorn_server = uvicorn.Server(
62
+ config=uvicorn.Config(
63
+ app=app,
64
+ host="127.0.0.1",
65
+ port=port,
66
+ log_level="error",
67
+ )
68
+ )
69
+ uvicorn_server.run()
70
+
71
+
72
+ @contextmanager
73
+ def run_server_in_process(
74
+ server_fn: Callable[[str, int], None],
75
+ ) -> Generator[str, None, None]:
76
+ """
77
+ Context manager that runs a Starlette app in a separate process and returns the
78
+ server URL. When the context manager is exited, the server process is killed.
79
+
80
+ Args:
81
+ app: The Starlette app to run.
82
+
83
+ Returns:
84
+ The server URL.
85
+ """
86
+ host = "127.0.0.1"
87
+ with socket.socket() as s:
88
+ s.bind((host, 0))
89
+ port = s.getsockname()[1]
90
+
91
+ proc = multiprocessing.Process(target=server_fn, args=(host, port), daemon=True)
92
+ proc.start()
93
+
94
+ # Wait for server to be running
95
+ max_attempts = 100
96
+ attempt = 0
97
+ while attempt < max_attempts and proc.is_alive():
98
+ try:
99
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
100
+ s.connect((host, port))
101
+ break
102
+ except ConnectionRefusedError:
103
+ time.sleep(0.01)
104
+ attempt += 1
105
+ else:
106
+ raise RuntimeError(f"Server failed to start after {max_attempts} attempts")
107
+
108
+ yield f"http://{host}:{port}"
109
+
110
+ proc.kill()
111
+ proc.join(timeout=2)
112
+ if proc.is_alive():
113
+ raise RuntimeError("Server process failed to terminate")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.2.10
3
+ Version: 2.3.0rc1
4
4
  Summary: The fast, Pythonic way to build MCP servers.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -19,7 +19,7 @@ Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: exceptiongroup>=1.2.2
21
21
  Requires-Dist: httpx>=0.28.1
22
- Requires-Dist: mcp<2.0.0,>=1.7.1
22
+ Requires-Dist: mcp
23
23
  Requires-Dist: openapi-pydantic>=0.5.1
24
24
  Requires-Dist: python-dotenv>=1.1.0
25
25
  Requires-Dist: rich>=13.9.4
@@ -0,0 +1,55 @@
1
+ fastmcp/__init__.py,sha256=yTAqLZORsPqbr7AE0ayw6zIYBeMlxQlI-3HE2WqbvHk,435
2
+ fastmcp/exceptions.py,sha256=QKVHbftoZp4YZQ2NxA-t1SjztqspFdX95YTFOAmr5EE,640
3
+ fastmcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ fastmcp/settings.py,sha256=rDClnYEpYjEl8VsvvVrKp9oaE4YLfNQcMoZ41H_bDL0,2968
5
+ fastmcp/cli/__init__.py,sha256=Ii284TNoG5lxTP40ETMGhHEq3lQZWxu9m9JuU57kUpQ,87
6
+ fastmcp/cli/claude.py,sha256=IAlcZ4qZKBBj09jZUMEx7EANZE_IR3vcu7zOBJmMOuU,4567
7
+ fastmcp/cli/cli.py,sha256=7s5RsV8D_tPs20EJcrCvyO-i69DmV60OiRGD21oEJSI,15728
8
+ fastmcp/client/__init__.py,sha256=BXO9NUhntZ5GnUACfaRCzDJ5IzxqFJs8qKG-CRMSco4,490
9
+ fastmcp/client/base.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ fastmcp/client/client.py,sha256=zfSLWSGqiBoADveKehsAL66CdyGGqmzVHR1q46zfdQY,15479
11
+ fastmcp/client/logging.py,sha256=Q8jYcZj4KA15Yiz3RP8tBXj8sd9IxL3VThF_Y0O4Upc,356
12
+ fastmcp/client/roots.py,sha256=IxI_bHwHTmg6c2H-s1av1ZgrRnNDieHtYwdGFbzXT5c,2471
13
+ fastmcp/client/sampling.py,sha256=UlDHxnd6k_HoU8RA3ob0g8-e6haJBc9u27N_v291QoI,1698
14
+ fastmcp/client/transports.py,sha256=rIOodm2_7tuwy2oMgobiSlZsU08Z2Iy0XJs0dhvprWE,18705
15
+ fastmcp/contrib/README.md,sha256=rKknYSI1T192UvSszqwwDlQ2eYQpxywrNTLoj177SYU,878
16
+ fastmcp/contrib/bulk_tool_caller/README.md,sha256=5aUUY1TSFKtz1pvTLSDqkUCkGkuqMfMZNsLeaNqEgAc,1960
17
+ fastmcp/contrib/bulk_tool_caller/__init__.py,sha256=xvGSSaUXTQrc31erBoi1Gh7BikgOliETDiYVTP3rLxY,75
18
+ fastmcp/contrib/bulk_tool_caller/bulk_tool_caller.py,sha256=2NcrGS59qvHo1lfbRaT8NSWfCxN66knciLxFvnGwCLY,4165
19
+ fastmcp/contrib/bulk_tool_caller/example.py,sha256=3RdsU2KrRwYZHEdVAmHOGJsO3ZJBxSaqz8BTznkPg7Y,321
20
+ fastmcp/contrib/mcp_mixin/README.md,sha256=9DDTJXWkA3yv1fp5V58gofmARPQ2xWDhblYGvUhKpDQ,1689
21
+ fastmcp/contrib/mcp_mixin/__init__.py,sha256=aw9IQ1ssNjCgws4ZNt8bkdpossAAGVAwwjBpMp9O5ZQ,153
22
+ fastmcp/contrib/mcp_mixin/example.py,sha256=GnunkXmtG5hLLTUsM8aW5ZURU52Z8vI4tNLl-fK7Dg0,1228
23
+ fastmcp/contrib/mcp_mixin/mcp_mixin.py,sha256=cfIRbnSxsVzglTD-auyTE0izVQeHP7Oz18qzYoBZJgg,7899
24
+ fastmcp/prompts/__init__.py,sha256=An8uMBUh9Hrb7qqcn_5_Hent7IOeSh7EA2IUVsIrtHc,179
25
+ fastmcp/prompts/prompt.py,sha256=psc-YiBRttbjETINaP9P9QV328yk96mDBsZgjOHVyKM,7777
26
+ fastmcp/prompts/prompt_manager.py,sha256=9VcioLE-AoUKe1e9SynNQME9SvWy0q1QAvO1ewIWVmI,3126
27
+ fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
28
+ fastmcp/resources/resource.py,sha256=Rx1My_fi1f-oqnQ9R_v7ejopAk4BJDfbB75-s4d31dM,2492
29
+ fastmcp/resources/resource_manager.py,sha256=_kJQMjb7U0NYPhWZEayDCgpMeYOnvtoCzi2sMZc71NA,9442
30
+ fastmcp/resources/template.py,sha256=X_x1wnmIWGykoKwhwGLZmvGysXIubVLHDrvSmbsu4kg,7306
31
+ fastmcp/resources/types.py,sha256=QPDeka_cM1hmvwW4FeFhqy6BEEi4MlwtpvhWUVWh5Fc,6459
32
+ fastmcp/server/__init__.py,sha256=bMD4aQD4yJqLz7-mudoNsyeV8UgQfRAg3PRwPvwTEds,119
33
+ fastmcp/server/context.py,sha256=ykitQygA7zT5prbFTLCuYlnAzuljf_9ErUT0FYBPv3E,8135
34
+ fastmcp/server/dependencies.py,sha256=1utkxFsV37HZcWBwI69JyngVN2ppGO_PEgxUlUHHy_Q,742
35
+ fastmcp/server/http.py,sha256=c_J6y1jkasC3WMCzo3LVXMwJbGrHVwQO2Qtl4IP5RlY,10085
36
+ fastmcp/server/openapi.py,sha256=0nANnwHJ5VZInNyo2f9ErmO0K3igMv6bwyxf3G-BSls,23473
37
+ fastmcp/server/proxy.py,sha256=qcBD2wWMcXA4dhqppStVH4UhsyWm0cpPUuItLpO6H6A,9621
38
+ fastmcp/server/server.py,sha256=usocXySZvmrp6GOJzQQrh1lUiAyVMkRnJhk4NBhp0FQ,40827
39
+ fastmcp/server/streamable_http_manager.py,sha256=noCZSybvbotyiHZbJ7PRIB6peFBGvjIM2Xavs0pLtGQ,8879
40
+ fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
41
+ fastmcp/tools/tool.py,sha256=lr9F90-A36Z6DT4LScJBW88uFq8xrSv00ivFxiERJe8,7786
42
+ fastmcp/tools/tool_manager.py,sha256=p2nHyLFgz28tbsLpWOurkbWRU2Z34_HcDohjrvwjI0E,3369
43
+ fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
44
+ fastmcp/utilities/cache.py,sha256=aV3oZ-ZhMgLSM9iAotlUlEy5jFvGXrVo0Y5Bj4PBtqY,707
45
+ fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNCEg,2990
46
+ fastmcp/utilities/json_schema.py,sha256=mSakhP8bENxhLFMwHJSxJAFllNeByIBDjVohwlpac6w,2026
47
+ fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,1159
48
+ fastmcp/utilities/openapi.py,sha256=Er3G1MyFwiWVxZXicXtD2j-BvttHEDTi1dgkq1KiBQc,51073
49
+ fastmcp/utilities/tests.py,sha256=uUV-8CkhCe5zZJkxhgJXnxrjJ3Yq7cCMZN8xWKGuqdY,3181
50
+ fastmcp/utilities/types.py,sha256=6CcqAQ1QqCO2HGSFlPS6FO5JRWnacjCcO2-EhyEnZV0,4400
51
+ fastmcp-2.3.0rc1.dist-info/METADATA,sha256=5hLCFQ8nJWFwllIUbC2D0V9YlOaA7lnNuJwoXTQrw7s,16385
52
+ fastmcp-2.3.0rc1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
53
+ fastmcp-2.3.0rc1.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
54
+ fastmcp-2.3.0rc1.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
55
+ fastmcp-2.3.0rc1.dist-info/RECORD,,
fastmcp/utilities/http.py DELETED
@@ -1,44 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from contextlib import (
4
- asynccontextmanager,
5
- )
6
- from contextvars import ContextVar
7
-
8
- from starlette.requests import Request
9
-
10
- from fastmcp.utilities.logging import get_logger
11
-
12
- logger = get_logger(__name__)
13
-
14
-
15
- _current_starlette_request: ContextVar[Request | None] = ContextVar(
16
- "starlette_request",
17
- default=None,
18
- )
19
-
20
-
21
- @asynccontextmanager
22
- async def starlette_request_context(request: Request):
23
- token = _current_starlette_request.set(request)
24
- try:
25
- yield
26
- finally:
27
- _current_starlette_request.reset(token)
28
-
29
-
30
- def get_current_starlette_request() -> Request | None:
31
- return _current_starlette_request.get()
32
-
33
-
34
- class RequestMiddleware:
35
- """
36
- Middleware that stores each request in a ContextVar
37
- """
38
-
39
- def __init__(self, app):
40
- self.app = app
41
-
42
- async def __call__(self, scope, receive, send):
43
- async with starlette_request_context(Request(scope)):
44
- await self.app(scope, receive, send)