Skip to content

fix(mcpserver): stop a bare NameError escaping tool registration on 3.14 - #3686

Closed
insuffer wants to merge 1 commit into
modelcontextprotocol:mainfrom
insuffer:fix/pep649-forward-ref-tool-registration
Closed

insuffer wants to merge 1 commit into
modelcontextprotocol:mainfrom
insuffer:fix/pep649-forward-ref-tool-registration

Conversation

@insuffer

@insuffer insuffer commented Oct 12, 2026 •

Copy link
Copy Markdown

What

Since Python 3.14 (PEP 649/749) annotations are evaluated lazily, so a tool may legally name a type the module defines further down, without quoting:

@app.tool()
def search(q: Query) -> list[Hit]: ...

class Query(BaseModel): ...
class Hit(BaseModel): ...

MCPServer.tool() builds the Tool eagerly, and three of its steps call inspect.signature(fn) while only needing the parameter names. The default annotation_format evaluates __annotate__, so registration died with a bare NameError: name 'Query' is not defined raised from inside inspect. Quoting the annotation — or from __future__ import annotations — failed one step later with InvalidSignature, so the error type depended on how the annotation was written.

How

Those three steps now read names through a new signature_parameters() in mcp/shared/_callable_inspection.py, which asks for annotationlib.Format.FORWARDREF on 3.14+ and keeps the plain inspect.signature call on older Pythons, where string annotations are already returned unevaluated:

  • resolve.py — find_resolved_parameters and build_resolver_plans
  • server.py — the resource decorator (the one place there that needs a default reads it from the same object)

Registration then reaches the step that genuinely needs the types and fails there with the InvalidSignature it already produced for a quoted annotation. That message now includes the unresolved name, so it says which type to move above the decorated function instead of only that something could not be evaluated:

InvalidSignature: Unable to evaluate type annotations for callable 'search': name 'Query' is not defined

Scope

The issue's preferred outcome is that the tool registers, by deferring the schema build until first use. That is a change to the Tool lifecycle and the issue explicitly leaves it to the maintainers' preference, so this PR delivers the outcome the issue also accepts — a clear InvalidSignature naming the tool and the unresolved name — and makes the failure consistent regardless of how the annotation is written. Deferring the build on top of this would be additive.

Tests

Three tests, all built with exec and gated on 3.14+, because an unquoted undefined name raises where the function is defined on older Pythons and would make the module fail to import:

  • test_func_metadata.py — the error names the callable and the missing type
  • test_resolve.py — find_resolved_parameters no longer raises a bare NameError for a lazily-annotated parameter
  • test_server.py — the issue's repro: registering a tool whose types are defined below it raises InvalidSignature naming 'search' and Query, not a NameError

Verified on Python 3.14.4: tests/server/mcpserver/ 644 passed (641 before, plus these three); the three fail on the unpatched source with exactly the NameError from the issue; ruff check, ruff format --check and pyright (strict) clean on the changed files. Full tests/ run has 4 failures, all stdio-subprocess tests that fail identically on the base commit in this environment.

Fixes #3668

Since Python 3.14 (PEP 649/749) annotations are evaluated lazily, so a tool may
legally name a type the module defines further down:

    @app.tool()
    def search(q: Query) -> list[Hit]: ...

    class Query(BaseModel): ...

`MCPServer.tool()` builds the `Tool` eagerly, and three of its steps call
`inspect.signature(fn)` while only needing the parameter *names*. The default
`annotation_format` evaluates `__annotate__`, so registration died with a bare
`NameError: name 'Query' is not defined` raised from inside `inspect`.

Those steps now read names through `signature_parameters()`, which asks for
`Format.FORWARDREF` on 3.14+ and keeps the plain call on older Pythons, where
string annotations are already returned unevaluated. Registration then reaches
the step that genuinely needs the types and fails there with the `InvalidSignature`
it already produced for a quoted annotation -- now naming the unresolved type, so
the error says which name to move above the decorated function instead of only
that something could not be evaluated.

Deferring the whole schema build so the tool would register and resolve on first
use is the other half of the issue and is left to the maintainers' preference;
this change makes the failure consistent and actionable either way.

Refs modelcontextprotocol#3668
@github-actions github-actions Bot added the missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md) label Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

This PR has been closed automatically. This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue, and you aren't currently assigned to #3668.

If a maintainer assigns you to #3668, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take.

You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way.

CONTRIBUTING.md has the full reasoning, but in short:

  • We're a small team with very little capacity to review community PRs right now.
  • Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.

Maintainers: reopen, remove missing-issue-link, or add bypass-issue-check to override.

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

Labels

missing-issue-link Auto-closed: PR needs a linked issue assigned to its author (see CONTRIBUTING.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@app.tool() raises NameError for parameter/return types defined later in the module on Python 3.14+ (PEP 649/749 deferred annotations)

1 participant