diff --git a/src/fastmcp/resources/resource_manager.py b/src/fastmcp/resources/resource_manager.py index 3950236a1..3e690d83e 100644 --- a/src/fastmcp/resources/resource_manager.py +++ b/src/fastmcp/resources/resource_manager.py @@ -19,9 +19,35 @@ class ResourceManager: self._templates: Dict[str, ResourceTemplate] = {} self.warn_on_duplicate_resources = warn_on_duplicate_resources + def add_resource(self, resource: Resource) -> Resource: + """Add a resource to the manager. + + Args: + resource: A Resource instance to add + + Returns: + The added resource. If a resource with the same URI already exists, + returns the existing resource. + """ + logger.debug( + "Adding resource", + extra={ + "uri": resource.uri, + "type": type(resource).__name__, + "name": resource.name, + }, + ) + existing = self._resources.get(str(resource.uri)) + if existing: + if self.warn_on_duplicate_resources: + logger.warning(f"Resource already exists: {resource.uri}") + return existing + self._resources[str(resource.uri)] = resource + return resource + def add_template( self, - func: Callable, + fn: Callable, uri_template: str, name: Optional[str] = None, description: Optional[str] = None, @@ -29,7 +55,7 @@ class ResourceManager: ) -> ResourceTemplate: """Add a template from a function.""" template = ResourceTemplate.from_function( - func, + fn, uri_template=uri_template, name=name, description=description, @@ -66,29 +92,3 @@ class ResourceManager: """List all registered templates.""" logger.debug("Listing templates", extra={"count": len(self._templates)}) return list(self._templates.values()) - - def add_resource(self, resource: Resource) -> Resource: - """Add a resource to the manager. - - Args: - resource: A Resource instance to add - - Returns: - The added resource. If a resource with the same URI already exists, - returns the existing resource. - """ - logger.debug( - "Adding resource", - extra={ - "uri": resource.uri, - "type": type(resource).__name__, - "name": resource.name, - }, - ) - existing = self._resources.get(str(resource.uri)) - if existing: - if self.warn_on_duplicate_resources: - logger.warning(f"Resource already exists: {resource.uri}") - return existing - self._resources[str(resource.uri)] = resource - return resource diff --git a/src/fastmcp/resources/templates.py b/src/fastmcp/resources/templates.py index 13d82c889..77c28e7dd 100644 --- a/src/fastmcp/resources/templates.py +++ b/src/fastmcp/resources/templates.py @@ -20,35 +20,35 @@ class ResourceTemplate(BaseModel): mime_type: str = Field( default="text/plain", description="MIME type of the resource content" ) - func: Callable = Field(exclude=True) + fn: Callable = Field(exclude=True) parameters: dict = Field(description="JSON schema for function parameters") @classmethod def from_function( cls, - func: Callable, + fn: Callable, uri_template: str, name: Optional[str] = None, description: Optional[str] = None, mime_type: Optional[str] = None, ) -> "ResourceTemplate": """Create a template from a function.""" - func_name = name or func.__name__ + func_name = name or fn.__name__ if func_name == "": raise ValueError("You must provide a name for lambda functions") # Get schema from TypeAdapter - will fail if function isn't properly typed - parameters = TypeAdapter(func).json_schema() + parameters = TypeAdapter(fn).json_schema() # ensure the arguments are properly cast - func = validate_call(func) + fn = validate_call(fn) return cls( uri_template=uri_template, name=func_name, - description=description or func.__doc__ or "", + description=description or fn.__doc__ or "", mime_type=mime_type or "text/plain", - func=func, + fn=fn, parameters=parameters, ) @@ -65,7 +65,7 @@ class ResourceTemplate(BaseModel): """Create a resource from the template with the given parameters.""" try: # Call function and check if result is a coroutine - result = self.func(**params) + result = self.fn(**params) if inspect.iscoroutine(result): result = await result @@ -74,7 +74,7 @@ class ResourceTemplate(BaseModel): name=self.name, description=self.description, mime_type=self.mime_type, - func=lambda: result, # Capture result in closure + fn=lambda: result, # Capture result in closure ) except Exception as e: raise ValueError(f"Error creating resource from template: {e}") diff --git a/src/fastmcp/resources/types.py b/src/fastmcp/resources/types.py index ea3df1a74..3dd8db115 100644 --- a/src/fastmcp/resources/types.py +++ b/src/fastmcp/resources/types.py @@ -46,12 +46,12 @@ class FunctionResource(Resource): - other types will be converted to JSON """ - func: Callable[[], Any] = Field(exclude=True) + fn: Callable[[], Any] = Field(exclude=True) async def read(self) -> Union[str, bytes]: """Read the resource by calling the wrapped function.""" try: - result = self.func() + result = self.fn() if isinstance(result, Resource): return await result.read() if isinstance(result, bytes): diff --git a/src/fastmcp/server.py b/src/fastmcp/server.py index 699d63243..fdf5871b9 100644 --- a/src/fastmcp/server.py +++ b/src/fastmcp/server.py @@ -188,7 +188,7 @@ class FastMCP: def add_tool( self, - func: Callable, + fn: Callable, name: Optional[str] = None, description: Optional[str] = None, ) -> None: @@ -198,11 +198,11 @@ class FastMCP: with the Context type annotation. See the @tool decorator for examples. Args: - func: The function to register as a tool + fn: The function to register as a tool name: Optional name for the tool (defaults to function name) description: Optional description of what the tool does """ - self._tool_manager.add_tool(func, name=name, description=description) + self._tool_manager.add_tool(fn, name=name, description=description) def tool( self, name: Optional[str] = None, description: Optional[str] = None @@ -238,9 +238,9 @@ class FastMCP: "Did you forget to call it? Use @tool() instead of @tool" ) - def decorator(func: Callable) -> Callable: - self.add_tool(func, name=name, description=description) - return func + def decorator(fn: Callable) -> Callable: + self.add_tool(fn, name=name, description=description) + return fn return decorator @@ -293,19 +293,19 @@ class FastMCP: "Did you forget to call it? Use @resource('uri') instead of @resource" ) - def decorator(func: Callable) -> Callable: - @functools.wraps(func) + def decorator(fn: Callable) -> Callable: + @functools.wraps(fn) def wrapper(*args: Any, **kwargs: Any) -> Any: - return func(*args, **kwargs) + return fn(*args, **kwargs) # Check if this should be a template has_uri_params = "{" in uri and "}" in uri - has_func_params = bool(inspect.signature(func).parameters) + has_func_params = bool(inspect.signature(fn).parameters) if has_uri_params or has_func_params: # Validate that URI params match function params uri_params = set(re.findall(r"{(\w+)}", uri)) - func_params = set(inspect.signature(func).parameters.keys()) + func_params = set(inspect.signature(fn).parameters.keys()) if uri_params != func_params: raise ValueError( @@ -328,7 +328,7 @@ class FastMCP: name=name, description=description, mime_type=mime_type or "text/plain", - func=wrapper, + fn=wrapper, ) self.add_resource(resource) return wrapper diff --git a/src/fastmcp/tools/base.py b/src/fastmcp/tools/base.py index de60a0faf..21c188cd1 100644 --- a/src/fastmcp/tools/base.py +++ b/src/fastmcp/tools/base.py @@ -15,7 +15,7 @@ if TYPE_CHECKING: class Tool(BaseModel): """Internal tool registration info.""" - func: Callable = Field(exclude=True) + fn: Callable = Field(exclude=True) name: str = Field(description="Name of the tool") description: str = Field(description="Description of what the tool does") parameters: dict = Field(description="JSON schema for tool parameters") @@ -27,36 +27,36 @@ class Tool(BaseModel): @classmethod def from_function( cls, - func: Callable, + fn: Callable, name: Optional[str] = None, description: Optional[str] = None, context_kwarg: Optional[str] = None, ) -> "Tool": """Create a Tool from a function.""" - func_name = name or func.__name__ + func_name = name or fn.__name__ if func_name == "": raise ValueError("You must provide a name for lambda functions") - func_doc = description or func.__doc__ or "" - is_async = inspect.iscoroutinefunction(func) + func_doc = description or fn.__doc__ or "" + is_async = inspect.iscoroutinefunction(fn) # Get schema from TypeAdapter - will fail if function isn't properly typed - parameters = TypeAdapter(func).json_schema() + parameters = TypeAdapter(fn).json_schema() # Find context parameter if it exists if context_kwarg is None: - sig = inspect.signature(func) + sig = inspect.signature(fn) for param_name, param in sig.parameters.items(): if param.annotation is fastmcp.Context: context_kwarg = param_name break # ensure the arguments are properly cast - func = validate_call(func) + fn = validate_call(fn) return cls( - func=func, + fn=fn, name=func_name, description=func_doc, parameters=parameters, @@ -73,7 +73,7 @@ class Tool(BaseModel): # Call function with proper async handling if self.is_async: - return await self.func(**arguments) - return self.func(**arguments) + return await self.fn(**arguments) + return self.fn(**arguments) except Exception as e: raise ToolError(f"Error executing tool {self.name}: {e}") from e diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index c8d6d3090..2a4fca0ff 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -30,12 +30,12 @@ class ToolManager: def add_tool( self, - func: Callable, + fn: Callable, name: Optional[str] = None, description: Optional[str] = None, ) -> Tool: """Add a tool to the server.""" - tool = Tool.from_function(func, name=name, description=description) + tool = Tool.from_function(fn, name=name, description=description) existing = self._tools.get(tool.name) if existing: if self.warn_on_duplicate_tools: diff --git a/tests/prompts/__init__.py b/tests/prompts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/resources/test_function_resources.py b/tests/resources/test_function_resources.py index f0b21ac62..f124d398d 100644 --- a/tests/resources/test_function_resources.py +++ b/tests/resources/test_function_resources.py @@ -16,13 +16,13 @@ class TestFunctionResource: uri="fn://test", name="test", description="test function", - func=my_func, + fn=my_func, ) assert str(resource.uri) == "fn://test" assert resource.name == "test" assert resource.description == "test function" assert resource.mime_type == "text/plain" # default - assert resource.func == my_func + assert resource.fn == my_func async def test_read_text(self): """Test reading text from a FunctionResource.""" @@ -33,7 +33,7 @@ class TestFunctionResource: resource = FunctionResource( uri="function://test", name="test", - func=get_data, + fn=get_data, ) content = await resource.read() assert content == "Hello, world!" @@ -48,7 +48,7 @@ class TestFunctionResource: resource = FunctionResource( uri="function://test", name="test", - func=get_data, + fn=get_data, ) content = await resource.read() assert content == b"Hello, world!" @@ -62,7 +62,7 @@ class TestFunctionResource: resource = FunctionResource( uri="function://test", name="test", - func=get_data, + fn=get_data, ) content = await resource.read() assert '"key": "value"' in content @@ -76,7 +76,7 @@ class TestFunctionResource: resource = FunctionResource( uri="function://test", name="test", - func=failing_func, + fn=failing_func, ) with pytest.raises(ValueError, match="Error reading resource function://test"): await resource.read() @@ -90,7 +90,7 @@ class TestFunctionResource: resource = FunctionResource( uri="function://test", name="test", - func=lambda: MyModel(name="test"), + fn=lambda: MyModel(name="test"), ) content = await resource.read() assert content == '{"name": "test"}' @@ -108,7 +108,7 @@ class TestFunctionResource: resource = FunctionResource( uri="function://test", name="test", - func=get_data, + fn=get_data, ) content = await resource.read() assert isinstance(content, str) diff --git a/tests/resources/test_resource_manager.py b/tests/resources/test_resource_manager.py index c63b85935..0422cebae 100644 --- a/tests/resources/test_resource_manager.py +++ b/tests/resources/test_resource_manager.py @@ -99,7 +99,7 @@ class TestResourceManager: return f"Hello, {name}!" template = ResourceTemplate.from_function( - func=greet, + fn=greet, uri_template="greet://{name}", name="greeter", ) diff --git a/tests/resources/test_resource_template.py b/tests/resources/test_resource_template.py index 0f72e9a85..08faf2e25 100644 --- a/tests/resources/test_resource_template.py +++ b/tests/resources/test_resource_template.py @@ -12,7 +12,7 @@ class TestResourceTemplate: return f"Weather in {city} ({units})" template = ResourceTemplate.from_function( - func=weather, + fn=weather, uri_template="weather://{city}/current", name="weather", description="Get current weather", @@ -29,7 +29,7 @@ class TestResourceTemplate: ValueError, match="You must provide a name for lambda functions" ): ResourceTemplate.from_function( - func=lambda x: x, + fn=lambda x: x, uri_template="test://{x}", ) @@ -40,7 +40,7 @@ class TestResourceTemplate: return x template = ResourceTemplate.from_function( - func=dummy, + fn=dummy, uri_template="test://{x}/value", name="test", ) @@ -60,7 +60,7 @@ class TestResourceTemplate: return f"Hello, {name}!" template = ResourceTemplate.from_function( - func=greet, + fn=greet, uri_template="greet://{name}", name="greeter", ) @@ -81,7 +81,7 @@ class TestResourceTemplate: return value.encode() template = ResourceTemplate.from_function( - func=get_bytes, + fn=get_bytes, uri_template="bytes://{value}", name="bytes", ) @@ -102,7 +102,7 @@ class TestResourceTemplate: return {"key": key, "value": 123} template = ResourceTemplate.from_function( - func=get_data, + fn=get_data, uri_template="data://{key}", name="data", ) @@ -124,7 +124,7 @@ class TestResourceTemplate: raise ValueError("Test error") template = ResourceTemplate.from_function( - func=failing_func, + fn=failing_func, uri_template="fail://{x}", name="fail", ) @@ -139,7 +139,7 @@ class TestResourceTemplate: return f"Hello, {name}!" template = ResourceTemplate.from_function( - func=greet, + fn=greet, uri_template="greet://{name}", name="greeter", ) @@ -160,7 +160,7 @@ class TestResourceTemplate: return value.encode() template = ResourceTemplate.from_function( - func=get_bytes, + fn=get_bytes, uri_template="bytes://{value}", name="bytes", ) @@ -181,7 +181,7 @@ class TestResourceTemplate: return {"key": key, "value": 123} template = ResourceTemplate.from_function( - func=get_data, + fn=get_data, uri_template="data://{key}", name="data", ) @@ -203,7 +203,7 @@ class TestResourceTemplate: raise ValueError("Test error") template = ResourceTemplate.from_function( - func=failing_func, + fn=failing_func, uri_template="fail://{x}", name="fail", ) @@ -223,7 +223,7 @@ class TestResourceTemplate: return async_helper(name) # Returns coroutine template = ResourceTemplate.from_function( - func=get_greeting, + fn=get_greeting, uri_template="greet://{name}", name="greeter", ) diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py index ffb92e099..658a1f1d9 100644 --- a/tests/resources/test_resources.py +++ b/tests/resources/test_resources.py @@ -15,7 +15,7 @@ class TestResourceValidation: resource = FunctionResource( uri="http://example.com/data", name="test", - func=dummy_func, + fn=dummy_func, ) assert str(resource.uri) == "http://example.com/data" @@ -24,7 +24,7 @@ class TestResourceValidation: FunctionResource( uri="invalid", name="test", - func=dummy_func, + fn=dummy_func, ) # Missing host @@ -32,7 +32,7 @@ class TestResourceValidation: FunctionResource( uri="http://", name="test", - func=dummy_func, + fn=dummy_func, ) def test_resource_name_from_uri(self): @@ -43,7 +43,7 @@ class TestResourceValidation: resource = FunctionResource( uri="resource://my-resource", - func=dummy_func, + fn=dummy_func, ) assert resource.name == "my-resource" @@ -56,14 +56,14 @@ class TestResourceValidation: # Must provide either name or URI with pytest.raises(ValueError, match="Either name or uri must be provided"): FunctionResource( - func=dummy_func, + fn=dummy_func, ) # Explicit name takes precedence over URI resource = FunctionResource( uri="resource://uri-name", name="explicit-name", - func=dummy_func, + fn=dummy_func, ) assert resource.name == "explicit-name" @@ -76,14 +76,14 @@ class TestResourceValidation: # Default mime type resource = FunctionResource( uri="resource://test", - func=dummy_func, + fn=dummy_func, ) assert resource.mime_type == "text/plain" # Custom mime type resource = FunctionResource( uri="resource://test", - func=dummy_func, + fn=dummy_func, mime_type="application/json", ) assert resource.mime_type == "application/json" diff --git a/tests/test_server.py b/tests/test_server.py index 29d892c39..947f28f21 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -205,7 +205,7 @@ class TestServerResources: def get_text(): return "Hello, world!" - resource = FunctionResource(uri="resource://test", name="test", func=get_text) + resource = FunctionResource(uri="resource://test", name="test", fn=get_text) mcp.add_resource(resource) async with client_session(mcp._mcp_server) as client: @@ -221,7 +221,7 @@ class TestServerResources: resource = FunctionResource( uri="resource://binary", name="binary", - func=get_binary, + fn=get_binary, is_binary=True, mime_type="application/octet-stream", )