Jeremiah Lowin commited on
Commit
aef8e8e
·
unverified ·
2 Parent(s): 748718c5338a1c

Merge pull request #347 from jlowin/context

Browse files
docs/servers/context.mdx CHANGED
@@ -23,6 +23,17 @@ The `Context` object provides a clean interface to access MCP features within yo
23
 
24
  To use the context object within any of your functions, simply add a parameter to your function signature and type-hint it as `Context`. FastMCP will automatically inject the context instance when your function is called.
25
 
 
 
 
 
 
 
 
 
 
 
 
26
  ```python
27
  from fastmcp import FastMCP, Context
28
 
@@ -31,43 +42,40 @@ mcp = FastMCP(name="ContextDemo")
31
  @mcp.tool()
32
  async def process_file(file_uri: str, ctx: Context) -> str:
33
  """Processes a file, using context for logging and resource access."""
34
- request_id = ctx.request_id
35
- await ctx.info(f"[{request_id}] Starting processing for {file_uri}")
 
36
 
37
- try:
38
- # Use context to read a resource
39
- contents_list = await ctx.read_resource(file_uri)
40
- if not contents_list:
41
- await ctx.warning(f"Resource {file_uri} is empty.")
42
- return "Resource empty"
43
 
44
- data = contents_list[0].content # Assuming TextResourceContents
45
- await ctx.debug(f"Read {len(data)} bytes from {file_uri}")
46
 
47
- # Report progress
48
- await ctx.report_progress(progress=50, total=100)
49
-
50
- # Simulate work
51
- processed_data = data.upper() # Example processing
 
 
 
 
 
 
 
 
52
 
53
- await ctx.report_progress(progress=100, total=100)
54
- await ctx.info(f"Processing complete for {file_uri}")
55
 
56
- return f"Processed data length: {len(processed_data)}"
57
 
58
- except Exception as e:
59
- # Use context to log errors
60
- await ctx.error(f"Error processing {file_uri}: {str(e)}")
61
- raise # Re-raise to send error back to client
 
 
62
  ```
63
 
64
- **Key Points:**
65
-
66
- - The parameter name (e.g., `ctx`, `context`) doesn't matter, only the type hint `Context` is important.
67
- - The context parameter can be placed anywhere in your function's signature.
68
- - The context is optional - functions that don't need it can omit the parameter.
69
- - Context is only available during a request; attempting to use context methods outside a request will raise errors.
70
- - Context methods are async, so your function usually needs to be async as well.
71
 
72
  ## Context Capabilities
73
 
@@ -305,60 +313,3 @@ async def handle_web_request(ctx: Context) -> dict:
305
  Direct use of `session` or `request_context` requires understanding the low-level MCP Python SDK and may be less stable than using the methods provided directly on the `Context` object.
306
  </Warning>
307
 
308
- ## Using Context in Different Components
309
-
310
- All FastMCP components (tools, resources, templates, and prompts) can use the Context object following the same pattern - simply add a parameter with the `Context` type annotation.
311
-
312
- ### Context in Resources and Templates
313
-
314
- Resources and resource templates can access context to customize their behavior:
315
-
316
- ```python
317
- @mcp.resource("resource://user-data")
318
- async def get_user_data(ctx: Context) -> dict:
319
- """Fetch personalized user data based on the request context."""
320
- user_id = ctx.client_id or "anonymous"
321
- await ctx.info(f"Fetching data for user {user_id}")
322
-
323
- # Example of using context for dynamic resource generation
324
- return {
325
- "user_id": user_id,
326
- "last_access": datetime.now().isoformat(),
327
- "request_id": ctx.request_id
328
- }
329
-
330
- @mcp.resource("resource://users/{user_id}/profile")
331
- async def get_user_profile(user_id: str, ctx: Context) -> dict:
332
- """Fetch user profile from database with context-aware logging."""
333
- await ctx.info(f"Fetching profile for user {user_id}")
334
-
335
- # Example of using context in a template resource
336
- # In a real implementation, you might query a database
337
- return {
338
- "id": user_id,
339
- "name": f"User {user_id}",
340
- "request_id": ctx.request_id
341
- }
342
- ```
343
-
344
- ### Context in Prompts
345
-
346
- Prompts can use context to generate more dynamic templates:
347
-
348
- ```python
349
- @mcp.prompt()
350
- async def data_analysis_request(dataset: str, ctx: Context) -> str:
351
- """Generate a request to analyze data with contextual information."""
352
- await ctx.info(f"Generating data analysis prompt for {dataset}")
353
-
354
- # Could use context to read configuration or personalize the prompt
355
- return f"""Please analyze the following dataset: {dataset}
356
-
357
- Request initiated at: {datetime.now().isoformat()}
358
- Request ID: {ctx.request_id}
359
- """
360
- ```
361
-
362
- <VersionBadge version="2.3.0" />
363
-
364
- All FastMCP objects now support context injection using the same consistent pattern, making it easy to add session-aware capabilities to all aspects of your MCP server.
 
23
 
24
  To use the context object within any of your functions, simply add a parameter to your function signature and type-hint it as `Context`. FastMCP will automatically inject the context instance when your function is called.
25
 
26
+ **Key Points:**
27
+
28
+ - The parameter name (e.g., `ctx`, `context`) doesn't matter, only the type hint `Context` is important.
29
+ - The context parameter can be placed anywhere in your function's signature; it will not be exposed to MCP clients as a valid parameter.
30
+ - The context is optional - functions that don't need it can omit the parameter entirely.
31
+ - Context methods are async, so your function usually needs to be async as well.
32
+ - The type hint can be a union (`Context | None`) or use `Annotated[]` and it will still work properly.
33
+ - Context is only available during a request; attempting to use context methods outside a request will raise errors. If you need to debug or call your context methods outside of a request, you can type your variable as `Context | None=None` to avoid missing argument errors.
34
+
35
+ ### Tools
36
+
37
  ```python
38
  from fastmcp import FastMCP, Context
39
 
 
42
  @mcp.tool()
43
  async def process_file(file_uri: str, ctx: Context) -> str:
44
  """Processes a file, using context for logging and resource access."""
45
+ # Context is available as the ctx parameter
46
+ return "Processed file"
47
+ ```
48
 
49
+ ### Resources and Templates
 
 
 
 
 
50
 
51
+ <VersionBadge version="2.2.5" />
 
52
 
53
+ ```python
54
+ @mcp.resource("resource://user-data")
55
+ async def get_user_data(ctx: Context) -> dict:
56
+ """Fetch personalized user data based on the request context."""
57
+ # Context is available as the ctx parameter
58
+ return {"user_id": "example"}
59
+
60
+ @mcp.resource("resource://users/{user_id}/profile")
61
+ async def get_user_profile(user_id: str, ctx: Context) -> dict:
62
+ """Fetch user profile with context-aware logging."""
63
+ # Context is available as the ctx parameter
64
+ return {"id": user_id}
65
+ ```
66
 
67
+ ### Prompts
 
68
 
69
+ <VersionBadge version="2.2.5" />
70
 
71
+ ```python
72
+ @mcp.prompt()
73
+ async def data_analysis_request(dataset: str, ctx: Context) -> str:
74
+ """Generate a request to analyze data with contextual information."""
75
+ # Context is available as the ctx parameter
76
+ return f"Please analyze the following dataset: {dataset}"
77
  ```
78
 
 
 
 
 
 
 
 
79
 
80
  ## Context Capabilities
81
 
 
313
  Direct use of `session` or `request_context` requires understanding the low-level MCP Python SDK and may be less stable than using the methods provided directly on the `Context` object.
314
  </Warning>
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/__init__.py CHANGED
@@ -2,9 +2,10 @@
2
 
3
  from importlib.metadata import version
4
 
5
-
6
  from fastmcp.server.server import FastMCP
7
  from fastmcp.server.context import Context
 
 
8
  from fastmcp.client import Client
9
  from fastmcp.utilities.types import Image
10
  from . import client, settings
 
2
 
3
  from importlib.metadata import version
4
 
 
5
  from fastmcp.server.server import FastMCP
6
  from fastmcp.server.context import Context
7
+ import fastmcp.server
8
+
9
  from fastmcp.client import Client
10
  from fastmcp.utilities.types import Image
11
  from . import client, settings
src/fastmcp/prompts/prompt.py CHANGED
@@ -12,6 +12,7 @@ from mcp.types import Prompt as MCPPrompt
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
 
 
15
  from fastmcp.utilities.json_schema import prune_params
16
  from fastmcp.utilities.types import (
17
  _convert_set_defaults,
@@ -20,10 +21,7 @@ from fastmcp.utilities.types import (
20
  )
21
 
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
27
 
28
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
29
 
@@ -76,9 +74,6 @@ class Prompt(BaseModel):
76
  None, description="Arguments that can be passed to the prompt"
77
  )
78
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
79
- context_kwarg: str | None = Field(
80
- None, description="Name of the kwarg that should receive context"
81
- )
82
 
83
  @classmethod
84
  def from_function(
@@ -87,7 +82,6 @@ class Prompt(BaseModel):
87
  name: str | None = None,
88
  description: str | None = None,
89
  tags: set[str] | None = None,
90
- context_kwarg: str | None = None,
91
  ) -> Prompt:
92
  """Create a Prompt from a function.
93
 
@@ -97,7 +91,7 @@ class Prompt(BaseModel):
97
  - A dict (converted to a message)
98
  - A sequence of any of the above
99
  """
100
- from fastmcp import Context
101
 
102
  func_name = name or fn.__name__
103
 
@@ -115,8 +109,8 @@ class Prompt(BaseModel):
115
  parameters = type_adapter.json_schema()
116
 
117
  # Auto-detect context parameter if not provided
118
- if context_kwarg is None:
119
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
120
  if context_kwarg:
121
  parameters = prune_params(parameters, params=[context_kwarg])
122
 
@@ -141,15 +135,15 @@ class Prompt(BaseModel):
141
  arguments=arguments,
142
  fn=fn,
143
  tags=tags or set(),
144
- context_kwarg=context_kwarg,
145
  )
146
 
147
  async def render(
148
  self,
149
  arguments: dict[str, Any] | None = None,
150
- context: Context[ServerSessionT, LifespanContextT] | None = None,
151
  ) -> list[PromptMessage]:
152
  """Render the prompt with arguments."""
 
 
153
  # Validate required arguments
154
  if self.arguments:
155
  required = {arg.name for arg in self.arguments if arg.required}
@@ -161,8 +155,9 @@ class Prompt(BaseModel):
161
  try:
162
  # Prepare arguments with context
163
  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
 
166
 
167
  # Call function and check if result is a coroutine
168
  result = self.fn(**kwargs)
 
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
 
15
+ from fastmcp.server.dependencies import get_context
16
  from fastmcp.utilities.json_schema import prune_params
17
  from fastmcp.utilities.types import (
18
  _convert_set_defaults,
 
21
  )
22
 
23
  if TYPE_CHECKING:
24
+ pass
 
 
 
25
 
26
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
27
 
 
74
  None, description="Arguments that can be passed to the prompt"
75
  )
76
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
 
 
 
77
 
78
  @classmethod
79
  def from_function(
 
82
  name: str | None = None,
83
  description: str | None = None,
84
  tags: set[str] | None = None,
 
85
  ) -> Prompt:
