-
Notifications
You must be signed in to change notification settings - Fork 1.9k
[DNM] Support functools.partial as tools, prompts, and resources #3870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import functools | ||
| import inspect | ||
| import warnings | ||
| from collections.abc import Callable | ||
|
|
@@ -14,7 +13,7 @@ | |
| from pydantic.json_schema import SkipJsonSchema | ||
|
|
||
| import fastmcp | ||
| from fastmcp.decorators import resolve_task_config | ||
| from fastmcp.decorators import resolve_task_config, set_fastmcp_meta | ||
| from fastmcp.exceptions import FastMCPDeprecationWarning | ||
| from fastmcp.resources.base import Resource, ResourceResult | ||
| from fastmcp.server.auth.authorization import AuthCheck | ||
|
|
@@ -27,6 +26,11 @@ | |
| call_sync_fn_in_threadpool, | ||
| is_coroutine_function, | ||
| ) | ||
| from fastmcp.utilities.callable_utils import ( | ||
| get_callable_name, | ||
| is_callable_object, | ||
| prepare_callable, | ||
| ) | ||
| from fastmcp.utilities.mime import resolve_ui_mime_type | ||
|
|
||
| if TYPE_CHECKING: | ||
|
|
@@ -159,27 +163,12 @@ def from_function( | |
|
|
||
| uri_obj = AnyUrl(metadata.uri) | ||
|
|
||
| # Get function name - use class name for callable objects | ||
| func_name = ( | ||
| metadata.name or getattr(fn, "__name__", None) or fn.__class__.__name__ | ||
| ) | ||
| func_name = metadata.name or get_callable_name(fn) | ||
|
|
||
| # Normalize task to TaskConfig and validate | ||
| task_value = metadata.task | ||
| if task_value is None: | ||
| task_config = TaskConfig(mode="forbidden") | ||
| elif isinstance(task_value, bool): | ||
| task_config = TaskConfig.from_bool(task_value) | ||
| else: | ||
| task_config = task_value | ||
| task_config = TaskConfig.normalize(metadata.task) | ||
| task_config.validate_function(fn, func_name) | ||
|
|
||
| # if the fn is a callable class, we need to get the __call__ method from here out | ||
| if not inspect.isroutine(fn) and not isinstance(fn, functools.partial): | ||
| fn = fn.__call__ | ||
| # if the fn is a staticmethod, we need to work with the underlying function | ||
| if isinstance(fn, staticmethod): | ||
| fn = fn.__func__ | ||
| fn = prepare_callable(fn) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Calling Useful? React with 👍 / 👎. |
||
|
|
||
| # Transform Context type annotations to Depends() for unified DI | ||
| fn = transform_context_annotations(fn) | ||
|
|
@@ -259,7 +248,7 @@ def resource( | |
| if isinstance(annotations, dict): | ||
| annotations = Annotations(**annotations) | ||
|
|
||
| if inspect.isroutine(uri): | ||
| if is_callable_object(uri): | ||
| raise TypeError( | ||
| "The @resource decorator requires a URI. " | ||
| "Use @resource('uri') instead of @resource" | ||
|
|
@@ -325,8 +314,7 @@ def attach_metadata(fn: F) -> F: | |
| task=task, | ||
| auth=auth, | ||
| ) | ||
| target = fn.__func__ if hasattr(fn, "__func__") else fn | ||
| target.__fastmcp__ = metadata | ||
| set_fastmcp_meta(fn, metadata) | ||
| return fn | ||
|
|
||
| def decorator(fn: F) -> F: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import functools | ||
| import inspect | ||
| import re | ||
| from collections.abc import Callable | ||
|
|
@@ -30,6 +29,7 @@ | |
| without_injected_parameters, | ||
| ) | ||
| from fastmcp.server.tasks.config import TaskConfig, TaskMeta | ||
| from fastmcp.utilities.callable_utils import get_callable_name, prepare_callable | ||
| from fastmcp.utilities.components import FastMCPComponent | ||
| from fastmcp.utilities.json_schema import compress_schema | ||
| from fastmcp.utilities.mime import resolve_ui_mime_type | ||
|
|
@@ -488,7 +488,7 @@ def from_function( | |
| ) -> FunctionResourceTemplate: | ||
| """Create a template from a function.""" | ||
|
|
||
| func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__ | ||
| func_name = name or get_callable_name(fn) | ||
| if func_name == "<lambda>": | ||
| raise ValueError("You must provide a name for lambda functions") | ||
|
|
||
|
|
@@ -555,21 +555,10 @@ def from_function( | |
|
|
||
| description = description if description is not None else inspect.getdoc(fn) | ||
|
|
||
| # Normalize task to TaskConfig and validate | ||
| if task is None: | ||
| task_config = TaskConfig(mode="forbidden") | ||
| elif isinstance(task, bool): | ||
| task_config = TaskConfig.from_bool(task) | ||
| else: | ||
| task_config = task | ||
| task_config = TaskConfig.normalize(task) | ||
| task_config.validate_function(fn, func_name) | ||
|
|
||
| # if the fn is a callable class, we need to get the __call__ method from here out | ||
| if not inspect.isroutine(fn) and not isinstance(fn, functools.partial): | ||
| fn = fn.__call__ | ||
| # if the fn is a staticmethod, we need to work with the underlying function | ||
| if isinstance(fn, staticmethod): | ||
| fn = fn.__func__ | ||
| fn = prepare_callable(fn) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Call-site validation for Useful? React with 👍 / 👎. |
||
|
|
||
| # Transform Context type annotations to Depends() for unified DI | ||
| fn = transform_context_annotations(fn) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allowing
functools.partialthrough_dispatch_decoratornow routesapp.tool(partial_fn)into_register, but that path still requiresgetattr(fn, "__name__", None), which is missing for partials unless users calledfunctools.update_wrapperor passedname=. In practice,app.tool(functools.partial(add, y=10))now fails withValueError("Cannot determine tool name..."), while other tool entry points (Tool.from_function/mcp.add_tool) can infer names for the same callable. This leaves partial support inconsistent and broken for theFastMCPApp.toolAPI.Useful? React with 👍 / 👎.