fastmcp 2.2.9__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.
fastmcp/__init__.py CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  from importlib.metadata import version
4
4
 
5
-
6
5
  from fastmcp.server.server import FastMCP
7
6
  from fastmcp.server.context import Context
7
+ import fastmcp.server
8
+
8
9
  from fastmcp.client import Client
9
10
  from fastmcp.utilities.types import Image
10
11
  from . import client, settings
fastmcp/cli/cli.py CHANGED
@@ -334,7 +334,7 @@ def run(
334
334
  str | None,
335
335
  typer.Option(
336
336
  "--host",
337
- help="Host to bind to when using sse transport (default: 0.0.0.0)",
337
+ help="Host to bind to when using sse transport (default: 127.0.0.1)",
338
338
  ),
339
339
  ] = None,
340
340
  port: Annotated[
fastmcp/client/client.py CHANGED
@@ -108,9 +108,10 @@ class Client:
108
108
 
109
109
  # --- MCP Client Methods ---
110
110
 
111
- async def ping(self) -> None:
111
+ async def ping(self) -> bool:
112
112
  """Send a ping request."""
113
- await self.session.send_ping()
113
+ result = await self.session.send_ping()
114
+ return isinstance(result, mcp.types.EmptyResult)
114
115
 
115
116
  async def progress(
116
117
  self,
@@ -1,9 +1,11 @@
1
1
  import abc
2
2
  import contextlib
3
3
  import datetime
4
+ import inspect
4
5
  import os
5
6
  import shutil
6
7
  import sys
8
+ import warnings
7
9
  from collections.abc import AsyncIterator
8
10
  from pathlib import Path
9
11
  from typing import Any, TypedDict
@@ -18,6 +20,7 @@ from mcp.client.session import (
18
20
  )
19
21
  from mcp.client.sse import sse_client
20
22
  from mcp.client.stdio import stdio_client
23
+ from mcp.client.streamable_http import streamablehttp_client
21
24
  from mcp.client.websocket import websocket_client
22
25
  from mcp.shared.memory import create_connected_server_and_client_session
23
26
  from pydantic import AnyUrl
@@ -125,6 +128,33 @@ class SSETransport(ClientTransport):
125
128
  return f"<SSE(url='{self.url}')>"
126
129
 
127
130
 
131
+ class StreamableHttpTransport(ClientTransport):
132
+ """Transport implementation that connects to an MCP server via Streamable HTTP Requests."""
133
+
134
+ def __init__(self, url: str | AnyUrl, headers: dict[str, str] | None = None):
135
+ if isinstance(url, AnyUrl):
136
+ url = str(url)
137
+ if not isinstance(url, str) or not url.startswith("http"):
138
+ raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.")
139
+ self.url = url
140
+ self.headers = headers or {}
141
+
142
+ @contextlib.asynccontextmanager
143
+ async def connect_session(
144
+ self, **session_kwargs: Unpack[SessionKwargs]
145
+ ) -> AsyncIterator[ClientSession]:
146
+ async with streamablehttp_client(self.url, headers=self.headers) as transport:
147
+ read_stream, write_stream, _ = transport
148
+ async with ClientSession(
149
+ read_stream, write_stream, **session_kwargs
150
+ ) as session:
151
+ await session.initialize()
152
+ yield session
153
+
154
+ def __repr__(self) -> str:
155
+ return f"<StreamableHttp(url='{self.url}')>"
156
+
157
+
128
158
  class StdioTransport(ClientTransport):
129
159
  """
130
160
  Base transport for connecting to an MCP server via subprocess with stdio.
@@ -422,6 +452,8 @@ def infer_transport(
422
452
  This function attempts to infer the correct transport type from the provided
423
453
  argument, handling various input types and converting them to the appropriate
424
454
  ClientTransport subclass.
455
+
456
+ For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
425
457
  """
426
458
  # the transport is already a ClientTransport
427
459
  if isinstance(transport, ClientTransport):
@@ -442,7 +474,19 @@ def infer_transport(
442
474
 
443
475
  # the transport is an http(s) URL
444
476
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
445
- return SSETransport(url=transport)
477
+ if str(transport).rstrip("/").endswith("/sse"):
478
+ warnings.warn(
479
+ inspect.cleandoc(
480
+ """
481
+ As of FastMCP 2.3.0, HTTP URLs are inferred to use Streamable HTTP.
482
+ The provided URL ends in `/sse`, so you may encounter unexpected behavior.
483
+ If you intended to use SSE, please use the `SSETransport` class directly.
484
+ """
485
+ ),
486
+ category=UserWarning,
487
+ stacklevel=2,
488
+ )
489
+ return StreamableHttpTransport(url=transport)
446
490
 
447
491
  # the transport is a websocket URL
448
492
  elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
fastmcp/prompts/prompt.py CHANGED
@@ -12,6 +12,7 @@ from mcp.types import Prompt as MCPPrompt
12
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
14
 
15
+ from fastmcp.server.dependencies import get_context
15
16
  from fastmcp.utilities.json_schema import prune_params
16
17
  from fastmcp.utilities.types import (
17
18
  _convert_set_defaults,
@@ -20,10 +21,7 @@ from fastmcp.utilities.types import (
20
21
  )
21
22
 
22
23
  if TYPE_CHECKING:
23
- from mcp.server.session import ServerSessionT
24
- from mcp.shared.context import LifespanContextT
25
-
26
- from fastmcp.server import Context
24
+ pass
27
25
 
28
26
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
29
27
 
@@ -76,9 +74,6 @@ class Prompt(BaseModel):
76
74
  None, description="Arguments that can be passed to the prompt"
77
75
  )
78
76
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
79
- context_kwarg: str | None = Field(
80
- None, description="Name of the kwarg that should receive context"
81
- )
82
77
 
83
78
  @classmethod
84
79
  def from_function(
@@ -87,7 +82,6 @@ class Prompt(BaseModel):
87
82
  name: str | None = None,
88
83
  description: str | None = None,
89
84
  tags: set[str] | None = None,
90
- context_kwarg: str | None = None,
91
85
  ) -> Prompt:
92
86
  """Create a Prompt from a function.
93
87
 
@@ -97,7 +91,7 @@ class Prompt(BaseModel):
97
91
  - A dict (converted to a message)
98
92
  - A sequence of any of the above
99
93
  """
100
- from fastmcp import Context
94
+ from fastmcp.server.context import Context
101
95
 
102
96
  func_name = name or fn.__name__
103
97
 
@@ -115,8 +109,8 @@ class Prompt(BaseModel):
115
109
  parameters = type_adapter.json_schema()
116
110
 
117
111
  # Auto-detect context parameter if not provided
118
- if context_kwarg is None:
119
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
112
+
113
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
120
114
  if context_kwarg:
121
115
  parameters = prune_params(parameters, params=[context_kwarg])
122
116
 
@@ -141,15 +135,15 @@ class Prompt(BaseModel):
141
135
  arguments=arguments,
142
136
  fn=fn,
143
137
  tags=tags or set(),
144
- context_kwarg=context_kwarg,
145
138
  )
146
139
 
147
140
  async def render(
148
141
  self,
149
142
  arguments: dict[str, Any] | None = None,
150
- context: Context[ServerSessionT, LifespanContextT] | None = None,
151
143
  ) -> list[PromptMessage]:
152
144
  """Render the prompt with arguments."""
145
+ from fastmcp.server.context import Context
146
+
153
147
  # Validate required arguments
154
148
  if self.arguments:
155
149
  required = {arg.name for arg in self.arguments if arg.required}
@@ -161,8 +155,9 @@ class Prompt(BaseModel):
161
155
  try:
162
156
  # Prepare arguments with context
163
157
  kwargs = arguments.copy() if arguments else {}
164
- if self.context_kwarg is not None and context is not None:
165
- kwargs[self.context_kwarg] = context
158
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
159
+ if context_kwarg and context_kwarg not in kwargs:
160
+ kwargs[context_kwarg] = get_context()
166
161
 
167
162
  # Call function and check if result is a coroutine
168
163
  result = self.fn(**kwargs)
@@ -13,10 +13,7 @@ from fastmcp.settings import DuplicateBehavior
13
13
  from fastmcp.utilities.logging import get_logger
14
14
 
15
15
  if TYPE_CHECKING:
16
- from mcp.server.session import ServerSessionT
17
- from mcp.shared.context import LifespanContextT
18
-
19
- from fastmcp.server import Context
16
+ pass
20
17
 
21
18
  logger = get_logger(__name__)
22
19
 
@@ -82,19 +79,15 @@ class PromptManager:
82
79
  self,
83
80
  name: str,
84
81
  arguments: dict[str, Any] | None = None,
85
- context: Context[ServerSessionT, LifespanContextT] | None = None,
86
82
  ) -> GetPromptResult:
87
83
  """Render a prompt by name with arguments."""
88
84
  prompt = self.get_prompt(name)
89
85
  if not prompt:
90
86
  raise NotFoundError(f"Unknown prompt: {name}")
91
87
 
92
- messages = await prompt.render(arguments, context=context)
88
+ messages = await prompt.render(arguments)
93
89
 
94
- return GetPromptResult(
95
- description=prompt.description,
96
- messages=messages,
97
- )
90
+ return GetPromptResult(description=prompt.description, messages=messages)
98
91
 
99
92
  def has_prompt(self, key: str) -> bool:
100
93
  """Check if a prompt exists."""
@@ -20,10 +20,7 @@ from pydantic import (
20
20
  from fastmcp.utilities.types import _convert_set_defaults
21
21
 
22
22
  if TYPE_CHECKING:
23
- from mcp.server.session import ServerSessionT
24
- from mcp.shared.context import LifespanContextT
25
-
26
- from fastmcp.server import Context
23
+ pass
27
24
 
28
25
 
29
26
  class Resource(BaseModel, abc.ABC):
@@ -66,9 +63,7 @@ class Resource(BaseModel, abc.ABC):
66
63
  raise ValueError("Either name or uri must be provided")
67
64
 
68
65
  @abc.abstractmethod
69
- async def read(
70
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
71
- ) -> str | bytes:
66
+ async def read(self) -> str | bytes:
72
67
  """Read the resource content."""
73
68
  pass
74
69
 
@@ -109,7 +109,7 @@ class ResourceManager:
109
109
  The added resource. If a resource with the same URI already exists,
110
110
  returns the existing resource.
111
111
  """
112
- resource = FunctionResource.from_function(
112
+ resource = FunctionResource(
113
113
  fn=fn,
114
114
  uri=AnyUrl(uri),
115
115
  name=name,
@@ -219,12 +219,11 @@ class ResourceManager:
219
219
  return True
220
220
  return False
221
221
 
222
- async def get_resource(self, uri: AnyUrl | str, context=None) -> Resource:
222
+ async def get_resource(self, uri: AnyUrl | str) -> Resource:
223
223
  """Get resource by URI, checking concrete resources first, then templates.
224
224
 
225
225
  Args:
226
226
  uri: The URI of the resource to get
227
- context: Optional context object to pass to template resources
228
227
 
229
228
  Raises:
230
229
  NotFoundError: If no resource or template matching the URI is found.
@@ -244,7 +243,6 @@ class ResourceManager:
244
243
  return await template.create_resource(
245
244
  uri_str,
246
245
  params=params,
247
- context=context,
248
246
  )
249
247
  except Exception as e:
250
248
  raise ValueError(f"Error creating resource from template: {e}")
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import re
7
7
  from collections.abc import Callable
8
- from typing import TYPE_CHECKING, Annotated, Any
8
+ from typing import Annotated, Any
9
9
  from urllib.parse import unquote
10
10
 
11
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
@@ -20,17 +20,12 @@ from pydantic import (
20
20
  )
21
21
 
22
22
  from fastmcp.resources.types import FunctionResource, Resource
23
+ from fastmcp.server.dependencies import get_context
23
24
  from fastmcp.utilities.types import (
24
25
  _convert_set_defaults,
25
26
  find_kwarg_by_type,
26
27
  )
27
28
 
28
- if TYPE_CHECKING:
29
- from mcp.server.session import ServerSessionT
30
- from mcp.shared.context import LifespanContextT
31
-
32
- from fastmcp.server import Context
33
-
34
29
 
35
30
  def build_regex(template: str) -> re.Pattern:
36
31
  parts = re.split(r"(\{[^}]+\})", template)
@@ -79,9 +74,6 @@ class ResourceTemplate(BaseModel):
79
74
  parameters: dict[str, Any] = Field(
80
75
  description="JSON schema for function parameters"
81
76
  )
82
- context_kwarg: str | None = Field(
83
- None, description="Name of the kwarg that should receive context"
84
- )
85
77
 
86
78
  @field_validator("mime_type", mode="before")
87
79
  @classmethod
@@ -100,10 +92,9 @@ class ResourceTemplate(BaseModel):
100
92
  description: str | None = None,
101
93
  mime_type: str | None = None,
102
94
  tags: set[str] | None = None,
103
- context_kwarg: str | None = None,
104
95
  ) -> ResourceTemplate:
105
96
  """Create a template from a function."""
106
- from fastmcp import Context
97
+ from fastmcp.server.context import Context
107
98
 
108
99
  func_name = name or fn.__name__
109
100
  if func_name == "<lambda>":
@@ -119,8 +110,8 @@ class ResourceTemplate(BaseModel):
119
110
  )
