Repository: jlowin/fastmcp (python)

Issue:
Allow optional types for resource template params
It would be nice if I could register a function as a resource_template where the function takes optional parameters and the resource_template only be required to fulfill the mandatory parameters.

`ValueError: Mismatch between URI parameters {'name'} and function parameters {'name', 'id', 'new_name'}`

e.x

```
def do_cool_thing(new_name: str, name: Optional[str], id: Optional[str] ):
   ...

 mcp.add_resource_fn(uri="scheme://{name}/{new_name}", do_cool_thing)
 mcp.add_resource_fn(uri="scheme://{id}/{new_name}", do_cool_thing)
```

Pull request description:
Allow resource templates to have optional / excluded arguments
Closes #162 

Patch:
diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx
index 3088b3d..c9363b7 100644
--- a/docs/servers/resources.mdx
+++ b/docs/servers/resources.mdx
@@ -183,40 +183,101 @@ from fastmcp import FastMCP
 mcp = FastMCP(name="DataServer")
 
 # Template URI includes {city} placeholder
-@mcp.resource("data://weather/{city}")
-# Function accepts 'city' parameter matching the placeholder
-def get_weather_for_city(city: str) -> dict:
+@mcp.resource("weather://{city}/current")
+def get_weather(city: str) -> dict:
     """Provides weather information for a specific city."""
-    print(f"Server: Generating weather for city: {city}...")
-    # In reality, call a weather API using the 'city' parameter
-    temp = 20 + len(city) % 5 # Dummy logic
-    condition = "Sunny" if len(city) % 2 == 0 else "Cloudy"
-    return {"city": city.capitalize(), "temperature": temp, "unit": "celsius", "condition": condition}
-
-# Template with an integer parameter
-@mcp.resource("users://{user_id}/profile")
-async def get_user_profile(user_id: int) -> dict:
-    """Retrieves a user's profile information by ID."""
-    print(f"Server: Generating profile for user ID: {user_id}...")
-    # In reality, fetch from database using user_id
-    # FastMCP uses Pydantic to auto-convert the string URI part to int
-    if user_id == 1:
-        return {"id": user_id, "name": "Alice", "email": "alice@example.com", "status": "active"}
-    elif user_id == 2:
-        return {"id": user_id, "name": "Bob", "email": "bob@example.com", "status": "inactive"}
+    # In a real implementation, this would call a weather API
+    # Here we're using simplified logic for example purposes
+    return {
+        "city": city.capitalize(),
+        "temperature": 22,
+        "condition": "Sunny",
+        "unit": "celsius"
+    }
+
+# Template with multiple parameters
+@mcp.resource("repos://{owner}/{repo}/info")
+def get_repo_info(owner: str, repo: str) -> dict:
+    """Retrieves information about a GitHub repository."""
+    # In a real implementation, this would call the GitHub API
+    return {
+        "owner": owner,
+        "name": repo,
+        "full_name": f"{owner}/{repo}",
+        "stars": 120,
+        "forks": 48
+    }
+```
+
+With these templates defined, clients can request:
+- `weather://london/current` → Returns weather for London
+- `repos://fastmcp/docs/info` → Returns info about the fastmcp/docs repository
+
+### Parameters and Default Values
+
+When creating resource templates, FastMCP enforces two rules for the relationship between URI template parameters and function parameters:
+
+1. **Required Function Parameters:** All function parameters without default values (required parameters) must appear in the URI template.
+2. **URI Parameters:** All URI template parameters must exist as function parameters.
+
+However, function parameters with default values don't need to be included in the URI template. When a client requests a resource, FastMCP will:
+
+- Extract parameter values from the URI for parameters included in the template
+- Use default values for any function parameters not in the URI template
+
+This allows for flexible API designs. For example, a simple search template with optional parameters:
+
+```python
+@mcp.resource("search://{query}")
+def search_resources(query: str, max_results: int = 10, include_archived: bool = False) -> dict:
+    """Search for resources matching the query string."""
+    # Only 'query' is required in the URI, the other parameters use their defaults
+    results = perform_search(query, limit=max_results, archived=include_archived)
+    return {
+        "query": query,
+        "max_results": max_results,
+        "include_archived": include_archived,
+        "results": results
+    }
+```
+
+With this template, clients can request `search://python` and the function will be called with `query="python", max_results=10, include_archived=False`. MCP Developers can still call the underlying `search_resources` function directly with more specific parameters.
+
+An even more powerful pattern is registering a single function with multiple URI templates, allowing different ways to access the same data:
+
+```python
+# Define a user lookup function that can be accessed by different identifiers
+@mcp.resource("users://email/{email}")
+@mcp.resource("users://name/{name}")
+def lookup_user(name: str | None = None, email: str | None = None) -> dict:
+    """Look up a user by either name or email."""
+    if email:
+        return find_user_by_email(email) # pseudocode
+    elif name:
+        return find_user_by_name(name) # pseudocode
     else:
-        # Example of returning an error structure
-        return {"error": f"User with ID {user_id} not found"}
+        return {"error": "No lookup parameters provided"}
 ```
 
