Skip to content

Commit 6c7819a

Browse files
committed
Path and Query bug fixes and tests and example.
1 parent 5873bdd commit 6c7819a

File tree

7 files changed

+116
-43
lines changed

7 files changed

+116
-43
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,9 @@ Resources are how you expose data to LLMs. They're similar to GET endpoints in a
269269

270270
<!-- snippet-source examples/snippets/servers/basic_resource.py -->
271271
```python
272-
from mcp.server.fastmcp import FastMCP
272+
from typing import Annotated
273+
274+
from mcp.server.fastmcp import FastMCP, Path, Query
273275

274276
mcp = FastMCP(name="Resource Example")
275277

@@ -381,6 +383,26 @@ def get_weather_data(
381383
weather_info += f"\nDay {day}: {forecast_temp}{temp_unit}"
382384

383385
return weather_info
386+
387+
388+
@mcp.resource("api://data/{user_id}/{region}/{city}/{file_path:path}")
389+
def resource_fn(
390+
# Path parameters
391+
user_id: Annotated[int, Path(gt=0, description="User ID")], # explicit Path
392+
region, # inferred path # type: ignore
393+
city: str, # inferred path
394+
file_path: str, # inferred path {file_path:path}
395+
# Required query parameter (no default)
396+
version: int,
397+
# Optional query parameters (defaults or Query(...))
398+
format: Annotated[str, Query("json", description="Output format")],
399+
include_metadata: bool = False,
400+
tags: list[str] = [],
401+
lang: str = "en",
402+
debug: bool = False,
403+
precision: float = 0.5,
404+
) -> str:
405+
return f"{user_id}/{region}/{city}/{file_path}"
384406
```
385407

386408
_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_

examples/snippets/servers/basic_resource.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from mcp.server.fastmcp import FastMCP
1+
from typing import Annotated
2+
3+
from mcp.server.fastmcp import FastMCP, Path, Query
24

35
mcp = FastMCP(name="Resource Example")
46