120
111
 
121
112
  # Auto-detect context parameter if not provided
122
- if context_kwarg is None:
123
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
113
+
114
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
124
115
 
125
116
  # Validate that URI params match function params
126
117
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
@@ -170,25 +161,22 @@ class ResourceTemplate(BaseModel):
170
161
  fn=fn,
171
162
  parameters=parameters,
172
163
  tags=tags or set(),
173
- context_kwarg=context_kwarg,
174
164
  )
175
165
 
176
166
  def matches(self, uri: str) -> dict[str, Any] | None:
177
167
  """Check if URI matches template and extract parameters."""
178
168
  return match_uri_template(uri, self.uri_template)
179
169
 
180
- async def create_resource(
181
- self,
182
- uri: str,
183
- params: dict[str, Any],
184
- context: Context[ServerSessionT, LifespanContextT] | None = None,
185
- ) -> Resource:
170
+ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
186
171
  """Create a resource from the template with the given parameters."""
172
+ from fastmcp.server.context import Context
173
+
187
174
  try:
188
175
  # Add context to parameters if needed
189
176
  kwargs = params.copy()
190
- if self.context_kwarg is not None and context is not None:
191
- kwargs[self.context_kwarg] = context
177
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
178
+ if context_kwarg and context_kwarg not in kwargs:
179
+ kwargs[context_kwarg] = get_context()
192
180
 
193
181
  # Call function and check if result is a coroutine
194
182
  result = self.fn(**kwargs)
@@ -202,7 +190,6 @@ class ResourceTemplate(BaseModel):
202
190
  mime_type=self.mime_type,
203
191
  fn=lambda **kwargs: result, # Capture result in closure
204
192
  tags=self.tags,
205
- context_kwarg=self.context_kwarg,
206
193
  )
207
194
  except Exception as e:
208
195
  raise ValueError(f"Error creating resource from template: {e}")
@@ -15,14 +15,12 @@ import pydantic.json
15
15
  import pydantic_core
16
16
  from pydantic import Field, ValidationInfo
17
17
 
18
- import fastmcp
19
18
  from fastmcp.resources.resource import Resource
19
+ from fastmcp.server.dependencies import get_context
20
+ from fastmcp.utilities.types import find_kwarg_by_type
20
21
 
21
22
  if TYPE_CHECKING:
22
- from mcp.server.session import ServerSessionT
23
- from mcp.shared.context import LifespanContextT
24
-
25
- from fastmcp.server import Context
23
+ pass
26
24
 
27
25
 
28
26
  class TextResource(Resource):
@@ -30,9 +28,7 @@ class TextResource(Resource):
30
28
 
31
29
  text: str = Field(description="Text content of the resource")
32
30
 
33
- async def read(
34
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
35
- ) -> str:
31
+ async def read(self) -> str:
36
32
  """Read the text content."""
37
33
  return self.text
38
34
 
@@ -42,9 +38,7 @@ class BinaryResource(Resource):
42
38
 
43
39
  data: bytes = Field(description="Binary content of the resource")
44
40
 
45
- async def read(
46
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
47
- ) -> bytes:
41
+ async def read(self) -> bytes:
48
42
  """Read the binary content."""
49
43
  return self.data
50
44
 
@@ -63,40 +57,23 @@ class FunctionResource(Resource):
63
57
  """
64
58
 
65
59
  fn: Callable[[], Any]
66
- context_kwarg: str | None = Field(
67
- default=None, description="Name of the kwarg that should receive context"
68
- )
69
60
 
70
- @classmethod
71
- def from_function(
72
- cls, fn: Callable[[], Any], context_kwarg: str | None = None, **kwargs
73
- ) -> FunctionResource:
74
- if context_kwarg is None:
75
- parameters = inspect.signature(fn).parameters
76
- context_param = next(
77
- (p for p in parameters.values() if p.annotation is fastmcp.Context),
78
- None,
79
- )
80
- if context_param is not None:
81
- context_kwarg = context_param.name
82
- return cls(fn=fn, context_kwarg=context_kwarg, **kwargs)
83
-
84
- async def read(
85
- self,
86
- context: Context[ServerSessionT, LifespanContextT] | None = None,
87
- ) -> str | bytes:
61
+ async def read(self) -> str | bytes:
88
62
  """Read the resource by calling the wrapped function."""
63
+ from fastmcp.server.context import Context
64
+
89
65
  try:
90
66
  kwargs = {}
91
- if self.context_kwarg is not None:
92
- kwargs[self.context_kwarg] = context
67
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
68
+ if context_kwarg is not None:
69
+ kwargs[context_kwarg] = get_context()
93
70
 
94
71
  result = self.fn(**kwargs)
95
72
  if inspect.iscoroutinefunction(self.fn):
96
73
  result = await result
97
74
 
98
75
  if isinstance(result, Resource):
99
- return await result.read(context=context)
76
+ return await result.read()
100
77
  elif isinstance(result, bytes):
101
78
  return result
102
79
  elif isinstance(result, str):
@@ -140,9 +117,7 @@ class FileResource(Resource):
140
117
  mime_type = info.data.get("mime_type", "text/plain")
141
118
  return not mime_type.startswith("text/")
142
119
 
143
- async def read(
144
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
145
- ) -> str | bytes:
120
+ async def read(self) -> str | bytes:
146
121
  """Read the file content."""
147
122
  try:
148
123
  if self.is_binary:
@@ -160,9 +135,7 @@ class HttpResource(Resource):
160
135
  default="application/json", description="MIME type of the resource content"
161
136
  )
162
137
 
163
- async def read(
164
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
165
- ) -> str | bytes:
138
+ async def read(self) -> str | bytes:
166
139
  """Read the HTTP content."""
167
140
  async with httpx.AsyncClient() as client:
168
141
  response = await client.get(self.url)
@@ -214,9 +187,7 @@ class DirectoryResource(Resource):
214
187
  except Exception as e:
215
188
  raise ValueError(f"Error listing directory {self.path}: {e}")
216
189
 
217
- async def read(
218
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
219
- ) -> str: # Always returns JSON string
190
+ async def read(self) -> str: # Always returns JSON string
220
191
  """Read the directory listing."""
221
192
  try:
222
193
  files = await anyio.to_thread.run_sync(self.list_files)
@@ -1,5 +1,6 @@
1
1
  from .server import FastMCP
2
2
  from .context import Context
3
+ from . import dependencies
3
4
 
4
5
 
5
6
  __all__ = ["FastMCP", "Context"]
fastmcp/server/context.py CHANGED
@@ -1,11 +1,14 @@
1
1
  from __future__ import annotations as _annotations
2
2
 
3
- from typing import Any, Generic
3
+ import warnings
4
+ from collections.abc import Generator
5
+ from contextlib import contextmanager
6
+ from contextvars import ContextVar, Token
7
+ from dataclasses import dataclass
4
8
 
5
9
  from mcp import LoggingLevel
6
10
  from mcp.server.lowlevel.helper_types import ReadResourceContents
7
- from mcp.server.session import ServerSessionT
8
- from mcp.shared.context import LifespanContextT, RequestContext
11
+ from mcp.shared.context import RequestContext
9
12
  from mcp.types import (
10
13
  CreateMessageResult,
11
14
  ImageContent,
@@ -13,18 +16,29 @@ from mcp.types import (
13
16
  SamplingMessage,
14
17
  TextContent,
15
18
  )
16
- from pydantic import BaseModel, ConfigDict
17
19
  from pydantic.networks import AnyUrl
18
20
  from starlette.requests import Request
19
21
 
22
+ import fastmcp.server.dependencies
20
23
  from fastmcp.server.server import FastMCP
21
- from fastmcp.utilities.http import get_current_starlette_request
22
24
  from fastmcp.utilities.logging import get_logger
23
25
 
24
26
  logger = get_logger(__name__)
25
27
 
28
+ _current_context: ContextVar[Context | None] = ContextVar("context", default=None)
26
29
 
27
- class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
30
+
31
+ @contextmanager
32
+ def set_context(context: Context) -> Generator[Context, None, None]:
33
+ token = _current_context.set(context)
34
+ try:
35
+ yield context
36
+ finally:
37
+ _current_context.reset(token)
38
+
39
+
40
+ @dataclass
41
+ class Context:
28
42
  """Context object providing access to MCP capabilities.
29
43
 
30
44
  This provides a cleaner interface to MCP's RequestContext functionality.
@@ -56,37 +70,30 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
56
70
 
57
71
  The context parameter name can be anything as long as it's annotated with Context.
58
72
  The context is optional - tools that don't need it can omit the parameter.
73
+
59
74
  """
60
75
 
61
- _request_context: RequestContext[ServerSessionT, LifespanContextT] | None
62
- _fastmcp: FastMCP | None
76
+ def __init__(self, fastmcp: FastMCP):
77
+ self.fastmcp = fastmcp
78
+ self._tokens: list[Token] = []
63
79
 
64
- model_config = ConfigDict(arbitrary_types_allowed=True)
80
+ def __enter__(self) -> Context:
81
+ """Enter the context manager and set this context as the current context."""
82
+ # Always set this context and save the token
83
+ token = _current_context.set(self)
84
+ self._tokens.append(token)
85
+ return self
65
86
 
66
- def __init__(
67
- self,
68
- *,
69
- request_context: RequestContext[ServerSessionT, LifespanContextT] | None = None,
70
- fastmcp: FastMCP | None = None,
71
- **kwargs: Any,
72
- ):
73
- super().__init__(**kwargs)
74
- self._request_context = request_context
75
- self._fastmcp = fastmcp
87
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
88
+ """Exit the context manager and reset the most recent token."""
89
+ if self._tokens:
90
+ token = self._tokens.pop()
91
+ _current_context.reset(token)
76
92
 
77
93
  @property
78
- def fastmcp(self) -> FastMCP:
79
- """Access to the FastMCP server."""
80
- if self._fastmcp is None:
81
- raise ValueError("Context is not available outside of a request")
82
- return self._fastmcp
83
-
84
- @property
85
- def request_context(self) -> RequestContext[ServerSessionT, LifespanContextT]:
94
+ def request_context(self) -> RequestContext:
86
95
  """Access to the underlying request context."""
87
- if self._request_context is None:
88
- raise ValueError("Context is not available outside of a request")
89
- return self._request_context
96
+ return self.fastmcp._mcp_server.request_context
90
97
 
91
98
  async def report_progress(
92
99
  self, progress: float, total: float | None = None
@@ -120,10 +127,8 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
120
127
  Returns:
121
128
  The resource content as either text or bytes
122
129
  """
123
- assert self._fastmcp is not None, (
124
- "Context is not available outside of a request"
125
- )
126
- return await self._fastmcp._mcp_read_resource(uri)
130
+ assert self.fastmcp is not None, "Context is not available outside of a request"
131
+ return await self.fastmcp._mcp_read_resource(uri)
127
132
 
128
133
  async def log(
129
134
  self,
@@ -229,7 +234,14 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
229
234
 
230
235
  def get_http_request(self) -> Request:
231
236
  """Get the active starlette request."""
232
- request = get_current_starlette_request()
233
- if request is None:
234
- raise ValueError("Request is not available outside a Starlette request")
235
- return request
237
+
238
+ # Deprecation warning, added in FastMCP 2.2.11
239
+ warnings.warn(
240
+ "Context.get_http_request() is deprecated and will be removed in a future version. "
241
+ "Use get_http_request() from fastmcp.server.dependencies instead. "
242
+ "See https://gofastmcp.com/patterns/http-requests for more details.",
243
+ DeprecationWarning,
244
+ stacklevel=2,
245
+ )
246
+
247
+ return fastmcp.server.dependencies.get_http_request()