+Now an LLM or client can retrieve user information in two different ways:
+- `users://email/alice@example.com` → Looks up user by email (with name=None)
+- `users://name/Bob` → Looks up user by name (with email=None)
+
+In this stacked decorator pattern:
+- The `name` parameter is only provided when using the `users://name/{name}` template
+- The `email` parameter is only provided when using the `users://email/{email}` template
+- Each parameter defaults to `None` when not included in the URI
+- The function logic handles whichever parameter is provided
+
 **How Templates Work:**
 
 1.  **Definition:** When FastMCP sees `{...}` placeholders in the `@resource` URI and matching function parameters, it registers a `ResourceTemplate`.
 2.  **Discovery:** Clients list templates via `resources/listResourceTemplates`.
-3.  **Request & Matching:** A client requests a specific URI, e.g., `data://weather/london`. FastMCP matches this to the `data://weather/{city}` template.
+3.  **Request & Matching:** A client requests a specific URI, e.g., `weather://london/current`. FastMCP matches this to the `weather://{city}/current` template.
 4.  **Parameter Extraction:** It extracts the parameter value: `city="london"`.
-5.  **Type Conversion & Function Call:** It converts the extracted string `"london"` to the type hinted in the function (`str` in this case) and calls `get_weather_for_city(city="london")`. For `users://1/profile`, it converts `"1"` to `int` before calling `get_user_profile(user_id=1)`.
-6.  **Response:** The function's return value is formatted (e.g., dict to JSON) and sent back as the content of the requested resource URI (`data://weather/london`).
+5.  **Type Conversion & Function Call:** It converts extracted values to the types hinted in the function and calls `get_weather(city="london")`.
+6.  **Default Values:** For any function parameters with default values not included in the URI template, FastMCP uses the default values.
+7.  **Response:** The function's return value is formatted (e.g., dict to JSON) and sent back as the resource content.
 
 Templates provide a powerful way to expose parameterized data access points following REST-like principles.
 
diff --git a/src/fastmcp/resources/resource_manager.py b/src/fastmcp/resources/resource_manager.py
index 2cfe2d6..d33aa6f 100644
--- a/src/fastmcp/resources/resource_manager.py
+++ b/src/fastmcp/resources/resource_manager.py
@@ -2,7 +2,6 @@
 
 import copy
 import inspect
-import re
 from collections.abc import Callable
 from typing import Any
 
@@ -52,7 +51,7 @@ class ResourceManager:
         has_uri_params = "{" in uri and "}" in uri
         has_func_params = bool(inspect.signature(fn).parameters)
 
-        if has_uri_params and has_func_params:
+        if has_uri_params or has_func_params:
             return self.add_template_from_fn(
                 fn, uri, name, description, mime_type, tags
             )
