fastmcp 2.2.7__py3-none-any.whl → 2.2.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,229 +0,0 @@
1
- import inspect
2
- import json
3
- from collections.abc import Awaitable, Callable, Sequence
4
- from typing import (
5
- Annotated,
6
- Any,
7
- ForwardRef,
8
- )
9
-
10
- from pydantic import (
11
- BaseModel,
12
- ConfigDict,
13
- Field,
14
- TypeAdapter,
15
- ValidationError,
16
- WithJsonSchema,
17
- create_model,
18
- )
19
- from pydantic._internal._typing_extra import eval_type_backport
20
- from pydantic.fields import FieldInfo
21
- from pydantic_core import PydanticUndefined
22
-
23
- from fastmcp.exceptions import InvalidSignature
24
- from fastmcp.utilities.logging import get_logger
25
-
26
- logger = get_logger(__name__)
27
-
28
-
29
- class ArgModelBase(BaseModel):
30
- """A model representing the arguments to a function."""
31
-
32
- def model_dump_one_level(self) -> dict[str, Any]:
33
- """Return a dict of the model's fields, one level deep.
34
-
35
- That is, sub-models etc are not dumped - they are kept as pydantic models.
36
- """
37
- kwargs: dict[str, Any] = {}
38
- for field_name in self.__class__.model_fields.keys():
39
- kwargs[field_name] = getattr(self, field_name)
40
- return kwargs
41
-
42
- model_config = ConfigDict(
43
- arbitrary_types_allowed=True,
44
- )
45
-
46
-
47
- class FuncMetadata(BaseModel):
48
- arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)]
49
- # We can add things in the future like
50
- # - Maybe some args are excluded from attempting to parse from JSON
51
- # - Maybe some args are special (like context) for dependency injection
52
-
53
- async def call_fn_with_arg_validation(
54
- self,
55
- fn: Callable[..., Any] | Awaitable[Any],
56
- fn_is_async: bool,
57
- arguments_to_validate: dict[str, Any],
58
- arguments_to_pass_directly: dict[str, Any] | None,
59
- ) -> Any:
60
- """Call the given function with arguments validated and injected.
61
-
62
- Arguments are first attempted to be parsed from JSON, then validated against
63
- the argument model, before being passed to the function.
64
- """
65
- arguments_pre_parsed = self.pre_parse_json(arguments_to_validate)
66
- arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed)
67
- arguments_parsed_dict = arguments_parsed_model.model_dump_one_level()
68
-
69
- arguments_parsed_dict |= arguments_to_pass_directly or {}
70
-
71
- if fn_is_async:
72
- if isinstance(fn, Awaitable):
73
- return await fn
74
- return await fn(**arguments_parsed_dict)
75
- if isinstance(fn, Callable):
76
- return fn(**arguments_parsed_dict)
77
- raise TypeError("fn must be either Callable or Awaitable")
78
-
79
- def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:
80
- """Pre-parse data from JSON.
81
-
82
- Return a dict with same keys as input but with values parsed from JSON
83
- if appropriate.
84
-
85
- This is to handle cases like `["a", "b", "c"]` being passed in as JSON inside
86
- a string rather than an actual list. Claude desktop is prone to this - in fact
87
- it seems incapable of NOT doing this. For sub-models, it tends to pass
88
- dicts (JSON objects) as JSON strings, which can be pre-parsed here.
89
- """
90
- new_data = data.copy() # Shallow copy
91
- for field_name, field_info in self.arg_model.model_fields.items():
92
- if field_name not in data.keys():
93
- continue
94
- if isinstance(data[field_name], str):
95
- try:
96
- pre_parsed = json.loads(data[field_name])
97
-
98
- # Check if the pre_parsed value is valid for the field
99
- validator = TypeAdapter(field_info.annotation)
100
- validator.validate_python(pre_parsed)
101
- except (json.JSONDecodeError, ValidationError):
102
- continue # Not JSON or invalid for the field
103
- if isinstance(pre_parsed, str | int | float):
104
- # This is likely that the raw value is e.g. `"hello"` which we
105
- # Should really be parsed as '"hello"' in Python - but if we parse
106
- # it as JSON it'll turn into just 'hello'. So we skip it.
107
- continue
108
- new_data[field_name] = pre_parsed
109
- assert new_data.keys() == data.keys()
110
- return new_data
111
-
112
- model_config = ConfigDict(
113
- arbitrary_types_allowed=True,
114
- )
115
-
116
-
117
- def func_metadata(
118
- func: Callable[..., Any], skip_names: Sequence[str] = ()
119
- ) -> FuncMetadata:
120
- """Given a function, return metadata including a pydantic model representing its
121
- signature.
122
-
123
- The use case for this is
124
- ```
125
- meta = func_to_pyd(func)
126
- validated_args = meta.arg_model.model_validate(some_raw_data_dict)
127
- return func(**validated_args.model_dump_one_level())
128
- ```
129
-
130
- **critically** it also provides pre-parse helper to attempt to parse things from
131
- JSON.
132
-
133
- Args:
134
- func: The function to convert to a pydantic model
135
- skip_names: A list of parameter names to skip. These will not be included in
136
- the model.
137
- Returns:
138
- A pydantic model representing the function's signature.
139
- """
140
- if isinstance(func, classmethod):
141
- sig = _get_typed_signature(func.__func__)
142
- else:
143
- sig = _get_typed_signature(func)
144
- params = sig.parameters
145
- dynamic_pydantic_model_params: dict[str, Any] = {}
146
- globalns = getattr(func, "__globals__", {})
147
- for param in params.values():
148
- if param.name.startswith("_"):
149
- raise InvalidSignature(
150
- f"Parameter {param.name} of {func.__name__} cannot start with '_'"
151
- )
152
- if param.name in skip_names:
153
- continue
154
- annotation = param.annotation
155
-
156
- # `x: None` / `x: None = None`
157
- if annotation is None:
158
- annotation = Annotated[
159
- None,
160
- Field(
161
- default=param.default
162
- if param.default is not inspect.Parameter.empty
163
- else PydanticUndefined
164
- ),
165
- ]
166
-
167
- # Untyped field
168
- if annotation is inspect.Parameter.empty:
169
- annotation = Annotated[
170
- Any,
171
- Field(),
172
- # 🤷
173
- WithJsonSchema({"title": param.name, "type": "string"}),
174
- ]
175
-
176
- field_info = FieldInfo.from_annotated_attribute(
177
- _get_typed_annotation(annotation, globalns),
178
- param.default
179
- if param.default is not inspect.Parameter.empty
180
- else PydanticUndefined,
181
- )
182
- dynamic_pydantic_model_params[param.name] = (field_info.annotation, field_info)
183
- continue
184
-
185
- arguments_model = create_model(
186
- f"{func.__name__}Arguments",
187
- **dynamic_pydantic_model_params,
188
- __base__=ArgModelBase,
189
- )
190
- resp = FuncMetadata(arg_model=arguments_model)
191
- return resp
192
-
193
-
194
- def _get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
195
- def try_eval_type(
196
- value: Any, globalns: dict[str, Any], localns: dict[str, Any]
197
- ) -> tuple[Any, bool]:
198
- try:
199
- return eval_type_backport(value, globalns, localns), True
200
- except NameError:
201
- return value, False
202
-
203
- if isinstance(annotation, str):
204
- annotation = ForwardRef(annotation)
205
- annotation, status = try_eval_type(annotation, globalns, globalns)
206
-
207
- # This check and raise could perhaps be skipped, and we (FastMCP) just call
208
- # model_rebuild right before using it 🤷
209
- if status is False:
210
- raise InvalidSignature(f"Unable to evaluate type annotation {annotation}")
211
-
212
- return annotation
213
-
214
-
215
- def _get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
216
- """Get function signature while evaluating forward references"""
217
- signature = inspect.signature(call)
218
- globalns = getattr(call, "__globals__", {})
219
- typed_params = [
220
- inspect.Parameter(
221
- name=param.name,
222
- kind=param.kind,
223
- default=param.default,
224
- annotation=_get_typed_annotation(param.annotation, globalns),
225
- )
226
- for param in signature.parameters.values()
227
- ]
228
- typed_signature = inspect.Signature(typed_params)
229
- return typed_signature