|
1 | 1 | import base64 |
2 | 2 | import logging |
| 3 | +import sys |
3 | 4 | from collections.abc import AsyncIterator |
4 | 5 | from contextlib import asynccontextmanager |
5 | 6 | from dataclasses import dataclass |
|
57 | 58 | from mcp.server.context import ServerRequestContext |
58 | 59 | from mcp.server.mcpserver import Context, MCPServer, RequestStateSecurity, Resolve, ResourceSecurity |
59 | 60 | from mcp.server.mcpserver.exceptions import ( |
| 61 | + InvalidSignature, |
60 | 62 | ResourceError, |
61 | 63 | ResourceNotFoundError, |
62 | 64 | ToolError, |
@@ -3318,3 +3320,46 @@ async def refuse_listen(ctx: ServerRequestContext[Any, Any], call_next: Any) -> |
3318 | 3320 | pass # pragma: no cover - the refusal precedes the stream |
3319 | 3321 | assert exc_info.value.error.code == INVALID_REQUEST |
3320 | 3322 | assert exc_info.value.error.message == "not permitted to watch the requested resources" |
| 3323 | + |
| 3324 | + |
| 3325 | +# The issue's repro: a tool whose parameter and return types are defined *below* |
| 3326 | +# it, which PEP 649 (3.14+) makes legal without quoting. Built with exec so the |
| 3327 | +# undefined names are never statically visible -- on older Pythons an unquoted |
| 3328 | +# undefined annotation raises where the function is defined, which would make |
| 3329 | +# this module fail to import. |
| 3330 | +_FORWARD_REF_TOOL_SOURCE = """ |
| 3331 | +from pydantic import BaseModel |
| 3332 | +
|
| 3333 | +app = MCPServer("repro") |
| 3334 | +
|
| 3335 | +
|
| 3336 | +@app.tool() |
| 3337 | +def search(q: Query) -> list[Hit]: |
| 3338 | + \"\"\"Search.\"\"\" |
| 3339 | + return [Hit(id=1)] |
| 3340 | +
|
| 3341 | +
|
| 3342 | +class Query(BaseModel): |
| 3343 | + text: str |
| 3344 | +
|
| 3345 | +
|
| 3346 | +class Hit(BaseModel): |
| 3347 | + id: int |
| 3348 | +""" |
| 3349 | + |
| 3350 | + |
| 3351 | +@pytest.mark.skipif(sys.version_info < (3, 14), reason="PEP 649 lazy annotations are 3.14+") |
| 3352 | +async def test_tool_with_types_defined_below_it_fails_with_a_named_error(): |
| 3353 | + """Registration must not leak a bare NameError from inside inspect.signature. |
| 3354 | +
|
| 3355 | + It raises `InvalidSignature` naming the tool and the type that could not be |
| 3356 | + resolved yet, the same error a quoted annotation already produced. #3668. |
| 3357 | + """ |
| 3358 | + namespace: dict[str, Any] = {"MCPServer": MCPServer} |
| 3359 | + |
| 3360 | + with pytest.raises(InvalidSignature) as exc_info: |
| 3361 | + exec(_FORWARD_REF_TOOL_SOURCE, namespace) # noqa: S102 |
| 3362 | + |
| 3363 | + message = str(exc_info.value) |
| 3364 | + assert "'search'" in message |
| 3365 | + assert "Query" in message |
0 commit comments