@@ -138,16 +137,6 @@ class ResourceManager:
     ) -> ResourceTemplate:
         """Create a template from a function."""
 
-        # Validate that URI params match function params
-        uri_params = set(re.findall(r"{(\w+)}", uri_template))
-        func_params = set(inspect.signature(fn).parameters.keys())
-
-        if uri_params != func_params:
-            raise ValueError(
-                f"Mismatch between URI parameters {uri_params} "
-                f"and function parameters {func_params}"
-            )
-
         template = ResourceTemplate.from_function(
             fn,
             uri_template=uri_template,
diff --git a/src/fastmcp/resources/template.py b/src/fastmcp/resources/template.py
index 022312b..aadae32 100644
--- a/src/fastmcp/resources/template.py
+++ b/src/fastmcp/resources/template.py
@@ -13,6 +13,11 @@ from fastmcp.resources.types import FunctionResource, Resource
 from fastmcp.utilities.types import _convert_set_defaults
 
 
+class MyModel(BaseModel):
+    key: str
+    value: int
+
+
 class ResourceTemplate(BaseModel):
     """A template for dynamically creating resources."""
 
@@ -47,6 +52,30 @@ class ResourceTemplate(BaseModel):
         if func_name == "<lambda>":
             raise ValueError("You must provide a name for lambda functions")
 
+        # Validate that URI params match function params
+        uri_params = set(re.findall(r"{(\w+)}", uri_template))
+        if not uri_params:
+            raise ValueError("URI template must contain at least one parameter")
+
+        func_params = set(inspect.signature(fn).parameters.keys())
+
+        # get the parameters that are required
+        required_params = {
+            p
+            for p in func_params
+            if inspect.signature(fn).parameters[p].default is inspect.Parameter.empty
+        }
+
+        if not required_params.issubset(uri_params):
+            raise ValueError(
+                f"URI parameters {uri_params} must be a subset of the required function arguments: {required_params}"
+            )
+
+        if not uri_params.issubset(func_params):
+            raise ValueError(
+                f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
+            )
+
         # Get schema from TypeAdapter - will fail if function isn't properly typed
         parameters = TypeAdapter(fn).json_schema()
 
diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py
index 4ac62bc..d353a92 100644
--- a/src/fastmcp/server/server.py
+++ b/src/fastmcp/server/server.py
@@ -363,7 +363,7 @@ class FastMCP(Generic[LifespanResultT]):
 
         def decorator(fn: AnyFunction) -> AnyFunction:
             self.add_tool(fn, name=name, description=description, tags=tags)
-            return DecoratedFunction(fn)
+            return fn
 
         return decorator
 
@@ -469,7 +469,7 @@ class FastMCP(Generic[LifespanResultT]):
                 mime_type=mime_type,
                 tags=tags,
             )
-            return DecoratedFunction(fn)
+            return fn
 
         return decorator
 


Test patch:
diff --git a/tests/resources/test_resource_template.py b/tests/resources/test_resource_template.py
index 2d9d9b7..6687f58 100644
--- a/tests/resources/test_resource_template.py
+++ b/tests/resources/test_resource_template.py
@@ -46,6 +46,99 @@ class TestResourceTemplate:
         assert template.matches("test://foo") is None
         assert template.matches("other://foo/123") is None
 
