11"""Concrete resource implementations."""
22
3+ from __future__ import annotations
4+
35import inspect
46import json
57from collections .abc import Callable
68from pathlib import Path
7- from typing import Any
9+ from typing import TYPE_CHECKING , Any
810
911import anyio
1012import anyio .to_thread
1113import httpx
1214import pydantic
1315import pydantic_core
14- from pydantic import Field , ValidationInfo , validate_call
16+ from pydantic import Field , ValidationInfo
1517
1618from mcp .server .mcpserver .resources .base import Resource
19+ from mcp .server .mcpserver .utilities .context_injection import find_context_parameter
1720from mcp .types import Annotations , Icon
1821
22+ if TYPE_CHECKING :
23+ from mcp .server .context import LifespanContextT , RequestT
24+ from mcp .server .mcpserver .server import Context
25+
1926
2027class TextResource (Resource ):
2128 """A resource that reads from a string."""
2229
2330 text : str = Field (description = "Text content of the resource" )
2431
25- async def read (self ) -> str :
32+ async def read (
33+ self ,
34+ context : Context [LifespanContextT , RequestT ] | None = None ,
35+ ) -> str :
2636 """Read the text content."""
2737 return self .text # pragma: no cover
2838
@@ -32,7 +42,10 @@ class BinaryResource(Resource):
3242
3343 data : bytes = Field (description = "Binary content of the resource" )
3444
35- async def read (self ) -> bytes :
45+ async def read (
46+ self ,
47+ context : Context [LifespanContextT , RequestT ] | None = None ,
48+ ) -> bytes :
3649 """Read the binary content."""
3750 return self .data # pragma: no cover
3851
@@ -50,13 +63,22 @@ class FunctionResource(Resource):
5063 - other types will be converted to JSON
5164 """
5265
53- fn : Callable [[], Any ] = Field (exclude = True )
66+ fn : Callable [..., Any ] = Field (exclude = True )
67+ context_kwarg : str | None = Field (default = None , description = "Name of the kwarg that should receive context" )
5468
55- async def read (self ) -> str | bytes :
69+ async def read (
70+ self ,
71+ context : Context [LifespanContextT , RequestT ] | None = None ,
72+ ) -> str | bytes :
5673 """Read the resource by calling the wrapped function."""
5774 try :
58- # Call the function first to see if it returns a coroutine
59- result = self .fn ()
75+ # Inject context if needed
76+ kwargs : dict [str , Any ] = {}
77+ if self .context_kwarg is not None and context is not None :
78+ kwargs [self .context_kwarg ] = context
79+
80+ # Call the function
81+ result = self .fn (** kwargs )
6082 # If it's a coroutine, await it
6183 if inspect .iscoroutine (result ):
6284 result = await result
@@ -84,14 +106,14 @@ def from_function(
84106 icons : list [Icon ] | None = None ,
85107 annotations : Annotations | None = None ,
86108 meta : dict [str , Any ] | None = None ,
87- ) -> " FunctionResource" :
109+ ) -> FunctionResource :
88110 """Create a FunctionResource from a function."""
89111 func_name = name or fn .__name__
90112 if func_name == "<lambda>" : # pragma: no cover
91113 raise ValueError ("You must provide a name for lambda functions" )
92114
93- # ensure the arguments are properly cast
94- fn = validate_call (fn )
115+ # Find context parameter if it exists
116+ context_kwarg = find_context_parameter (fn )
95117
96118 return cls (
97119 uri = uri ,
@@ -100,6 +122,7 @@ def from_function(
100122 description = description or fn .__doc__ or "" ,
101123 mime_type = mime_type or "text/plain" ,
102124 fn = fn ,
125+ context_kwarg = context_kwarg ,
103126 icons = icons ,
104127 annotations = annotations ,
105128 meta = meta ,
@@ -139,7 +162,10 @@ def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> boo
139162 mime_type = info .data .get ("mime_type" , "text/plain" )
140163 return not mime_type .startswith ("text/" )
141164
142- async def read (self ) -> str | bytes :
165+ async def read (
166+ self ,
167+ context : Context [LifespanContextT , RequestT ] | None = None ,
168+ ) -> str | bytes :
143169 """Read the file content."""
144170 try :
145171 if self .is_binary :
@@ -155,7 +181,10 @@ class HttpResource(Resource):
155181 url : str = Field (description = "URL to fetch content from" )
156182 mime_type : str = Field (default = "application/json" , description = "MIME type of the resource content" )
157183
158- async def read (self ) -> str | bytes :
184+ async def read (
185+ self ,
186+ context : Context [LifespanContextT , RequestT ] | None = None ,
187+ ) -> str | bytes :
159188 """Read the HTTP content."""
160189 async with httpx .AsyncClient () as client : # pragma: no cover
161190 response = await client .get (self .url )
@@ -193,7 +222,10 @@ def list_files(self) -> list[Path]: # pragma: no cover
193222 except Exception as e :
194223 raise ValueError (f"Error listing directory { self .path } : { e } " )
195224
196- async def read (self ) -> str : # Always returns JSON string # pragma: no cover
225+ async def read (
226+ self ,
227+ context : Context [LifespanContextT , RequestT ] | None = None ,
228+ ) -> str : # Always returns JSON string # pragma: no cover
197229 """Read the directory listing."""
198230 try :
199231 files = await anyio .to_thread .run_sync (self .list_files )
0 commit comments