@@ -110,3 +112,23 @@ def get_weather_data(
110112
weather_info += f"\nDay {day}: {forecast_temp}{temp_unit}"
111113

112114
return weather_info
115+
116+
117+
@mcp.resource("api://data/{user_id}/{region}/{city}/{file_path:path}")
118+
def resource_fn(
119+
# Path parameters
120+
user_id: Annotated[int, Path(gt=0, description="User ID")], # explicit Path
121+
region, # inferred path # type: ignore
122+
city: str, # inferred path
123+
file_path: str, # inferred path {file_path:path}
124+
# Required query parameter (no default)
125+
version: int,
126+
# Optional query parameters (defaults or Query(...))
127+
format: Annotated[str, Query("json", description="Output format")],
128+
include_metadata: bool = False,
129+
tags: list[str] = [],
130+
lang: str = "en",
131+
debug: bool = False,
132+
precision: float = 0.5,
133+
) -> str:
134+
return f"{user_id}/{region}/{city}/{file_path}"

src/mcp/server/fastmcp/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from mcp.types import Icon
66

77
from .server import Context, FastMCP
8+
from .utilities.param_functions import Path, Query
89
from .utilities.types import Audio, Image
910

1011
__version__ = version("mcp")
11-
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon"]
12+
__all__ = ["FastMCP", "Context", "Image", "Audio", "Icon", "Path", "Query"]

src/mcp/server/fastmcp/utilities/param_functions.py

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Annotated, Any
33

44
from pydantic.version import VERSION as PYDANTIC_VERSION
5-
from typing_extensions import Doc, deprecated
5+
from typing_extensions import Doc
66

77
from mcp.server.fastmcp.utilities import params
88

@@ -258,7 +258,6 @@ def Path( # noqa: PLR0913
258258
max_digits=max_digits,
259259
decimal_places=decimal_places,
260260
examples=examples,
261-
deprecated=deprecated, # type: ignore
262261
include_in_schema=include_in_schema,
263262
json_schema_extra=json_schema_extra,
264263
)
@@ -455,16 +454,6 @@ def Query( # noqa: PLR0913
455454
"""
456455
),
457456
] = None,
458-
deprecated: Annotated[
459-
deprecated | str | bool | None,
460-
Doc(
461-
"""
462-
Mark this parameter field as deprecated.
463-
464-
It will affect the generated OpenAPI (e.g. visible at `/docs`).
465-
"""
466-
),
467-
] = None,
468457
include_in_schema: Annotated[
469458
bool,
470459
Doc(
@@ -484,19 +473,6 @@ def Query( # noqa: PLR0913
484473
"""
485474
),
486475
] = None,
487-
**extra: Annotated[
488-
Any,
489-
Doc(
490-
"""
491-
Include extra fields used by the JSON Schema.
492-
"""
493-
),
494-
deprecated(
495-
"""
496-
The `extra` kwargs is deprecated. Use `json_schema_extra` instead.
497-
"""
498-
),
499-
],
500476
) -> Any:
501477
return params.Query(
502478
default=default,
@@ -521,8 +497,6 @@ def Query( # noqa: PLR0913
521497
max_digits=max_digits,
522498
decimal_places=decimal_places,
523499
examples=examples,
524-
deprecated=deprecated,
525500
include_in_schema=include_in_schema,
526501
json_schema_extra=json_schema_extra,
527-
**extra,
528502
)

src/mcp/server/fastmcp/utilities/param_validation.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,21 @@
77
from collections.abc import Callable
88
from typing import Annotated, Any, get_args, get_origin
99

10+
from pydantic.version import VERSION as PYDANTIC_VERSION
11+
1012
from mcp.server.fastmcp.utilities.convertors import CONVERTOR_TYPES, Convertor
11-
from mcp.server.fastmcp.utilities.params import Path
13+
from mcp.server.fastmcp.utilities.params import Path, Query
14+
15+
PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
16+
PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
17+
18+
if not PYDANTIC_V2:
19+
from pydantic.fields import Undefined # type: ignore[attr-defined]
20+
else:
21+
from pydantic.v1.fields import Undefined
22+
23+
# difference between not given not needed, not given maybe needed.
24+
_Unset: Any = Undefined # type: ignore
1225

1326

1427
def validate_and_sync_params(
@@ -65,6 +78,10 @@ def _extract_function_params(
6578
for meta in args[1:]:
6679
if isinstance(meta, Path):
6780
explicit_path.add(name)
81+
if isinstance(meta, Query):
82+
if meta.default is not Undefined:
83+
fn_defaults[name] = meta.default
84+
6885
fn_param_types[name] = base_type
6986

7087
# IGNORE TYPES caused a circular import with Context so using it as a string

src/mcp/server/fastmcp/utilities/params.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from pydantic.fields import FieldInfo
66
from pydantic.version import VERSION as PYDANTIC_VERSION
7-
from typing_extensions import deprecated
87

98
PYDANTIC_VERSION_MINOR_TUPLE = tuple(int(x) for x in PYDANTIC_VERSION.split(".")[:2])
109
PYDANTIC_V2 = PYDANTIC_VERSION_MINOR_TUPLE[0] == 2
@@ -52,7 +51,6 @@ def __init__( # noqa: PLR0913
5251
max_digits: int | None = _Unset,
5352
decimal_places: int | None = _Unset,
5453
examples: list[Any] | None = None,
55-
deprecated: deprecated | str | bool | None = None,
5654
include_in_schema: bool = True,
5755
json_schema_extra: dict[str, Any] | None = None,
5856
):
@@ -78,10 +76,6 @@ def __init__( # noqa: PLR0913
7876
if examples is not None:
7977
kwargs["examples"] = examples
8078
current_json_schema_extra = json_schema_extra
81-
if PYDANTIC_VERSION_MINOR_TUPLE < (2, 7):
82-
self.deprecated = deprecated
83-
else:
84-
kwargs["deprecated"] = deprecated
8579
if PYDANTIC_V2:
8680
kwargs.update(
8781
{
@@ -134,7 +128,6 @@ def __init__( # noqa: PLR0913
134128
max_digits: int | None = _Unset,
135129
decimal_places: int | None = _Unset,
136130
examples: list[Any] | None = None,
137-
deprecated: deprecated | str | bool | None = None,
138131
include_in_schema: bool = True,
139132
json_schema_extra: dict[str, Any] | None = None,
140133
):
@@ -163,7 +156,6 @@ def __init__( # noqa: PLR0913
163156
allow_inf_nan=allow_inf_nan,
164157
max_digits=max_digits,
165158
decimal_places=decimal_places,
166-
deprecated=deprecated,
167159
examples=examples,
168160
include_in_schema=include_in_schema,
169161
json_schema_extra=json_schema_extra,
@@ -199,7 +191,6 @@ def __init__( # noqa: PLR0913
199191
max_digits: int | None = _Unset,
200192
decimal_places: int | None = _Unset,
201193
examples: list[Any] | None = None,
202-
deprecated: deprecated | str | bool | None = None,
203194
include_in_schema: bool = True,
204195
json_schema_extra: dict[str, Any] | None = None,
205196
):
@@ -226,7 +217,6 @@ def __init__( # noqa: PLR0913
226217
allow_inf_nan=allow_inf_nan,
227218
max_digits=max_digits,
228219
decimal_places=decimal_places,
229-
deprecated=deprecated,
230220
examples=examples,
231221
include_in_schema=include_in_schema,
232222
json_schema_extra=json_schema_extra,

tests/server/fastmcp/resources/test_resource_template.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import json
2-
from typing import Any
2+
from typing import Annotated, Any
33

44
import pytest
55
from pydantic import BaseModel
66

7-
from mcp.server.fastmcp import FastMCP
7+
from mcp.server.fastmcp import FastMCP, Path, Query
88
from mcp.server.fastmcp.resources import FunctionResource, ResourceTemplate
99
from mcp.types import Annotations
1010

@@ -606,3 +606,50 @@ def get_item(item_id: str) -> str:
606606
# Verify the resource works correctly
607607
content = await resource.read()
608608
assert content == "Item 123"
609+
610+
def test_full_parameter_inference(self):
611+
"""Test MCP path and query parameter inference: path, required query, optional query."""
612+
613+
# Function under test
614+
def resource_fn(
615+
# Path parameters
616+
user_id: Annotated[int, Path(gt=0, description="User ID")], # explicit Path
617+
region, # inferred path # type: ignore
618+
city: str, # inferred path
619+
file_path: str, # inferred path {file_path:path}
620+
# Required query parameter (no default)
621+
version: int,
622+
# Optional query parameters (defaults or Query(...))
623+
format: Annotated[str, Query("json", description="Output format")],
624+
include_metadata: bool = False,
625+
tags: list[str] = [],
626+
lang: str = "en",
627+
debug: bool = False,
628+
precision: float = 0.5,
629+
) -> str:
630+
return f"{user_id}/{region}/{city}/{file_path}"
631+
632+
# Create resource template
633+
template = ResourceTemplate.from_function(
634+
fn=resource_fn, # type: ignore
635+
uri_template="api://data/{user_id}/{region}/{city}/{file_path:path}",
636+
name="full_resource",
637+
)
638+
639+
# --- Assertions ---
640+
641+
# Path parameters
642+
assert template.path_params == {"user_id", "region", "city", "file_path"}
643+
644+
# Required query parameters (no default)
645+
assert template.required_query_params == {"version"}
646+
647+
# Optional query parameters (have default or Query)
648+
assert template.optional_query_params == {
649+
"include_metadata",
650+
"tags",
651+
"format",
652+
"lang",
653+
"debug",
654+
"precision",
655+
}

0 commit comments

Comments
 (0)