Skip to content
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies = [
"mcp>=1.17.0,<2.0.0",
"openapi-pydantic>=0.5.1",
"platformdirs>=4.0.0",
"pydocket>=0.12.0",
"rich>=13.9.4",
"cyclopts>=3.0.0",
"authlib>=1.5.2",
Expand Down
3 changes: 3 additions & 0 deletions src/fastmcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
enable_rich_tracebacks=settings.enable_rich_tracebacks,
)

from docket import Depends

from fastmcp.server.server import FastMCP
from fastmcp.server.context import Context
import fastmcp.server
Expand Down Expand Up @@ -50,6 +52,7 @@ def __getattr__(name: str):
__all__ = [
"Client",
"Context",
"Depends",
"FastMCP",
"client",
"settings",
Expand Down
47 changes: 15 additions & 32 deletions src/fastmcp/prompts/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
from pydantic import Field, TypeAdapter

from fastmcp.exceptions import PromptError
from fastmcp.server.dependencies import get_context
from fastmcp.server.dependencies import get_context, without_injected_parameters
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.types import (
FastMCPBaseModel,
find_kwarg_by_type,
get_cached_typeadapter,
)

Expand Down Expand Up @@ -178,7 +177,6 @@ def from_function(
- A dict (converted to a message)
- A sequence of any of the above
"""
from fastmcp.server.context import Context

func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__

Expand All @@ -201,15 +199,10 @@ def from_function(
if isinstance(fn, staticmethod):
fn = fn.__func__

type_adapter = get_cached_typeadapter(fn)
wrapper_fn = without_injected_parameters(fn)
type_adapter = get_cached_typeadapter(wrapper_fn)
parameters = type_adapter.json_schema()

# Auto-detect context parameter if not provided

context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
prune_params = [context_kwarg] if context_kwarg else None

parameters = compress_schema(parameters, prune_params=prune_params)
parameters = compress_schema(parameters, prune_titles=True)

# Convert parameters to PromptArguments
arguments: list[PromptArgument] = []
Expand All @@ -224,7 +217,6 @@ def from_function(
if (
sig_param.annotation != inspect.Parameter.empty
and sig_param.annotation is not str
and param_name != context_kwarg
):
# Get the JSON schema for this specific parameter type
try:
Expand Down Expand Up @@ -266,23 +258,16 @@ def from_function(

def _convert_string_arguments(self, kwargs: dict[str, Any]) -> dict[str, Any]:
"""Convert string arguments to expected types based on function signature."""
from fastmcp.server.context import Context
from fastmcp.server.dependencies import without_injected_parameters

sig = inspect.signature(self.fn)
wrapper_fn = without_injected_parameters(self.fn)
sig = inspect.signature(wrapper_fn)
converted_kwargs = {}

# Find context parameter name if any
context_param_name = find_kwarg_by_type(self.fn, kwarg_type=Context)

for param_name, param_value in kwargs.items():
if param_name in sig.parameters:
param = sig.parameters[param_name]

# Skip Context parameters - they're handled separately
if param_name == context_param_name:
converted_kwargs[param_name] = param_value
continue

# If parameter has no annotation or annotation is str, pass as-is
if (
param.annotation == inspect.Parameter.empty
Expand Down Expand Up @@ -320,7 +305,7 @@ async def render(
arguments: dict[str, Any] | None = None,
) -> list[PromptMessage]:
"""Render the prompt with arguments."""
from fastmcp.server.context import Context
from fastmcp.server.dependencies import resolve_dependencies

# Validate required arguments
if self.arguments:
Expand All @@ -331,19 +316,17 @@ async def render(
raise ValueError(f"Missing required arguments: {missing}")

try:
# Prepare arguments with context
# Prepare arguments
kwargs = arguments.copy() if arguments else {}
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
if context_kwarg and context_kwarg not in kwargs:
kwargs[context_kwarg] = get_context()

# Convert string arguments to expected types when needed
# Convert string arguments to expected types BEFORE validation
kwargs = self._convert_string_arguments(kwargs)

# Call function and check if result is a coroutine
result = self.fn(**kwargs)
if inspect.isawaitable(result):
result = await result
async with resolve_dependencies(self.fn, kwargs) as resolved_kwargs:
result = self.fn(**resolved_kwargs)

if inspect.isawaitable(result):
result = await result

# Validate messages
if not isinstance(result, list | tuple):
Expand Down
14 changes: 5 additions & 9 deletions src/fastmcp/resources/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from fastmcp.server.dependencies import get_context
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.types import (
find_kwarg_by_type,
get_fn_name,
)

Expand Down Expand Up @@ -204,16 +203,13 @@ def from_function(

async def read(self) -> str | bytes:
"""Read the resource by calling the wrapped function."""
from fastmcp.server.context import Context
from fastmcp.server.dependencies import resolve_dependencies

kwargs = {}
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
if context_kwarg is not None:
kwargs[context_kwarg] = get_context()
async with resolve_dependencies(self.fn, {}) as kwargs:
result = self.fn(**kwargs)

result = self.fn(**kwargs)
if inspect.isawaitable(result):
result = await result
if inspect.isawaitable(result):
result = await result

if isinstance(result, Resource):
return await result.read()
Expand Down
64 changes: 23 additions & 41 deletions src/fastmcp/resources/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,10 @@
)

from fastmcp.resources.resource import Resource
from fastmcp.server.dependencies import get_context
from fastmcp.server.dependencies import get_context, without_injected_parameters
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.types import (
find_kwarg_by_type,
get_cached_typeadapter,
)
from fastmcp.utilities.types import get_cached_typeadapter


def extract_query_params(uri_template: str) -> set[str]:
Expand Down Expand Up @@ -242,42 +239,35 @@ class FunctionResourceTemplate(ResourceTemplate):

async def read(self, arguments: dict[str, Any]) -> str | bytes:
"""Read the resource content."""
from fastmcp.server.context import Context

# Add context to parameters if needed
kwargs = arguments.copy()
context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
if context_kwarg and context_kwarg not in kwargs:
kwargs[context_kwarg] = get_context()
from fastmcp.server.dependencies import resolve_dependencies

# Type coercion for query parameters (which arrive as strings)
# Get function signature for type hints
kwargs = arguments.copy()
sig = inspect.signature(self.fn)
for param_name, param_value in list(kwargs.items()):
if param_name in sig.parameters and isinstance(param_value, str):
param = sig.parameters[param_name]
annotation = param.annotation

# Skip if no annotation or annotation is str
if annotation is inspect.Parameter.empty or annotation is str:
continue

# Handle common type coercions
try:
if annotation is int:
kwargs[param_name] = int(param_value)
elif annotation is float:
kwargs[param_name] = float(param_value)
elif annotation is bool:
# Handle boolean strings
kwargs[param_name] = param_value.lower() in ("true", "1", "yes")
except (ValueError, AttributeError):
# Let validate_call handle the error
pass

result = self.fn(**kwargs)
if inspect.isawaitable(result):
result = await result
async with resolve_dependencies(self.fn, kwargs) as resolved_kwargs:
result = self.fn(**resolved_kwargs)

if inspect.isawaitable(result):
result = await result

return result

@classmethod
Expand All @@ -296,7 +286,6 @@ def from_function(
meta: dict[str, Any] | None = None,
) -> FunctionResourceTemplate:
"""Create a template from a function."""
from fastmcp.server.context import Context

func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
if func_name == "<lambda>":
Expand All @@ -311,10 +300,6 @@ def from_function(
"Functions with *args are not supported as resource templates"
)

# Auto-detect context parameter if not provided

context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)

# Extract path and query parameters from URI template
path_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
query_params = extract_query_params(uri_template)
Expand All @@ -323,24 +308,23 @@ def from_function(
if not all_uri_params:
raise ValueError("URI template must contain at least one parameter")

func_params = set(sig.parameters.keys())
if context_kwarg:
func_params.discard(context_kwarg)
# Use wrapper to get user-facing parameters (excludes injected params)
wrapper_fn = without_injected_parameters(fn)
user_sig = inspect.signature(wrapper_fn)
func_params = set(user_sig.parameters.keys())

# Get required and optional function parameters
required_params = {
p
for p in func_params
if sig.parameters[p].default is inspect.Parameter.empty
and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
and p != context_kwarg
if user_sig.parameters[p].default is inspect.Parameter.empty
and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
}
optional_params = {
p
for p in func_params
if sig.parameters[p].default is not inspect.Parameter.empty
and sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
and p != context_kwarg
if user_sig.parameters[p].default is not inspect.Parameter.empty
and user_sig.parameters[p].kind != inspect.Parameter.VAR_KEYWORD
}

# Validate RFC 6570 query parameters
Expand Down Expand Up @@ -377,15 +361,13 @@ def from_function(
if isinstance(fn, staticmethod):
fn = fn.__func__

type_adapter = get_cached_typeadapter(fn)
wrapper_fn = without_injected_parameters(fn)
type_adapter = get_cached_typeadapter(wrapper_fn)
parameters = type_adapter.json_schema()
parameters = compress_schema(parameters, prune_titles=True)

# compress the schema
prune_params = [context_kwarg] if context_kwarg else None
parameters = compress_schema(parameters, prune_params=prune_params)

# ensure the arguments are properly cast
fn = validate_call(fn)
# Use validate_call on wrapper for runtime type coercion
fn = validate_call(wrapper_fn)

return cls(
uri_template=uri_template,
Expand Down
Loading
Loading