Jeremiah Lowin commited on
Commit
a537180
·
1 Parent(s): 8565566

Dry out retrieving context kwarg

Browse files
src/fastmcp/prompts/prompt.py CHANGED
@@ -12,9 +12,11 @@ from mcp.types import Prompt as MCPPrompt
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
 
 
15
  from fastmcp.utilities.types import (
16
  _convert_set_defaults,
17
- is_class_member_of_type,
 
18
  )
19
 
20
  if TYPE_CHECKING:
@@ -110,34 +112,24 @@ class Prompt(BaseModel):
110
  if func_name == "<lambda>":
111
  raise ValueError("You must provide a name for lambda functions")
112
 
 
 
 
113
  # Auto-detect context parameter if not provided
114
  if context_kwarg is None:
115
- if inspect.ismethod(fn) and hasattr(fn, "__func__"):
116
- sig = inspect.signature(fn.__func__)
117
- else:
118
- sig = inspect.signature(fn)
119
- for param_name, param in sig.parameters.items():
120
- if is_class_member_of_type(param.annotation, Context):
121
- context_kwarg = param_name
122
- break
123
-
124
- # Get schema from TypeAdapter - will fail if function isn't properly typed
125
- parameters = TypeAdapter(fn).json_schema()
126
 
127
  # Convert parameters to PromptArguments
128
  arguments: list[PromptArgument] = []
129
  if "properties" in parameters:
130
  for param_name, param in parameters["properties"].items():
131
- # Skip context parameter
132
- if param_name == context_kwarg:
133
- continue
134
-
135
- required = param_name in parameters.get("required", [])
136
  arguments.append(
137
  PromptArgument(
138
  name=param_name,
139
  description=param.get("description"),
140
- required=required,
141
  )
142
  )
143
 
 
12
  from mcp.types import PromptArgument as MCPPromptArgument
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
 
15
+ from fastmcp.utilities.json_schema import prune_params
16
  from fastmcp.utilities.types import (
17
  _convert_set_defaults,
18
+ find_kwarg_by_type,
19
+ get_cached_typeadapter,
20
  )
21
 
22
  if TYPE_CHECKING:
 
112
  if func_name == "<lambda>":
113
  raise ValueError("You must provide a name for lambda functions")
114
 
115
+ type_adapter = get_cached_typeadapter(fn)
116
+ parameters = type_adapter.json_schema()
117
+
118
  # Auto-detect context parameter if not provided
119
  if context_kwarg is None:
120
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
121
+ if context_kwarg:
122
+ parameters = prune_params(parameters, params=[context_kwarg])
 
 
 
 
 
 
 
 
123
 
124
  # Convert parameters to PromptArguments
125
  arguments: list[PromptArgument] = []
126
  if "properties" in parameters:
127
  for param_name, param in parameters["properties"].items():
 
 
 
 
 
128
  arguments.append(
129
  PromptArgument(
130
  name=param_name,
131
  description=param.get("description"),
132
+ required=param_name in parameters.get("required", []),
133
  )
134
  )
135
 
