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
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,38 @@ 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+ args = {}
60+ if self .context_kwarg :
61+ args [self .context_kwarg ] = context
5462
55- async def read (self ) -> str | bytes :
56- """Read the resource by calling the wrapped function."""
5763 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
64+ if inspect .iscoroutinefunction (self .fn ):
65+ result = await self .fn (** args )
66+ else :
67+ result = self .fn (** args )
68+
69+ # Support cases where a sync function returns a coroutine
6170 if inspect .iscoroutine (result ):
62- result = await result
71+ result = await result # pragma: no cover
6372
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 ):
73+ # Support returning a Resource instance (recursive read)
74+ if isinstance ( result , Resource ):
75+ return await result . read ( context ) # pragma: no cover
76+
77+ if isinstance (result , str | bytes ):
6978 return result
70- else :
71- return pydantic_core .to_json (result , fallback = str , indent = 2 ).decode ()
79+ if isinstance (result , pydantic .BaseModel ):
80+ return result .model_dump_json (indent = 2 )
81+
82+ # For other types, convert to a JSON string
83+ try :
84+ return json .dumps (pydantic_core .to_jsonable_python (result ))
85+ except pydantic_core .PydanticSerializationError :
86+ return json .dumps (str (result ))
7287 except Exception as e :
7388 raise ValueError (f"Error reading resource { self .uri } : { e } " )
7489
@@ -86,8 +101,10 @@ def from_function(
86101 ) -> "FunctionResource" :
87102 """Create a FunctionResource from a function."""
88103 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" )
104+ if func_name == "<lambda>" :
105+ raise ValueError ("You must provide a name for lambda functions" ) # pragma: no cover
106+
107+ context_kwarg = find_context_parameter (fn )
91108
92109 # ensure the arguments are properly cast
93110 fn = validate_call (fn )
@@ -100,6 +117,7 @@ def from_function(
100117 mime_type = mime_type or "text/plain" ,
101118 fn = fn ,
102119 icons = icons ,
120+ context_kwarg = context_kwarg ,
103121 annotations = annotations ,
104122 )
105123
@@ -125,7 +143,7 @@ class FileResource(Resource):
125143 def validate_absolute_path (cls , path : Path ) -> Path : # pragma: no cover
126144 """Ensure path is absolute."""
127145 if not path .is_absolute ():
128- raise ValueError ("Path must be absolute" )
146+ raise ValueError ("Path must be absolute" ) # pragma: no cover
129147 return path
130148
131149 @pydantic .field_validator ("is_binary" )
@@ -137,7 +155,7 @@ def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> boo
137155 mime_type = info .data .get ("mime_type" , "text/plain" )
138156 return not mime_type .startswith ("text/" )
139157
140- async def read (self ) -> str | bytes :
158+ async def read (self , context : Any | None = None ) -> str | bytes :
141159 """Read the file content."""
142160 try :
143161 if self .is_binary :
@@ -153,7 +171,7 @@ class HttpResource(Resource):
153171 url : str = Field (description = "URL to fetch content from" )
154172 mime_type : str = Field (default = "application/json" , description = "MIME type of the resource content" )
155173
156- async def read (self ) -> str | bytes :
174+ async def read (self , context : Any | None = None ) -> str | bytes :
157175 """Read the HTTP content."""
158176 async with httpx .AsyncClient () as client : # pragma: no cover
159177 response = await client .get (self .url )
@@ -191,7 +209,7 @@ def list_files(self) -> list[Path]: # pragma: no cover
191209 except Exception as e :
192210 raise ValueError (f"Error listing directory { self .path } : { e } " )
193211
194- async def read (self ) -> str : # Always returns JSON string # pragma: no cover
212+ async def read (self , context : Any | None = None ) -> str : # Always returns JSON string # pragma: no cover
195213 """Read the directory listing."""
196214 try :
197215 files = await anyio .to_thread .run_sync (self .list_files )
0 commit comments