Skip to content

Comments

[Draft] Support functools.partial as tools, prompts, and resources#3269

Open
strawgate wants to merge 2 commits intomainfrom
claude/issue-3266-20260221-2108
Open

[Draft] Support functools.partial as tools, prompts, and resources#3269
strawgate wants to merge 2 commits intomainfrom
claude/issue-3266-20260221-2108

Conversation

@strawgate
Copy link
Collaborator

Closes #3266

functools.partial objects failed with a PydanticSchemaGenerationError because inspect.isroutine() returns False for them, causing FastMCP to unwrap them via .__call__ into a method-wrapper that Pydantic can't handle. Additionally, functools.update_wrapper() sets __wrapped__ which causes Pydantic to follow it back to the original function, ignoring the partial's bound arguments.

This PR teaches FastMCP to recognize functools.partial as a first-class callable throughout the tool, prompt, and resource pipelines — skipping the __call__ unwrap and stripping __wrapped__ so Pydantic sees the partial's own signature.

import functools
from fastmcp import FastMCP

mcp = FastMCP("demo")

def add(x: int, y: int) -> int:
    return x + y

add_ten = functools.partial(add, y=10)
functools.update_wrapper(add_ten, add)

mcp.add_tool(add_ten)

Generated with Claude Code

Marvin Context Protocol and others added 2 commits February 21, 2026 21:18
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>

🤖 Generated with Claude Code
@strawgate
Copy link
Collaborator Author

/marvin why do we need

        if isinstance(fn, functools.partial) and hasattr(fn, "__wrapped__"):
            fn = functools.partial(fn.func, *fn.args, **fn.keywords)

@marvin-context-protocol marvin-context-protocol bot added bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality. labels Feb 21, 2026
@marvin-context-protocol
Copy link
Contributor

marvin-context-protocol bot commented Feb 21, 2026

Claude finished @strawgate's task in 1m 34s —— View job


Why the __wrapped__ stripping is needed

The code:

if isinstance(fn, functools.partial) and hasattr(fn, "__wrapped__"):
    fn = functools.partial(fn.func, *fn.args, **fn.keywords)

exists to counteract an interaction between functools.update_wrapper() and Pydantic's schema generation.

The setup: When a user writes the common pattern:

add_ten = functools.partial(add, y=10)
functools.update_wrapper(add_ten, add)  # sets add_ten.__wrapped__ = add

update_wrapper copies attributes from add onto the partial, including setting add_ten.__wrapped__ = add (the original, unwrapped function).

The problem: Pydantic's schema generation follows __wrapped__ to introspect the function. When it sees __wrapped__ on the partial, it follows the attribute back to the original add(x: int, y: int) and builds a schema requiring both x and y — completely ignoring that y is already bound by the partial to 10.

Without this fix, Tool.from_function(add_ten) would generate an input schema demanding both x and y, and calling the tool with just {"x": 5} would fail validation.

The fix: Reconstructing a fresh functools.partial from the same components (.func, .args, .keywords) produces a functionally identical partial but without the __wrapped__ attribute that update_wrapper injected. Pydantic then correctly introspects the partial's own signature, sees that y is bound with default 10, and generates a schema where only x is required.

The guard hasattr(fn, "__wrapped__") ensures this reconstruction only happens when update_wrapper was actually called — a bare functools.partial(add, y=10) without update_wrapper already works correctly and doesn't need this treatment.


Marvin Context Protocol | Type /marvin to interact further

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ef84c0eab

ℹ️ 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".

Comment on lines +109 to +110
if isinstance(fn, functools.partial) and hasattr(fn, "__wrapped__"):
fn = functools.partial(fn.func, *fn.args, **fn.keywords)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve partial metadata when stripping wrapped

Reconstructing the functools.partial object here removes attributes that functools.update_wrapper added (notably __annotations__ and module context), and downstream schema generation relies on that metadata for resolving postponed/forward type hints. For wrapped partial tools that use string or forward-referenced annotations, this can degrade or break input/output schema generation during registration. Prefer removing only __wrapped__ (or copying metadata onto the new partial) so bound-argument handling does not discard type information.

Useful? React with 👍 / 👎.

Comment on lines +176 to +177
if isinstance(fn, functools.partial) and hasattr(fn, "__wrapped__"):
fn = functools.partial(fn.func, *fn.args, **fn.keywords)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep resource docstring after partial normalization

This creates a fresh partial and drops the wrapped function metadata copied by update_wrapper, including __doc__; later description=metadata.description or inspect.getdoc(fn) will then use the generic functools.partial doc instead of the resource function's docstring. As a result, resources registered from wrapped partials expose incorrect descriptions unless callers always pass description explicitly.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working. Reports of errors, unexpected behavior, or broken functionality. server Related to FastMCP server implementation or server-side functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

What does this error message require me to do, please? [not a bug]

1 participant