[DNM] Support functools.partial as tools, prompts, and resources#3870
[DNM] Support functools.partial as tools, prompts, and resources#3870
Conversation
Test Failure AnalysisSummary: The Root Cause: The new test file imports Suggested Solution: Fix the import in # Change this:
from fastmcp.tools.tool import Tool
# To this:
from fastmcp.tools.base import ToolDetailed AnalysisFailing check: Log excerpt: Module structure (
There is no Related Files
🤖 Triage by Marvin |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4478c68418
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # 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.
Preserve partial doc metadata when creating resources
Calling prepare_callable(fn) here reconstructs functools.partial objects that were wrapped with functools.update_wrapper, which drops copied attributes like __doc__; later in FunctionResource.from_function the default description is derived from inspect.getdoc(fn), so resources created from wrapped partials now get the generic functools.partial docstring instead of the original function doc. This is a user-visible regression for mcp.add_resource(partial_fn)/@resource(...) flows that rely on inferred descriptions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d26254a84d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # 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.
Prepare partials before validating resource-template params
Call-site validation for FunctionResourceTemplate.from_function runs on the original callable before this prepare_callable step, so functools.update_wrapper-wrapped partials are still validated against the unbound signature. In practice, a partial that binds a required parameter (for example partial(fn, fmt='json') when fmt has no default) is incorrectly rejected as missing URI params, even though the partial already supplies it. Running prepare_callable before the earlier signature/parameter checks avoids this false ValueError and makes wrapped partials behave consistently with the new partial support.
Useful? React with 👍 / 👎.
e5a7d40 to
d73e6b2
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d73e6b2426
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if is_callable_object(name_or_fn): | ||
| return register(name_or_fn, name) |
There was a problem hiding this comment.
Derive app tool names for partial callables
Allowing functools.partial through _dispatch_decorator now routes app.tool(partial_fn) into _register, but that path still requires getattr(fn, "__name__", None), which is missing for partials unless users called functools.update_wrapper or passed name=. In practice, app.tool(functools.partial(add, y=10)) now fails with ValueError("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 the FastMCPApp.tool API.
Useful? React with 👍 / 👎.
functools.partial objects failed at registration (@mcp.tool didn't recognize them) and at call time (update_wrapper set __wrapped__ causing Pydantic to ignore bound arguments). Introduces centralized utilities replacing scattered patterns across 11 files: callable_utils.py: - is_callable_object(): TypeGuard replacing inspect.isroutine() in 7 decorator entry points — recognizes partials as callables - get_callable_name(): Extracts useful names from any callable type, including partials without update_wrapper - prepare_callable(): Strips __wrapped__, unwraps callable classes and staticmethod — replaces 4 duplicated blocks decorators.py: - set_fastmcp_meta(): Attaches __fastmcp__ metadata through __func__ for bound methods — replaces 5 identical 2-line blocks TaskConfig: - normalize(): Converts bool|TaskConfig|None to TaskConfig — replaces 4 identical 6-line if/elif/else blocks No behavior changes beyond the bug fix: existing lambda rejection, validation, and error handling remain per-module policy. Closes #3266 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Unit tests for callable_utils.py (is_callable_object, get_callable_name, prepare_callable) covering functions, partials, callable classes, staticmethod, and lambda edge cases. Integration tests for partial support across tools, prompts, and resources via both add_tool/add_prompt and decorator APIs. 🤖 Generated with Claude Code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d73e6b2 to
eb45781
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eb4578170f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Strip __wrapped__ from partials so Pydantic sees the partial's own | ||
| # signature with bound args removed, not the original function's signature. | ||
| if isinstance(fn, functools.partial) and hasattr(fn, "__wrapped__"): | ||
| fn = functools.partial(fn.func, *fn.args, **fn.keywords) |
There was a problem hiding this comment.
Preserve annotation metadata when cloning wrapped partials
Rebuilding a functools.partial here drops attributes copied by functools.update_wrapper (notably annotation/module context), so partials that rely on deferred annotations (e.g. from __future__ import annotations with custom types like 'User') can no longer be introspected by downstream schema generation and fail with NameError during tool/prompt/resource registration. This is a regression for wrapped partials that previously carried enough metadata for type resolution; the clone should retain the copied metadata while removing only __wrapped__.
Useful? React with 👍 / 👎.
Supersedes #3269. Closes #3266.
Bug fix
functools.partialobjects failed in two ways:@mcp.tool(partial_fn)raisedTypeErrorbecauseinspect.isroutine()returns False for partialsmcp.add_tool(partial_fn)withupdate_wrappercaused Pydantic to follow__wrapped__back to the original signature, ignoring bound arguments — tools accepted the wrong parameters at call timeBoth issues affected prompts and resources too, not just tools.
Consolidation (no behavior changes)
The fix required touching callable-handling code scattered across 11 files. Rather than adding 13 more
isinstance(fn, functools.partial)checks (as #3269 did), this PR centralizes the patterns into focused utilities:is_callable_object()inspect.isroutine()checks in decorator gatesget_callable_name()getattr(fn, "__name__") or fn.__class__.__name__prepare_callable()set_fastmcp_meta()target = fn.__func__; target.__fastmcp__ = metadataTaskConfig.normalize()Each utility does one thing and is directly tested.
🤖 Generated with Claude Code