Spaces:
Running
Running
Jeremiah Lowin commited on
Support string annotations (#1255)
Browse files- docs/servers/tools.mdx +50 -22
- src/fastmcp/utilities/types.py +55 -43
- tests/server/test_server_interactions.py +12 -0
- tests/utilities/test_types.py +75 -0
docs/servers/tools.mdx
CHANGED
|
@@ -163,9 +163,8 @@ def my_tool() -> None:
|
|
| 163 |
```
|
| 164 |
</CodeGroup>
|
| 165 |
|
| 166 |
-
### Tool Parameters
|
| 167 |
|
| 168 |
-
###
|
| 169 |
|
| 170 |
Type annotations for parameters are essential for proper tool functionality. They:
|
| 171 |
1. Inform the LLM about the expected data types for each parameter
|
|
@@ -185,9 +184,55 @@ def analyze_text(
|
|
| 185 |
# Implementation...
|
| 186 |
```
|
| 187 |
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
```python
|
| 193 |
from typing import Annotated
|
|
@@ -227,26 +272,9 @@ Field provides several validation and documentation features:
|
|
| 227 |
- `pattern`: Regex pattern for string validation
|
| 228 |
- `default`: Default value if parameter is omitted
|
| 229 |
|
| 230 |
-
#### Supported Types
|
| 231 |
-
|
| 232 |
-
FastMCP supports a wide range of type annotations, including all Pydantic types:
|
| 233 |
|
| 234 |
-
| Type Annotation | Example | Description |
|
| 235 |
-
| :---------------------- | :---------------------------- | :---------------------------------- |
|
| 236 |
-
| Basic types | `int`, `float`, `str`, `bool` | Simple scalar values - see [Built-in Types](#built-in-types) |
|
| 237 |
-
| Binary data | `bytes` | Binary content - see [Binary Data](#binary-data) |
|
| 238 |
-
| Date and Time | `datetime`, `date`, `timedelta` | Date and time objects - see [Date and Time Types](#date-and-time-types) |
|
| 239 |
-
| Collection types | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items - see [Collection Types](#collection-types) |
|
| 240 |
-
| Optional types | `float \| None`, `Optional[float]`| Parameters that may be null/omitted - see [Union and Optional Types](#union-and-optional-types) |
|
| 241 |
-
| Union types | `str \| int`, `Union[str, int]`| Parameters accepting multiple types - see [Union and Optional Types](#union-and-optional-types) |
|
| 242 |
-
| Constrained types | `Literal["A", "B"]`, `Enum` | Parameters with specific allowed values - see [Constrained Types](#constrained-types) |
|
| 243 |
-
| Paths | `Path` | File system paths - see [Paths](#paths) |
|
| 244 |
-
| UUIDs | `UUID` | Universally unique identifiers - see [UUIDs](#uuids) |
|
| 245 |
-
| Pydantic models | `UserData` | Complex structured data - see [Pydantic Models](#pydantic-models) |
|
| 246 |
-
|
| 247 |
-
For additional type annotations not listed here, see the [Parameter Types](#parameter-types) section below for more detailed information and examples.
|
| 248 |
|
| 249 |
-
###
|
| 250 |
|
| 251 |
FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.
|
| 252 |
|
|
|
|
| 163 |
```
|
| 164 |
</CodeGroup>
|
| 165 |
|
|
|
|
| 166 |
|
| 167 |
+
### Type Annotations
|
| 168 |
|
| 169 |
Type annotations for parameters are essential for proper tool functionality. They:
|
| 170 |
1. Inform the LLM about the expected data types for each parameter
|
|
|
|
| 184 |
# Implementation...
|
| 185 |
```
|
| 186 |
|
| 187 |
+
FastMCP supports a wide range of type annotations, including all Pydantic types:
|
| 188 |
+
|
| 189 |
+
| Type Annotation | Example | Description |
|
| 190 |
+
| :---------------------- | :---------------------------- | :---------------------------------- |
|
| 191 |
+
| Basic types | `int`, `float`, `str`, `bool` | Simple scalar values - see [Built-in Types](#built-in-types) |
|
| 192 |
+
| Binary data | `bytes` | Binary content - see [Binary Data](#binary-data) |
|
| 193 |
+
| Date and Time | `datetime`, `date`, `timedelta` | Date and time objects - see [Date and Time Types](#date-and-time-types) |
|
| 194 |
+
| Collection types | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items - see [Collection Types](#collection-types) |
|
| 195 |
+
| Optional types | `float \| None`, `Optional[float]`| Parameters that may be null/omitted - see [Union and Optional Types](#union-and-optional-types) |
|
| 196 |
+
| Union types | `str \| int`, `Union[str, int]`| Parameters accepting multiple types - see [Union and Optional Types](#union-and-optional-types) |
|
| 197 |
+
| Constrained types | `Literal["A", "B"]`, `Enum` | Parameters with specific allowed values - see [Constrained Types](#constrained-types) |
|
| 198 |
+
| Paths | `Path` | File system paths - see [Paths](#paths) |
|
| 199 |
+
| UUIDs | `UUID` | Universally unique identifiers - see [UUIDs](#uuids) |
|
| 200 |
+
| Pydantic models | `UserData` | Complex structured data - see [Pydantic Models](#pydantic-models) |
|
| 201 |
+
|
| 202 |
+
For additional type annotations not listed here, see the [Parameter Types](#parameter-types) section below for more detailed information and examples.
|
| 203 |
+
### Parameter Metadata
|
| 204 |
+
|
| 205 |
+
You can provide additional metadata about parameters in several ways:
|
| 206 |
+
|
| 207 |
+
#### Simple String Descriptions
|
| 208 |
+
|
| 209 |
+
<VersionBadge version="2.11.0" />
|
| 210 |
+
|
| 211 |
+
For basic parameter descriptions, you can use a convenient shorthand with `Annotated`:
|
| 212 |
+
|
| 213 |
+
```python
|
| 214 |
+
from typing import Annotated
|
| 215 |
+
|
| 216 |
+
@mcp.tool
|
| 217 |
+
def process_image(
|
| 218 |
+
image_url: Annotated[str, "URL of the image to process"],
|
| 219 |
+
resize: Annotated[bool, "Whether to resize the image"] = False,
|
| 220 |
+
width: Annotated[int, "Target width in pixels"] = 800,
|
| 221 |
+
format: Annotated[str, "Output image format"] = "jpeg"
|
| 222 |
+
) -> dict:
|
| 223 |
+
"""Process an image with optional resizing."""
|
| 224 |
+
# Implementation...
|
| 225 |
+
```
|
| 226 |
|
| 227 |
+
This shorthand syntax is equivalent to using `Field(description=...)` but more concise for simple descriptions.
|
| 228 |
+
|
| 229 |
+
<Tip>
|
| 230 |
+
This shorthand syntax is only applied to `Annotated` types with a single string description.
|
| 231 |
+
</Tip>
|
| 232 |
+
|
| 233 |
+
#### Advanced Metadata with Field
|
| 234 |
+
|
| 235 |
+
For validation constraints and advanced metadata, use Pydantic's `Field` class with `Annotated`:
|
| 236 |
|
| 237 |
```python
|
| 238 |
from typing import Annotated
|
|
|
|
| 272 |
- `pattern`: Regex pattern for string validation
|
| 273 |
- `default`: Default value if parameter is omitted
|
| 274 |
|
|
|
|
|
|
|
|
|
|
| 275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
+
### Optional Arguments
|
| 278 |
|
| 279 |
FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.
|
| 280 |
|
src/fastmcp/utilities/types.py
CHANGED
|
@@ -20,7 +20,7 @@ from typing import (
|
|
| 20 |
|
| 21 |
import mcp.types
|
| 22 |
from mcp.types import Annotations
|
| 23 |
-
from pydantic import AnyUrl, BaseModel, ConfigDict, TypeAdapter, UrlConstraints
|
| 24 |
|
| 25 |
T = TypeVar("T")
|
| 26 |
|
|
@@ -43,53 +43,65 @@ def get_cached_typeadapter(cls: T) -> TypeAdapter[T]:
|
|
| 43 |
However, this isn't feasible for user-generated functions. Instead, we use a
|
| 44 |
cache to minimize the cost of creating them as much as possible.
|
| 45 |
"""
|
| 46 |
-
# For functions,
|
| 47 |
-
#
|
| 48 |
-
# Normally this could be done by setting e.g. parent_depth=3 to reflect the
|
| 49 |
-
# globals in the parent stack, but this utility function can't make that assumption.
|
| 50 |
if inspect.isfunction(cls) or inspect.ismethod(cls):
|
| 51 |
-
# Only try to resolve annotations if the function has them
|
| 52 |
if hasattr(cls, "__annotations__") and cls.__annotations__:
|
| 53 |
try:
|
| 54 |
-
#
|
| 55 |
resolved_hints = get_type_hints(cls, include_extras=True)
|
| 56 |
-
# Check if we need to create a new function with resolved annotations
|
| 57 |
-
if resolved_hints != cls.__annotations__:
|
| 58 |
-
# Create a new function object with resolved annotations
|
| 59 |
-
import types
|
| 60 |
-
|
| 61 |
-
# Handle both functions and methods
|
| 62 |
-
if inspect.ismethod(cls):
|
| 63 |
-
actual_func = cls.__func__
|
| 64 |
-
code = actual_func.__code__
|
| 65 |
-
globals_dict = actual_func.__globals__
|
| 66 |
-
name = actual_func.__name__
|
| 67 |
-
defaults = actual_func.__defaults__
|
| 68 |
-
closure = actual_func.__closure__
|
| 69 |
-
else:
|
| 70 |
-
code = cls.__code__
|
| 71 |
-
globals_dict = cls.__globals__
|
| 72 |
-
name = cls.__name__
|
| 73 |
-
defaults = cls.__defaults__
|
| 74 |
-
closure = cls.__closure__
|
| 75 |
-
|
| 76 |
-
new_func = types.FunctionType(
|
| 77 |
-
code,
|
| 78 |
-
globals_dict,
|
| 79 |
-
name,
|
| 80 |
-
defaults,
|
| 81 |
-
closure,
|
| 82 |
-
)
|
| 83 |
-
new_func.__dict__.update(cls.__dict__)
|
| 84 |
-
new_func.__module__ = cls.__module__
|
| 85 |
-
new_func.__qualname__ = getattr(cls, "__qualname__", cls.__name__)
|
| 86 |
-
new_func.__annotations__ = resolved_hints
|
| 87 |
-
return TypeAdapter(new_func)
|
| 88 |
except Exception:
|
| 89 |
-
# If resolution fails,
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
return TypeAdapter(cls)
|
| 95 |
|
|
|
|
| 20 |
|
| 21 |
import mcp.types
|
| 22 |
from mcp.types import Annotations
|
| 23 |
+
from pydantic import AnyUrl, BaseModel, ConfigDict, Field, TypeAdapter, UrlConstraints
|
| 24 |
|
| 25 |
T = TypeVar("T")
|
| 26 |
|
|
|
|
| 43 |
However, this isn't feasible for user-generated functions. Instead, we use a
|
| 44 |
cache to minimize the cost of creating them as much as possible.
|
| 45 |
"""
|
| 46 |
+
# For functions, process annotations to handle forward references and convert
|
| 47 |
+
# Annotated[Type, "string"] to Annotated[Type, Field(description="string")]
|
|
|
|
|
|
|
| 48 |
if inspect.isfunction(cls) or inspect.ismethod(cls):
|
|
|
|
| 49 |
if hasattr(cls, "__annotations__") and cls.__annotations__:
|
| 50 |
try:
|
| 51 |
+
# Resolve forward references first
|
| 52 |
resolved_hints = get_type_hints(cls, include_extras=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
except Exception:
|
| 54 |
+
# If forward reference resolution fails, use original annotations
|
| 55 |
+
resolved_hints = cls.__annotations__
|
| 56 |
+
|
| 57 |
+
# Process annotations to convert string descriptions to Fields
|
| 58 |
+
processed_hints = {}
|
| 59 |
+
|
| 60 |
+
for name, annotation in resolved_hints.items():
|
| 61 |
+
# Check if this is Annotated[Type, "string"] and convert to Annotated[Type, Field(description="string")]
|
| 62 |
+
if (
|
| 63 |
+
get_origin(annotation) is Annotated
|
| 64 |
+
and len(get_args(annotation)) == 2
|
| 65 |
+
and isinstance(get_args(annotation)[1], str)
|
| 66 |
+
):
|
| 67 |
+
base_type, description = get_args(annotation)
|
| 68 |
+
processed_hints[name] = Annotated[
|
| 69 |
+
base_type, Field(description=description)
|
| 70 |
+
]
|
| 71 |
+
else:
|
| 72 |
+
processed_hints[name] = annotation
|
| 73 |
+
|
| 74 |
+
# Create new function if annotations changed
|
| 75 |
+
if processed_hints != cls.__annotations__:
|
| 76 |
+
import types
|
| 77 |
+
|
| 78 |
+
# Handle both functions and methods
|
| 79 |
+
if inspect.ismethod(cls):
|
| 80 |
+
actual_func = cls.__func__
|
| 81 |
+
code = actual_func.__code__
|
| 82 |
+
globals_dict = actual_func.__globals__
|
| 83 |
+
name = actual_func.__name__
|
| 84 |
+
defaults = actual_func.__defaults__
|
| 85 |
+
closure = actual_func.__closure__
|
| 86 |
+
else:
|
| 87 |
+
code = cls.__code__
|
| 88 |
+
globals_dict = cls.__globals__
|
| 89 |
+
name = cls.__name__
|
| 90 |
+
defaults = cls.__defaults__
|
| 91 |
+
closure = cls.__closure__
|
| 92 |
+
|
| 93 |
+
new_func = types.FunctionType(
|
| 94 |
+
code,
|
| 95 |
+
globals_dict,
|
| 96 |
+
name,
|
| 97 |
+
defaults,
|
| 98 |
+
closure,
|
| 99 |
+
)
|
| 100 |
+
new_func.__dict__.update(cls.__dict__)
|
| 101 |
+
new_func.__module__ = cls.__module__
|
| 102 |
+
new_func.__qualname__ = getattr(cls, "__qualname__", cls.__name__)
|
| 103 |
+
new_func.__annotations__ = processed_hints
|
| 104 |
+
return TypeAdapter(new_func)
|
| 105 |
|
| 106 |
return TypeAdapter(cls)
|
| 107 |
|
tests/server/test_server_interactions.py
CHANGED
|
@@ -891,6 +891,18 @@ class TestToolParameters:
|
|
| 891 |
):
|
| 892 |
await client.call_tool("send_timedelta", {"x": 1000})
|
| 893 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
|
| 895 |
class TestToolOutputSchema:
|
| 896 |
@pytest.mark.parametrize("annotation", [str, int, float, bool, list, AnyUrl])
|
|
|
|
| 891 |
):
|
| 892 |
await client.call_tool("send_timedelta", {"x": 1000})
|
| 893 |
|
| 894 |
+
async def test_annotated_string_description(self):
|
| 895 |
+
mcp = FastMCP()
|
| 896 |
+
|
| 897 |
+
@mcp.tool
|
| 898 |
+
def f(x: Annotated[int, "A number"]):
|
| 899 |
+
return x
|
| 900 |
+
|
| 901 |
+
async with Client(mcp) as client:
|
| 902 |
+
tools = await client.list_tools()
|
| 903 |
+
assert len(tools) == 1
|
| 904 |
+
assert tools[0].inputSchema["properties"]["x"]["description"] == "A number"
|
| 905 |
+
|
| 906 |
|
| 907 |
class TestToolOutputSchema:
|
| 908 |
@pytest.mark.parametrize("annotation", [str, int, float, bool, list, AnyUrl])
|
tests/utilities/test_types.py
CHANGED
|
@@ -7,12 +7,14 @@ from typing import Annotated, Any
|
|
| 7 |
|
| 8 |
import pytest
|
| 9 |
from mcp.types import BlobResourceContents, TextResourceContents
|
|
|
|
| 10 |
|
| 11 |
from fastmcp.utilities.types import (
|
| 12 |
Audio,
|
| 13 |
File,
|
| 14 |
Image,
|
| 15 |
find_kwarg_by_type,
|
|
|
|
| 16 |
is_class_member_of_type,
|
| 17 |
issubclass_safe,
|
| 18 |
replace_type,
|
|
@@ -617,3 +619,76 @@ class TestReplaceType:
|
|
| 617 |
def test_replace_type(self, input, type_map, expected):
|
| 618 |
"""Test replacing a type with another type."""
|
| 619 |
assert replace_type(input, type_map) == expected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
import pytest
|
| 9 |
from mcp.types import BlobResourceContents, TextResourceContents
|
| 10 |
+
from pydantic import Field
|
| 11 |
|
| 12 |
from fastmcp.utilities.types import (
|
| 13 |
Audio,
|
| 14 |
File,
|
| 15 |
Image,
|
| 16 |
find_kwarg_by_type,
|
| 17 |
+
get_cached_typeadapter,
|
| 18 |
is_class_member_of_type,
|
| 19 |
issubclass_safe,
|
| 20 |
replace_type,
|
|
|
|
| 619 |
def test_replace_type(self, input, type_map, expected):
|
| 620 |
"""Test replacing a type with another type."""
|
| 621 |
assert replace_type(input, type_map) == expected
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
class TestAnnotationStringDescriptions:
|
| 625 |
+
"""Test the new functionality for string descriptions in Annotated types."""
|
| 626 |
+
|
| 627 |
+
def test_get_cached_typeadapter_with_string_descriptions(self):
|
| 628 |
+
"""Test TypeAdapter creation with string descriptions."""
|
| 629 |
+
|
| 630 |
+
def func(name: Annotated[str, "The user's name"]) -> str:
|
| 631 |
+
return f"Hello {name}"
|
| 632 |
+
|
| 633 |
+
adapter = get_cached_typeadapter(func)
|
| 634 |
+
schema = adapter.json_schema()
|
| 635 |
+
|
| 636 |
+
# Should have description in schema
|
| 637 |
+
assert "properties" in schema
|
| 638 |
+
assert "name" in schema["properties"]
|
| 639 |
+
assert schema["properties"]["name"]["description"] == "The user's name"
|
| 640 |
+
|
| 641 |
+
def test_multiple_string_annotations(self):
|
| 642 |
+
"""Test function with multiple string-annotated parameters."""
|
| 643 |
+
|
| 644 |
+
def func(
|
| 645 |
+
name: Annotated[str, "User's name"],
|
| 646 |
+
email: Annotated[str, "User's email"],
|
| 647 |
+
age: int,
|
| 648 |
+
) -> str:
|
| 649 |
+
return f"{name} ({email}) is {age}"
|
| 650 |
+
|
| 651 |
+
adapter = get_cached_typeadapter(func)
|
| 652 |
+
schema = adapter.json_schema()
|
| 653 |
+
|
| 654 |
+
# Both annotated parameters should have descriptions
|
| 655 |
+
assert schema["properties"]["name"]["description"] == "User's name"
|
| 656 |
+
assert schema["properties"]["email"]["description"] == "User's email"
|
| 657 |
+
# Non-annotated parameter should not have description
|
| 658 |
+
assert "description" not in schema["properties"]["age"]
|
| 659 |
+
|
| 660 |
+
def test_annotated_with_more_than_string_unchanged(self):
|
| 661 |
+
"""Test that Annotated with more than just a string is unchanged."""
|
| 662 |
+
|
| 663 |
+
def func(name: Annotated[str, "desc", "extra"]) -> str:
|
| 664 |
+
return f"Hello {name}"
|
| 665 |
+
|
| 666 |
+
adapter = get_cached_typeadapter(func)
|
| 667 |
+
schema = adapter.json_schema()
|
| 668 |
+
|
| 669 |
+
# Should not have description since it's not exactly length 2
|
| 670 |
+
assert "description" not in schema["properties"]["name"]
|
| 671 |
+
|
| 672 |
+
def test_annotated_with_non_string_unchanged(self):
|
| 673 |
+
"""Test that Annotated with non-string second arg is unchanged."""
|
| 674 |
+
|
| 675 |
+
def func(name: Annotated[str, 42]) -> str:
|
| 676 |
+
return f"Hello {name}"
|
| 677 |
+
|
| 678 |
+
adapter = get_cached_typeadapter(func)
|
| 679 |
+
schema = adapter.json_schema()
|
| 680 |
+
|
| 681 |
+
# Should not have description since second arg is not string
|
| 682 |
+
assert "description" not in schema["properties"]["name"]
|
| 683 |
+
|
| 684 |
+
def test_existing_field_unchanged(self):
|
| 685 |
+
"""Test that existing Field annotations are unchanged."""
|
| 686 |
+
|
| 687 |
+
def func(name: Annotated[str, Field(description="Field desc")]) -> str:
|
| 688 |
+
return f"Hello {name}"
|
| 689 |
+
|
| 690 |
+
adapter = get_cached_typeadapter(func)
|
| 691 |
+
schema = adapter.json_schema()
|
| 692 |
+
|
| 693 |
+
# Should keep the Field description
|
| 694 |
+
assert schema["properties"]["name"]["description"] == "Field desc"
|