+    def test_template_uri_validation(self):
+        """Test validation rule: URI template must have at least one parameter."""
+
+        def my_func() -> dict:
+            return {"data": "value"}
+
+        with pytest.raises(
+            ValueError, match="URI template must contain at least one parameter"
+        ):
+            ResourceTemplate.from_function(
+                fn=my_func,
+                uri_template="test://no-params",
+                name="test",
+            )
+
+    def test_template_uri_params_subset_of_function_params(self):
+        """Test validation rule: URI parameters must be a subset of function parameters."""
+
+        def my_func(key: str, value: int) -> dict:
+            return {"key": key, "value": value}
+
+        # This should work - URI params are a subset of function params
+        template = ResourceTemplate.from_function(
+            fn=my_func,
+            uri_template="test://{key}/{value}",
+            name="test",
+        )
+        assert template.uri_template == "test://{key}/{value}"
+
+        # This should fail - 'unknown' is not a function parameter
+        with pytest.raises(
+            ValueError,
+            match="URI parameters .* must be a subset of the required function arguments",
+        ):
+            ResourceTemplate.from_function(
+                fn=my_func,
+                uri_template="test://{key}/{unknown}",
+                name="test",
+            )
+
+    def test_required_params_subset_of_uri_params(self):
+        """Test validation rule: Required function parameters must be in URI parameters."""
+
+        # Function with required parameters
+        def func_with_required(
+            required_param: str, optional_param: str = "default"
+        ) -> dict:
+            return {"required": required_param, "optional": optional_param}
+
+        # This should work - required param is in URI
+        template = ResourceTemplate.from_function(
+            fn=func_with_required,
+            uri_template="test://{required_param}",
+            name="test",
+        )
+        assert template.uri_template == "test://{required_param}"
+
+        # This should fail - required param is not in URI
+        with pytest.raises(
+            ValueError,
+            match="URI parameters .* must be a subset of the required function arguments",
+        ):
+            ResourceTemplate.from_function(
+                fn=func_with_required,
+                uri_template="test://{optional_param}",
+                name="test",
+            )
+
+    def test_multiple_required_params(self):
+        """Test validation with multiple required parameters."""
+
+        def multi_required(param1: str, param2: int, optional: str = "default") -> dict:
+            return {"p1": param1, "p2": param2, "opt": optional}
+
+        # This works - all required params in URI
+        template = ResourceTemplate.from_function(
+            fn=multi_required,
+            uri_template="test://{param1}/{param2}",
+            name="test",
+        )
+        assert template.uri_template == "test://{param1}/{param2}"
+
+        # This fails - missing one required param
+        with pytest.raises(
+            ValueError,
+            match="URI parameters .* must be a subset of the required function arguments",
+        ):
+            ResourceTemplate.from_function(
+                fn=multi_required,
+                uri_template="test://{param1}",
+                name="test",
+            )
+
     @pytest.mark.anyio
     async def test_create_resource(self):
         """Test creating a resource from a template."""
diff --git a/tests/server/test_server.py b/tests/server/test_server.py
index 004e113..56e558e 100644
--- a/tests/server/test_server.py
+++ b/tests/server/test_server.py
@@ -840,22 +840,28 @@ class TestServerResources:
 
 
 class TestServerResourceTemplates:
-    async def test_resource_with_params(self):
+    async def test_resource_with_params_not_in_uri(self):
         """Test that a resource with function parameters raises an error if the URI
         parameters don't match"""
         mcp = FastMCP()
 
-        with pytest.raises(ValueError, match="mismatch between URI parameters"):
+        with pytest.raises(
+            ValueError,
+            match="URI template must contain at least one parameter",
+        ):
 
             @mcp.resource("resource://data")
             def get_data_fn(param: str) -> str:
                 return f"Data: {param}"
 
-    async def test_resource_with_uri_params(self):
+    async def test_resource_with_uri_params_without_args(self):
         """Test that a resource with URI parameters is automatically a template"""
         mcp = FastMCP()
 
-        with pytest.raises(ValueError, match="mismatch between URI parameters"):
+        with pytest.raises(
+            ValueError,
+            match="URI parameters .* must be a subset of the function arguments",
+        ):
 
             @mcp.resource("resource://{param}")
             def get_data() -> str:
@@ -886,7 +892,10 @@ class TestServerResourceTemplates:
         """Test that mismatched parameters raise an error"""
         mcp = FastMCP()
 
-        with pytest.raises(ValueError, match="Mismatch between URI parameters"):
+        with pytest.raises(
+            ValueError,
+            match="URI parameters .* must be a subset of the required function arguments",
+        ):
 
             @mcp.resource("resource://{name}/data")
             def get_data(user: str) -> str:
@@ -911,7 +920,10 @@ class TestServerResourceTemplates:
         """Test that mismatched parameters raise an error"""
         mcp = FastMCP()
 
-        with pytest.raises(ValueError, match="Mismatch between URI parameters"):
+        with pytest.raises(
+            ValueError,
+            match="URI parameters .* must be a subset of the required function arguments",
+        ):
 
             @mcp.resource("resource://{org}/{repo}/data")
             def get_data_mismatched(org: str, repo_2: str) -> str:
@@ -929,6 +941,32 @@ class TestServerResourceTemplates:
             assert isinstance(result[0], TextResourceContents)
             assert result[0].text == "Static data"
 
+    async def test_template_with_default_params(self):
+        """Test that a template with default function parameters works when those parameters
+        are not in the URI template"""
+        mcp = FastMCP()
+
+        @mcp.resource("math://add/{x}")
+        def add(x: int, y: int = 10) -> int:
+            return x + y
+
+        # Verify it's registered as a template
+        templates = mcp.list_resource_templates()
+        assert len(templates) == 1
+        assert templates[0].uri_template == "math://add/{x}"
+
+        # Call the template and verify it uses the default value
+        async with Client(mcp) as client:
+            result = await client.read_resource(AnyUrl("math://add/5"))
+            assert isinstance(result[0], TextResourceContents)
+            assert result[0].text == "15"  # 5 + default 10
+
+        # Can also call with explicit params
+        resource = await mcp._resource_manager.get_resource("math://add/7")
+        assert isinstance(resource, FunctionResource)
+        result = await resource.read()
+        assert result == "17"  # 7 + default 10
+
     async def test_template_to_resource_conversion(self):
         """Test that templates are properly converted to resources when accessed"""
         mcp = FastMCP()
@@ -947,6 +985,54 @@ class TestServerResourceTemplates:
         result = await resource.read()
         assert result == "Data for test"
 
+    async def test_stacked_resource_template_decorators(self):
+        """Test that multiple resource decorators can be stacked on the same function."""
+        mcp = FastMCP()
+
+        # Define a function with multiple stacked resource decorators
+        @mcp.resource("users://email/{email}")
+        @mcp.resource("users://name/{name}")
+        def lookup_user(name: str | None = None, email: str | None = None) -> dict:
+            """Look up a user by either name or email."""
+            # In a real implementation, this would query a database
+            if email:
+                return {
+                    "found_by": "email",
+                    "name": f"User for {email}",
+                    "email": email,
+                }
+            else:
+                return {
+                    "found_by": "name",
+                    "name": name,
+                    "email": f"{name.lower()}@example.com" if name else None,
+                }
+
+        # Verify both templates are registered
+        templates = mcp.list_resource_templates()
+        assert len(templates) == 2
+        template_uris = {t.uri_template for t in templates}
+        assert "users://email/{email}" in template_uris
+        assert "users://name/{name}" in template_uris
+
+        # Test lookup by email
+        async with Client(mcp) as client:
+            email_result = await client.read_resource(
+                AnyUrl("users://email/user@example.com")
+            )
+            assert isinstance(email_result[0], TextResourceContents)
+            email_data = json.loads(email_result[0].text)
+            assert email_data["found_by"] == "email"
+            assert email_data["email"] == "user@example.com"
+
+            # Test lookup by name
+            name_result = await client.read_resource(AnyUrl("users://name/John"))
+            assert isinstance(name_result[0], TextResourceContents)
+            name_data = json.loads(name_result[0].text)
+            assert name_data["found_by"] == "name"
+            assert name_data["name"] == "John"
+            assert name_data["email"] == "john@example.com"
+
 
 class TestContextInjection:
     """Test context injection in tools."""