fastmcp 2.2.4__py3-none-any.whl → 2.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastmcp/prompts/prompt.py CHANGED
@@ -1,9 +1,11 @@
1
1
  """Base classes for FastMCP prompts."""
2
2
 
3
+ from __future__ import annotations as _annotations
4
+
3
5
  import inspect
4
6
  import json
5
7
  from collections.abc import Awaitable, Callable, Sequence
6
- from typing import Annotated, Any, Literal
8
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
7
9
 
8
10
  import pydantic_core
9
11
  from mcp.types import EmbeddedResource, ImageContent, TextContent
@@ -13,6 +15,12 @@ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_ca
13
15
 
14
16
  from fastmcp.utilities.types import _convert_set_defaults
15
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
+
16
24
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
17
25
 
18
26
 
@@ -72,6 +80,9 @@ class Prompt(BaseModel):
72
80
  None, description="Arguments that can be passed to the prompt"
73
81
  )
74
82
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
83
+ context_kwarg: str | None = Field(
84
+ None, description="Name of the kwarg that should receive context"
85
+ )
75
86
 
76
87
  @classmethod
77
88
  def from_function(
@@ -80,7 +91,8 @@ class Prompt(BaseModel):
80
91
  name: str | None = None,
81
92
  description: str | None = None,
82
93
  tags: set[str] | None = None,
83
- ) -> "Prompt":
94
+ context_kwarg: str | None = None,
95
+ ) -> Prompt:
84
96
  """Create a Prompt from a function.
85
97
 
86
98
  The function can return:
@@ -89,11 +101,24 @@ class Prompt(BaseModel):
89
101
  - A dict (converted to a message)
90
102
  - A sequence of any of the above
91
103
  """
104
+ from fastmcp import Context
105
+
92
106
  func_name = name or fn.__name__
93
107
 
94
108
  if func_name == "<lambda>":
95
109
  raise ValueError("You must provide a name for lambda functions")
96
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
+
97
122
  # Get schema from TypeAdapter - will fail if function isn't properly typed
98
123
  parameters = TypeAdapter(fn).json_schema()
99
124
 
@@ -101,6 +126,10 @@ class Prompt(BaseModel):
101
126
  arguments: list[PromptArgument] = []
102
127
  if "properties" in parameters:
103
128
  for param_name, param in parameters["properties"].items():
129
+ # Skip context parameter
130
+ if param_name == context_kwarg:
131
+ continue
132
+
104
133
  required = param_name in parameters.get("required", [])