86
  """Create a Prompt from a function.
87
 
 
91
  - A dict (converted to a message)
92
  - A sequence of any of the above
93
  """
94
+ from fastmcp.server.context import Context
95
 
96
  func_name = name or fn.__name__
97
 
 
109
  parameters = type_adapter.json_schema()
110
 
111
  # Auto-detect context parameter if not provided
112
+
113
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
114
  if context_kwarg:
115
  parameters = prune_params(parameters, params=[context_kwarg])
116
 
 
135
  arguments=arguments,
136
  fn=fn,
137
  tags=tags or set(),
 
138
  )
139
 
140
  async def render(
141
  self,
142
  arguments: dict[str, Any] | None = None,
 
143
  ) -> list[PromptMessage]:
144
  """Render the prompt with arguments."""
145
+ from fastmcp.server.context import Context
146
+
147
  # Validate required arguments
148
  if self.arguments:
149
  required = {arg.name for arg in self.arguments if arg.required}
 
155
  try:
156
  # Prepare arguments with context
157
  kwargs = arguments.copy() if arguments else {}
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()
161
 
162
  # Call function and check if result is a coroutine
163
  result = self.fn(**kwargs)
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -13,10 +13,7 @@ from fastmcp.settings import DuplicateBehavior
13
  from fastmcp.utilities.logging import get_logger
14
 
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
20
 
21
  logger = get_logger(__name__)
22
 
@@ -82,19 +79,15 @@ class PromptManager:
82
  self,
83
  name: str,
84
  arguments: dict[str, Any] | None = None,
85
- context: Context[ServerSessionT, LifespanContextT] | None = None,
86
  ) -> GetPromptResult:
87
  """Render a prompt by name with arguments."""
88
  prompt = self.get_prompt(name)
89
  if not prompt:
90
  raise NotFoundError(f"Unknown prompt: {name}")
91
 
92
- messages = await prompt.render(arguments, context=context)
93
 
94
- return GetPromptResult(
95
- description=prompt.description,
96
- messages=messages,
97
- )
98
 
99
  def has_prompt(self, key: str) -> bool:
100
  """Check if a prompt exists."""
 
13
  from fastmcp.utilities.logging import get_logger
14
 
15
  if TYPE_CHECKING:
16
+ pass
 
 
 
17
 
18
  logger = get_logger(__name__)
19
 
 
79
  self,
80
  name: str,
81
  arguments: dict[str, Any] | None = None,
 
82
  ) -> GetPromptResult:
83
  """Render a prompt by name with arguments."""
84
  prompt = self.get_prompt(name)
85
  if not prompt:
86
  raise NotFoundError(f"Unknown prompt: {name}")
87
 
88
+ messages = await prompt.render(arguments)
89
 
90
+ return GetPromptResult(description=prompt.description, messages=messages)
 
 
 
91
 
92
  def has_prompt(self, key: str) -> bool:
93
  """Check if a prompt exists."""
src/fastmcp/resources/resource.py CHANGED
@@ -20,10 +20,7 @@ from pydantic import (
20
  from fastmcp.utilities.types import _convert_set_defaults
21
 
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
27
 
28
 
29
  class Resource(BaseModel, abc.ABC):
@@ -66,9 +63,7 @@ class Resource(BaseModel, abc.ABC):
66
  raise ValueError("Either name or uri must be provided")
67
 
68
  @abc.abstractmethod
69
- async def read(
70
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
71
- ) -> str | bytes:
72
  """Read the resource content."""
73
  pass
74
 
 
20
  from fastmcp.utilities.types import _convert_set_defaults
21
 
22
  if TYPE_CHECKING:
23
+ pass
 
 
 
24
 
25
 
26
  class Resource(BaseModel, abc.ABC):
 
63
  raise ValueError("Either name or uri must be provided")
64
 
65
  @abc.abstractmethod
66
+ async def read(self) -> str | bytes:
 
 
67
  """Read the resource content."""
68
  pass
69
 
src/fastmcp/resources/resource_manager.py CHANGED
@@ -109,7 +109,7 @@ class ResourceManager:
109
  The added resource. If a resource with the same URI already exists,
110
  returns the existing resource.
111
  """
112
- resource = FunctionResource.from_function(
113
  fn=fn,
114
  uri=AnyUrl(uri),
115
  name=name,
@@ -219,12 +219,11 @@ class ResourceManager:
219
  return True
220
  return False
221
 
222
- async def get_resource(self, uri: AnyUrl | str, context=None) -> Resource:
223
  """Get resource by URI, checking concrete resources first, then templates.
224
 
225
  Args:
226
  uri: The URI of the resource to get
227
- context: Optional context object to pass to template resources
228
 
229
  Raises:
230
  NotFoundError: If no resource or template matching the URI is found.
@@ -244,7 +243,6 @@ class ResourceManager:
244
  return await template.create_resource(
245
  uri_str,
246
  params=params,
247
- context=context,
248
  )
249
  except Exception as e:
250
  raise ValueError(f"Error creating resource from template: {e}")
 
109
  The added resource. If a resource with the same URI already exists,
110
  returns the existing resource.
111
  """
112
+ resource = FunctionResource(
113
  fn=fn,
114
  uri=AnyUrl(uri),
115
  name=name,
 
219
  return True
220
  return False
221
 
222
+ async def get_resource(self, uri: AnyUrl | str) -> Resource:
223
  """Get resource by URI, checking concrete resources first, then templates.
224
 
225
  Args:
226
  uri: The URI of the resource to get
 
227
 
228
  Raises:
229
  NotFoundError: If no resource or template matching the URI is found.
 
243
  return await template.create_resource(
244
  uri_str,
245
  params=params,
 
246
  )
247
  except Exception as e:
248
  raise ValueError(f"Error creating resource from template: {e}")
src/fastmcp/resources/template.py CHANGED
@@ -5,7 +5,7 @@ from __future__ import annotations
5
  import inspect
6
  import re
7
  from collections.abc import Callable
8
- from typing import TYPE_CHECKING, Annotated, Any
9
  from urllib.parse import unquote
10
 
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
@@ -20,17 +20,12 @@ from pydantic import (
20
  )
21
 
22
  from fastmcp.resources.types import FunctionResource, Resource
 
23
  from fastmcp.utilities.types import (
24
  _convert_set_defaults,
25
  find_kwarg_by_type,
26
  )
27
 
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
 
35
  def build_regex(template: str) -> re.Pattern:
36
  parts = re.split(r"(\{[^}]+\})", template)
@@ -79,9 +74,6 @@ class ResourceTemplate(BaseModel):
79
  parameters: dict[str, Any] = Field(
80
  description="JSON schema for function parameters"
81
  )
82
- context_kwarg: str | None = Field(
83
- None, description="Name of the kwarg that should receive context"
84
- )
85
 
86
  @field_validator("mime_type", mode="before")
87
  @classmethod
@@ -100,10 +92,9 @@ class ResourceTemplate(BaseModel):
100
  description: str | None = None,
101
  mime_type: str | None = None,
102
  tags: set[str] | None = None,
103
- context_kwarg: str | None = None,
104
  ) -> ResourceTemplate:
105
  """Create a template from a function."""
106
- from fastmcp import Context
107
 
108
  func_name = name or fn.__name__
109
  if func_name == "<lambda>":
@@ -119,8 +110,8 @@ class ResourceTemplate(BaseModel):
119
  )
120
 
121
  # Auto-detect context parameter if not provided
122
- if context_kwarg is None:
123
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
124
 
125
  # Validate that URI params match function params
126
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
@@ -170,25 +161,22 @@ class ResourceTemplate(BaseModel):
170
  fn=fn,
171
  parameters=parameters,
172
  tags=tags or set(),
173
- context_kwarg=context_kwarg,
174
  )
175
 
176
  def matches(self, uri: str) -> dict[str, Any] | None:
177
  """Check if URI matches template and extract parameters."""
178
  return match_uri_template(uri, self.uri_template)
179
 
180
- async def create_resource(
181
- self,
182
- uri: str,
183
- params: dict[str, Any],
184
- context: Context[ServerSessionT, LifespanContextT] | None = None,
185
- ) -> Resource:
186
  """Create a resource from the template with the given parameters."""
 
 
187
  try:
188
  # Add context to parameters if needed
189
  kwargs = params.copy()
190
- if self.context_kwarg is not None and context is not None:
191
- kwargs[self.context_kwarg] = context
 
192
 
193
  # Call function and check if result is a coroutine
194
  result = self.fn(**kwargs)
@@ -202,7 +190,6 @@ class ResourceTemplate(BaseModel):
202
  mime_type=self.mime_type,
203
  fn=lambda **kwargs: result, # Capture result in closure
204
  tags=self.tags,
205
- context_kwarg=self.context_kwarg,
206
  )
207
  except Exception as e:
208
  raise ValueError(f"Error creating resource from template: {e}")
 
5
  import inspect
6
  import re
7
  from collections.abc import Callable
8
+ from typing import Annotated, Any
9
  from urllib.parse import unquote
10
 
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
 
20
  )
21
 
22
  from fastmcp.resources.types import FunctionResource, Resource
23
+ from fastmcp.server.dependencies import get_context
24
  from fastmcp.utilities.types import (
25
  _convert_set_defaults,
26
  find_kwarg_by_type,
27
  )
28
 
 
 
 
 
 
 
29
 
30
  def build_regex(template: str) -> re.Pattern:
31
  parts = re.split(r"(\{[^}]+\})", template)
 
74
  parameters: dict[str, Any] = Field(
75
  description="JSON schema for function parameters"
76
  )
 
 
 
77
 
78
  @field_validator("mime_type", mode="before")
79
  @classmethod
 
92
  description: str | None = None,
93
  mime_type: str | None = None,
94
  tags: set[str] | None = None,
 
95
  ) -> ResourceTemplate:
96
  """Create a template from a function."""
97
+ from fastmcp.server.context import Context
98
 
99
  func_name = name or fn.__name__
100
  if func_name == "<lambda>":
 
110
  )
111
 
112
  # Auto-detect context parameter if not provided
113
+
114
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
115
 
116
  # Validate that URI params match function params
117
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
 
161
  fn=fn,
162
  parameters=parameters,
163
  tags=tags or set(),
 
164
  )
165
 
166
  def matches(self, uri: str) -> dict[str, Any] | None:
167
  """Check if URI matches template and extract parameters."""
168
  return match_uri_template(uri, self.uri_template)
169
 
170
+ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
 
 
 
 
 
171
  """Create a resource from the template with the given parameters."""
172
+ from fastmcp.server.context import Context
173
+
174
  try:
175
  # Add context to parameters if needed
176
  kwargs = params.copy()
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()
180
 
181
  # Call function and check if result is a coroutine
182
  result = self.fn(**kwargs)
 
190
  mime_type=self.mime_type,
191
  fn=lambda **kwargs: result, # Capture result in closure
192
  tags=self.tags,
 
193
  )
194
  except Exception as e:
195
  raise ValueError(f"Error creating resource from template: {e}")
src/fastmcp/resources/types.py CHANGED
@@ -15,14 +15,12 @@ import pydantic.json
15
  import pydantic_core
16
  from pydantic import Field, ValidationInfo
17
 
