Jeremiah Lowin commited on
Commit
6d5c6bc
·
unverified ·
2 Parent(s): a348840df7a6d4

Merge pull request #263 from jlowin/context

Browse files

Support context injection in resources, templates, and prompts (like tools)

docs/servers/context.mdx CHANGED
@@ -1,16 +1,16 @@
1
  ---
2
  title: MCP Context
3
  sidebarTitle: Context
4
- description: Access MCP capabilities like logging, progress, and resources within your tools.
5
  icon: rectangle-code
6
  ---
7
  import { VersionBadge } from '/snippets/version-badge.mdx'
8
 
9
- When defining FastMCP [tools](/servers/tools), your functions might need to interact with the underlying MCP session or access server capabilities. FastMCP provides the `Context` object for this purpose.
10
 
11
  ## What Is Context?
12
 
13
- The `Context` object provides a clean interface to access MCP features within your tool functions, including:
14
 
15
  - **Logging**: Send debug, info, warning, and error messages back to the client
16
  - **Progress Reporting**: Update the client on the progress of long-running operations
@@ -21,7 +21,7 @@ The `Context` object provides a clean interface to access MCP features within yo
21
 
22
  ## Accessing the Context
23
 
24
- To use the context object within your tool function, simply add a parameter to your function signature and type-hint it as `Context`. FastMCP will automatically inject the context instance when your tool is called.
25
 
26
  ```python
27
  from fastmcp import FastMCP, Context
@@ -65,15 +65,15 @@ async def process_file(file_uri: str, ctx: Context) -> str:
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 - tools that don't need it can omit the parameter.
69
- - Context is only available within tool functions during a request; attempting to use context methods outside a request will raise errors.
70
- - Context methods are async, so your tool function usually needs to be async as well.
71
 
72
  ## Context Capabilities
73
 
74
  ### Logging
75
 
76
- Send log messages back to the MCP client. This is useful for debugging and providing visibility into tool execution during a request.
77
 
78
  ```python
79
  @mcp.tool()
@@ -97,14 +97,14 @@ async def analyze_data(data: list[float], ctx: Context) -> dict:
97
  **Available Logging Methods:**
98
 
99
  - **`ctx.debug(message: str)`**: Low-level details useful for debugging
100
- - **`ctx.info(message: str)`**: General information about tool execution
101
  - **`ctx.warning(message: str)`**: Potential issues that didn't prevent execution
102
  - **`ctx.error(message: str)`**: Errors that occurred during execution
103
  - **`ctx.log(level: Literal["debug", "info", "warning", "error"], message: str, logger_name: str | None = None)`**: Generic log method supporting custom logger names
104
 
105
  ### Progress Reporting
106
 
107
- For long-running tools, notify the client about the progress of the operation. This allows clients to display progress indicators and provide a better user experience.
108
 
109
  ```python
110
  @mcp.tool()
@@ -137,7 +137,7 @@ Progress reporting requires the client to have sent a `progressToken` in the ini
137
 
138
  ### Resource Access
139
 
140
- Read data from resources registered with your FastMCP server. This allows tools to access files, configuration, or dynamically generated content.
141
 
142
  ```python
143
  @mcp.tool()
@@ -177,7 +177,7 @@ The returned content is typically accessed via `content_list[0].content` and can
177
 
178
  <VersionBadge version="2.0.0" />
179
 
180
- Request the client's LLM to generate text based on provided messages. This is useful when your tool needs to leverage the LLM's capabilities to process data or generate responses.
181
 
182
  ```python
183
  @mcp.tool()
@@ -279,6 +279,60 @@ async def advanced_tool(ctx: Context) -> str:
279
  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.
280
  </Warning>
281
 
282
- ## Using Context in Other Components
283
 
284
- Currently, Context is primarily designed for use within tool functions. Support for Context in other components like resources and prompts is planned for future releases.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: MCP Context
3
  sidebarTitle: Context
4
+ description: Access MCP capabilities like logging, progress, and resources within your MCP objects.
5
  icon: rectangle-code
6
  ---
7
  import { VersionBadge } from '/snippets/version-badge.mdx'
8
 
9
+ When defining FastMCP [tools](/servers/tools), [resources](/servers/resources), resource templates, or [prompts](/servers/prompts), your functions might need to interact with the underlying MCP session or access server capabilities. FastMCP provides the `Context` object for this purpose.
10
 
11
  ## What Is Context?
12
 
13
+ The `Context` object provides a clean interface to access MCP features within your functions, including:
14
 
15
  - **Logging**: Send debug, info, warning, and error messages back to the client
16
  - **Progress Reporting**: Update the client on the progress of long-running operations
 
21
 
22
  ## Accessing the Context
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
 
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
 
74
  ### Logging
75
 
76
+ Send log messages back to the MCP client. This is useful for debugging and providing visibility into function execution during a request.
77
 
78
  ```python
79
  @mcp.tool()
 
97
  **Available Logging Methods:**
98
 
99
  - **`ctx.debug(message: str)`**: Low-level details useful for debugging
100
+ - **`ctx.info(message: str)`**: General information about execution
101
  - **`ctx.warning(message: str)`**: Potential issues that didn't prevent execution
102
  - **`ctx.error(message: str)`**: Errors that occurred during execution
103
  - **`ctx.log(level: Literal["debug", "info", "warning", "error"], message: str, logger_name: str | None = None)`**: Generic log method supporting custom logger names
104
 
105
  ### Progress Reporting
106
 
107
+ For long-running operations, notify the client about the progress. This allows clients to display progress indicators and provide a better user experience.
108
 
109
  ```python
110
  @mcp.tool()
 
137
 
138
  ### Resource Access
139
 
140
+ Read data from resources registered with your FastMCP server. This allows functions to access files, configuration, or dynamically generated content.
141
 
142
  ```python
143
  @mcp.tool()
 
177
 
178
  <VersionBadge version="2.0.0" />
179
 
180
+ Request the client's LLM to generate text based on provided messages. This is useful when your function needs to leverage the LLM's capabilities to process data or generate responses.
181
 
182
  ```python
183
  @mcp.tool()
 
279
  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.
280
  </Warning>
281
 
282
+ ## Using Context in Different Components
283
 
284
+ 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.
285
+
286
+ ### Context in Resources and Templates
287
+
288
+ Resources and resource templates can access context to customize their behavior:
289
+
290
+ ```python
291
+ @mcp.resource("resource://user-data")
292
+ async def get_user_data(ctx: Context) -> dict:
293
+ """Fetch personalized user data based on the request context."""
294
+ user_id = ctx.client_id or "anonymous"
295
+ await ctx.info(f"Fetching data for user {user_id}")
296
+
297
+ # Example of using context for dynamic resource generation
298
+ return {
299
+ "user_id": user_id,
300
+ "last_access": datetime.now().isoformat(),
301
+ "request_id": ctx.request_id
302
+ }
303
+
304
+ @mcp.resource("resource://users/{user_id}/profile")
305
+ async def get_user_profile(user_id: str, ctx: Context) -> dict:
306
+ """Fetch user profile from database with context-aware logging."""
307
+ await ctx.info(f"Fetching profile for user {user_id}")
308
+
309
+ # Example of using context in a template resource
310
+ # In a real implementation, you might query a database
311
+ return {
312
+ "id": user_id,
313
+ "name": f"User {user_id}",
314
+ "request_id": ctx.request_id
315
+ }
316
+ ```
317
+
318
+ ### Context in Prompts
319
+
320
+ Prompts can use context to generate more dynamic templates:
321
+
322
+ ```python
323
+ @mcp.prompt()
324
+ async def data_analysis_request(dataset: str, ctx: Context) -> str:
325
+ """Generate a request to analyze data with contextual information."""
326
+ await ctx.info(f"Generating data analysis prompt for {dataset}")
327
+
328
+ # Could use context to read configuration or personalize the prompt
329
+ return f"""Please analyze the following dataset: {dataset}
330
+
331
+ Request initiated at: {datetime.now().isoformat()}
332
+ Request ID: {ctx.request_id}
333
+ """
334
+ ```
335
+
336
+ <VersionBadge version="2.3.0" />
337
+
338
+ 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.
docs/servers/prompts.mdx CHANGED
@@ -171,33 +171,24 @@ async def data_based_prompt(data_id: str) -> str:
171
 
172
  Use `async def` when your prompt function performs I/O operations like network requests, database queries, file I/O, or external service calls.
173
 
174
- ### The MCP Session
175
 
176
- Prompts can access the MCP features via the `Context` object, just like tools.
177
 
178
- ```python
179
- from fastmcp import Context
 
 
 
 
180
 
181
  @mcp.prompt()
182
  async def generate_report_request(report_type: str, ctx: Context) -> str:
183
- """Generates a request for a report based on available data."""
184
- # Log the request
185
- await ctx.info(f"Generating prompt for report type: {report_type}")
186
-
187
- # Could potentially use ctx.read_resource to fetch data
188
- # Or ctx.sample to get additional input from the LLM
189
-
190
- return f"Please create a {report_type} report based on the available data."
191
  ```
192
 
193
- Using the `ctx` parameter (based on its `Context` type hint), you can access:
194
-
195
- - **Logging:** `ctx.debug()`, `ctx.info()`, etc.
196
- - **Resource Access:** `ctx.read_resource(uri)`
197
- - **LLM Sampling:** `ctx.sample(...)`
198
- - **Request Info:** `ctx.request_id`, `ctx.client_id`
199
-
200
- Refer to the [Context documentation](/servers/context) for more details on these capabilities.
201
 
202
  ## Server Behavior
203
 
 
171
 
172
  Use `async def` when your prompt function performs I/O operations like network requests, database queries, file I/O, or external service calls.
173
 
174
+ ### Accessing MCP Context
175
 
176
+ <VersionBadge version="2.2.5" />
177
 
178
+ Prompts can access additional MCP information and features through the `Context` object. To access it, add a parameter to your prompt function with a type annotation of `Context`:
179
+
180
+ ```python {6}
181
+ from fastmcp import FastMCP, Context
182
+
183
+ mcp = FastMCP(name="PromptServer")
184
 
185
  @mcp.prompt()
186
  async def generate_report_request(report_type: str, ctx: Context) -> str:
187
+ """Generates a request for a report."""
188
+ return f"Please create a {report_type} report. Request ID: {ctx.request_id}"
 
 
 
 
 
 
189
  ```
190
 
191
+ For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
 
 
 
 
 
 
 
192
 
193
  ## Server Behavior
194
 
docs/servers/resources.mdx CHANGED
@@ -95,6 +95,36 @@ def get_application_status() -> dict:
95
  - **`mime_type`**: Specifies the content type (FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types).
96
  - **`tags`**: A set of strings for categorization, potentially used by clients for filtering.
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  ### Asynchronous Resources
100
 
@@ -205,6 +235,8 @@ Note that this parameter is only available when using `add_resource()` directly
205
 
206
  Resource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature.
207
 
 
 
208
  Resource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `"user://profile/{name}"` is registered, MCP clients could request `"user://profile/ford"` or `"user://profile/marvin"` to retrieve either of those two user profiles as resources, without having to register each resource individually.
209
 
210
  Here is a complete example that shows how to define two resource templates:
@@ -379,28 +411,6 @@ In this stacked decorator pattern:
379
 
380
  Templates provide a powerful way to expose parameterized data access points following REST-like principles.
381
 
382
- ### Custom Template Keys
383
-
384
- <VersionBadge version="2.2.0" />
385
-
386
- Similar to resources, you can provide custom keys when directly adding templates:
387
-
388
- ```python
389
- from fastmcp.resources import ResourceTemplate
390
-
391
- # Create a template with a function
392
- template = ResourceTemplate.from_function(
393
- my_function,
394
- uri_template="data://{id}/details",
395
- name="Data Details"
396
- )
397
-
398
- # Register with a custom key
399
- mcp._resource_manager.add_template(template, key="custom://{id}/view")
400
- ```
401
-
402
- This allows accessing the same template implementation through different URI patterns.
403
-
404
  ## Server Behavior
405
 
406
  ### Duplicate Resources
 
95
  - **`mime_type`**: Specifies the content type (FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types).
96
  - **`tags`**: A set of strings for categorization, potentially used by clients for filtering.
97
 
98
+ ### Accessing MCP Context
99
+
100
+ <VersionBadge version="2.2.5" />
101
+
102
+ Resources and resource templates can access additional MCP information and features through the `Context` object. To access it, add a parameter to your resource function with a type annotation of `Context`:
103
+
104
+ ```python {6, 14}
105
+ from fastmcp import FastMCP, Context
106
+
107
+ mcp = FastMCP(name="DataServer")
108
+
109
+ @mcp.resource("resource://system-status")
110
+ async def get_system_status(ctx: Context) -> dict:
111
+ """Provides system status information."""
112
+ return {
113
+ "status": "operational",
114
+ "request_id": ctx.request_id
115
+ }
116
+
117
+ @mcp.resource("resource://{name}/details")
118
+ async def get_details(name: str, ctx: Context) -> dict:
119
+ """Get details for a specific name."""
120
+ return {
121
+ "name": name,
122
+ "accessed_at": ctx.request_id
123
+ }
124
+ ```
125
+
126
+ For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
127
+
128
 
129
  ### Asynchronous Resources
130
 
 
235
 
236
  Resource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature.
237
 
238
+ Resource templates share most configuration options with regular resources (name, description, mime_type, tags), but add the ability to define URI parameters that map to function parameters.
239
+
240
  Resource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `"user://profile/{name}"` is registered, MCP clients could request `"user://profile/ford"` or `"user://profile/marvin"` to retrieve either of those two user profiles as resources, without having to register each resource individually.
241
 
242
  Here is a complete example that shows how to define two resource templates:
 
411
 
412
  Templates provide a powerful way to expose parameterized data access points following REST-like principles.
413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  ## Server Behavior
415
 
416
  ### Duplicate Resources
docs/servers/tools.mdx CHANGED
@@ -263,7 +263,8 @@ FastMCP automatically catches exceptions raised within your tool function:
263
 
264
  Using informative exceptions helps the LLM understand failures and react appropriately.
265
 
266
- ### Accessing MCP Context
 
267
 
268
  Tools can access MCP features like logging, reading resources, or reporting progress through the `Context` object. To use it, add a parameter to your tool function with the type hint `Context`.
269
 
@@ -304,39 +305,6 @@ The Context object provides access to:
304
 
305
  For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
306
 
307
- ## Server Behavior
308
-
309
- ### Duplicate Tools
310
-
311
- <VersionBadge version="2.1.0" />
312
-
313
- You can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance.
314
-
315
- ```python
316
- from fastmcp import FastMCP
317
-
318
- mcp = FastMCP(
319
- name="StrictServer",
320
- # Configure behavior for duplicate tool names
321
- on_duplicate_tools="error"
322
- )
323
-
324
- @mcp.tool()
325
- def my_tool(): return "Version 1"
326
-
327
- # This will now raise a ValueError because 'my_tool' already exists
328
- # and on_duplicate_tools is set to "error".
329
- # @mcp.tool()
330
- # def my_tool(): return "Version 2"
331
- ```
332
-
333
- The duplicate behavior options are:
334
-
335
- - `"warn"` (default): Logs a warning and the new tool replaces the old one.
336
- - `"error"`: Raises a `ValueError`, preventing the duplicate registration.
337
- - `"replace"`: Silently replaces the existing tool with the new one.
338
- - `"ignore"`: Keeps the original tool and ignores the new registration attempt.
339
-
340
  ## Parameter Types
341
 
342
  FastMCP supports a wide variety of parameter types to give you flexibility when designing your tools.
@@ -663,3 +631,36 @@ Common validation options include:
663
  | `description` | Any | Human-readable description (appears in schema) |
664
 
665
  When a client sends invalid data, FastMCP will return a validation error explaining why the parameter failed validation.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
  Using informative exceptions helps the LLM understand failures and react appropriately.
265
 
266
+ ## MCP Context
267
+
268
 
269
  Tools can access MCP features like logging, reading resources, or reporting progress through the `Context` object. To use it, add a parameter to your tool function with the type hint `Context`.
270
 
 
305
 
306
  For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
307
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  ## Parameter Types
309
 
310
  FastMCP supports a wide variety of parameter types to give you flexibility when designing your tools.
 
631
  | `description` | Any | Human-readable description (appears in schema) |
632
 
633
  When a client sends invalid data, FastMCP will return a validation error explaining why the parameter failed validation.
634
+
635
+ ## Server Behavior
636
+
637
+ ### Duplicate Tools
638
+
639
+ <VersionBadge version="2.1.0" />
640
+
641
+ You can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance.
642
+
643
+ ```python
644
+ from fastmcp import FastMCP
645
+
646
+ mcp = FastMCP(
647
+ name="StrictServer",
648
+ # Configure behavior for duplicate tool names
649
+ on_duplicate_tools="error"
650
+ )
651
+
652
+ @mcp.tool()
653
+ def my_tool(): return "Version 1"
654
+
655
+ # This will now raise a ValueError because 'my_tool' already exists
656
+ # and on_duplicate_tools is set to "error".
657
+ # @mcp.tool()
658
+ # def my_tool(): return "Version 2"
659
+ ```
660
+
661
+ The duplicate behavior options are:
662
+
663
+ - `"warn"` (default): Logs a warning and the new tool replaces the old one.
664
+ - `"error"`: Raises a `ValueError`, preventing the duplicate registration.
665
+ - `"replace"`: Silently replaces the existing tool with the new one.
666
+ - `"ignore"`: Keeps the original tool and ignores the new registration attempt.
src/fastmcp/prompts/prompt.py CHANGED
@@ -1,9 +1,11 @@
1
  """Base classes for FastMCP prompts."""
2
 
 
 
3
  import inspect
4
  import json
5
  from collections.abc import Awaitable, Callable, Sequence
6
- from typing import Annotated, Any, Literal
7
 
8
  import pydantic_core
9
  from mcp.types import EmbeddedResource, ImageContent, TextContent
@@ -13,6 +15,12 @@ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_ca
13
 
14
  from fastmcp.utilities.types import _convert_set_defaults
15
 
 
 
 
 
 
 
16
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
17
 
18
 
@@ -72,6 +80,9 @@ class Prompt(BaseModel):
72
  None, description="Arguments that can be passed to the prompt"
73
  )
74
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
 
 
 
75
 
76
  @classmethod
77
  def from_function(
@@ -80,7 +91,8 @@ class Prompt(BaseModel):
80
  name: str | None = None,
81
  description: str | None = None,
82
  tags: set[str] | None = None,
83
- ) -> "Prompt":
 
84
  """Create a Prompt from a function.
85
 
86
  The function can return:
@@ -89,11 +101,24 @@ class Prompt(BaseModel):
89
  - A dict (converted to a message)
90
  - A sequence of any of the above
91
  """
 
 
92
  func_name = name or fn.__name__
93
 
94
  if func_name == "<lambda>":
95
  raise ValueError("You must provide a name for lambda functions")
96
 
 
 
 
 
 
 
 
 
 
 
 
97
  # Get schema from TypeAdapter - will fail if function isn't properly typed
98
  parameters = TypeAdapter(fn).json_schema()
99
 
@@ -101,6 +126,10 @@ class Prompt(BaseModel):
101
  arguments: list[PromptArgument] = []
102
  if "properties" in parameters:
103
  for param_name, param in parameters["properties"].items():
 
 
 
 
104
  required = param_name in parameters.get("required", [])
105
  arguments.append(
106
  PromptArgument(
@@ -119,9 +148,14 @@ class Prompt(BaseModel):
119
  arguments=arguments,
120
  fn=fn,
121
  tags=tags or set(),
 
122
  )
123
 
124
- async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
 
 
 
 
125
  """Render the prompt with arguments."""
126
  # Validate required arguments
127
  if self.arguments:
@@ -132,8 +166,13 @@ class Prompt(BaseModel):
132
  raise ValueError(f"Missing required arguments: {missing}")
133
 
134
  try:
 
 
 
 
 
135
  # Call function and check if result is a coroutine
136
- result = self.fn(**(arguments or {}))
137
  if inspect.iscoroutine(result):
138
  result = await result
139
 
 
1
  """Base classes for FastMCP prompts."""
2
 
3
+ from __future__ import annotations as _annotations
4
+
5
  import inspect
6
  import json
7
  from collections.abc import Awaitable, Callable, Sequence
8
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
9
 
10
  import pydantic_core
11
  from mcp.types import EmbeddedResource, ImageContent, TextContent
 
15
 
16
  from fastmcp.utilities.types import _convert_set_defaults
17
 
18
+ if TYPE_CHECKING:
19
+ from mcp.server.session import ServerSessionT
20
+ from mcp.shared.context import LifespanContextT
21
+
22
+ from fastmcp.server import Context
23
+
24
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
25
 
26
 
 
80
  None, description="Arguments that can be passed to the prompt"
81
  )
82
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
83
+ context_kwarg: str | None = Field(
84
+ None, description="Name of the kwarg that should receive context"
85
+ )
86
 
87
  @classmethod
88
  def from_function(
 
91
  name: str | None = None,
92
  description: str | None = None,
93
  tags: set[str] | None = None,
94
+ context_kwarg: str | None = None,
95
+ ) -> Prompt:
96
  """Create a Prompt from a function.
97
 
98
  The function can return:
 
101
  - A dict (converted to a message)
102
  - A sequence of any of the above
103
  """
104
+ from fastmcp import Context
105
+
106
  func_name = name or fn.__name__
107
 
108
  if func_name == "<lambda>":
109
  raise ValueError("You must provide a name for lambda functions")
110
 
111
+ # Auto-detect context parameter if not provided
112
+ if context_kwarg is None:
113
+ if inspect.ismethod(fn) and hasattr(fn, "__func__"):
114
+ sig = inspect.signature(fn.__func__)
115
+ else:
116
+ sig = inspect.signature(fn)
117
+ for param_name, param in sig.parameters.items():
118
+ if param.annotation is Context:
119
+ context_kwarg = param_name
120
+ break
121
+
122
  # Get schema from TypeAdapter - will fail if function isn't properly typed
123
  parameters = TypeAdapter(fn).json_schema()
124
 
 
126
  arguments: list[PromptArgument] = []
127
  if "properties" in parameters:
128
  for param_name, param in parameters["properties"].items():
129
+ # Skip context parameter
130
+ if param_name == context_kwarg:
131
+ continue
132
+
133
  required = param_name in parameters.get("required", [])
134
  arguments.append(
135
  PromptArgument(
 
148
  arguments=arguments,
149
  fn=fn,
150
  tags=tags or set(),
151
+ context_kwarg=context_kwarg,
152
  )
153
 
154
+ async def render(
155
+ self,
156
+ arguments: dict[str, Any] | None = None,
157
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
158
+ ) -> list[Message]:
159
  """Render the prompt with arguments."""
160
  # Validate required arguments
161
  if self.arguments:
 
166
  raise ValueError(f"Missing required arguments: {missing}")
167
 
168
  try:
169
+ # Prepare arguments with context
170
+ kwargs = arguments.copy() if arguments else {}
171
+ if self.context_kwarg is not None and context is not None:
172
+ kwargs[self.context_kwarg] = context
173
+
174
  # Call function and check if result is a coroutine
175
+ result = self.fn(**kwargs)
176
  if inspect.iscoroutine(result):
177
  result = await result
178
 
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -1,13 +1,21 @@
1
  """Prompt management functionality."""
2
 
 
 
3
  from collections.abc import Awaitable, Callable
4
- from typing import Any
5
 
6
  from fastmcp.exceptions import NotFoundError
7
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
8
  from fastmcp.settings import DuplicateBehavior
9
  from fastmcp.utilities.logging import get_logger
10
 
 
 
 
 
 
 
11
  logger = get_logger(__name__)
12
 
13
 
@@ -69,14 +77,17 @@ class PromptManager:
69
  return prompt
70
 
71
  async def render_prompt(
72
- self, name: str, arguments: dict[str, Any] | None = None
 
 
 
73
  ) -> list[Message]:
74
  """Render a prompt by name with arguments."""
75
  prompt = self.get_prompt(name)
76
  if not prompt:
77
  raise NotFoundError(f"Unknown prompt: {name}")
78
 
79
- return await prompt.render(arguments)
80
 
81
  def has_prompt(self, key: str) -> bool:
82
  """Check if a prompt exists."""
 
1
  """Prompt management functionality."""
2
 
3
+ from __future__ import annotations as _annotations
4
+
5
  from collections.abc import Awaitable, Callable
6
+ from typing import TYPE_CHECKING, Any
7
 
8
  from fastmcp.exceptions import NotFoundError
9
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
10
  from fastmcp.settings import DuplicateBehavior
11
  from fastmcp.utilities.logging import get_logger
12
 
13
+ if TYPE_CHECKING:
14
+ from mcp.server.session import ServerSessionT
15
+ from mcp.shared.context import LifespanContextT
16
+
17
+ from fastmcp.server import Context
18
+
19
  logger = get_logger(__name__)
20
 
21
 
 
77
  return prompt
78
 
79
  async def render_prompt(
80
+ self,
81
+ name: str,
82
+ arguments: dict[str, Any] | None = None,
83
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
84
  ) -> list[Message]:
85
  """Render a prompt by name with arguments."""
86
  prompt = self.get_prompt(name)
87
  if not prompt:
88
  raise NotFoundError(f"Unknown prompt: {name}")
89
 
90
+ return await prompt.render(arguments, context=context)
91
 
92
  def has_prompt(self, key: str) -> bool:
93
  """Check if a prompt exists."""
src/fastmcp/resources/resource.py CHANGED
@@ -1,7 +1,9 @@
1
  """Base classes and interfaces for FastMCP resources."""
2
 
 
 
3
  import abc
4
- from typing import Annotated, Any
5
 
6
  from mcp.types import Resource as MCPResource
7
  from pydantic import (
@@ -17,6 +19,12 @@ from pydantic import (
17
 
18
  from fastmcp.utilities.types import _convert_set_defaults
19
 
 
 
 
 
 
 
20
 
21
  class Resource(BaseModel, abc.ABC):
22
  """Base class for all resources."""
@@ -58,7 +66,9 @@ class Resource(BaseModel, abc.ABC):
58
  raise ValueError("Either name or uri must be provided")
59
 
60
  @abc.abstractmethod
61
- async def read(self) -> str | bytes:
 
 
62
  """Read the resource content."""
63
  pass
64
 
 
1
  """Base classes and interfaces for FastMCP resources."""
2
 
3
+ from __future__ import annotations
4
+
5
  import abc
6
+ from typing import TYPE_CHECKING, Annotated, Any
7
 
8
  from mcp.types import Resource as MCPResource
9
  from pydantic import (
 
19
 
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):
30
  """Base class for all resources."""
 
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
 
src/fastmcp/resources/resource_manager.py CHANGED
@@ -61,9 +61,16 @@ class ResourceManager:
61
  The added resource or template. If a resource or template with the same URI already exists,
62
  returns the existing resource or template.
63
  """
 
 
64
  # Check if this should be a template
65
  has_uri_params = "{" in uri and "}" in uri
66
- has_func_params = bool(inspect.signature(fn).parameters)
 
 
 
 
 
67
 
68
  if has_uri_params or has_func_params:
69
  return self.add_template_from_fn(
@@ -102,12 +109,12 @@ class ResourceManager:
102
  The added resource. If a resource with the same URI already exists,
103
  returns the existing resource.
104
  """
105
- resource = FunctionResource(
 
106
  uri=AnyUrl(uri),
107
  name=name,
108
  description=description,
109
  mime_type=mime_type or "text/plain",
110
- fn=fn,
111
  tags=tags or set(),
112
  )
113
  return self.add_resource(resource)
@@ -212,9 +219,13 @@ class ResourceManager:
212
  return True
213
  return False
214
 
215
- async def get_resource(self, uri: AnyUrl | str) -> Resource:
216
  """Get resource by URI, checking concrete resources first, then templates.
217
 
 
 
 
 
218
  Raises:
219
  NotFoundError: If no resource or template matching the URI is found.
220
  """
@@ -230,7 +241,11 @@ class ResourceManager:
230
  # Try to match against the storage key (which might be a custom key)
231
  if params := match_uri_template(uri_str, storage_key):
232
  try:
233
- return await template.create_resource(uri_str, params)
 
 
 
 
234
  except Exception as e:
235
  raise ValueError(f"Error creating resource from template: {e}")
236
 
 
61
  The added resource or template. If a resource or template with the same URI already exists,
62
  returns the existing resource or template.
63
  """
64
+ from fastmcp.server.context import Context
65
+
66
  # Check if this should be a template
67
  has_uri_params = "{" in uri and "}" in uri
68
+ # check if the function has any parameters (other than injected context)
69
+ has_func_params = any(
70
+ p
71
+ for p in inspect.signature(fn).parameters.values()
72
+ if p.annotation is not Context
73
+ )
74
 
75
  if has_uri_params or has_func_params:
76
  return self.add_template_from_fn(
 
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,
116
  description=description,
117
  mime_type=mime_type or "text/plain",
 
118
  tags=tags or set(),
119
  )
120
  return self.add_resource(resource)
 
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.
231
  """
 
241
  # Try to match against the storage key (which might be a custom key)
242
  if params := match_uri_template(uri_str, storage_key):
243
  try:
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}")
251
 
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 Annotated, Any
9
  from urllib.parse import unquote
10
 
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
@@ -22,6 +22,12 @@ from pydantic import (
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.utilities.types import _convert_set_defaults
24
 
 
 
 
 
 
 
25
 
26
  def build_regex(template: str) -> re.Pattern:
27
  parts = re.split(r"(\{[^}]+\})", template)
@@ -70,6 +76,9 @@ class ResourceTemplate(BaseModel):
70
  parameters: dict[str, Any] = Field(
71
  description="JSON schema for function parameters"
72
  )
 
 
 
73
 
74
  @field_validator("mime_type", mode="before")
75
  @classmethod
@@ -88,18 +97,34 @@ class ResourceTemplate(BaseModel):
88
  description: str | None = None,
89
  mime_type: str | None = None,
90
  tags: set[str] | None = None,
 
91
  ) -> ResourceTemplate:
92
  """Create a template from a function."""
 
 
93
  func_name = name or fn.__name__
94
  if func_name == "<lambda>":
95
  raise ValueError("You must provide a name for lambda functions")
96
 
 
 
 
 
 
 
 
 
 
 
 
97
  # Validate that URI params match function params
98
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
99
  if not uri_params:
100
  raise ValueError("URI template must contain at least one parameter")
101
 
102
  func_params = set(inspect.signature(fn).parameters.keys())
 
 
103
 
104
  # get the parameters that are required
105
  required_params = {
@@ -107,6 +132,8 @@ class ResourceTemplate(BaseModel):
107
  for p in func_params
108
  if inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
109
  }
 
 
110
 
111
  if not required_params.issubset(uri_params):
112
  raise ValueError(
@@ -132,17 +159,28 @@ class ResourceTemplate(BaseModel):
132
  fn=fn,
133
  parameters=parameters,
134
  tags=tags or set(),
 
135
  )
136
 
137
  def matches(self, uri: str) -> dict[str, Any] | None:
138
  """Check if URI matches template and extract parameters."""
139
  return match_uri_template(uri, self.uri_template)
140
 
141
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
 
 
 
 
 
142
  """Create a resource from the template with the given parameters."""
143
  try:
 
 
 
 
 
144
  # Call function and check if result is a coroutine
145
- result = self.fn(**params)
146
  if inspect.iscoroutine(result):
147
  result = await result
148
 
@@ -151,8 +189,9 @@ class ResourceTemplate(BaseModel):
151
  name=self.name,
152
  description=self.description,
153
  mime_type=self.mime_type,
154
- fn=lambda: result, # Capture result in closure
155
  tags=self.tags,
 
156
  )
157
  except Exception as e:
158
  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 TYPE_CHECKING, Annotated, Any
9
  from urllib.parse import unquote
10
 
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
 
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.utilities.types import _convert_set_defaults
24
 
25
+ if TYPE_CHECKING:
26
+ from mcp.server.session import ServerSessionT
27
+ from mcp.shared.context import LifespanContextT
28
+
29
+ from fastmcp.server import Context
30
+
31
 
32
  def build_regex(template: str) -> re.Pattern:
33
  parts = re.split(r"(\{[^}]+\})", template)
 
76
  parameters: dict[str, Any] = Field(
77
  description="JSON schema for function parameters"
78
  )
79
+ context_kwarg: str | None = Field(
80
+ None, description="Name of the kwarg that should receive context"
81
+ )
82
 
83
  @field_validator("mime_type", mode="before")
84
  @classmethod
 
97
  description: str | None = None,
98
  mime_type: str | None = None,
99
  tags: set[str] | None = None,
100
+ context_kwarg: str | None = None,
101
  ) -> ResourceTemplate:
102
  """Create a template from a function."""
103
+ from fastmcp import Context
104
+
105
  func_name = name or fn.__name__
106
  if func_name == "<lambda>":
107
  raise ValueError("You must provide a name for lambda functions")
108
 
109
+ # Auto-detect context parameter if not provided
110
+ if context_kwarg is None:
111
+ if inspect.ismethod(fn) and hasattr(fn, "__func__"):
112
+ sig = inspect.signature(fn.__func__)
113
+ else:
114
+ sig = inspect.signature(fn)
115
+ for param_name, param in sig.parameters.items():
116
+ if param.annotation is Context:
117
+ context_kwarg = param_name
118
+ break
119
+
120
  # Validate that URI params match function params
121
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
122
  if not uri_params:
123
  raise ValueError("URI template must contain at least one parameter")
124
 
125
  func_params = set(inspect.signature(fn).parameters.keys())
126
+ if context_kwarg:
127
+ func_params.discard(context_kwarg)
128
 
129
  # get the parameters that are required
130
  required_params = {
 
132
  for p in func_params
133
  if inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
134
  }
135
+ if context_kwarg and context_kwarg in required_params:
136
+ required_params.discard(context_kwarg)
137
 
138
  if not required_params.issubset(uri_params):
139
  raise ValueError(
 
159
  fn=fn,
160
  parameters=parameters,
161
  tags=tags or set(),
162
+ context_kwarg=context_kwarg,
163
  )
164
 
165
  def matches(self, uri: str) -> dict[str, Any] | None:
166
  """Check if URI matches template and extract parameters."""
167
  return match_uri_template(uri, self.uri_template)
168
 
169
+ async def create_resource(
170
+ self,
171
+ uri: str,
172
+ params: dict[str, Any],
173
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
174
+ ) -> Resource:
175
  """Create a resource from the template with the given parameters."""
176
  try:
177
+ # Add context to parameters if needed
178
+ kwargs = params.copy()
179
+ if self.context_kwarg is not None and context is not None:
180
+ kwargs[self.context_kwarg] = context
181
+
182
  # Call function and check if result is a coroutine
183
+ result = self.fn(**kwargs)
184
  if inspect.iscoroutine(result):
185
  result = await result
186
 
 
189
  name=self.name,
190
  description=self.description,
191
  mime_type=self.mime_type,
192
+ fn=lambda **kwargs: result, # Capture result in closure
193
  tags=self.tags,
194
+ context_kwarg=self.context_kwarg,
195
  )
196
  except Exception as e:
197
  raise ValueError(f"Error creating resource from template: {e}")
src/fastmcp/resources/types.py CHANGED
@@ -1,10 +1,12 @@
1
  """Concrete resource implementations."""
2
 
 
 
3
  import inspect
4
  import json
5
  from collections.abc import Callable
6
  from pathlib import Path
7
- from typing import Any
8
 
9
  import anyio
10
  import anyio.to_thread
@@ -13,15 +15,24 @@ import pydantic.json
13
  import pydantic_core
14
  from pydantic import Field, ValidationInfo
15
 
 
16
  from fastmcp.resources.resource import Resource
17
 
 
 
 
 
 
 
18
 
19
  class TextResource(Resource):
20
  """A resource that reads from a string."""
21
 
22
  text: str = Field(description="Text content of the resource")
23
 
24
- async def read(self) -> str:
 
 
25
  """Read the text content."""
26
  return self.text
27
 
@@ -31,7 +42,9 @@ class BinaryResource(Resource):
31
 
32
  data: bytes = Field(description="Binary content of the resource")
33
 
34
- async def read(self) -> bytes:
 
 
35
  """Read the binary content."""
36
  return self.data
37
 
@@ -50,15 +63,40 @@ class FunctionResource(Resource):
50
  """
51
 
52
  fn: Callable[[], Any]
 
 
 
53
 
54
- async def read(self) -> str | bytes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  """Read the resource by calling the wrapped function."""
56
  try:
57
- result = (
58
- await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
59
- )
 
 
 
 
 
60
  if isinstance(result, Resource):
61
- return await result.read()
62
  if isinstance(result, bytes):
63
  return result
64
  if isinstance(result, str):
@@ -105,7 +143,9 @@ class FileResource(Resource):
105
  mime_type = info.data.get("mime_type", "text/plain")
106
  return not mime_type.startswith("text/")
107
 
108
- async def read(self) -> str | bytes:
 
 
109
  """Read the file content."""
110
  try:
111
  if self.is_binary:
@@ -123,7 +163,9 @@ class HttpResource(Resource):
123
  default="application/json", description="MIME type of the resource content"
124
  )
125
 
126
- async def read(self) -> str | bytes:
 
 
127
  """Read the HTTP content."""
128
  async with httpx.AsyncClient() as client:
129
  response = await client.get(self.url)
@@ -175,7 +217,9 @@ class DirectoryResource(Resource):
175
  except Exception as e:
176
  raise ValueError(f"Error listing directory {self.path}: {e}")
177
 
178
- async def read(self) -> str: # Always returns JSON string
 
 
179
  """Read the directory listing."""
180
  try:
181
  files = await anyio.to_thread.run_sync(self.list_files)
 
1
  """Concrete resource implementations."""
2
 
3
+ from __future__ import annotations
4
+
5
  import inspect
6
  import json
7
  from collections.abc import Callable
8
  from pathlib import Path
9
+ from typing import TYPE_CHECKING, Any
10
 
11
  import anyio
12
  import anyio.to_thread
 
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):
29
  """A resource that reads from a string."""
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
 
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
  """
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
  if isinstance(result, bytes):
101
  return result
102
  if isinstance(result, str):
 
143
  mime_type = info.data.get("mime_type", "text/plain")
144
  return not mime_type.startswith("text/")
145
 
146
+ async def read(
147
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
148
+ ) -> str | bytes:
149
  """Read the file content."""
150
  try:
151
  if self.is_binary:
 
163
  default="application/json", description="MIME type of the resource content"
164
  )
165
 
166
+ async def read(
167
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
168
+ ) -> str | bytes:
169
  """Read the HTTP content."""
170
  async with httpx.AsyncClient() as client:
171
  response = await client.get(self.url)
 
217
  except Exception as e:
218
  raise ValueError(f"Error listing directory {self.path}: {e}")
219
 
220
+ async def read(
221
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
222
+ ) -> str: # Always returns JSON string
223
  """Read the directory listing."""
224
  try:
225
  files = await anyio.to_thread.run_sync(self.list_files)
src/fastmcp/server/openapi.py CHANGED
@@ -1,11 +1,13 @@
1
  """FastMCP server implementation for OpenAPI integration."""
2
 
 
 
3
  import enum
4
  import json
5
  import re
6
  from dataclasses import dataclass
7
  from re import Pattern
8
- from typing import Any, Literal
9
 
10
  import httpx
11
  from mcp.types import TextContent
@@ -22,6 +24,12 @@ from fastmcp.utilities.openapi import (
22
  format_description_with_responses,
23
  )
24
 
 
 
 
 
 
 
25
  logger = get_logger(__name__)
26
 
27
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
@@ -257,7 +265,9 @@ class OpenAPIResource(Resource):
257
  self._client = client
258
  self._route = route
259
 
260
- async def read(self) -> str | bytes:
 
 
261
  """Fetch the resource data by making an HTTP request."""
262
  try:
263
  # Extract path parameters from the URI if present
@@ -347,11 +357,17 @@ class OpenAPIResourceTemplate(ResourceTemplate):
347
  fn=lambda **kwargs: None,
348
  parameters=parameters,
349
  tags=tags,
 
350
  )
351
  self._client = client
352
  self._route = route
353
 
354
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
 
 
 
 
 
355
  """Create a resource with the given parameters."""
356
  # Generate a URI for this resource instance
357
  uri_parts = []
 
1
  """FastMCP server implementation for OpenAPI integration."""
2
 
3
+ from __future__ import annotations
4
+
5
  import enum
6
  import json
7
  import re
8
  from dataclasses import dataclass
9
  from re import Pattern
10
+ from typing import TYPE_CHECKING, Any, Literal
11
 
12
  import httpx
13
  from mcp.types import TextContent
 
24
  format_description_with_responses,
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__)
34
 
35
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
 
265
  self._client = client
266
  self._route = route
267
 
268
+ async def read(
269
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
270
+ ) -> str | bytes:
271
  """Fetch the resource data by making an HTTP request."""
272
  try:
273
  # Extract path parameters from the URI if present
 
357
  fn=lambda **kwargs: None,
358
  parameters=parameters,
359
  tags=tags,
360
+ context_kwarg=None,
361
  )
362
  self._client = client
363
  self._route = route
364
 
365
+ async def create_resource(
366
+ self,
367
+ uri: str,
368
+ params: dict[str, Any],
369
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
370
+ ) -> Resource:
371
  """Create a resource with the given parameters."""
372
  # Generate a URI for this resource instance
373
  uri_parts = []
src/fastmcp/server/proxy.py CHANGED
@@ -1,4 +1,6 @@
1
- from typing import Any, cast
 
 
2
  from urllib.parse import quote
3
 
4
  import mcp.types
@@ -25,6 +27,12 @@ from fastmcp.tools.tool import Tool
25
  from fastmcp.utilities.func_metadata import func_metadata
26
  from fastmcp.utilities.logging import get_logger
27
 
 
 
 
 
 
 
28
  logger = get_logger(__name__)
29
 
30
 
@@ -33,12 +41,12 @@ def _proxy_passthrough():
33
 
34
 
35
  class ProxyTool(Tool):
36
- def __init__(self, client: "Client", **kwargs):
37
  super().__init__(**kwargs)
38
  self._client = client
39
 
40
  @classmethod
41
- async def from_client(cls, client: "Client", tool: mcp.types.Tool) -> "ProxyTool":
42
  return cls(
43
  client=client,
44
  name=tool.name,
@@ -50,7 +58,9 @@ class ProxyTool(Tool):
50
  )
51
 
52
  async def run(
53
- self, arguments: dict[str, Any], context: Context | None = None
 
 
54
  ) -> Any:
55
  # the client context manager will swallow any exceptions inside a TaskGroup
56
  # so we return the raw result and raise an exception ourselves
@@ -64,17 +74,15 @@ class ProxyTool(Tool):
64
 
65
 
66
  class ProxyResource(Resource):
67
- def __init__(
68
- self, client: "Client", *, _value: str | bytes | None = None, **kwargs
69
- ):
70
  super().__init__(**kwargs)
71
  self._client = client
72
  self._value = _value
73
 
74
  @classmethod
75
  async def from_client(
76
- cls, client: "Client", resource: mcp.types.Resource
77
- ) -> "ProxyResource":
78
  return cls(
79
  client=client,
80
  uri=resource.uri,
@@ -83,7 +91,9 @@ class ProxyResource(Resource):
83
  mime_type=resource.mimeType,
84
  )
85
 
86
- async def read(self) -> str | bytes:
 
 
87
  if self._value is not None:
88
  return self._value
89
 
@@ -98,14 +108,14 @@ class ProxyResource(Resource):
98
 
99
 
100
  class ProxyTemplate(ResourceTemplate):
101
- def __init__(self, client: "Client", **kwargs):
102
  super().__init__(**kwargs)
103
  self._client = client
104
 
105
  @classmethod
106
  async def from_client(
107
- cls, client: "Client", template: mcp.types.ResourceTemplate
108
- ) -> "ProxyTemplate":
109
  return cls(
110
  client=client,
111
  uri_template=template.uriTemplate,
@@ -115,7 +125,12 @@ class ProxyTemplate(ResourceTemplate):
115
  parameters={},
116
  )
117
 
118
- async def create_resource(self, uri: str, params: dict[str, Any]) -> ProxyResource:
 
 
 
 
 
119
  # dont use the provided uri, because it may not be the same as the
120
  # uri_template on the remote server.
121
  # quote params to ensure they are valid for the uri_template
@@ -144,14 +159,12 @@ class ProxyTemplate(ResourceTemplate):
144
 
145
 
146
  class ProxyPrompt(Prompt):
147
- def __init__(self, client: "Client", **kwargs):
148
  super().__init__(**kwargs)
149
  self._client = client
150
 
151
  @classmethod
152
- async def from_client(
153
- cls, client: "Client", prompt: mcp.types.Prompt
154
- ) -> "ProxyPrompt":
155
  return cls(
156
  client=client,
157
  name=prompt.name,
@@ -160,14 +173,18 @@ class ProxyPrompt(Prompt):
160
  fn=_proxy_passthrough,
161
  )
162
 
163
- async def render(self, arguments: dict[str, Any]) -> list[Message]:
 
 
 
 
164
  async with self._client:
165
  result = await self._client.get_prompt(self.name, arguments)
166
  return [Message(role=m.role, content=m.content) for m in result]
167
 
168
 
169
  class FastMCPProxy(FastMCP):
170
- def __init__(self, client: "Client", **kwargs):
171
  super().__init__(**kwargs)
172
  self.client = client
173
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, cast
4
  from urllib.parse import quote
5
 
6
  import mcp.types
 
27
  from fastmcp.utilities.func_metadata import func_metadata
28
  from fastmcp.utilities.logging import get_logger
29
 
30
+ if TYPE_CHECKING:
31
+ from mcp.server.session import ServerSessionT
32
+ from mcp.shared.context import LifespanContextT
33
+
34
+ from fastmcp.server import Context
35
+
36
  logger = get_logger(__name__)
37
 
38
 
 
41
 
42
 
43
  class ProxyTool(Tool):
44
+ def __init__(self, client: Client, **kwargs):
45
  super().__init__(**kwargs)
46
  self._client = client
47
 
48
  @classmethod
49
+ async def from_client(cls, client: Client, tool: mcp.types.Tool) -> ProxyTool:
50
  return cls(
51
  client=client,
52
  name=tool.name,
 
58
  )
59
 
60
  async def run(
61
+ self,
62
+ arguments: dict[str, Any],
63
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
64
  ) -> Any:
65
  # the client context manager will swallow any exceptions inside a TaskGroup
66
  # so we return the raw result and raise an exception ourselves
 
74
 
75
 
76
  class ProxyResource(Resource):
77
+ def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
 
 
78
  super().__init__(**kwargs)
79
  self._client = client
80
  self._value = _value
81
 
82
  @classmethod
83
  async def from_client(
84
+ cls, client: Client, resource: mcp.types.Resource
85
+ ) -> ProxyResource:
86
  return cls(
87
  client=client,
88
  uri=resource.uri,
 
91
  mime_type=resource.mimeType,
92
  )
93
 
94
+ async def read(
95
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
96
+ ) -> str | bytes:
97
  if self._value is not None:
98
  return self._value
99
 
 
108
 
109
 
110
  class ProxyTemplate(ResourceTemplate):
111
+ def __init__(self, client: Client, **kwargs):
112
  super().__init__(**kwargs)
113
  self._client = client
114
 
115
  @classmethod
116
  async def from_client(
117
+ cls, client: Client, template: mcp.types.ResourceTemplate
118
+ ) -> ProxyTemplate:
119
  return cls(
120
  client=client,
121
  uri_template=template.uriTemplate,
 
125
  parameters={},
126
  )
127
 
128
+ async def create_resource(
129
+ self,
130
+ uri: str,
131
+ params: dict[str, Any],
132
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
133
+ ) -> ProxyResource:
134
  # dont use the provided uri, because it may not be the same as the
135
  # uri_template on the remote server.
136
  # quote params to ensure they are valid for the uri_template
 
159
 
160
 
161
  class ProxyPrompt(Prompt):
162
+ def __init__(self, client: Client, **kwargs):
163
  super().__init__(**kwargs)
164
  self._client = client
165
 
166
  @classmethod
167
+ async def from_client(cls, client: Client, prompt: mcp.types.Prompt) -> ProxyPrompt:
 
 
168
  return cls(
169
  client=client,
170
  name=prompt.name,
 
173
  fn=_proxy_passthrough,
174
  )
175
 
176
+ async def render(
177
+ self,
178
+ arguments: dict[str, Any],
179
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
180
+ ) -> list[Message]:
181
  async with self._client:
182
  result = await self._client.get_prompt(self.name, arguments)
183
  return [Message(role=m.role, content=m.content) for m in result]
184
 
185
 
186
  class FastMCPProxy(FastMCP):
187
+ def __init__(self, client: Client, **kwargs):
188
  super().__init__(**kwargs)
189
  self.client = client
190
 
src/fastmcp/server/server.py CHANGED
@@ -398,9 +398,10 @@ class FastMCP(Generic[LifespanResultT]):
398
  server.
399
  """
400
  if self._resource_manager.has_resource(uri):
401
- resource = await self._resource_manager.get_resource(uri)
 
402
  try:
403
- content = await resource.read()
404
  return [
405
  ReadResourceContents(content=content, mime_type=resource.mime_type)
406
  ]
@@ -424,7 +425,10 @@ class FastMCP(Generic[LifespanResultT]):
424
 
425
  """
426
  if self._prompt_manager.has_prompt(name):
427
- messages = await self._prompt_manager.render_prompt(name, arguments)
 
 
 
428
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
429
  else:
430
  for server in self._mounted_servers.values():
@@ -562,6 +566,10 @@ class FastMCP(Generic[LifespanResultT]):
562
  - bytes for binary content
563
  - other types will be converted to JSON
564
 
 
 
 
 
565
  If the URI contains parameters (e.g. "resource://{param}") or the function
566
  has parameters, it will be registered as a template resource.
567
 
@@ -586,6 +594,11 @@ class FastMCP(Generic[LifespanResultT]):
586
  def get_weather(city: str) -> str:
587
  return f"Weather for {city}"
588
 
 
 
 
 
 
589
  @server.resource("resource://{city}/weather")
590
  async def get_weather(city: str) -> str:
591
  data = await fetch_weather(city)
@@ -639,6 +652,10 @@ class FastMCP(Generic[LifespanResultT]):
639
  ) -> Callable[[AnyFunction], AnyFunction]:
640
  """Decorator to register a prompt.
641
 
 
 
 
 
642
  Args:
643
  name: Optional name for the prompt (defaults to function name)
644
  description: Optional description of what the prompt does
@@ -655,6 +672,17 @@ class FastMCP(Generic[LifespanResultT]):
655
  }
656
  ]
657
 
 
 
 
 
 
 
 
 
 
 
 
658
  @server.prompt()
659
  async def analyze_file(path: str) -> list[Message]:
660
  content = await read_file(path)
 
398
  server.
399
  """
400
  if self._resource_manager.has_resource(uri):
401
+ context = self.get_context()
402
+ resource = await self._resource_manager.get_resource(uri, context=context)
403
  try:
404
+ content = await resource.read(context=context)
405
  return [
406
  ReadResourceContents(content=content, mime_type=resource.mime_type)
407
  ]
 
425
 
426
  """
427
  if self._prompt_manager.has_prompt(name):
428
+ context = self.get_context()
429
+ messages = await self._prompt_manager.render_prompt(
430
+ name, arguments=arguments or {}, context=context
431
+ )
432
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
433
  else:
434
  for server in self._mounted_servers.values():
 
566
  - bytes for binary content
567
  - other types will be converted to JSON
568
 
569
+ Resources can optionally request a Context object by adding a parameter with the
570
+ Context type annotation. The context provides access to MCP capabilities like
571
+ logging, progress reporting, and session information.
572
+
573
  If the URI contains parameters (e.g. "resource://{param}") or the function
574
  has parameters, it will be registered as a template resource.
575
 
 
594
  def get_weather(city: str) -> str:
595
  return f"Weather for {city}"
596
 
597
+ @server.resource("resource://{city}/weather")
598
+ def get_weather_with_context(city: str, ctx: Context) -> str:
599
+ ctx.info(f"Fetching weather for {city}")
600
+ return f"Weather for {city}"
601
+
602
  @server.resource("resource://{city}/weather")
603
  async def get_weather(city: str) -> str:
604
  data = await fetch_weather(city)
 
652
  ) -> Callable[[AnyFunction], AnyFunction]:
653
  """Decorator to register a prompt.
654
 
655
+ Prompts can optionally request a Context object by adding a parameter with the
656
+ Context type annotation. The context provides access to MCP capabilities like
657
+ logging, progress reporting, and session information.
658
+
659
  Args:
660
  name: Optional name for the prompt (defaults to function name)
661
  description: Optional description of what the prompt does
 
672
  }
673
  ]
674
 
675
+ @server.prompt()
676
+ def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
677
+ ctx.info(f"Analyzing table {table_name}")
678
+ schema = read_table_schema(table_name)
679
+ return [
680
+ {
681
+ "role": "user",
682
+ "content": f"Analyze this schema:\n{schema}"
683
+ }
684
+ ]
685
+
686
  @server.prompt()
687
  async def analyze_file(path: str) -> list[Message]:
688
  content = await read_file(path)
src/fastmcp/tools/tool.py CHANGED
@@ -101,13 +101,16 @@ class Tool(BaseModel):
101
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
102
  """Run the tool with arguments."""
103
  try:
104
- result = await self.fn_metadata.call_fn_with_arg_validation(
105
- self.fn,
106
- self.is_async,
107
- arguments,
108
  {self.context_kwarg: context}
109
  if self.context_kwarg is not None
110
- else None,
 
 
 
 
 
 
111
  )
112
  return _convert_to_content(result)
113
  except Exception as e:
 
101
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
102
  """Run the tool with arguments."""
103
  try:
104
+ pass_args = (
 
 
 
105
  {self.context_kwarg: context}
106
  if self.context_kwarg is not None
107
+ else None
108
+ )
109
+ result = await self.fn_metadata.call_fn_with_arg_validation(
110
+ fn=self.fn,
111
+ fn_is_async=self.is_async,
112
+ arguments_to_validate=arguments,
113
+ arguments_to_pass_directly=pass_args,
114
  )
115
  return _convert_to_content(result)
116
  except Exception as e:
tests/server/test_server_interactions.py CHANGED
@@ -682,7 +682,147 @@ class TestToolParameters:
682
  assert result[0].text == "0:16:40"
683
 
684
 
685
- class TestResources:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
686
  async def test_text_resource(self):
687
  mcp = FastMCP()
688
 
@@ -756,6 +896,21 @@ class TestResources:
756
  assert result[0].blob == base64.b64encode(b"Binary file data").decode()
757
 
758
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
759
  class TestResourceTemplates:
760
  async def test_resource_with_params_not_in_uri(self):
761
  """Test that a resource with function parameters raises an error if the URI
@@ -1026,144 +1181,19 @@ class TestResourceTemplates:
1026
  assert result[0].text == "Template resource 1: a/b"
1027
 
1028
 
1029
- class TestContextInjection:
1030
- """Test context injection in tools."""
1031
-
1032
- async def test_context_detection(self):
1033
- """Test that context parameters are properly detected."""
1034
  mcp = FastMCP()
1035
 
1036
- def tool_with_context(x: int, ctx: Context) -> str:
1037
- return f"Request {ctx.request_id}: {x}"
1038
-
1039
- mcp.add_tool(tool_with_context)
1040
- async with Client(mcp) as client:
1041
- tools = await client.list_tools()
1042
- assert len(tools) == 1
1043
- assert tools[0].name == "tool_with_context"
1044
-
1045
- async def test_context_injection(self):
1046
- """Test that context is properly injected into tool calls."""
1047
- mcp = FastMCP()
1048
-
1049
- def tool_with_context(x: int, ctx: Context) -> str:
1050
- assert ctx.request_id is not None
1051
- return f"Request {ctx.request_id}: {x}"
1052
-
1053
- mcp.add_tool(tool_with_context)
1054
- async with Client(mcp) as client:
1055
- result = await client.call_tool("tool_with_context", {"x": 42})
1056
- assert len(result) == 1
1057
- content = result[0]
1058
- assert isinstance(content, TextContent)
1059
- assert "Request" in content.text
1060
- assert "42" in content.text
1061
-
1062
- async def test_async_context(self):
1063
- """Test that context works in async functions."""
1064
- mcp = FastMCP()
1065
-
1066
- async def async_tool(x: int, ctx: Context) -> str:
1067
- assert ctx.request_id is not None
1068
- return f"Async request {ctx.request_id}: {x}"
1069
-
1070
- mcp.add_tool(async_tool)
1071
- async with Client(mcp) as client:
1072
- result = await client.call_tool("async_tool", {"x": 42})
1073
- assert len(result) == 1
1074
- content = result[0]
1075
- assert isinstance(content, TextContent)
1076
- assert "Async request" in content.text
1077
- assert "42" in content.text
1078
-
1079
- async def test_context_logging(self):
1080
- from unittest.mock import patch
1081
-
1082
- import mcp.server.session
1083
-
1084
- """Test that context logging methods work."""
1085
- mcp = FastMCP()
1086
-
1087
- async def logging_tool(msg: str, ctx: Context) -> str:
1088
- await ctx.debug("Debug message")
1089
- await ctx.info("Info message")
1090
- await ctx.warning("Warning message")
1091
- await ctx.error("Error message")
1092
- return f"Logged messages for {msg}"
1093
-
1094
- mcp.add_tool(logging_tool)
1095
-
1096
- with patch("mcp.server.session.ServerSession.send_log_message") as mock_log:
1097
- async with Client(mcp) as client:
1098
- result = await client.call_tool("logging_tool", {"msg": "test"})
1099
- assert len(result) == 1
1100
- content = result[0]
1101
- assert isinstance(content, TextContent)
1102
- assert "Logged messages for test" in content.text
1103
-
1104
- assert mock_log.call_count == 4
1105
- mock_log.assert_any_call(
1106
- level="debug", data="Debug message", logger=None
1107
- )
1108
- mock_log.assert_any_call(level="info", data="Info message", logger=None)
1109
- mock_log.assert_any_call(
1110
- level="warning", data="Warning message", logger=None
1111
- )
1112
- mock_log.assert_any_call(
1113
- level="error", data="Error message", logger=None
1114
- )
1115
-
1116
- async def test_optional_context(self):
1117
- """Test that context is optional."""
1118
- mcp = FastMCP()
1119
-
1120
- def no_context(x: int) -> int:
1121
- return x * 2
1122
-
1123
- mcp.add_tool(no_context)
1124
- async with Client(mcp) as client:
1125
- result = await client.call_tool("no_context", {"x": 21})
1126
- assert len(result) == 1
1127
- content = result[0]
1128
- assert isinstance(content, TextContent)
1129
- assert content.text == "42"
1130
-
1131
- async def test_context_resource_access(self):
1132
- """Test that context can access resources."""
1133
- mcp = FastMCP()
1134
-
1135
- @mcp.resource("test://data")
1136
- def test_resource() -> str:
1137
- return "resource data"
1138
-
1139
- @mcp.tool()
1140
- async def tool_with_resource(ctx: Context) -> str:
1141
- r_iter = await ctx.read_resource("test://data")
1142
- r_list = list(r_iter)
1143
- assert len(r_list) == 1
1144
- r = r_list[0]
1145
- return f"Read resource: {r.content} with mime type {r.mime_type}"
1146
-
1147
- async with Client(mcp) as client:
1148
- result = await client.call_tool("tool_with_resource", {})
1149
- assert len(result) == 1
1150
- content = result[0]
1151
- assert isinstance(content, TextContent)
1152
- assert "Read resource: resource data" in content.text
1153
-
1154
- async def test_tool_decorator_with_tags(self):
1155
- """Test that the tool decorator properly sets tags."""
1156
- mcp = FastMCP()
1157
-
1158
- @mcp.tool(tags={"example", "test-tag"})
1159
- def sample_tool(x: int) -> int:
1160
- return x * 2
1161
 
1162
- # Verify the tool exists
1163
  async with Client(mcp) as client:
1164
- tools = await client.list_tools()
1165
- assert len(tools) == 1
1166
- # Note: MCPTool from the client API doesn't expose tags
1167
 
1168
 
1169
  class TestPrompts:
@@ -1350,3 +1380,19 @@ class TestPrompts:
1350
  assert len(prompts_dict) == 1
1351
  prompt = prompts_dict["sample_prompt"]
1352
  assert prompt.tags == {"example", "test-tag"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
682
  assert result[0].text == "0:16:40"
683
 
684
 
685
+ class TestToolContextInjection:
686
+ """Test context injection in tools."""
687
+
688
+ async def test_context_detection(self):
689
+ """Test that context parameters are properly detected."""
690
+ mcp = FastMCP()
691
+
692
+ def tool_with_context(x: int, ctx: Context) -> str:
693
+ return f"Request {ctx.request_id}: {x}"
694
+
695
+ mcp.add_tool(tool_with_context)
696
+ async with Client(mcp) as client:
697
+ tools = await client.list_tools()
698
+ assert len(tools) == 1
699
+ assert tools[0].name == "tool_with_context"
700
+
701
+ async def test_context_injection(self):
702
+ """Test that context is properly injected into tool calls."""
703
+ mcp = FastMCP()
704
+
705
+ @mcp.tool()
706
+ def tool_with_context(x: int, ctx: Context) -> str:
707
+ assert isinstance(ctx, Context)
708
+ assert ctx.request_id is not None
709
+ return ctx.request_id
710
+
711
+ async with Client(mcp) as client:
712
+ result = await client.call_tool("tool_with_context", {"x": 42})
713
+ assert len(result) == 1
714
+ content = result[0]
715
+ assert isinstance(content, TextContent)
716
+ assert content.text == "1"
717
+
718
+ async def test_async_context(self):
719
+ """Test that context works in async functions."""
720
+ mcp = FastMCP()
721
+
722
+ async def async_tool(x: int, ctx: Context) -> str:
723
+ assert ctx.request_id is not None
724
+ return f"Async request {ctx.request_id}: {x}"
725
+
726
+ mcp.add_tool(async_tool)
727
+ async with Client(mcp) as client:
728
+ result = await client.call_tool("async_tool", {"x": 42})
729
+ assert len(result) == 1
730
+ content = result[0]
731
+ assert isinstance(content, TextContent)
732
+ assert "Async request" in content.text
733
+ assert "42" in content.text
734
+
735
+ async def test_context_logging(self):
736
+ from unittest.mock import patch
737
+
738
+ import mcp.server.session
739
+
740
+ """Test that context logging methods work."""
741
+ mcp = FastMCP()
742
+
743
+ async def logging_tool(msg: str, ctx: Context) -> str:
744
+ await ctx.debug("Debug message")
745
+ await ctx.info("Info message")
746
+ await ctx.warning("Warning message")
747
+ await ctx.error("Error message")
748
+ return f"Logged messages for {msg}"
749
+
750
+ mcp.add_tool(logging_tool)
751
+
752
+ with patch("mcp.server.session.ServerSession.send_log_message") as mock_log:
753
+ async with Client(mcp) as client:
754
+ result = await client.call_tool("logging_tool", {"msg": "test"})
755
+ assert len(result) == 1
756
+ content = result[0]
757
+ assert isinstance(content, TextContent)
758
+ assert "Logged messages for test" in content.text
759
+
760
+ assert mock_log.call_count == 4
761
+ mock_log.assert_any_call(
762
+ level="debug", data="Debug message", logger=None
763
+ )
764
+ mock_log.assert_any_call(level="info", data="Info message", logger=None)
765
+ mock_log.assert_any_call(
766
+ level="warning", data="Warning message", logger=None
767
+ )
768
+ mock_log.assert_any_call(
769
+ level="error", data="Error message", logger=None
770
+ )
771
+
772
+ async def test_optional_context(self):
773
+ """Test that context is optional."""
774
+ mcp = FastMCP()
775
+
776
+ def no_context(x: int) -> int:
777
+ return x * 2
778
+
779
+ mcp.add_tool(no_context)
780
+ async with Client(mcp) as client:
781
+ result = await client.call_tool("no_context", {"x": 21})
782
+ assert len(result) == 1
783
+ content = result[0]
784
+ assert isinstance(content, TextContent)
785
+ assert content.text == "42"
786
+
787
+ async def test_context_resource_access(self):
788
+ """Test that context can access resources."""
789
+ mcp = FastMCP()
790
+
791
+ @mcp.resource("test://data")
792
+ def test_resource() -> str:
793
+ return "resource data"
794
+
795
+ @mcp.tool()
796
+ async def tool_with_resource(ctx: Context) -> str:
797
+ r_iter = await ctx.read_resource("test://data")
798
+ r_list = list(r_iter)
799
+ assert len(r_list) == 1
800
+ r = r_list[0]
801
+ return f"Read resource: {r.content} with mime type {r.mime_type}"
802
+
803
+ async with Client(mcp) as client:
804
+ result = await client.call_tool("tool_with_resource", {})
805
+ assert len(result) == 1
806
+ content = result[0]
807
+ assert isinstance(content, TextContent)
808
+ assert "Read resource: resource data" in content.text
809
+
810
+ async def test_tool_decorator_with_tags(self):
811
+ """Test that the tool decorator properly sets tags."""
812
+ mcp = FastMCP()
813
+
814
+ @mcp.tool(tags={"example", "test-tag"})
815
+ def sample_tool(x: int) -> int:
816
+ return x * 2
817
+
818
+ # Verify the tool exists
819
+ async with Client(mcp) as client:
820
+ tools = await client.list_tools()
821
+ assert len(tools) == 1
822
+ # Note: MCPTool from the client API doesn't expose tags
823
+
824
+
825
+ class TestResource:
826
  async def test_text_resource(self):
827
  mcp = FastMCP()
828
 
 
896
  assert result[0].blob == base64.b64encode(b"Binary file data").decode()
897
 
898
 
899
+ class TestResourceContext:
900
+ async def test_resource_with_context_annotation_gets_context(self):
901
+ mcp = FastMCP()
902
+
903
+ @mcp.resource("resource://test")
904
+ def resource_with_context(ctx: Context) -> str:
905
+ assert isinstance(ctx, Context)
906
+ return ctx.request_id
907
+
908
+ async with Client(mcp) as client:
909
+ result = await client.read_resource(AnyUrl("resource://test"))
910
+ assert isinstance(result[0], TextResourceContents)
911
+ assert result[0].text == "1"
912
+
913
+
914
  class TestResourceTemplates:
915
  async def test_resource_with_params_not_in_uri(self):
916
  """Test that a resource with function parameters raises an error if the URI
 
1181
  assert result[0].text == "Template resource 1: a/b"
1182
 
1183
 
1184
+ class TestResourceTemplateContext:
1185
+ async def test_resource_template_context(self):
 
 
 
1186
  mcp = FastMCP()
1187
 
1188
+ @mcp.resource("resource://{param}")
1189
+ def resource_template(param: str, ctx: Context) -> str:
1190
+ assert isinstance(ctx, Context)
1191
+ return f"Resource template: {param} {ctx.request_id}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1192
 
 
1193
  async with Client(mcp) as client:
1194
+ result = await client.read_resource(AnyUrl("resource://test"))
1195
+ assert isinstance(result[0], TextResourceContents)
1196
+ assert result[0].text == "Resource template: test 1"
1197
 
1198
 
1199
  class TestPrompts:
 
1380
  assert len(prompts_dict) == 1
1381
  prompt = prompts_dict["sample_prompt"]
1382
  assert prompt.tags == {"example", "test-tag"}
1383
+
1384
+
1385
+ class TestPromptContext:
1386
+ async def test_prompt_context(self):
1387
+ mcp = FastMCP()
1388
+
1389
+ @mcp.prompt()
1390
+ def prompt_fn(name: str, ctx: Context) -> str:
1391
+ assert isinstance(ctx, Context)
1392
+ return f"Hello, {name}! {ctx.request_id}"
1393
+
1394
+ async with Client(mcp) as client:
1395
+ result = await client.get_prompt("prompt_fn", {"name": "World"})
1396
+ assert len(result) == 1
1397
+ message = result[0]
1398
+ assert message.role == "user"