File tree Expand file tree Collapse file tree
src/mcp/server/mcpserver/utilities Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -33,8 +33,18 @@ def _is_input_required_type(obj: Any) -> bool:
3333 return isinstance (obj , type ) and issubclass (obj , InputRequiredResult )
3434
3535
36+ def _unwrap_annotated (annotation : Any ) -> Any :
37+ """Strip `Annotated[...]` down to its underlying type, e.g. for
38+ `Annotated[str, Field(description=...)]` (idiomatic for described params).
39+ """
40+ if get_origin (annotation ) is Annotated :
41+ return get_args (annotation )[0 ]
42+ return annotation
43+
44+
3645def _is_optional_str (annotation : Any ) -> bool :
37- """Whether `annotation` is `str | None` (`Optional[str]`), modulo `None`.
46+ """Whether `annotation` is `str | None` (`Optional[str]`), modulo `None`
47+ and `Annotated` wrapping.
3848
3949 Used by `pre_parse_json` to skip JSON pre-parsing for such annotations:
4050 unlike e.g. `str | list[str]`, no member of this union other than `str`
@@ -46,7 +56,7 @@ def _is_optional_str(annotation: Any) -> bool:
4656 origin = get_origin (annotation )
4757 if not is_union_origin (origin ):
4858 return False
49- non_none_args = [arg for arg in get_args (annotation ) if arg is not type (None )]
59+ non_none_args = [_unwrap_annotated ( arg ) for arg in get_args (annotation ) if arg is not type (None )]
5060 return non_none_args == [str ]
5161
5262
Original file line number Diff line number Diff line change @@ -233,6 +233,24 @@ def func_with_optional_str(body: str | None = None): # pragma: no cover
233233 assert result ["body" ] is None
234234
235235
236+ def test_annotated_optional_str_is_never_json_pre_parsed ():
237+ """`Annotated[str, Field(...)] | None` — idiomatic FastMCP style for a
238+ described optional string param — must get the same treatment as plain
239+ `str | None`: the `Annotated` wrapper must not defeat the `str` check.
240+ """
241+
242+ def func_with_annotated_str (
243+ body : Annotated [str , Field (description = "a body" )] | None = None ,
244+ ): # pragma: no cover
245+ return body
246+
247+ meta = func_metadata (func_with_annotated_str )
248+
249+ json_payload = '{"blocks": ["a", "b"]}'
250+ result = meta .pre_parse_json ({"body" : json_payload })
251+ assert result ["body" ] == json_payload
252+
253+
236254def test_skip_names ():
237255 """Test that skipped parameters are not included in the model"""
238256
You can’t perform that action at this time.
0 commit comments