18
- import fastmcp
19
  from fastmcp.resources.resource import Resource
 
 
20
 
21
  if TYPE_CHECKING:
22
- from mcp.server.session import ServerSessionT
23
- from mcp.shared.context import LifespanContextT
24
-
25
- from fastmcp.server import Context
26
 
27
 
28
  class TextResource(Resource):
@@ -30,9 +28,7 @@ class TextResource(Resource):
30
 
31
  text: str = Field(description="Text content of the resource")
32
 
33
- async def read(
34
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
35
- ) -> str:
36
  """Read the text content."""
37
  return self.text
38
 
@@ -42,9 +38,7 @@ class BinaryResource(Resource):
42
 
43
  data: bytes = Field(description="Binary content of the resource")
44
 
45
- async def read(
46
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
47
- ) -> bytes:
48
  """Read the binary content."""
49
  return self.data
50
 
@@ -63,40 +57,23 @@ class FunctionResource(Resource):
63
  """
64
 
65
  fn: Callable[[], Any]
66
- context_kwarg: str | None = Field(
67
- default=None, description="Name of the kwarg that should receive context"
68
- )
69
 
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:
88
  """Read the resource by calling the wrapped function."""
 
 
89
  try:
90
  kwargs = {}
91
- if self.context_kwarg is not None:
92
- kwargs[self.context_kwarg] = context
 
93
 
94
  result = self.fn(**kwargs)
95
  if inspect.iscoroutinefunction(self.fn):
96
  result = await result
97
 
98
  if isinstance(result, Resource):
99
- return await result.read(context=context)
100
  elif isinstance(result, bytes):
101
  return result
102
  elif isinstance(result, str):
@@ -140,9 +117,7 @@ class FileResource(Resource):
140
  mime_type = info.data.get("mime_type", "text/plain")
141
  return not mime_type.startswith("text/")
142
 
143
- async def read(
144
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
145
- ) -> str | bytes:
146
  """Read the file content."""
147
  try:
148
  if self.is_binary:
@@ -160,9 +135,7 @@ class HttpResource(Resource):
160
  default="application/json", description="MIME type of the resource content"
161
  )
162
 
163
- async def read(
164
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
165
- ) -> str | bytes:
166
  """Read the HTTP content."""
167
  async with httpx.AsyncClient() as client:
168
  response = await client.get(self.url)
@@ -214,9 +187,7 @@ class DirectoryResource(Resource):
214
  except Exception as e:
215
  raise ValueError(f"Error listing directory {self.path}: {e}")
216
 
217
- async def read(
218
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
219
- ) -> str: # Always returns JSON string
220
  """Read the directory listing."""
221
  try:
222
  files = await anyio.to_thread.run_sync(self.list_files)
 
15
  import pydantic_core
16
  from pydantic import Field, ValidationInfo
17
 
 
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
21
 
22
  if TYPE_CHECKING:
23
+ pass
 
 
 
24
 
25
 
26
  class TextResource(Resource):
 
28
 
29
  text: str = Field(description="Text content of the resource")
30
 
31
+ async def read(self) -> str:
 
 
32
  """Read the text content."""
33
  return self.text
34
 
 
38
 
39
  data: bytes = Field(description="Binary content of the resource")
40
 
41
+ async def read(self) -> bytes:
 
 
42
  """Read the binary content."""
43
  return self.data
44
 
 
57
  """
58
 
59
  fn: Callable[[], Any]
 
 
 
60
 
61
+ async def read(self) -> str | bytes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  """Read the resource by calling the wrapped function."""
63
+ from fastmcp.server.context import Context
64
+
65
  try:
66
  kwargs = {}
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()
70
 
71
  result = self.fn(**kwargs)
72
  if inspect.iscoroutinefunction(self.fn):
73
  result = await result
74
 
75
  if isinstance(result, Resource):
76
+ return await result.read()
77
  elif isinstance(result, bytes):
78
  return result
79
  elif isinstance(result, str):
 
117
  mime_type = info.data.get("mime_type", "text/plain")
118
  return not mime_type.startswith("text/")
119
 
120
+ async def read(self) -> str | bytes:
 
 
121
  """Read the file content."""
122
  try:
123
  if self.is_binary:
 
135
  default="application/json", description="MIME type of the resource content"
136
  )
137
 
138
+ async def read(self) -> str | bytes:
 
 
139
  """Read the HTTP content."""
140
  async with httpx.AsyncClient() as client:
141
  response = await client.get(self.url)
 
187
  except Exception as e:
188
  raise ValueError(f"Error listing directory {self.path}: {e}")
189
 
190
+ async def read(self) -> str: # Always returns JSON string
 
 
191
  """Read the directory listing."""
192
  try:
193
  files = await anyio.to_thread.run_sync(self.list_files)
src/fastmcp/server/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
  from .server import FastMCP
2
  from .context import Context
 
3
 
4
 
5
  __all__ = ["FastMCP", "Context"]
 
1
  from .server import FastMCP
2
  from .context import Context
3
+ from . import dependencies
4
 
5
 
6
  __all__ = ["FastMCP", "Context"]
src/fastmcp/server/context.py CHANGED
@@ -1,11 +1,13 @@
1
  from __future__ import annotations as _annotations
2
 
3
- from typing import Any, Generic
 
 
 
4
 
5
  from mcp import LoggingLevel
6
  from mcp.server.lowlevel.helper_types import ReadResourceContents
7
- from mcp.server.session import ServerSessionT
8
- from mcp.shared.context import LifespanContextT, RequestContext
9
  from mcp.types import (
10
  CreateMessageResult,
11
  ImageContent,
@@ -13,18 +15,29 @@ from mcp.types import (
13
  SamplingMessage,
14
  TextContent,
15
  )
16
- from pydantic import BaseModel, ConfigDict
17
  from pydantic.networks import AnyUrl
18
  from starlette.requests import Request
19
 
 
20
  from fastmcp.server.server import FastMCP
21
- from fastmcp.utilities.http import get_current_starlette_request
22
  from fastmcp.utilities.logging import get_logger
23
 
24
  logger = get_logger(__name__)
25
 
 
26
 
27
- class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
 
 
 
 
 
 
 
 
 
 
 
28
  """Context object providing access to MCP capabilities.
29
 
30
  This provides a cleaner interface to MCP's RequestContext functionality.
@@ -56,37 +69,30 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
56
 
57
  The context parameter name can be anything as long as it's annotated with Context.
58
  The context is optional - tools that don't need it can omit the parameter.
 
59
  """
60
 
61
- _request_context: RequestContext[ServerSessionT, LifespanContextT] | None
62
- _fastmcp: FastMCP | None
 
63
 
64
- model_config = ConfigDict(arbitrary_types_allowed=True)
 
 
 
 
 
65
 
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
76
 
77
  @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]:
86
  """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
90
 
91
  async def report_progress(
92
  self, progress: float, total: float | None = None
@@ -120,10 +126,8 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
120
  Returns:
121
  The resource content as either text or bytes
122
  """
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)
127
 