105
134
  arguments.append(
106
135
  PromptArgument(
@@ -119,9 +148,14 @@ class Prompt(BaseModel):
119
148
  arguments=arguments,
120
149
  fn=fn,
121
150
  tags=tags or set(),
151
+ context_kwarg=context_kwarg,
122
152
  )
123
153
 
124
- async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
154
+ async def render(
155
+ self,
156
+ arguments: dict[str, Any] | None = None,
157
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
158
+ ) -> list[Message]:
125
159
  """Render the prompt with arguments."""
126
160
  # Validate required arguments
127
161
  if self.arguments:
@@ -132,8 +166,13 @@ class Prompt(BaseModel):
132
166
  raise ValueError(f"Missing required arguments: {missing}")
133
167
 
134
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
+
135
174
  # Call function and check if result is a coroutine
136
- result = self.fn(**(arguments or {}))
175
+ result = self.fn(**kwargs)
137
176
  if inspect.iscoroutine(result):
138
177
  result = await result
139
178
 
@@ -1,13 +1,21 @@
1
1
  """Prompt management functionality."""
2
2
 
3
+ from __future__ import annotations as _annotations
4
+
3
5
  from collections.abc import Awaitable, Callable
4
- from typing import Any
6
+ from typing import TYPE_CHECKING, Any
5
7
 
6
8
  from fastmcp.exceptions import NotFoundError
7
9
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
8
10
  from fastmcp.settings import DuplicateBehavior
9
11
  from fastmcp.utilities.logging import get_logger
10
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
+
11
19
  logger = get_logger(__name__)
12
20
 
13
21
 
@@ -69,14 +77,17 @@ class PromptManager:
69
77
  return prompt
70
78
 
71
79
  async def render_prompt(
72
- self, name: str, arguments: dict[str, Any] | None = None
80
+ self,
81
+ name: str,
82
+ arguments: dict[str, Any] | None = None,
83
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
73
84
  ) -> list[Message]:
74
85
  """Render a prompt by name with arguments."""
75
86
  prompt = self.get_prompt(name)
76
87
  if not prompt:
77
88
  raise NotFoundError(f"Unknown prompt: {name}")
78
89
 
79
- return await prompt.render(arguments)
90
+ return await prompt.render(arguments, context=context)
80
91
 
81
92
  def has_prompt(self, key: str) -> bool:
82
93
  """Check if a prompt exists."""
@@ -1,7 +1,9 @@
1
1
  """Base classes and interfaces for FastMCP resources."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import abc
4
- from typing import Annotated, Any
6
+ from typing import TYPE_CHECKING, Annotated, Any
5
7
 
6
8
  from mcp.types import Resource as MCPResource
7
9
  from pydantic import (
@@ -17,6 +19,12 @@ from pydantic import (
17
19
 
18
20
  from fastmcp.utilities.types import _convert_set_defaults
19
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
+
20
28
 
21
29
  class Resource(BaseModel, abc.ABC):
22
30
  """Base class for all resources."""
@@ -58,7 +66,9 @@ class Resource(BaseModel, abc.ABC):
58
66
  raise ValueError("Either name or uri must be provided")
59
67
 
60
68
  @abc.abstractmethod
61
- async def read(self) -> str | bytes:
69
+ async def read(
70
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
71
+ ) -> str | bytes:
62
72
  """Read the resource content."""
63
73
  pass
64
74
 
@@ -61,9 +61,16 @@ class ResourceManager:
61
61
  The added resource or template. If a resource or template with the same URI already exists,
62
62
  returns the existing resource or template.
63
63
  """
64
+ from fastmcp.server.context import Context
65
+
64
66
  # Check if this should be a template
65
67
  has_uri_params = "{" in uri and "}" in uri
66
- has_func_params = bool(inspect.signature(fn).parameters)
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
+ )
67
74
 
68
75
  if has_uri_params or has_func_params:
69
76
  return self.add_template_from_fn(
@@ -102,12 +109,12 @@ class ResourceManager:
102
109
  The added resource. If a resource with the same URI already exists,
103
110
  returns the existing resource.
104
111
  """
105
- resource = FunctionResource(
112
+ resource = FunctionResource.from_function(
113
+ fn=fn,
106
114
  uri=AnyUrl(uri),
107
115
  name=name,
108
116
  description=description,
109
117
  mime_type=mime_type or "text/plain",
110
- fn=fn,
111
118
  tags=tags or set(),
112
119
  )
113
120
  return self.add_resource(resource)
@@ -212,9 +219,13 @@ class ResourceManager:
212
219
  return True
213
220
  return False
214
221
 
215
- async def get_resource(self, uri: AnyUrl | str) -> Resource:
222
+ async def get_resource(self, uri: AnyUrl | str, context=None) -> Resource:
216
223
  """Get resource by URI, checking concrete resources first, then templates.
217
224
 
225
+ Args:
226
+ uri: The URI of the resource to get
227
+ context: Optional context object to pass to template resources
228
+
218
229
  Raises:
219
230
  NotFoundError: If no resource or template matching the URI is found.
220
231
  """
@@ -230,7 +241,11 @@ class ResourceManager:
230
241
  # Try to match against the storage key (which might be a custom key)
231
242
  if params := match_uri_template(uri_str, storage_key):
232
243
  try:
233
- return await template.create_resource(uri_str, params)
244
+ return await template.create_resource(
245
+ uri_str,
246
+ params=params,
247
+ context=context,
248
+ )
234
249
  except Exception as e:
235
250
  raise ValueError(f"Error creating resource from template: {e}")
236
251
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import inspect
6
6
  import re
7
7
  from collections.abc import Callable
8
- from typing import Annotated, Any
8
+ from typing import TYPE_CHECKING, Annotated, Any
9
9
  from urllib.parse import unquote
10
10
 
11
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
@@ -22,6 +22,12 @@ from pydantic import (
22
22
  from fastmcp.resources.types import FunctionResource, Resource
23
23
  from fastmcp.utilities.types import _convert_set_defaults
24
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
+
25
31
 
26
32
  def build_regex(template: str) -> re.Pattern:
27
33
  parts = re.split(r"(\{[^}]+\})", template)
@@ -70,6 +76,9 @@ class ResourceTemplate(BaseModel):
70
76
  parameters: dict[str, Any] = Field(
71
77
  description="JSON schema for function parameters"
72
78
  )
79
+ context_kwarg: str | None = Field(
80
+ None, description="Name of the kwarg that should receive context"
81
+ )
73
82
 
74
83
  @field_validator("mime_type", mode="before")
75
84
  @classmethod
@@ -88,18 +97,34 @@ class ResourceTemplate(BaseModel):
88
97
  description: str | None = None,
89
98
  mime_type: str | None = None,
90
99
  tags: set[str] | None = None,
100
+ context_kwarg: str | None = None,
91
101
  ) -> ResourceTemplate:
92
102
  """Create a template from a function."""
103
+ from fastmcp import Context
104
+
93
105
  func_name = name or fn.__name__
94
106
  if func_name == "<lambda>":
95
107
  raise ValueError("You must provide a name for lambda functions")
96
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
+
97
120
  # Validate that URI params match function params
98
121
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
99
122
  if not uri_params:
100
123
  raise ValueError("URI template must contain at least one parameter")
101
124
 
102
125
  func_params = set(inspect.signature(fn).parameters.keys())
126
+ if context_kwarg:
127
+ func_params.discard(context_kwarg)
103
128
 
104
129
  # get the parameters that are required
105
130
  required_params = {
@@ -107,6 +132,8 @@ class ResourceTemplate(BaseModel):
107
132
  for p in func_params
108
133
  if inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
109
134
  }
135
+ if context_kwarg and context_kwarg in required_params:
136
+ required_params.discard(context_kwarg)
110
137
 
111
138
  if not required_params.issubset(uri_params):
112
139
  raise ValueError(
@@ -132,17 +159,28 @@ class ResourceTemplate(BaseModel):
132
159
  fn=fn,
133
160
  parameters=parameters,
134
161
  tags=tags or set(),
162
+ context_kwarg=context_kwarg,
135
163
  )
136
164
 
137
165
  def matches(self, uri: str) -> dict[str, Any] | None:
138
166
  """Check if URI matches template and extract parameters."""
139
167
  return match_uri_template(uri, self.uri_template)
140
168
 
141
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
169
+ async def create_resource(
170
+ self,
171
+ uri: str,
172
+ params: dict[str, Any],
173
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
174
+ ) -> Resource:
142
175
  """Create a resource from the template with the given parameters."""
143
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
+
144
182
  # Call function and check if result is a coroutine
145
- result = self.fn(**params)
183
+ result = self.fn(**kwargs)
146
184
  if inspect.iscoroutine(result):
147
185
  result = await result
148
186
 
@@ -151,8 +189,9 @@ class ResourceTemplate(BaseModel):
151
189
  name=self.name,
152
190
  description=self.description,
153
191
  mime_type=self.mime_type,
154
- fn=lambda: result, # Capture result in closure
192
+ fn=lambda **kwargs: result, # Capture result in closure
155
193
  tags=self.tags,
194
+ context_kwarg=self.context_kwarg,
156
195
  )
157
196
  except Exception as e:
158
197
  raise ValueError(f"Error creating resource from template: {e}")
@@ -1,10 +1,12 @@
1
1
  """Concrete resource implementations."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import inspect
4
6
  import json
5
7
  from collections.abc import Callable
6
8
  from pathlib import Path
7
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any
8
10
 
9
11
  import anyio
10
12
  import anyio.to_thread
@@ -13,15 +15,24 @@ import pydantic.json
13
15
  import pydantic_core
14
16
  from pydantic import Field, ValidationInfo
15
17
 
18
+ import fastmcp
16
19
  from fastmcp.resources.resource import Resource
17
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
+
18
27
 
19
28
  class TextResource(Resource):
20
29
  """A resource that reads from a string."""
21
30
 
22
31
  text: str = Field(description="Text content of the resource")
23
32
 
24
- async def read(self) -> str:
33
+ async def read(
34
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
35
+ ) -> str:
25
36
  """Read the text content."""
26
37
  return self.text
27
38
 
@@ -31,7 +42,9 @@ class BinaryResource(Resource):
31
42
 
32
43
  data: bytes = Field(description="Binary content of the resource")
33
44
 
34
- async def read(self) -> bytes:
45
+ async def read(
46
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
47
+ ) -> bytes:
35
48
  """Read the binary content."""
36
49
  return self.data
37
50
 
@@ -50,15 +63,40 @@ class FunctionResource(Resource):
50
63
  """
51
64
 
52
65
  fn: Callable[[], Any]
66
+ context_kwarg: str | None = Field(
67
+ default=None, description="Name of the kwarg that should receive context"
68
+ )
53
69
 
54
- async def read(self) -> str | bytes:
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:
55
88
  """Read the resource by calling the wrapped function."""
56
89
  try:
57
- result = (
58
- await self.fn() if inspect.iscoroutinefunction(self.fn) else self.fn()
59
- )
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
+
60
98
  if isinstance(result, Resource):
61
- return await result.read()
99
+ return await result.read(context=context)
62
100
  if isinstance(result, bytes):
63
101
  return result
64
102
  if isinstance(result, str):
@@ -105,7 +143,9 @@ class FileResource(Resource):
105
143
  mime_type = info.data.get("mime_type", "text/plain")
106
144
  return not mime_type.startswith("text/")
107
145
 
108
- async def read(self) -> str | bytes:
146
+ async def read(
147
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
148
+ ) -> str | bytes:
109
149
  """Read the file content."""
110
150
  try:
111
151
  if self.is_binary:
@@ -123,7 +163,9 @@ class HttpResource(Resource):
123
163
  default="application/json", description="MIME type of the resource content"
124
164
  )
125
165
 
126
- async def read(self) -> str | bytes:
166
+ async def read(
167
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
168
+ ) -> str | bytes:
127
169
  """Read the HTTP content."""
128
170
  async with httpx.AsyncClient() as client:
129
171
  response = await client.get(self.url)
@@ -175,7 +217,9 @@ class DirectoryResource(Resource):
175
217
  except Exception as e:
176
218
  raise ValueError(f"Error listing directory {self.path}: {e}")
177
219
 
178
- async def read(self) -> str: # Always returns JSON string
220
+ async def read(
221
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
222
+ ) -> str: # Always returns JSON string
179
223
  """Read the directory listing."""
180
224
  try:
181
225
  files = await anyio.to_thread.run_sync(self.list_files)
fastmcp/server/openapi.py CHANGED
@@ -1,11 +1,13 @@
1
1
  """FastMCP server implementation for OpenAPI integration."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import enum
4
6
  import json
5
7
  import re
6
8
  from dataclasses import dataclass
7
9
  from re import Pattern
8
- from typing import Any, Literal
10
+ from typing import TYPE_CHECKING, Any, Literal
9
11
 
10
12
  import httpx
11
13
  from mcp.types import TextContent
@@ -22,6 +24,12 @@ from fastmcp.utilities.openapi import (
22
24
  format_description_with_responses,
23
25
  )
24
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
+
25
33
  logger = get_logger(__name__)
26
34
 
27
35
  HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
@@ -257,7 +265,9 @@ class OpenAPIResource(Resource):
257
265
  self._client = client
258
266
  self._route = route
259
267
 
260
- async def read(self) -> str | bytes:
268
+ async def read(
269
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
270
+ ) -> str | bytes:
261
271
  """Fetch the resource data by making an HTTP request."""
262
272
  try:
263
273
  # Extract path parameters from the URI if present
@@ -347,11 +357,17 @@ class OpenAPIResourceTemplate(ResourceTemplate):
347
357
  fn=lambda **kwargs: None,
348
358
  parameters=parameters,
349
359
  tags=tags,
360
+ context_kwarg=None,
350
361
  )
351
362
  self._client = client
352
363
  self._route = route
353
364
 
354
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
365
+ async def create_resource(
366
+ self,
367
+ uri: str,
368
+ params: dict[str, Any],
369
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
370
+ ) -> Resource:
355
371
  """Create a resource with the given parameters."""
356
372
  # Generate a URI for this resource instance
357
373
  uri_parts = []
fastmcp/server/proxy.py CHANGED
@@ -1,4 +1,6 @@
1
- from typing import Any, cast
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, cast
2
4
  from urllib.parse import quote
3
5
 
4
6
  import mcp.types
@@ -25,6 +27,12 @@ from fastmcp.tools.tool import Tool
25
27
  from fastmcp.utilities.func_metadata import func_metadata
26
28
  from fastmcp.utilities.logging import get_logger
27
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
+
28
36
  logger = get_logger(__name__)
29
37
 
30
38
 
@@ -33,12 +41,12 @@ def _proxy_passthrough():
33
41
 
34
42
 
35
43
  class ProxyTool(Tool):
36
- def __init__(self, client: "Client", **kwargs):
44
+ def __init__(self, client: Client, **kwargs):
37
45
  super().__init__(**kwargs)
38
46
  self._client = client
39
47
 
40
48
  @classmethod
41
- async def from_client(cls, client: "Client", tool: mcp.types.Tool) -> "ProxyTool":
49
+ async def from_client(cls, client: Client, tool: mcp.types.Tool) -> ProxyTool:
42
50
  return cls(
43
51
  client=client,
44
52
  name=tool.name,
@@ -50,7 +58,9 @@ class ProxyTool(Tool):
50
58
  )
51
59
 
52
60
  async def run(
53
- self, arguments: dict[str, Any], context: Context | None = None
61
+ self,
62
+ arguments: dict[str, Any],
63
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
54
64
  ) -> Any:
55
65
  # the client context manager will swallow any exceptions inside a TaskGroup
56
66
  # so we return the raw result and raise an exception ourselves
@@ -64,17 +74,15 @@ class ProxyTool(Tool):
64
74
 
65
75
 
66
76
  class ProxyResource(Resource):
67
- def __init__(
68
- self, client: "Client", *, _value: str | bytes | None = None, **kwargs
69
- ):
77
+ def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
70
78
  super().__init__(**kwargs)
71
79
  self._client = client
72
80
  self._value = _value
73
81
 
74
82
  @classmethod
75
83
  async def from_client(
76
- cls, client: "Client", resource: mcp.types.Resource
77
- ) -> "ProxyResource":
84
+ cls, client: Client, resource: mcp.types.Resource
85
+ ) -> ProxyResource:
78
86
  return cls(
79
87
  client=client,
80
88
  uri=resource.uri,
@@ -83,7 +91,9 @@ class ProxyResource(Resource):
83
91
  mime_type=resource.mimeType,
84
92
  )
85
93
 
86
- async def read(self) -> str | bytes:
94
+ async def read(
95
+ self, context: Context[ServerSessionT, LifespanContextT] | None = None
96
+ ) -> str | bytes:
87
97
  if self._value is not None:
88
98
  return self._value
89
99
 
@@ -98,14 +108,14 @@ class ProxyResource(Resource):
98
108
 
99
109
 
100
110
  class ProxyTemplate(ResourceTemplate):
101
- def __init__(self, client: "Client", **kwargs):
111
+ def __init__(self, client: Client, **kwargs):
102
112
  super().__init__(**kwargs)
103
113
  self._client = client
104
114
 
105
115
  @classmethod
106
116
  async def from_client(
107
- cls, client: "Client", template: mcp.types.ResourceTemplate
108
- ) -> "ProxyTemplate":
117
+ cls, client: Client, template: mcp.types.ResourceTemplate
118
+ ) -> ProxyTemplate:
109
119
  return cls(
110
120
  client=client,
111
121
  uri_template=template.uriTemplate,
@@ -115,7 +125,12 @@ class ProxyTemplate(ResourceTemplate):
115
125
  parameters={},
116
126
  )
117
127
 
118
- async def create_resource(self, uri: str, params: dict[str, Any]) -> ProxyResource:
128
+ async def create_resource(
129
+ self,
130
+ uri: str,
131
+ params: dict[str, Any],
132
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
133
+ ) -> ProxyResource:
119
134
  # dont use the provided uri, because it may not be the same as the
120
135
  # uri_template on the remote server.
121
136
  # quote params to ensure they are valid for the uri_template
@@ -144,14 +159,12 @@ class ProxyTemplate(ResourceTemplate):
144
159
 
145
160
 
146
161
  class ProxyPrompt(Prompt):
147
- def __init__(self, client: "Client", **kwargs):
162
+ def __init__(self, client: Client, **kwargs):
148
163
  super().__init__(**kwargs)
149
164
  self._client = client
150
165
 
151
166
  @classmethod
152
- async def from_client(
153
- cls, client: "Client", prompt: mcp.types.Prompt
154
- ) -> "ProxyPrompt":
167
+ async def from_client(cls, client: Client, prompt: mcp.types.Prompt) -> ProxyPrompt:
155
168
  return cls(
156
169
  client=client,
157
170
  name=prompt.name,
@@ -160,14 +173,18 @@ class ProxyPrompt(Prompt):
160
173
  fn=_proxy_passthrough,
161
174
  )
162
175
 
163
- async def render(self, arguments: dict[str, Any]) -> list[Message]:
176
+ async def render(
177
+ self,
178
+ arguments: dict[str, Any],
179
+ context: Context[ServerSessionT, LifespanContextT] | None = None,
180
+ ) -> list[Message]:
164
181
  async with self._client:
165
182
  result = await self._client.get_prompt(self.name, arguments)
166
183
  return [Message(role=m.role, content=m.content) for m in result]
167
184
 
168
185
 
169
186
  class FastMCPProxy(FastMCP):
170
- def __init__(self, client: "Client", **kwargs):
187
+ def __init__(self, client: Client, **kwargs):
171
188
  super().__init__(**kwargs)
172
189
  self.client = client
173
190
 
fastmcp/server/server.py CHANGED
@@ -398,9 +398,10 @@ class FastMCP(Generic[LifespanResultT]):
398
398
  server.
399
399
  """
400
400
  if self._resource_manager.has_resource(uri):
401
- resource = await self._resource_manager.get_resource(uri)
401
+ context = self.get_context()
402
+ resource = await self._resource_manager.get_resource(uri, context=context)
402
403
  try:
403
- content = await resource.read()
404
+ content = await resource.read(context=context)
404
405
  return [
405
406
  ReadResourceContents(content=content, mime_type=resource.mime_type)
406
407
  ]
@@ -424,7 +425,10 @@ class FastMCP(Generic[LifespanResultT]):
424
425
 
425
426
  """
426
427
  if self._prompt_manager.has_prompt(name):
427
- messages = await self._prompt_manager.render_prompt(name, arguments)
428
+ context = self.get_context()
429
+ messages = await self._prompt_manager.render_prompt(
430
+ name, arguments=arguments or {}, context=context
431
+ )
428
432
  return GetPromptResult(messages=pydantic_core.to_jsonable_python(messages))
429
433
  else:
430
434
  for server in self._mounted_servers.values():
@@ -562,6 +566,10 @@ class FastMCP(Generic[LifespanResultT]):
562
566
  - bytes for binary content
563
567
  - other types will be converted to JSON
564
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
+
565
573
  If the URI contains parameters (e.g. "resource://{param}") or the function
566
574
  has parameters, it will be registered as a template resource.
567
575
 
@@ -586,6 +594,11 @@ class FastMCP(Generic[LifespanResultT]):
586
594
  def get_weather(city: str) -> str:
587
595
  return f"Weather for {city}"
588
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
+
589
602
  @server.resource("resource://{city}/weather")
590
603
  async def get_weather(city: str) -> str:
591
604
  data = await fetch_weather(city)
@@ -639,6 +652,10 @@ class FastMCP(Generic[LifespanResultT]):
639
652
  ) -> Callable[[AnyFunction], AnyFunction]:
640
653
  """Decorator to register a prompt.
641
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
+
642
659
  Args:
643
660
  name: Optional name for the prompt (defaults to function name)
644
661
  description: Optional description of what the prompt does
@@ -655,6 +672,17 @@ class FastMCP(Generic[LifespanResultT]):
655
672
  }
656
673
  ]
657
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
+
658
686
  @server.prompt()
659
687
  async def analyze_file(path: str) -> list[Message]:
660
688
  content = await read_file(path)
fastmcp/tools/tool.py CHANGED
@@ -101,13 +101,16 @@ class Tool(BaseModel):
101
101
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
102
102
  """Run the tool with arguments."""
103
103
  try:
104
- result = await self.fn_metadata.call_fn_with_arg_validation(
105
- self.fn,
106
- self.is_async,
107
- arguments,
104
+ pass_args = (
108
105
  {self.context_kwarg: context}
109
106
  if self.context_kwarg is not None
110
- else 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,
111
114
  )
112
115
  return _convert_to_content(result)
113
116
  except Exception as e:
@@ -163,9 +166,22 @@ def _convert_to_content(
163
166
 
164
167
  return other_content + mcp_types
165
168
 
169
+ # if the result is a bytes object, convert it to a text content object
166
170
  if not isinstance(result, str):
167
171
  try:
168
- result = json.dumps(pydantic_core.to_jsonable_python(result))
172
+ jsonable_result = pydantic_core.to_jsonable_python(result)
173
+ if jsonable_result is None:
174
+ return [TextContent(type="text", text="null")]
175
+ elif isinstance(jsonable_result, bool):
176
+ return [
177
+ TextContent(
178
+ type="text", text="true" if jsonable_result else "false"
179
+ )
180
+ ]
181
+ elif isinstance(jsonable_result, str | int | float):
182
+ return [TextContent(type="text", text=str(jsonable_result))]
183
+ else:
184
+ return [TextContent(type="text", text=json.dumps(jsonable_result))]
169
185
  except Exception:
170
186
  result = str(result)
171
187
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.2.4
3
+ Version: 2.2.5
4
4
  Summary: The fast, Pythonic way to build MCP servers.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -21,20 +21,20 @@ fastmcp/contrib/mcp_mixin/__init__.py,sha256=aw9IQ1ssNjCgws4ZNt8bkdpossAAGVAwwjB
21
21
  fastmcp/contrib/mcp_mixin/example.py,sha256=GnunkXmtG5hLLTUsM8aW5ZURU52Z8vI4tNLl-fK7Dg0,1228
22
22
  fastmcp/contrib/mcp_mixin/mcp_mixin.py,sha256=cfIRbnSxsVzglTD-auyTE0izVQeHP7Oz18qzYoBZJgg,7899
23
23
  fastmcp/prompts/__init__.py,sha256=LtPAv2JKIu54AwUd3iwv-HUd4DPcwgEqy6itEd3BH_E,194
24
- fastmcp/prompts/prompt.py,sha256=mQ6iRnt7J8oKBUhlgPDYXnIzwwDNWCk4heTqCmv1sco,6622
25
- fastmcp/prompts/prompt_manager.py,sha256=tMob9a-igjuzf6oTPLPGidFpJdg5JaPJVlYgyNkiCbE,2901
24
+ fastmcp/prompts/prompt.py,sha256=xNSlvs-vRB-kz7xnMYf2RwkiilbE0HcOoXgMS6gUogk,7974
25
+ fastmcp/prompts/prompt_manager.py,sha256=4CInlSWASjHUfGu2i0ig2ZICzHHxCMFGTXhuXgYdukQ,3237
26
26
  fastmcp/resources/__init__.py,sha256=t0x1j8lc74rjUKtXe9H5Gs4fpQt82K4NgBK6Y7A0xTg,467
27
- fastmcp/resources/resource.py,sha256=5FN2a7dpNwf7FSEYTNvQvkTxtodu1OPxSlJL-U-8yrM,2413
28
- fastmcp/resources/resource_manager.py,sha256=_0itubfjYvfkA_wXKa4DQN5YpE7ejXhsE1hdt7m8XwU,9072
29
- fastmcp/resources/template.py,sha256=3JL3I_pommv-K8Q0NLVMfbGAXtL7iuBaV-2yyUMxgnE,5832
30
- fastmcp/resources/types.py,sha256=tigil7z-SUJMakGXzDLIGSqTepPrAsRpwqwtBA4yoUY,6168
27
+ fastmcp/resources/resource.py,sha256=GGG0XugoIMbgAJRMVxsBcZLbt19W3COy8PyTh_uoWjs,2705
28
+ fastmcp/resources/resource_manager.py,sha256=yfQNCEUooZiQ8LNslnAzjQp4Vh-y1YOlFyGEQZ0BtAg,9586
29
+ fastmcp/resources/template.py,sha256=oa85KiuTjh3C7aZvMwmO4fwbTi6IvwlQ3fxizJuv3dk,7261
30
+ fastmcp/resources/types.py,sha256=c1z6BQSosgrlPQ3v67DuXCvDjCJMq9Xl45npEpyk0ik,7710
31
31
  fastmcp/server/__init__.py,sha256=pdkghG11VLMZiluQ-4_rl2JK1LMWmV003m9dDRUN8W4,92
32
32
  fastmcp/server/context.py,sha256=s1885AZRipKB3VltfaO3VEtMxGefKs8fdZByj-4tbNI,7120
33
- fastmcp/server/openapi.py,sha256=44jqx5Y08BiQX4Ftd2ZCncOnt8wt9aNuBzLUXBYMc3A,20750
34
- fastmcp/server/proxy.py,sha256=JHbxnOKbxyD5Jg2M_zSlNGKVBSZ5NUlVhQoKf442wxo,9619
35
- fastmcp/server/server.py,sha256=PFhnwa24diSKCz8KO39q43yuSHSbqYrzgnSspc-SPfg,31721
33
+ fastmcp/server/openapi.py,sha256=hFMOVe-bzudxP8SE-CqQhUWlUCVF5inGfMVL28HlqDs,21179
34
+ fastmcp/server/proxy.py,sha256=xOufto2gIfLk2BZfjhpLdZOlKDlJk5Rn6hCP0pzvaCU,10110
35
+ fastmcp/server/server.py,sha256=89RreIOw0siZmc6SlVlYWm6d6cFvjfVPY7mczXCNGFM,33032
36
36
  fastmcp/tools/__init__.py,sha256=ocw-SFTtN6vQ8fgnlF8iNAOflRmh79xS1xdO0Bc3QPE,96
37
- fastmcp/tools/tool.py,sha256=hAdeQaJ-1AgPyVZPvCQKYFkK0opccJWa39xWGFAWlzA,5975
37
+ fastmcp/tools/tool.py,sha256=k797XAeXdcUuBfeuvxkEy8xckXi7xSzQVgkzL876rBQ,6755
38
38
  fastmcp/tools/tool_manager.py,sha256=hClv7fwj0cQSSwW0i-Swt7xiVqR4T9LVmr1Tp704nW4,3283
39
39
  fastmcp/utilities/__init__.py,sha256=-imJ8S-rXmbXMWeDamldP-dHDqAPg_wwmPVz-LNX14E,31
40
40
  fastmcp/utilities/decorators.py,sha256=AjhjsetQZF4YOPV5MTZmIxO21iFp_4fDIS3O2_KNCEg,2990
@@ -42,8 +42,8 @@ fastmcp/utilities/func_metadata.py,sha256=iYXnx7MILOSL8mUQ6Rtq_6U7qA08OkoEN2APY8
42
42
  fastmcp/utilities/logging.py,sha256=zav8pnFxG_fvGJHUV2XpobmT9WVrmv1mlQBSCz-CPx4,1159
43
43
  fastmcp/utilities/openapi.py,sha256=PrH3usbTblaVC6jIH1UGiPEfgB2sSCLj33zA5dH7o_s,45193
44
44
  fastmcp/utilities/types.py,sha256=m2rPYMzO-ZFvvZ46N-1-Xqyw693K7yq9Z2xR4pVELyk,2091
45
- fastmcp-2.2.4.dist-info/METADATA,sha256=3qgwiPo7x3pPDatCPYyvKwPfRRmtd_INFQclHkfNxDY,27769
46
- fastmcp-2.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
- fastmcp-2.2.4.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
48
- fastmcp-2.2.4.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
49
- fastmcp-2.2.4.dist-info/RECORD,,
45
+ fastmcp-2.2.5.dist-info/METADATA,sha256=JKwmnn7QiN_KbNkC3ZyTpTEoUOTIDHPoCZFraw3eMR8,27769
46
+ fastmcp-2.2.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
47
+ fastmcp-2.2.5.dist-info/entry_points.txt,sha256=ff8bMtKX1JvXyurMibAacMSKbJEPmac9ffAKU9mLnM8,44
48
+ fastmcp-2.2.5.dist-info/licenses/LICENSE,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
49
+ fastmcp-2.2.5.dist-info/RECORD,,