1414from pydantic import AnyUrl , Field , ValidationInfo , validate_call
1515
1616from mcp .server .fastmcp .resources .base import Resource
17+ from mcp .server .fastmcp .utilities .context_injection import find_context_parameter , inject_context
1718from mcp .types import Annotations , Icon
1819
1920
@@ -22,7 +23,7 @@ class TextResource(Resource):
2223
2324 text : str = Field (description = "Text content of the resource" )
2425
25- async def read (self ) -> str :
26+ async def read (self , context : Any | None = None ) -> str :
2627 """Read the text content."""
2728 return self .text # pragma: no cover
2829
@@ -32,7 +33,7 @@ class BinaryResource(Resource):
3233
3334 data : bytes = Field (description = "Binary content of the resource" )
3435
35- async def read (self ) -> bytes :
36+ async def read (self , context : Any | None = None ) -> bytes :
3637 """Read the binary content."""
3738 return self .data # pragma: no cover
3839
@@ -51,24 +52,39 @@ class FunctionResource(Resource):
5152 """
5253
5354 fn : Callable [[], Any ] = Field (exclude = True )
55+ context_kwarg : str | None = Field (None , exclude = True )
56+
57+ async def read (self , context : Any | None = None ) -> str | bytes :
58+ """Read the resource content by calling the function."""
59+ # Inject context using utility which handles optimization
60+ # If context_kwarg is set, it's used directly (fast)
61+ # If not set (manual init), it falls back to inspection (safe)
62+ args = inject_context (self .fn , {}, context , self .context_kwarg )
5463
55- async def read (self ) -> str | bytes :
56- """Read the resource by calling the wrapped function."""
5764 try :
58- # Call the function first to see if it returns a coroutine
59- result = self .fn ()
60- # If it's a coroutine, await it
65+ if inspect .iscoroutinefunction (self .fn ):
66+ result = await self .fn (** args )
67+ else :
68+ result = self .fn (** args )
69+
70+ # Support cases where a sync function returns a coroutine
6171 if inspect .iscoroutine (result ):
62- result = await result
72+ result = await result # pragma: no cover
6373
64- if isinstance ( result , Resource ): # pragma: no cover
65- return await result . read ()
66- elif isinstance ( result , bytes ):
67- return result
68- elif isinstance (result , str ):
74+ # Support returning a Resource instance (recursive read)
75+ if isinstance ( result , Resource ):
76+ return await result . read ( context ) # pragma: no cover
77+
78+ if isinstance (result , str | bytes ):
6979 return result
70- else :
71- return pydantic_core .to_json (result , fallback = str , indent = 2 ).decode ()
80+ if isinstance (result , pydantic .BaseModel ):
81+ return result .model_dump_json (indent = 2 )
82+
83+ # For other types, convert to a JSON string
84+ try :
85+ return json .dumps (pydantic_core .to_jsonable_python (result ))
86+ except pydantic_core .PydanticSerializationError :
87+ return json .dumps (str (result ))
7288 except Exception as e :
7389 raise ValueError (f"Error reading resource { self .uri } : { e } " )
7490
@@ -86,8 +102,10 @@ def from_function(
86102 ) -> "FunctionResource" :
87103 """Create a FunctionResource from a function."""
88104 func_name = name or fn .__name__
89- if func_name == "<lambda>" : # pragma: no cover
90- raise ValueError ("You must provide a name for lambda functions" )
105+ if func_name == "<lambda>" :
106+ raise ValueError ("You must provide a name for lambda functions" ) # pragma: no cover
107+
108+ context_kwarg = find_context_parameter (fn ) or ""
91109
92110 # ensure the arguments are properly cast
93111 fn = validate_call (fn )
@@ -100,6 +118,7 @@ def from_function(
100118 mime_type = mime_type or "text/plain" ,
101119 fn = fn ,
102120 icons = icons ,
121+ context_kwarg = context_kwarg ,
103122 annotations = annotations ,
104123 )
105124
@@ -125,7 +144,7 @@ class FileResource(Resource):
125144 def validate_absolute_path (cls , path : Path ) -> Path : # pragma: no cover
126145 """Ensure path is absolute."""
127146 if not path .is_absolute ():
128- raise ValueError ("Path must be absolute" )
147+ raise ValueError ("Path must be absolute" ) # pragma: no cover
129148 return path
130149
131150 @pydantic .field_validator ("is_binary" )
@@ -137,7 +156,7 @@ def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> boo
137156 mime_type = info .data .get ("mime_type" , "text/plain" )
138157 return not mime_type .startswith ("text/" )
139158
140- async def read (self ) -> str | bytes :
159+ async def read (self , context : Any | None = None ) -> str | bytes :
141160 """Read the file content."""
142161 try :
143162 if self .is_binary :
@@ -153,7 +172,7 @@ class HttpResource(Resource):
153172 url : str = Field (description = "URL to fetch content from" )
154173 mime_type : str = Field (default = "application/json" , description = "MIME type of the resource content" )
155174
156- async def read (self ) -> str | bytes :
175+ async def read (self , context : Any | None = None ) -> str | bytes :
157176 """Read the HTTP content."""
158177 async with httpx .AsyncClient () as client : # pragma: no cover
159178 response = await client .get (self .url )
@@ -191,7 +210,7 @@ def list_files(self) -> list[Path]: # pragma: no cover
191210 except Exception as e :
192211 raise ValueError (f"Error listing directory { self .path } : { e } " )
193212
194- async def read (self ) -> str : # Always returns JSON string # pragma: no cover
213+ async def read (self , context : Any | None = None ) -> str : # Always returns JSON string # pragma: no cover
195214 """Read the directory listing."""
196215 try :
197216 files = await anyio .to_thread .run_sync (self .list_files )
0 commit comments