128
  async def log(
129
  self,
@@ -229,7 +233,5 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
229
 
230
  def get_http_request(self) -> Request:
231
  """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
 
1
  from __future__ import annotations as _annotations
2
 
3
+ from collections.abc import Generator
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar, Token
6
+ from dataclasses import dataclass
7
 
8
  from mcp import LoggingLevel
9
  from mcp.server.lowlevel.helper_types import ReadResourceContents
10
+ from mcp.shared.context import RequestContext
 
11
  from mcp.types import (
12
  CreateMessageResult,
13
  ImageContent,
 
15
  SamplingMessage,
16
  TextContent,
17
  )
 
18
  from pydantic.networks import AnyUrl
19
  from starlette.requests import Request
20
 
21
+ import fastmcp.server.dependencies
22
  from fastmcp.server.server import FastMCP
 
23
  from fastmcp.utilities.logging import get_logger
24
 
25
  logger = get_logger(__name__)
26
 
27
+ _current_context: ContextVar[Context | None] = ContextVar("context", default=None)
28
 
29
+
30
+ @contextmanager
31
+ def set_context(context: Context) -> Generator[Context, None, None]:
32
+ token = _current_context.set(context)
33
+ try:
34
+ yield context
35
+ finally:
36
+ _current_context.reset(token)
37
+
38
+
39
+ @dataclass
40
+ class Context:
41
  """Context object providing access to MCP capabilities.
42
 
43
  This provides a cleaner interface to MCP's RequestContext functionality.
 
69
 
70
  The context parameter name can be anything as long as it's annotated with Context.
71
  The context is optional - tools that don't need it can omit the parameter.
72
+
73
  """
74
 
75
+ def __init__(self, fastmcp: FastMCP):
76
+ self.fastmcp = fastmcp
77
+ self._tokens: list[Token] = []
78
 
79
+ def __enter__(self) -> Context:
80
+ """Enter the context manager and set this context as the current context."""
81
+ # Always set this context and save the token
82
+ token = _current_context.set(self)
83
+ self._tokens.append(token)
84
+ return self
85
 
86
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
87
+ """Exit the context manager and reset the most recent token."""
88
+ if self._tokens:
89
+ token = self._tokens.pop()
90
+ _current_context.reset(token)
 
 
 
 
 
91
 
92
  @property
93
+ def request_context(self) -> RequestContext:
 
 
 
 
 
 
 
94
  """Access to the underlying request context."""
95
+ return self.fastmcp._mcp_server.request_context
 
 
96
 
97
  async def report_progress(
98
  self, progress: float, total: float | None = None
 
126
  Returns:
127
  The resource content as either text or bytes
128
  """
129
+ assert self.fastmcp is not None, "Context is not available outside of a request"
130
+ return await self.fastmcp._mcp_read_resource(uri)
 
 
131
 
132
  async def log(
133
  self,
 
233
 
234
  def get_http_request(self) -> Request:
235
  """Get the active starlette request."""
236
+
237
+ return fastmcp.server.dependencies.get_http_request()
 
 
src/fastmcp/server/dependencies.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, ParamSpec, TypeVar
4
+
5
+ from starlette.requests import Request
6
+
7
+ if TYPE_CHECKING:
8
+ from fastmcp.server.context import Context
9
+
10
+ P = ParamSpec("P")
11
+ R = TypeVar("R")
12
+
13
+
14
+ # --- Context ---
15
+
16
+
17
+ def get_context() -> Context:
18
+ from fastmcp.server.context import _current_context
19
+
20
+ context = _current_context.get()
21
+ if context is None:
22
+ raise RuntimeError("No active context found.")
23
+ return context
24
+
25
+
26
+ # --- HTTP Request ---
27
+
28
+
29
+ def get_http_request() -> Request:
30
+ from fastmcp.server.http import _current_http_request
31
+
32
+ request = _current_http_request.get()
33
+ if request is None:
34
+ raise RuntimeError("No active HTTP request found.")
35
+ return request
src/fastmcp/{utilities → server}/http.py RENAMED
@@ -1,8 +1,7 @@
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
@@ -11,27 +10,22 @@ 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
  """
@@ -40,5 +34,5 @@ class RequestMiddleware:
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)
 
1
  from __future__ import annotations
2
 
3
+ from collections.abc import Generator
4
+ from contextlib import contextmanager
 
5
  from contextvars import ContextVar
6
 
7
  from starlette.requests import Request
 
10
 
11
  logger = get_logger(__name__)
12
 
13
+ _current_http_request: ContextVar[Request | None] = ContextVar(
14
+ "http_request",
 
15
  default=None,
16
  )
17
 
18
 
19
+ @contextmanager
20
+ def set_http_request(request: Request) -> Generator[Request, None, None]:
21
+ token = _current_http_request.set(request)
22
  try:
23
+ yield request
24
  finally:
25
+ _current_http_request.reset(token)
 
 
 
 
26
 
27
 
28
+ class RequestContextMiddleware:
29
  """
30
  Middleware that stores each request in a ContextVar
31
  """
 
34
  self.app = app
35
 
36
  async def __call__(self, scope, receive, send):
37
+ with set_http_request(Request(scope)):
38
  await self.app(scope, receive, send)
src/fastmcp/server/openapi.py CHANGED
@@ -25,9 +25,6 @@ from fastmcp.utilities.openapi import (
25
  )
26
 
27
  if TYPE_CHECKING:
28
- from mcp.server.session import ServerSessionT
29
- from mcp.shared.context import LifespanContextT
30
-
31
  from fastmcp.server import Context
32
 
33
  logger = get_logger(__name__)
@@ -132,7 +129,6 @@ class OpenAPITool(Tool):
132
  description=description,
133
  parameters=parameters,
134
  fn=self._execute_request, # We'll use an instance method instead of a global function
135
- context_kwarg="context", # Default context keyword argument
136
  tags=tags,
137
  annotations=annotations,
138
  serializer=serializer,
@@ -258,12 +254,10 @@ class OpenAPITool(Tool):
258
  raise ValueError(f"Request error: {str(e)}")
259
 
260
  async def run(
261
- self,
262
- arguments: dict[str, Any],
263
- context: Context[ServerSessionT, LifespanContextT] | None = None,
264
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
265
  """Run the tool with arguments and optional context."""
266
- response = await self._execute_request(**arguments, context=context)
267
  return _convert_to_content(response)
268
 
269
 
@@ -292,9 +286,7 @@ class OpenAPIResource(Resource):
292
  self._route = route
293
  self._timeout = timeout
294
 
295
- async def read(
296
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
297
- ) -> str | bytes:
298
  """Fetch the resource data by making an HTTP request."""
299
  try:
300
  # Extract path parameters from the URI if present
@@ -399,7 +391,6 @@ class OpenAPIResourceTemplate(ResourceTemplate):
399
  fn=lambda **kwargs: None,
400
  parameters=parameters,
401
  tags=tags,
402
- context_kwarg=None,
403
  )
404
  self._client = client
405
  self._route = route
@@ -409,7 +400,7 @@ class OpenAPIResourceTemplate(ResourceTemplate):
409
  self,
410
  uri: str,
411
  params: dict[str, Any],
412
- context: Context[ServerSessionT, LifespanContextT] | None = None,
413
  ) -> Resource:
414
  """Create a resource with the given parameters."""
415
  # Generate a URI for this resource instance
@@ -650,7 +641,5 @@ class FastMCPOpenAPI(FastMCP):
650
 
651
  async def _mcp_call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
652
  """Override the call_tool method to return the raw result without converting to content."""
653
-
654
- context = self.get_context()
655
- result = await self._tool_manager.call_tool(name, arguments, context=context)
656
  return result
 
25
  )
26
 
27
  if TYPE_CHECKING:
 
 
 
28
  from fastmcp.server import Context
29
 
30
  logger = get_logger(__name__)
 
129
  description=description,
130
  parameters=parameters,
131
  fn=self._execute_request, # We'll use an instance method instead of a global function
 
132
  tags=tags,
133
  annotations=annotations,
134
  serializer=serializer,
 
254
  raise ValueError(f"Request error: {str(e)}")
255
 
256
  async def run(
257
+ self, arguments: dict[str, Any]
 
 
258
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
259
  """Run the tool with arguments and optional context."""
260
+ response = await self._execute_request(**arguments)
261
  return _convert_to_content(response)
262
 
263
 
 
286
  self._route = route
287
  self._timeout = timeout
288
 
289
+ async def read(self) -> str | bytes:
 
 
290
  """Fetch the resource data by making an HTTP request."""
291
  try:
292
  # Extract path parameters from the URI if present
 
391
  fn=lambda **kwargs: None,
392
  parameters=parameters,
393
  tags=tags,
 
394
  )
395
  self._client = client
396
  self._route = route
 
400
  self,
401
  uri: str,
402
  params: dict[str, Any],
403
+ context: Context | None = None,
404
  ) -> Resource:
405
  """Create a resource with the given parameters."""
406
  # Generate a URI for this resource instance
 
641
 
642
  async def _mcp_call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
643
  """Override the call_tool method to return the raw result without converting to content."""
644
+ result = await self._tool_manager.call_tool(name, arguments)
 
 
645
  return result
src/fastmcp/server/proxy.py CHANGED
@@ -27,9 +27,6 @@ from fastmcp.tools.tool import Tool
27
  from fastmcp.utilities.logging import get_logger
28
 
29
  if TYPE_CHECKING:
30
- from mcp.server.session import ServerSessionT
31
- from mcp.shared.context import LifespanContextT
32
-
33
  from fastmcp.server import Context
34
 
35
  logger = get_logger(__name__)
@@ -57,7 +54,7 @@ class ProxyTool(Tool):
57
  async def run(
58
  self,
59
  arguments: dict[str, Any],
60
- context: Context[ServerSessionT, LifespanContextT] | None = None,
61
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
62
  # the client context manager will swallow any exceptions inside a TaskGroup
63
  # so we return the raw result and raise an exception ourselves
@@ -89,9 +86,7 @@ class ProxyResource(Resource):
89
  mime_type=resource.mimeType,
90
  )
91
 
92
- async def read(
93
- self, context: Context[ServerSessionT, LifespanContextT] | None = None
94
- ) -> str | bytes:
95
  if self._value is not None:
96
  return self._value
97
 
@@ -127,7 +122,7 @@ class ProxyTemplate(ResourceTemplate):
127
  self,
128
  uri: str,
129
  params: dict[str, Any],
130
- context: Context[ServerSessionT, LifespanContextT] | None = None,
131
  ) -> ProxyResource:
132
  # dont use the provided uri, because it may not be the same as the
133
  # uri_template on the remote server.
@@ -171,11 +166,7 @@ class ProxyPrompt(Prompt):
171
  fn=_proxy_passthrough,
172
  )
173
 
174
- async def render(
175
- self,
176
- arguments: dict[str, Any],
177
- context: Context[ServerSessionT, LifespanContextT] | None = None,
178
- ) -> list[PromptMessage]:
179
  async with self._client:
180
  result = await self._client.get_prompt(self.name, arguments)
181
  return result.messages
 
27
  from fastmcp.utilities.logging import get_logger
28
 
29
  if TYPE_CHECKING:
 
 
 
30
  from fastmcp.server import Context
31
 
32
  logger = get_logger(__name__)
 
54
  async def run(
55
  self,
56
  arguments: dict[str, Any],
57
+ context: Context | None = None,
58
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
59
  # the client context manager will swallow any exceptions inside a TaskGroup
60
  # so we return the raw result and raise an exception ourselves
 
86
  mime_type=resource.mimeType,
87
  )
88
 
89
+ async def read(self) -> str | bytes:
 
 
90
  if self._value is not None:
91
  return self._value
92
 
 
122
  self,
123
  uri: str,
124
  params: dict[str, Any],
125
+ context: Context | None = None,
126
  ) -> ProxyResource:
127
  # dont use the provided uri, because it may not be the same as the
128
  # uri_template on the remote server.
 
166
  fn=_proxy_passthrough,
167
  )
168
 
169
+ async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
 
 
 
 
170
  async with self._client:
171
  result = await self._client.get_prompt(self.name, arguments)
172
  return result.messages
src/fastmcp/server/server.py CHANGED
@@ -25,7 +25,6 @@ from mcp.server.auth.provider import OAuthAuthorizationServerProvider
25
  from mcp.server.lowlevel.helper_types import ReadResourceContents
26
  from mcp.server.lowlevel.server import LifespanResultT
27
  from mcp.server.lowlevel.server import Server as MCPServer
28
- from mcp.server.session import ServerSession
29
  from mcp.server.sse import SseServerTransport
30
  from mcp.server.stdio import stdio_server
31
  from mcp.types import (
@@ -49,120 +48,27 @@ from starlette.responses import Response
49
  from starlette.routing import Mount, Route
50
  from starlette.types import Receive, Scope, Send
51
 
52
- import fastmcp
53
  import fastmcp.settings
54
  from fastmcp.exceptions import NotFoundError, ResourceError
55
  from fastmcp.prompts import Prompt, PromptManager
56
  from fastmcp.prompts.prompt import PromptResult
57
  from fastmcp.resources import Resource, ResourceManager
58
  from fastmcp.resources.template import ResourceTemplate
 
59
  from fastmcp.tools import ToolManager
60
  from fastmcp.tools.tool import Tool
 
61
  from fastmcp.utilities.decorators import DecoratedFunction
62
- from fastmcp.utilities.http import RequestMiddleware
63
  from fastmcp.utilities.logging import configure_logging, get_logger
64
 
65
  if TYPE_CHECKING:
66
  from fastmcp.client import Client
67
- from fastmcp.server.context import Context
68
  from fastmcp.server.openapi import FastMCPOpenAPI
69
  from fastmcp.server.proxy import FastMCPProxy
70
 
71
  logger = get_logger(__name__)
72
 
73
- NOT_FOUND = object()
74
-
75
-
76
- class MountedServer:
77
- def __init__(
78
- self,
79
- prefix: str,
80
- server: FastMCP,
81
- tool_separator: str | None = None,
82
- resource_separator: str | None = None,
83
- prompt_separator: str | None = None,
84
- ):
85
- if tool_separator is None:
86
- tool_separator = "_"
87
- if resource_separator is None:
88
- resource_separator = "+"
89
- if prompt_separator is None:
90
- prompt_separator = "_"
91
-
92
- _validate_resource_prefix(f"{prefix}{resource_separator}")
93
-
94
- self.server = server
95
- self.prefix = prefix
96
- self.tool_separator = tool_separator
97
- self.resource_separator = resource_separator
98
- self.prompt_separator = prompt_separator
99
-
100
- async def get_tools(self) -> dict[str, Tool]:
101
- tools = await self.server.get_tools()
102
- return {
103
- f"{self.prefix}{self.tool_separator}{key}": tool
104
- for key, tool in tools.items()
105
- }
106
-
107
- async def get_resources(self) -> dict[str, Resource]:
108
- resources = await self.server.get_resources()
109
- return {
110
- f"{self.prefix}{self.resource_separator}{key}": resource
111
- for key, resource in resources.items()
112
- }
113
-
114
- async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
115
- templates = await self.server.get_resource_templates()
116
- return {
117
- f"{self.prefix}{self.resource_separator}{key}": template
118
- for key, template in templates.items()
119
- }
120
-
121
- async def get_prompts(self) -> dict[str, Prompt]:
122
- prompts = await self.server.get_prompts()
123
- return {
124
- f"{self.prefix}{self.prompt_separator}{key}": prompt
125
- for key, prompt in prompts.items()
126
- }
127
-
128
- def match_tool(self, key: str) -> bool:
129
- return key.startswith(f"{self.prefix}{self.tool_separator}")
130
-
131
- def strip_tool_prefix(self, key: str) -> str:
132
- return key.removeprefix(f"{self.prefix}{self.tool_separator}")
133
-
134
- def match_resource(self, key: str) -> bool:
135
- return key.startswith(f"{self.prefix}{self.resource_separator}")
136
-
137
- def strip_resource_prefix(self, key: str) -> str:
138
- return key.removeprefix(f"{self.prefix}{self.resource_separator}")
139
-
140
- def match_prompt(self, key: str) -> bool:
141
- return key.startswith(f"{self.prefix}{self.prompt_separator}")
142
-
143
- def strip_prompt_prefix(self, key: str) -> str:
144
- return key.removeprefix(f"{self.prefix}{self.prompt_separator}")
145
-
146
-
147
- class TimedCache:
148
- def __init__(self, expiration: datetime.timedelta):
149
- self.expiration = expiration
150
- self.cache: dict[Any, tuple[Any, datetime.datetime]] = {}
151
-
152
- def set(self, key: Any, value: Any) -> None:
153
- expires = datetime.datetime.now() + self.expiration
154
- self.cache[key] = (value, expires)
155
-
156
- def get(self, key: Any) -> Any:
157
- value = self.cache.get(key)
158
- if value is not None and value[1] > datetime.datetime.now():
159
- return value[0]
160
- else:
161
- return NOT_FOUND
162
-
163
- def clear(self) -> None:
164
- self.cache.clear()
165
-
166
 
167
  @asynccontextmanager
168
  async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
@@ -309,23 +215,9 @@ class FastMCP(Generic[LifespanResultT]):
309
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
310
  self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
311
 
312
- def get_context(self) -> Context[ServerSession, LifespanResultT]:
313
- """
314
- Returns a Context object. Note that the context will only be valid
315
- during a request; outside a request, most methods will error.
316
- """
317
-
318
- try:
319
- request_context = self._mcp_server.request_context
320
- except LookupError:
321
- request_context = None
322
- from fastmcp.server.context import Context
323
-
324
- return Context(request_context=request_context, fastmcp=self)
325
-
326
  async def get_tools(self) -> dict[str, Tool]:
327
  """Get all registered tools, indexed by registered key."""
328
- if (tools := self._cache.get("tools")) is NOT_FOUND:
329
  tools = {}
330
  for server in self._mounted_servers.values():
331
  server_tools = await server.get_tools()
@@ -336,7 +228,7 @@ class FastMCP(Generic[LifespanResultT]):
336
 
337
  async def get_resources(self) -> dict[str, Resource]:
338
  """Get all registered resources, indexed by registered key."""
339
- if (resources := self._cache.get("resources")) is NOT_FOUND:
340
  resources = {}
341
  for server in self._mounted_servers.values():
342
  server_resources = await server.get_resources()
@@ -347,7 +239,9 @@ class FastMCP(Generic[LifespanResultT]):
347
 
348
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
349
  """Get all registered resource templates, indexed by registered key."""
350
- if (templates := self._cache.get("resource_templates")) is NOT_FOUND:
 
 
351
  templates = {}
352
  for server in self._mounted_servers.values():
353
  server_templates = await server.get_resource_templates()
@@ -360,7 +254,7 @@ class FastMCP(Generic[LifespanResultT]):
360
  """
361
  List all available prompts.
362
  """
363
- if (prompts := self._cache.get("prompts")) is NOT_FOUND:
364
  prompts = {}
365
  for server in self._mounted_servers.values():
366
  server_prompts = await server.get_prompts()
@@ -458,43 +352,46 @@ class FastMCP(Generic[LifespanResultT]):
458
  self, key: str, arguments: dict[str, Any]
459
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
460
  """Call a tool by name with arguments."""
461
- if self._tool_manager.has_tool(key):
462
- context = self.get_context()
463
- result = await self._tool_manager.call_tool(key, arguments, context=context)
464
 
465
- else:
466
- for server in self._mounted_servers.values():
467
- if server.match_tool(key):
468
- new_key = server.strip_tool_prefix(key)
469
- result = await server.server._mcp_call_tool(new_key, arguments)
470
- break
471
  else:
472
- raise NotFoundError(f"Unknown tool: {key}")
473
- return result
 
 
 
 
 
 
474
 
475
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
476
  """
477
  Read a resource by URI, in the format expected by the low-level MCP
478
  server.
479
  """
480
- if self._resource_manager.has_resource(uri):
481
- context = self.get_context()
482
- resource = await self._resource_manager.get_resource(uri, context=context)
483
- try:
484
- content = await resource.read(context=context)
485
- return [
486
- ReadResourceContents(content=content, mime_type=resource.mime_type)
487
- ]
488
- except Exception as e:
489
- logger.error(f"Error reading resource {uri}: {e}")
490
- raise ResourceError(str(e))
491
- else:
492
- for server in self._mounted_servers.values():
493
- if server.match_resource(str(uri)):
494
- new_uri = server.strip_resource_prefix(str(uri))
495
- return await server.server._mcp_read_resource(new_uri)
496
  else:
497
- raise NotFoundError(f"Unknown resource: {uri}")
 
 
 
 
 
498
 
499
  async def _mcp_get_prompt(
500
  self, name: str, arguments: dict[str, Any] | None = None
@@ -504,19 +401,19 @@ class FastMCP(Generic[LifespanResultT]):
504
  MCP server.
505
 
506
  """
507
- if self._prompt_manager.has_prompt(name):
508
- context = self.get_context()
509
- prompt_result = await self._prompt_manager.render_prompt(
510
- name, arguments=arguments or {}, context=context
511
- )
512
- return prompt_result
513
- else:
514
- for server in self._mounted_servers.values():
515
- if server.match_prompt(name):
516
- new_key = server.strip_prompt_prefix(name)
517
- return await server.server._mcp_get_prompt(new_key, arguments)
518
  else:
519
- raise NotFoundError(f"Unknown prompt: {name}")
 
 
 
 
 
520
 
521
  def add_tool(
522
  self,
@@ -827,10 +724,11 @@ class FastMCP(Generic[LifespanResultT]):
827
  ) -> None:
828
  """Run the server using SSE transport."""
829
  uvicorn_config = uvicorn_config or {}
830
- # the SSE app hangs even when a signal is sent, so we disable the timeout to make it possible to close immediately.
831
- # see https://github.com/jlowin/fastmcp/issues/296
 
832
  uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
833
- app = RequestMiddleware(self.sse_app())
834
 
835
  config = uvicorn.Config(
836
  app,
@@ -1145,3 +1043,74 @@ def _validate_resource_prefix(prefix: str) -> None:
1145
  raise ValueError(
1146
  f"Resource prefix or separator would result in an invalid resource URI: {e}"
1147
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  from mcp.server.lowlevel.helper_types import ReadResourceContents
26
  from mcp.server.lowlevel.server import LifespanResultT
27
  from mcp.server.lowlevel.server import Server as MCPServer
 
28
  from mcp.server.sse import SseServerTransport
29
  from mcp.server.stdio import stdio_server
30
  from mcp.types import (
 
48
  from starlette.routing import Mount, Route
49
  from starlette.types import Receive, Scope, Send
50
 
51
+ import fastmcp.server
52
  import fastmcp.settings
53
  from fastmcp.exceptions import NotFoundError, ResourceError
54
  from fastmcp.prompts import Prompt, PromptManager
55
  from fastmcp.prompts.prompt import PromptResult
56
  from fastmcp.resources import Resource, ResourceManager
57
  from fastmcp.resources.template import ResourceTemplate
58
+ from fastmcp.server.http import RequestContextMiddleware
59
  from fastmcp.tools import ToolManager
60
  from fastmcp.tools.tool import Tool
61
+ from fastmcp.utilities.cache import TimedCache
62
  from fastmcp.utilities.decorators import DecoratedFunction
 
63
  from fastmcp.utilities.logging import configure_logging, get_logger
64
 
65
  if TYPE_CHECKING:
66
  from fastmcp.client import Client
 
67
  from fastmcp.server.openapi import FastMCPOpenAPI
68
  from fastmcp.server.proxy import FastMCPProxy
69
 
70
  logger = get_logger(__name__)
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  @asynccontextmanager
74
  async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
 
215
  self._mcp_server.get_prompt()(self._mcp_get_prompt)
216
  self._mcp_server.list_resource_templates()(self._mcp_list_resource_templates)
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  async def get_tools(self) -> dict[str, Tool]:
219
  """Get all registered tools, indexed by registered key."""
220
+ if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
221
  tools = {}
222
  for server in self._mounted_servers.values():
223
  server_tools = await server.get_tools()
 
228
 
229
  async def get_resources(self) -> dict[str, Resource]:
230
  """Get all registered resources, indexed by registered key."""
231
+ if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
232
  resources = {}
233
  for server in self._mounted_servers.values():
234
  server_resources = await server.get_resources()
 
239
 
240
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
241
  """Get all registered resource templates, indexed by registered key."""
242
+ if (
243
+ templates := self._cache.get("resource_templates")
244
+ ) is self._cache.NOT_FOUND:
245
  templates = {}
246
  for server in self._mounted_servers.values():
247
  server_templates = await server.get_resource_templates()
 
254
  """
255
  List all available prompts.
256
  """
257
+ if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
258
  prompts = {}
259
  for server in self._mounted_servers.values():
260
  server_prompts = await server.get_prompts()
 
352
  self, key: str, arguments: dict[str, Any]
353
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
354
  """Call a tool by name with arguments."""
 
 
 
355
 
356
+ with fastmcp.server.context.Context(fastmcp=self):
357
+ if self._tool_manager.has_tool(key):
358
+ result = await self._tool_manager.call_tool(key, arguments)
359
+
 
 
360
  else:
361
+ for server in self._mounted_servers.values():
362
+ if server.match_tool(key):
363
+ new_key = server.strip_tool_prefix(key)
364
+ result = await server.server._mcp_call_tool(new_key, arguments)
365
+ break
366
+ else:
367
+ raise NotFoundError(f"Unknown tool: {key}")
368
+ return result
369
 
370
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
371
  """
372
  Read a resource by URI, in the format expected by the low-level MCP
373
  server.
374
  """
375
+ with fastmcp.server.context.Context(fastmcp=self):
376
+ if self._resource_manager.has_resource(uri):
377
+ resource = await self._resource_manager.get_resource(uri)
378
+ try:
379
+ content = await resource.read()
380
+ return [
381
+ ReadResourceContents(
382
+ content=content, mime_type=resource.mime_type
383
+ )
384
+ ]
385
+ except Exception as e:
386
+ logger.error(f"Error reading resource {uri}: {e}")
387
+ raise ResourceError(str(e))
 
 
 
388
  else:
389
+ for server in self._mounted_servers.values():
390
+ if server.match_resource(str(uri)):
391
+ new_uri = server.strip_resource_prefix(str(uri))
392
+ return await server.server._mcp_read_resource(new_uri)
393
+ else:
394
+ raise NotFoundError(f"Unknown resource: {uri}")
395
 
396
  async def _mcp_get_prompt(
397
  self, name: str, arguments: dict[str, Any] | None = None
 
401
  MCP server.
402
 
403
  """
404
+ with fastmcp.server.context.Context(fastmcp=self):
405
+ if self._prompt_manager.has_prompt(name):
406
+ prompt_result = await self._prompt_manager.render_prompt(
407
+ name, arguments=arguments or {}
408
+ )
409
+ return prompt_result
 
 
 
 
 
410
  else:
411
+ for server in self._mounted_servers.values():
412
+ if server.match_prompt(name):
413
+ new_key = server.strip_prompt_prefix(name)
414
+ return await server.server._mcp_get_prompt(new_key, arguments)
415
+ else:
416
+ raise NotFoundError(f"Unknown prompt: {name}")
417
 
418
  def add_tool(
419
  self,
 
724
  ) -> None:
725
  """Run the server using SSE transport."""
726
  uvicorn_config = uvicorn_config or {}
727
+ # the SSE app hangs even when a signal is sent, so we disable the
728
+ # timeout to make it possible to close immediately. see
729
+ # https://github.com/jlowin/fastmcp/issues/296
730
  uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
731
+ app = RequestContextMiddleware(self.sse_app())
732
 
733
  config = uvicorn.Config(
734
  app,
 
1043
  raise ValueError(
1044
  f"Resource prefix or separator would result in an invalid resource URI: {e}"
1045
  )
1046
+
1047
+
1048
+ class MountedServer:
1049
+ def __init__(
1050
+ self,
1051
+ prefix: str,
1052
+ server: FastMCP,
1053
+ tool_separator: str | None = None,
1054
+ resource_separator: str | None = None,
1055
+ prompt_separator: str | None = None,
1056
+ ):
1057
+ if tool_separator is None:
1058
+ tool_separator = "_"
1059
+ if resource_separator is None:
1060
+ resource_separator = "+"
1061
+ if prompt_separator is None:
1062
+ prompt_separator = "_"
1063
+
1064
+ _validate_resource_prefix(f"{prefix}{resource_separator}")
1065
+
1066
+ self.server = server
1067
+ self.prefix = prefix
1068
+ self.tool_separator = tool_separator
1069
+ self.resource_separator = resource_separator
1070
+ self.prompt_separator = prompt_separator
1071
+
1072
+ async def get_tools(self) -> dict[str, Tool]:
1073
+ tools = await self.server.get_tools()
1074
+ return {
1075
+ f"{self.prefix}{self.tool_separator}{key}": tool
1076
+ for key, tool in tools.items()
1077
+ }
1078
+
1079
+ async def get_resources(self) -> dict[str, Resource]:
1080
+ resources = await self.server.get_resources()
1081
+ return {
1082
+ f"{self.prefix}{self.resource_separator}{key}": resource
1083
+ for key, resource in resources.items()
1084
+ }
1085
+
1086
+ async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
1087
+ templates = await self.server.get_resource_templates()
1088
+ return {
1089
+ f"{self.prefix}{self.resource_separator}{key}": template
1090
+ for key, template in templates.items()
1091
+ }
1092
+
1093
+ async def get_prompts(self) -> dict[str, Prompt]:
1094
+ prompts = await self.server.get_prompts()
1095
+ return {
1096
+ f"{self.prefix}{self.prompt_separator}{key}": prompt
1097
+ for key, prompt in prompts.items()
1098
+ }
1099
+
1100
+ def match_tool(self, key: str) -> bool:
1101
+ return key.startswith(f"{self.prefix}{self.tool_separator}")
1102
+
1103
+ def strip_tool_prefix(self, key: str) -> str:
1104
+ return key.removeprefix(f"{self.prefix}{self.tool_separator}")
1105
+
1106
+ def match_resource(self, key: str) -> bool:
1107
+ return key.startswith(f"{self.prefix}{self.resource_separator}")
1108
+
1109
+ def strip_resource_prefix(self, key: str) -> str:
1110
+ return key.removeprefix(f"{self.prefix}{self.resource_separator}")
1111
+
1112
+ def match_prompt(self, key: str) -> bool:
1113
+ return key.startswith(f"{self.prefix}{self.prompt_separator}")
1114
+
1115
+ def strip_prompt_prefix(self, key: str) -> str:
1116
+ return key.removeprefix(f"{self.prefix}{self.prompt_separator}")
src/fastmcp/tools/tool.py CHANGED
@@ -12,6 +12,7 @@ from pydantic import BaseModel, BeforeValidator, Field
12
 
13
  import fastmcp
14
  from fastmcp.exceptions import ToolError
 
15
  from fastmcp.utilities.json_schema import prune_params
16
  from fastmcp.utilities.logging import get_logger
17
  from fastmcp.utilities.types import (
@@ -22,10 +23,7 @@ from fastmcp.utilities.types import (
22
  )
23
 
24
  if TYPE_CHECKING:
25
- from mcp.server.session import ServerSessionT
26
- from mcp.shared.context import LifespanContextT
27
-
28
- from fastmcp.server import Context
29
 
30
  logger = get_logger(__name__)
31
 
@@ -41,9 +39,6 @@ class Tool(BaseModel):
41
  name: str = Field(description="Name of the tool")
42
  description: str = Field(description="Description of what the tool does")
43
  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
  tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
48
  default_factory=set, description="Tags for the tool"
49
  )
@@ -60,13 +55,12 @@ class Tool(BaseModel):
60
  fn: Callable[..., Any],
61
  name: str | None = None,
62
  description: str | None = None,
63
- context_kwarg: str | None = None,
64
  tags: set[str] | None = None,
65
  annotations: ToolAnnotations | None = None,
66
  serializer: Callable[[Any], str] | None = None,
67
  ) -> Tool:
68
  """Create a Tool from a function."""
69
- from fastmcp import Context
70
 
71
  # Reject functions with *args or **kwargs
72
  sig = inspect.signature(fn)
@@ -86,8 +80,7 @@ class Tool(BaseModel):
86
  type_adapter = get_cached_typeadapter(fn)
87
  schema = type_adapter.json_schema()
88
 
89
- if context_kwarg is None:
90
- context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
91
  if context_kwarg:
92
  schema = prune_params(schema, params=[context_kwarg])
93
 
@@ -96,25 +89,23 @@ class Tool(BaseModel):
96
  name=func_name,
97
  description=func_doc,
98
  parameters=schema,
99
- context_kwarg=context_kwarg,
100
  tags=tags or set(),
101
  annotations=annotations,
102
  serializer=serializer,
103
  )
104
 
105
  async def run(
106
- self,
107
- arguments: dict[str, Any],
108
- context: Context[ServerSessionT, LifespanContextT] | None = None,
109
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
110
  """Run the tool with arguments."""
 
111
 
112
- try:
113
- injected_args = (
114
- {self.context_kwarg: context} if self.context_kwarg is not None else {}
115
- )
116
 
117
- parsed_args = arguments.copy()
 
 
 
118
 
119
  if fastmcp.settings.settings.tool_attempt_parse_json_args:
120
  # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
@@ -125,7 +116,7 @@ class Tool(BaseModel):
125
  # which can be pre-parsed here.
126
  signature = inspect.signature(self.fn)
127
  for param_name in self.parameters["properties"]:
128
- arg = parsed_args.get(param_name, None)
129
  # if not in signature, we won't have annotations, so skip logic
130
  if param_name not in signature.parameters:
131
  continue
@@ -140,13 +131,13 @@ class Tool(BaseModel):
140
  ):
141
  continue
142
  try:
143
- parsed_args[param_name] = json.loads(arg)
144
 
145
  except json.JSONDecodeError:
146
  pass
147
 
148
  type_adapter = get_cached_typeadapter(self.fn)
149
- result = type_adapter.validate_python(parsed_args | injected_args)
150
  if inspect.isawaitable(result):
151
  result = await result
152
 
 
12
 
13
  import fastmcp
14
  from fastmcp.exceptions import ToolError
15
+ from fastmcp.server.dependencies import get_context
16
  from fastmcp.utilities.json_schema import prune_params
17
  from fastmcp.utilities.logging import get_logger
18
  from fastmcp.utilities.types import (
 
23
  )
24
 
25
  if TYPE_CHECKING:
26
+ pass
 
 
 
27
 
28
  logger = get_logger(__name__)
29
 
 
39
  name: str = Field(description="Name of the tool")
40
  description: str = Field(description="Description of what the tool does")
41
  parameters: dict[str, Any] = Field(description="JSON schema for tool parameters")
 
 
 
42
  tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
43
  default_factory=set, description="Tags for the tool"
44
  )
 
55
  fn: Callable[..., Any],
56
  name: str | None = None,
57
  description: str | None = None,
 
58
  tags: set[str] | None = None,
59
  annotations: ToolAnnotations | None = None,
60
  serializer: Callable[[Any], str] | None = None,
61
  ) -> Tool:
62
  """Create a Tool from a function."""
63
+ from fastmcp.server.context import Context
64
 
65
  # Reject functions with *args or **kwargs
66
  sig = inspect.signature(fn)
 
80
  type_adapter = get_cached_typeadapter(fn)
81
  schema = type_adapter.json_schema()
82
 
83
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
 
84
  if context_kwarg:
85
  schema = prune_params(schema, params=[context_kwarg])
86
 
 
89
  name=func_name,
90
  description=func_doc,
91
  parameters=schema,
 
92
  tags=tags or set(),
93
  annotations=annotations,
94
  serializer=serializer,
95
  )
96
 
97
  async def run(
98
+ self, arguments: dict[str, Any]
 
 
99
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
100
  """Run the tool with arguments."""
101
+ from fastmcp.server.context import Context
102
 
103
+ arguments = arguments.copy()
 
 
 
104
 
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()
109
 
110
  if fastmcp.settings.settings.tool_attempt_parse_json_args:
111
  # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
 
116
  # which can be pre-parsed here.
117
  signature = inspect.signature(self.fn)
118
  for param_name in self.parameters["properties"]:
119
+ arg = arguments.get(param_name, None)
120
  # if not in signature, we won't have annotations, so skip logic
121
  if param_name not in signature.parameters:
122
  continue
 
131
  ):
132
  continue
133
  try:
134
+ arguments[param_name] = json.loads(arg)
135
 
136
  except json.JSONDecodeError:
137
  pass
138
 
139
  type_adapter = get_cached_typeadapter(self.fn)
140
+ result = type_adapter.validate_python(arguments)
141
  if inspect.isawaitable(result):
142
  result = await result
143
 
src/fastmcp/tools/tool_manager.py CHANGED
@@ -3,7 +3,6 @@ from __future__ import annotations as _annotations
3
  from collections.abc import Callable
4
  from typing import TYPE_CHECKING, Any
5
 
6
- from mcp.shared.context import LifespanContextT
7
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
8
 
9
  from fastmcp.exceptions import NotFoundError
@@ -12,9 +11,7 @@ from fastmcp.tools.tool import Tool
12
  from fastmcp.utilities.logging import get_logger
13
 
14
  if TYPE_CHECKING:
15
- from mcp.server.session import ServerSessionT
16
-
17
- from fastmcp.server import Context
18
 
19
  logger = get_logger(__name__)
20
 
@@ -98,14 +95,11 @@ class ToolManager:
98
  return tool
99
 
100
  async def call_tool(
101
- self,
102
- key: str,
103
- arguments: dict[str, Any],
104
- context: Context[ServerSessionT, LifespanContextT] | None = None,
105
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
106
  """Call a tool by name with arguments."""
107
  tool = self.get_tool(key)
108
  if not tool:
109
  raise NotFoundError(f"Unknown tool: {key}")
110
 
111
- return await tool.run(arguments, context=context)
 
3
  from collections.abc import Callable
4
  from typing import TYPE_CHECKING, Any
5
 
 
6
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
7
 
8
  from fastmcp.exceptions import NotFoundError
 
11
  from fastmcp.utilities.logging import get_logger
12
 
13
  if TYPE_CHECKING:
14
+ pass
 
 
15
 
16
  logger = get_logger(__name__)
17
 
 
95
  return tool
96
 
97
  async def call_tool(
98
+ self, key: str, arguments: dict[str, Any]
 
 
 
99
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
100
  """Call a tool by name with arguments."""
101
  tool = self.get_tool(key)
102
  if not tool:
103
  raise NotFoundError(f"Unknown tool: {key}")
104
 
105
+ return await tool.run(arguments)
src/fastmcp/utilities/cache.py ADDED
@@ -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()
tests/prompts/test_prompt_manager.py CHANGED
@@ -1,8 +1,6 @@
1
  from typing import Annotated
2
 
3
  import pytest
4
- from mcp.server.session import ServerSessionT
5
- from mcp.shared.context import LifespanContextT
6
 
7
  from fastmcp import Context
8
  from fastmcp.exceptions import NotFoundError
@@ -308,38 +306,30 @@ class TestContextHandling:
308
  def prompt_with_context(x: int, ctx: Context) -> str:
309
  return str(x)
310
 
311
- prompt = Prompt.from_function(prompt_with_context)
312
- assert prompt.context_kwarg == "ctx"
313
 
314
  def prompt_without_context(x: int) -> str:
315
  return str(x)
316
 
317
- prompt = Prompt.from_function(prompt_without_context)
318
- assert prompt.context_kwarg is None
319
 
320
  def test_parameterized_context_parameter_detection(self):
321
  """Test that parameterized context parameters are properly detected in
322
  Prompt.from_function()."""
323
 
324
- def prompt_with_context(
325
- x: int, ctx: Context[ServerSessionT, LifespanContextT]
326
- ) -> str:
327
  return str(x)
328
 
329
- prompt = Prompt.from_function(prompt_with_context)
330
- assert prompt.context_kwarg == "ctx"
331
 
332
  def test_parameterized_union_context_parameter_detection(self):
333
  """Test that context parameters in a union are properly detected in
334
  Prompt.from_function()."""
335
 
336
- def prompt_with_context(
337
- x: int, ctx: Context[ServerSessionT, LifespanContextT] | None
338
- ) -> str:
339
  return str(x)
340
 
341
- prompt = Prompt.from_function(prompt_with_context)
342
- assert prompt.context_kwarg == "ctx"
343
 
344
  async def test_context_injection(self):
345
  """Test that context is properly injected during prompt rendering."""
@@ -349,17 +339,15 @@ class TestContextHandling:
349
  return str(x)
350
 
351
  prompt = Prompt.from_function(prompt_with_context)
352
- assert prompt.context_kwarg == "ctx"
353
 
354
  from fastmcp import FastMCP
355
 
356
  mcp = FastMCP()
357
- ctx = mcp.get_context()
 
 
 
358
 
359
- messages = await prompt.render(
360
- arguments={"x": 42},
361
- context=ctx,
362
- )
363
  assert len(messages) == 1
364
  assert isinstance(messages[0].content, TextContent)
365
  assert messages[0].content.text == "42"
@@ -371,12 +359,18 @@ class TestContextHandling:
371
  return str(x)
372
 
373
  prompt = Prompt.from_function(prompt_with_context)
374
- assert prompt.context_kwarg == "ctx"
375
 
376
- # Should not raise an error when context is not provided
377
- messages = await prompt.render(
378
- arguments={"x": 42},
379
- )
 
 
 
 
 
 
 
380
  assert len(messages) == 1
381
  assert isinstance(messages[0].content, TextContent)
382
  assert messages[0].content.text == "42"
@@ -388,5 +382,4 @@ class TestContextHandling:
388
  def prompt_with_context(x: int, ctx: Annotated[Context, "ctx"]) -> str:
389
  return str(x)
390
 
391
- prompt = Prompt.from_function(prompt_with_context)
392
- assert prompt.context_kwarg == "ctx"
 
1
  from typing import Annotated
2
 
3
  import pytest
 
 
4
 
5
  from fastmcp import Context
6
  from fastmcp.exceptions import NotFoundError
 
306
  def prompt_with_context(x: int, ctx: Context) -> str:
307
  return str(x)
308
 
309
+ Prompt.from_function(prompt_with_context)
 
310
 
311
  def prompt_without_context(x: int) -> str:
312
  return str(x)
313
 
314
+ Prompt.from_function(prompt_without_context)
 
315
 
316
  def test_parameterized_context_parameter_detection(self):
317
  """Test that parameterized context parameters are properly detected in
318
  Prompt.from_function()."""
319
 
320
+ def prompt_with_context(x: int, ctx: Context) -> str:
 
 
321
  return str(x)
322
 
323
+ Prompt.from_function(prompt_with_context)
 
324
 
325
  def test_parameterized_union_context_parameter_detection(self):
326
  """Test that context parameters in a union are properly detected in
327
  Prompt.from_function()."""
328
 
329
+ def prompt_with_context(x: int, ctx: Context | None) -> str:
 
 
330
  return str(x)
331
 
332
+ Prompt.from_function(prompt_with_context)
 
333
 
334
  async def test_context_injection(self):
335
  """Test that context is properly injected during prompt rendering."""
 
339
  return str(x)
340
 
341
  prompt = Prompt.from_function(prompt_with_context)
 
342
 
343
  from fastmcp import FastMCP
344
 
345
  mcp = FastMCP()
346
+ context = Context(fastmcp=mcp)
347
+
348
+ with context:
349
+ messages = await prompt.render(arguments={"x": 42})
350
 
 
 
 
 
351
  assert len(messages) == 1
352
  assert isinstance(messages[0].content, TextContent)
353
  assert messages[0].content.text == "42"
 
359
  return str(x)
360
 
361
  prompt = Prompt.from_function(prompt_with_context)
 
362
 
363
+ # Even for optional context, we need to provide a context
364
+ from fastmcp import FastMCP
365
+
366
+ mcp = FastMCP()
367
+ context = Context(fastmcp=mcp)
368
+
369
+ with context:
370
+ messages = await prompt.render(
371
+ arguments={"x": 42},
372
+ )
373
+
374
  assert len(messages) == 1
375
  assert isinstance(messages[0].content, TextContent)
376
  assert messages[0].content.text == "42"
 
382
  def prompt_with_context(x: int, ctx: Annotated[Context, "ctx"]) -> str:
383
  return str(x)
384
 
385
+ Prompt.from_function(prompt_with_context)
 
tests/resources/test_resource_template.py CHANGED
@@ -2,8 +2,6 @@ import json
2
  from urllib.parse import quote
3
 
4
  import pytest
5
- from mcp.server.session import ServerSessionT
6
- from mcp.shared.context import LifespanContextT
7
  from pydantic import BaseModel
8
 
9
  from fastmcp import Context
@@ -560,54 +558,46 @@ class TestContextHandling:
560
  def template_with_context(x: int, ctx: Context) -> str:
561
  return str(x)
562
 
563
- template = ResourceTemplate.from_function(
564
  fn=template_with_context,
565
  uri_template="test://{x}",
566
  name="test",
567
  )
568
- assert template.context_kwarg == "ctx"
569
 
570
  def template_without_context(x: int) -> str:
571
  return str(x)
572
 
573
- template = ResourceTemplate.from_function(
574
  fn=template_without_context,
575
  uri_template="test://{x}",
576
  name="test",
577
  )
578
- assert template.context_kwarg is None
579
 
580
  def test_parameterized_context_parameter_detection(self):
581
  """Test that parameterized context parameters are properly detected in
582
  ResourceTemplate.from_function()."""
583
 
584
- def template_with_context(
585
- x: int, ctx: Context[ServerSessionT, LifespanContextT]
586
- ) -> str:
587
  return str(x)
588
 
589
- template = ResourceTemplate.from_function(
590
  fn=template_with_context,
591
  uri_template="test://{x}",
592
  name="test",
593
  )
594
- assert template.context_kwarg == "ctx"
595
 
596
  def test_parameterized_union_context_parameter_detection(self):
597
  """Test that context parameters in a union are properly detected in
598
  ResourceTemplate.from_function()."""
599
 
600
- def template_with_context(
601
- x: int, ctx: Context[ServerSessionT, LifespanContextT] | None
602
- ) -> str:
603
  return str(x)
604
 
605
- template = ResourceTemplate.from_function(
606
  fn=template_with_context,
607
  uri_template="test://{x}",
608
  name="test",
609
  )
610
- assert template.context_kwarg == "ctx"
611
 
612
  async def test_context_injection(self):
613
  """Test that context is properly injected during resource creation."""
@@ -621,18 +611,18 @@ class TestContextHandling:
621
  uri_template="test://{x}",
622
  name="test",
623
  )
624
- assert template.context_kwarg == "ctx"
625
 
626
  from fastmcp import FastMCP
627
 
628
  mcp = FastMCP()
629
- ctx = mcp.get_context()
 
 
 
 
 
 
630
 
631
- resource = await template.create_resource(
632
- "test://42",
633
- {"x": 42},
634
- context=ctx,
635
- )
636
  assert isinstance(resource, FunctionResource)
637
  content = await resource.read()
638
  assert content == "42"
@@ -648,13 +638,19 @@ class TestContextHandling:
648
  uri_template="test://{x}",
649
  name="test",
650
  )
651
- assert template.context_kwarg == "ctx"
652
 
653
- # Should not raise an error when context is not provided
654
- resource = await template.create_resource(
655
- "test://42",
656
- {"x": 42},
657
- )
 
 
 
 
 
 
 
658
  assert isinstance(resource, FunctionResource)
659
  content = await resource.read()
660
  assert content == "42"
 
2
  from urllib.parse import quote
3
 
4
  import pytest
 
 
5
  from pydantic import BaseModel
6
 
7
  from fastmcp import Context
 
558
  def template_with_context(x: int, ctx: Context) -> str:
559
  return str(x)
560
 
561
+ ResourceTemplate.from_function(
562
  fn=template_with_context,
563
  uri_template="test://{x}",
564
  name="test",
565
  )
 
566
 
567
  def template_without_context(x: int) -> str:
568
  return str(x)
569
 
570
+ ResourceTemplate.from_function(
571
  fn=template_without_context,
572
  uri_template="test://{x}",
573
  name="test",
574
  )
 
575
 
576
  def test_parameterized_context_parameter_detection(self):
577
  """Test that parameterized context parameters are properly detected in
578
  ResourceTemplate.from_function()."""
579
 
580
+ def template_with_context(x: int, ctx: Context) -> str:
 
 
581
  return str(x)
582
 
583
+ ResourceTemplate.from_function(
584
  fn=template_with_context,
585
  uri_template="test://{x}",
586
  name="test",
587
  )
 
588
 
589
  def test_parameterized_union_context_parameter_detection(self):
590
  """Test that context parameters in a union are properly detected in
591
  ResourceTemplate.from_function()."""
592
 
593
+ def template_with_context(x: int, ctx: Context | None) -> str:
 
 
594
  return str(x)
595
 
596
+ ResourceTemplate.from_function(
597
  fn=template_with_context,
598
  uri_template="test://{x}",
599
  name="test",
600
  )
 
601
 
602
  async def test_context_injection(self):
603
  """Test that context is properly injected during resource creation."""
 
611
  uri_template="test://{x}",
612
  name="test",
613
  )
 
614
 
615
  from fastmcp import FastMCP
616
 
617
  mcp = FastMCP()
618
+ context = Context(fastmcp=mcp)
619
+
620
+ with context:
621
+ resource = await template.create_resource(
622
+ "test://42",
623
+ {"x": 42},
624
+ )
625
 
 
 
 
 
 
626
  assert isinstance(resource, FunctionResource)
627
  content = await resource.read()
628
  assert content == "42"
 
638
  uri_template="test://{x}",
639
  name="test",
640
  )
 
641
 
642
+ # Even for optional context, we need to provide a context
643
+ from fastmcp import FastMCP
644
+
645
+ mcp = FastMCP()
646
+ context = Context(fastmcp=mcp)
647
+
648
+ with context:
649
+ resource = await template.create_resource(
650
+ "test://42",
651
+ {"x": 42},
652
+ )
653
+
654
  assert isinstance(resource, FunctionResource)
655
  content = await resource.read()
656
  assert content == "42"
tests/tools/test_tool_manager.py CHANGED
@@ -5,8 +5,6 @@ from typing import Annotated, Any
5
 
6
  import pydantic_core
7
  import pytest
8
- from mcp.server.session import ServerSessionT
9
- from mcp.shared.context import LifespanContextT
10
  from mcp.types import ImageContent, TextContent
11
  from pydantic import BaseModel
12
 
@@ -403,10 +401,20 @@ class TestCallTools:
403
  manager = ToolManager()
404
  manager.add_tool_from_fn(name_shrimp)
405
 
406
- result = await manager.call_tool(
407
- "name_shrimp",
408
- {"tank": {"x": None, "shrimp": [{"name": "rex"}, {"name": "gertrude"}]}},
409
- )
 
 
 
 
 
 
 
 
 
 
410
  assert isinstance(result, list)
411
  assert len(result) == 1
412
  assert isinstance(result[0], TextContent)
@@ -498,14 +506,12 @@ class TestContextHandling:
498
  return str(x)
499
 
500
  manager = ToolManager()
501
- tool = manager.add_tool_from_fn(tool_with_context)
502
- assert tool.context_kwarg == "ctx"
503
 
504
  def tool_without_context(x: int) -> str:
505
  return str(x)
506
 
507
- tool = manager.add_tool_from_fn(tool_without_context)
508
- assert tool.context_kwarg is None
509
 
510
  async def test_context_injection(self):
511
  """Test that context is properly injected during tool execution."""
@@ -515,16 +521,17 @@ class TestContextHandling:
515
  return str(x)
516
 
517
  manager = ToolManager()
518
- tool = manager.add_tool_from_fn(tool_with_context)
519
- assert tool.context_kwarg == "ctx"
520
 
521
  mcp = FastMCP()
522
- ctx = mcp.get_context()
523
- result = await manager.call_tool("tool_with_context", {"x": 42}, context=ctx)
524
- assert isinstance(result, list)
525
- assert len(result) == 1
526
- assert isinstance(result[0], TextContent)
527
- assert result[0].text == "42"
 
 
528
 
529
  async def test_context_injection_async(self):
530
  """Test that context is properly injected in async tools."""
@@ -534,16 +541,17 @@ class TestContextHandling:
534
  return str(x)
535
 
536
  manager = ToolManager()
537
- tool = manager.add_tool_from_fn(async_tool)
538
- assert tool.context_kwarg == "ctx"
539
 
540
  mcp = FastMCP()
541
- ctx = mcp.get_context()
542
- result = await manager.call_tool("async_tool", {"x": 42}, context=ctx)
543
- assert isinstance(result, list)
544
- assert len(result) == 1
545
- assert isinstance(result[0], TextContent)
546
- assert result[0].text == "42"
 
 
547
 
548
  async def test_context_optional(self):
549
  """Test that context is optional when calling tools."""
@@ -553,48 +561,45 @@ class TestContextHandling:
553
  return x
554
 
555
  manager = ToolManager()
556
- tool = manager.add_tool_from_fn(tool_with_context)
557
- assert tool.context_kwarg == "ctx"
558
  # Should not raise an error when context is not provided
559
- result = await manager.call_tool("tool_with_context", {"x": 42})
560
- assert isinstance(result, list)
561
- assert len(result) == 1
562
- assert isinstance(result[0], TextContent)
563
- assert result[0].text == "42"
 
 
 
 
 
564
 
565
  def test_parameterized_context_parameter_detection(self):
566
  """Test that context parameters are properly detected in
567
  Tool.from_function()."""
568
 
569
- def tool_with_context(
570
- x: int, ctx: Context[ServerSessionT, LifespanContextT]
571
- ) -> str:
572
  return str(x)
573
 
574
  manager = ToolManager()
575
- tool = manager.add_tool_from_fn(tool_with_context)
576
- assert tool.context_kwarg == "ctx"
577
 
578
  def test_annotated_context_parameter_detection(self):
579
  def tool_with_context(x: int, ctx: Annotated[Context, "ctx"]) -> str:
580
  return str(x)
581
 
582
  manager = ToolManager()
583
- tool = manager.add_tool_from_fn(tool_with_context)
584
- assert tool.context_kwarg == "ctx"
585
 
586
  def test_parameterized_union_context_parameter_detection(self):
587
  """Test that context parameters are properly detected in
588
  Tool.from_function()."""
589
 
590
- def tool_with_context(
591
- x: int, ctx: Context[ServerSessionT, LifespanContextT] | None
592
- ) -> str:
593
  return str(x)
594
 
595
  manager = ToolManager()
596
- tool = manager.add_tool_from_fn(tool_with_context)
597
- assert tool.context_kwarg == "ctx"
598
 
599
  async def test_context_error_handling(self):
600
  """Test error handling when context injection fails."""
@@ -606,9 +611,13 @@ class TestContextHandling:
606
  manager.add_tool_from_fn(tool_with_context)
607
 
608
  mcp = FastMCP()
609
- ctx = mcp.get_context()
610
- with pytest.raises(ToolError, match="Error executing tool tool_with_context"):
611
- await manager.call_tool("tool_with_context", {"x": 42}, context=ctx)
 
 
 
 
612
 
613
 
614
  class TestCustomToolNames:
 
5
 
6
  import pydantic_core
7
  import pytest
 
 
8
  from mcp.types import ImageContent, TextContent
9
  from pydantic import BaseModel
10
 
 
401
  manager = ToolManager()
402
  manager.add_tool_from_fn(name_shrimp)
403
 
404
+ mcp = FastMCP()
405
+ context = Context(fastmcp=mcp)
406
+
407
+ with context:
408
+ result = await manager.call_tool(
409
+ "name_shrimp",
410
+ {
411
+ "tank": {
412
+ "x": None,
413
+ "shrimp": [{"name": "rex"}, {"name": "gertrude"}],
414
+ }
415
+ },
416
+ )
417
+
418
  assert isinstance(result, list)
419
  assert len(result) == 1
420
  assert isinstance(result[0], TextContent)
 
506
  return str(x)
507
 
508
  manager = ToolManager()
509
+ manager.add_tool_from_fn(tool_with_context)
 
510
 
511
  def tool_without_context(x: int) -> str:
512
  return str(x)
513
 
514
+ manager.add_tool_from_fn(tool_without_context)
 
515
 
516
  async def test_context_injection(self):
517
  """Test that context is properly injected during tool execution."""
 
521
  return str(x)
522
 
523
  manager = ToolManager()
524
+ manager.add_tool_from_fn(tool_with_context)
 
525
 
526
  mcp = FastMCP()
527
+ context = Context(fastmcp=mcp)
528
+
529
+ with context:
530
+ result = await manager.call_tool("tool_with_context", {"x": 42})
531
+ assert isinstance(result, list)
532
+ assert len(result) == 1
533
+ assert isinstance(result[0], TextContent)
534
+ assert result[0].text == "42"
535
 
536
  async def test_context_injection_async(self):
537
  """Test that context is properly injected in async tools."""
 
541
  return str(x)
542
 
543
  manager = ToolManager()
544
+ manager.add_tool_from_fn(async_tool)
 
545
 
546
  mcp = FastMCP()
547
+ context = Context(fastmcp=mcp)
548
+
549
+ with context:
550
+ result = await manager.call_tool("async_tool", {"x": 42})
551
+ assert isinstance(result, list)
552
+ assert len(result) == 1
553
+ assert isinstance(result[0], TextContent)
554
+ assert result[0].text == "42"
555
 
556
  async def test_context_optional(self):
557
  """Test that context is optional when calling tools."""
 
561
  return x
562
 
563
  manager = ToolManager()
564
+ manager.add_tool_from_fn(tool_with_context)
 
565
  # Should not raise an error when context is not provided
566
+
567
+ mcp = FastMCP()
568
+ context = Context(fastmcp=mcp)
569
+
570
+ with context:
571
+ result = await manager.call_tool("tool_with_context", {"x": 42})
572
+ assert isinstance(result, list)
573
+ assert len(result) == 1
574
+ assert isinstance(result[0], TextContent)
575
+ assert result[0].text == "42"
576
 
577
  def test_parameterized_context_parameter_detection(self):
578
  """Test that context parameters are properly detected in
579
  Tool.from_function()."""
580
 
581
+ def tool_with_context(x: int, ctx: Context) -> str:
 
 
582
  return str(x)
583
 
584
  manager = ToolManager()
585
+ manager.add_tool_from_fn(tool_with_context)
 
586
 
587
  def test_annotated_context_parameter_detection(self):
588
  def tool_with_context(x: int, ctx: Annotated[Context, "ctx"]) -> str:
589
  return str(x)
590
 
591
  manager = ToolManager()
592
+ manager.add_tool_from_fn(tool_with_context)
 
593
 
594
  def test_parameterized_union_context_parameter_detection(self):
595
  """Test that context parameters are properly detected in
596
  Tool.from_function()."""
597
 
598
+ def tool_with_context(x: int, ctx: Context | None) -> str:
 
 
599
  return str(x)
600
 
601
  manager = ToolManager()
602
+ manager.add_tool_from_fn(tool_with_context)
 
603
 
604
  async def test_context_error_handling(self):
605
  """Test error handling when context injection fails."""
 
611
  manager.add_tool_from_fn(tool_with_context)
612
 
613
  mcp = FastMCP()
614
+ context = Context(fastmcp=mcp)
615
+
616
+ with context:
617
+ with pytest.raises(
618
+ ToolError, match="Error executing tool tool_with_context"
619
+ ):
620
+ await manager.call_tool("tool_with_context", {"x": 42})
621
 
622
 
623
  class TestCustomToolNames: