Skip to content

Commit 9cc2558

Browse files
committed
fix: set default to none for optional field without default value
1 parent 71889d7 commit 9cc2558

File tree

2 files changed

+58
-3
lines changed

2 files changed

+58
-3
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import json
33
from collections.abc import Awaitable, Callable, Sequence
44
from itertools import chain
5-
from types import GenericAlias
6-
from typing import Annotated, Any, ForwardRef, cast, get_args, get_origin, get_type_hints
5+
from types import GenericAlias, UnionType
6+
from typing import Annotated, Any, ForwardRef, Union, cast, get_args, get_origin, get_type_hints
77

88
import pydantic_core
99
from pydantic import (
@@ -232,6 +232,12 @@ def func_metadata(
232232
WithJsonSchema({"title": param.name, "type": "string"}),
233233
]
234234

235+
# if it's Optional[SomeType] or SomeType | None and has no explicit default, set default=None
236+
origin = get_origin(annotation)
237+
if (origin is Union or origin is UnionType) and type(None) in get_args(annotation):
238+
if isinstance(param.default, FieldInfo) and param.default.default is PydanticUndefined:
239+
param.default.default = None
240+
235241
field_info = FieldInfo.from_annotated_attribute(
236242
_get_typed_annotation(annotation, globalns),
237243
param.default if param.default is not inspect.Parameter.empty else PydanticUndefined,

tests/server/fastmcp/test_func_metadata.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# pyright: reportUnknownLambdaType=false
66
from collections.abc import Callable
77
from dataclasses import dataclass
8-
from typing import Annotated, Any, TypedDict
8+
from typing import Annotated, Any, Optional, TypedDict
99

1010
import annotated_types
1111
import pytest
@@ -1094,3 +1094,52 @@ def func_with_reserved_json(
10941094
assert result["json"] == {"nested": "data"}
10951095
assert result["model_dump"] == [1, 2, 3]
10961096
assert result["normal"] == "plain string"
1097+
1098+
1099+
def test_optional_parameters_not_required():
1100+
"""Test that Optional parameters are not marked as required in JSON schema."""
1101+
1102+
def func_with_optional_params(
1103+
required: str = Field(description="This should be required"),
1104+
optional: str | None = Field(description="This should be optional"),
1105+
optional_with_default: str | None = Field(default="hello", description="This should be optional with default"),
1106+
optional_with_union_type: Optional[str] = Field(description="This should be optional"), # noqa: UP045
1107+
optional_with_union_type_and_default: Optional[str] | None = Field( # noqa: UP045
1108+
default="hello", description="This should be optional with default"
1109+
),
1110+
) -> str:
1111+
return f"{required}|{optional}|{optional_with_default}|{optional_with_union_type}|{optional_with_union_type_and_default}"
1112+
1113+
meta = func_metadata(func_with_optional_params)
1114+
assert meta.arg_model.model_json_schema() == {
1115+
"properties": {
1116+
"required": {"description": "This should be required", "title": "Required", "type": "string"},
1117+
"optional": {
1118+
"anyOf": [{"type": "string"}, {"type": "null"}],
1119+
"default": None,
1120+
"description": "This should be optional",
1121+
"title": "Optional",
1122+
},
1123+
"optional_with_default": {
1124+
"anyOf": [{"type": "string"}, {"type": "null"}],
1125+
"default": "hello",
1126+
"description": "This should be optional with default",
1127+
"title": "Optional With Default",
1128+
},
1129+
"optional_with_union_type": {
1130+
"anyOf": [{"type": "string"}, {"type": "null"}],
1131+
"default": None,
1132+
"description": "This should be optional",
1133+
"title": "Optional With Union Type",
1134+
},
1135+
"optional_with_union_type_and_default": {
1136+
"anyOf": [{"type": "string"}, {"type": "null"}],
1137+
"default": "hello",
1138+
"description": "This should be optional with default",
1139+
"title": "Optional With Union Type And Default",
1140+
},
1141+
},
1142+
"required": ["required"],
1143+
"title": "func_with_optional_paramsArguments",
1144+
"type": "object",
1145+
}

0 commit comments

Comments
 (0)