src/fastmcp/resources/template.py CHANGED
@@ -22,7 +22,7 @@ from pydantic import (
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.utilities.types import (
24
  _convert_set_defaults,
25
- is_class_member_of_type,
26
  )
27
 
28
  if TYPE_CHECKING:
@@ -111,14 +111,7 @@ class ResourceTemplate(BaseModel):
111
 
112
  # Auto-detect context parameter if not provided
113
  if context_kwarg is None:
114
- if inspect.ismethod(fn) and hasattr(fn, "__func__"):
115
- sig = inspect.signature(fn.__func__)
116
- else:
117
- sig = inspect.signature(fn)
118
- for param_name, param in sig.parameters.items():
119
- if is_class_member_of_type(param.annotation, Context):
120
- context_kwarg = param_name
121
- break
122
 
123
  # Validate that URI params match function params
124
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
 
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.utilities.types import (
24
  _convert_set_defaults,
25
+ find_kwarg_by_type,
26
  )
27
 
28
  if TYPE_CHECKING:
 
111
 
112
  # Auto-detect context parameter if not provided
113
  if context_kwarg is None:
114
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
 
 
 
 
 
 
 
115
 
116
  # Validate that URI params match function params
117
  uri_params = set(re.findall(r"{(\w+)(?:\*)?}", uri_template))
src/fastmcp/tools/tool.py CHANGED
@@ -16,8 +16,8 @@ from fastmcp.utilities.logging import get_logger
16
  from fastmcp.utilities.types import (
17
  Image,
18
  _convert_set_defaults,
 
19
  get_cached_typeadapter,
20
- is_class_member_of_type,
21
  )
22
 
23
  if TYPE_CHECKING:
@@ -74,18 +74,11 @@ class Tool(BaseModel):
74
 
75
  func_doc = description or fn.__doc__ or ""
76
 
77
- if inspect.ismethod(fn) and hasattr(fn, "__func__"):
78
- sig = inspect.signature(fn.__func__)
79
- else:
80
- sig = inspect.signature(fn)
81
- if context_kwarg is None:
82
- for param_name, param in sig.parameters.items():
83
- if is_class_member_of_type(param.annotation, Context):
84
- context_kwarg = param_name
85
- break
86
-
87
  type_adapter = get_cached_typeadapter(fn)
88
  schema = type_adapter.json_schema()
 
 
 
89
  if context_kwarg:
90
  schema = prune_params(schema, params=[context_kwarg])
91
 
 
16
  from fastmcp.utilities.types import (
17
  Image,
18
  _convert_set_defaults,
19
+ find_kwarg_by_type,
20
  get_cached_typeadapter,
 
21
  )
22
 
23
  if TYPE_CHECKING:
 
74
 
75
  func_doc = description or fn.__doc__ or ""
76
 
 
 
 
 
 
 
 
 
 
 
77
  type_adapter = get_cached_typeadapter(fn)
78
  schema = type_adapter.json_schema()
79
+
80
+ if context_kwarg is None:
81
+ context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
82
  if context_kwarg:
83
  schema = prune_params(schema, params=[context_kwarg])
84
 
src/fastmcp/utilities/types.py CHANGED
@@ -1,6 +1,8 @@
1
  """Common types used across FastMCP."""
2
 
3
  import base64
 
 
4
  from functools import lru_cache
5
  from pathlib import Path
6
  from types import UnionType
@@ -34,7 +36,12 @@ def issubclass_safe(cls: type, base: type) -> bool:
34
 
35
 
36
  def is_class_member_of_type(cls: type, base: type) -> bool:
37
- """Check if cls is a member of base, even if cls is a type variable."""
 
 
 
 
 
38
  origin = get_origin(cls)
39
  # Handle both types of unions: UnionType (from types module, used with | syntax)
40
  # and typing.Union (used with Union[] syntax)
@@ -50,6 +57,23 @@ def is_class_member_of_type(cls: type, base: type) -> bool:
50
  return issubclass_safe(cls, base)
51
 
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  def _convert_set_defaults(maybe_set: set[T] | list[T] | None) -> set[T]:
54
  """Convert a set or list to a set, defaulting to an empty set if None."""
55
  if maybe_set is None:
 
1
  """Common types used across FastMCP."""
2
 
3
  import base64
4
+ import inspect
5
+ from collections.abc import Callable
6
  from functools import lru_cache
7
  from pathlib import Path
8
  from types import UnionType
 
36
 
37
 
38
  def is_class_member_of_type(cls: type, base: type) -> bool:
39
+ """
40
+ Check if cls is a member of base, even if cls is a type variable.
41
+
42
+ Base can be a type, a UnionType, or an Annotated type. Generic types are not
43
+ considered members (e.g. T is not a member of list[T]).
44
+ """
45
  origin = get_origin(cls)
46
  # Handle both types of unions: UnionType (from types module, used with | syntax)
47
  # and typing.Union (used with Union[] syntax)
 
57
  return issubclass_safe(cls, base)
58
 
59
 
60
+ def find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None:
61
+ """
62
+ Find the name of the kwarg that is of type kwarg_type.
63
+
64
+ Includes union types that contain the kwarg_type, as well as Annotated types.
65
+ """
66
+ if inspect.ismethod(fn) and hasattr(fn, "__func__"):
67
+ sig = inspect.signature(fn.__func__)
68
+ else:
69
+ sig = inspect.signature(fn)
70
+
71
+ for name, param in sig.parameters.items():
72
+ if is_class_member_of_type(param.annotation, kwarg_type):
73
+ return name
74
+ return None
75
+
76
+
77
  def _convert_set_defaults(maybe_set: set[T] | list[T] | None) -> set[T]:
78
  """Convert a set or list to a set, defaulting to an empty set if None."""
79
  if maybe_set is None: