Jeremiah Lowin commited on
Commit
4dee55e
·
1 Parent(s): 582f82a

Formalize template/functiontemplate

Browse files
src/fastmcp/resources/resource.py CHANGED
@@ -93,8 +93,9 @@ class Resource(FastMCPBaseModel, abc.ABC):
93
  pass
94
 
95
  def __eq__(self, other: object) -> bool:
96
- if not isinstance(other, Resource):
97
  return False
 
98
  return self.model_dump() == other.model_dump()
99
 
100
  def to_mcp_resource(self, **overrides: Any) -> MCPResource:
 
93
  pass
94
 
95
  def __eq__(self, other: object) -> bool:
96
+ if type(self) is not type(other):
97
  return False
98
+ assert isinstance(other, type(self))
99
  return self.model_dump() == other.model_dump()
100
 
101
  def to_mcp_resource(self, **overrides: Any) -> MCPResource:
src/fastmcp/resources/template.py CHANGED
@@ -65,11 +65,28 @@ class ResourceTemplate(FastMCPBaseModel):
65
  mime_type: str = Field(
66
  default="text/plain", description="MIME type of the resource content"
67
  )
68
- fn: Callable[..., Any]
69
  parameters: dict[str, Any] = Field(
70
  description="JSON schema for function parameters"
71
  )
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  @field_validator("mime_type", mode="before")
74
  @classmethod
75
  def set_default_mime_type(cls, mime_type: str | None) -> str:
@@ -78,6 +95,70 @@ class ResourceTemplate(FastMCPBaseModel):
78
  return mime_type
79
  return "text/plain"
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  @classmethod
82
  def from_function(
83
  cls,
@@ -87,7 +168,7 @@ class ResourceTemplate(FastMCPBaseModel):
87
  description: str | None = None,
88
  mime_type: str | None = None,
89
  tags: set[str] | None = None,
90
- ) -> ResourceTemplate:
91
  """Create a template from a function."""
92
  from fastmcp.server.context import Context
93
 
@@ -166,48 +247,3 @@ class ResourceTemplate(FastMCPBaseModel):
166
  parameters=parameters,
167
  tags=tags or set(),
168
  )
169
-
170
- def matches(self, uri: str) -> dict[str, Any] | None:
171
- """Check if URI matches template and extract parameters."""
172
- return match_uri_template(uri, self.uri_template)
173
-
174
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
175
- """Create a resource from the template with the given parameters."""
176
- from fastmcp.server.context import Context
177
-
178
- # Add context to parameters if needed
179
- kwargs = params.copy()
180
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
181
- if context_kwarg and context_kwarg not in kwargs:
182
- kwargs[context_kwarg] = get_context()
183
-
184
- async def resource_read_fn() -> str | bytes:
185
- # Call function and check if result is a coroutine
186
- result = self.fn(**kwargs)
187
- if inspect.iscoroutine(result):
188
- result = await result
189
- return result
190
-
191
- return Resource.from_function(
192
- fn=resource_read_fn,
193
- uri=uri,
194
- name=self.name,
195
- description=self.description,
196
- mime_type=self.mime_type,
197
- tags=self.tags,
198
- )
199
-
200
- def __eq__(self, other: object) -> bool:
201
- if not isinstance(other, ResourceTemplate):
202
- return False
203
- return self.model_dump() == other.model_dump()
204
-
205
- def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
206
- """Convert the resource template to an MCPResourceTemplate."""
207
- kwargs = {
208
- "uriTemplate": self.uri_template,
209
- "name": self.name,
210
- "description": self.description,
211
- "mimeType": self.mime_type,
212
- }
213
- return MCPResourceTemplate(**kwargs | overrides)
 
65
  mime_type: str = Field(
66
  default="text/plain", description="MIME type of the resource content"
67
  )
 
68
  parameters: dict[str, Any] = Field(
69
  description="JSON schema for function parameters"
70
  )
71
 
72
+ @staticmethod
73
+ def from_function(
74
+ fn: Callable[..., Any],
75
+ uri_template: str,
76
+ name: str | None = None,
77
+ description: str | None = None,
78
+ mime_type: str | None = None,
79
+ tags: set[str] | None = None,
80
+ ) -> FunctionResourceTemplate:
81
+ return FunctionResourceTemplate.from_function(
82
+ fn=fn,
83
+ uri_template=uri_template,
84
+ name=name,
85
+ description=description,
86
+ mime_type=mime_type,
87
+ tags=tags,
88
+ )
89
+
90
  @field_validator("mime_type", mode="before")
91
  @classmethod
92
  def set_default_mime_type(cls, mime_type: str | None) -> str:
 
95
  return mime_type
96
  return "text/plain"
97
 
98
+ def matches(self, uri: str) -> dict[str, Any] | None:
99
+ """Check if URI matches template and extract parameters."""
100
+ return match_uri_template(uri, self.uri_template)
101
+
102
+ async def read(self, arguments: dict[str, Any]) -> str | bytes:
103
+ """Read the resource content."""
104
+ raise NotImplementedError(
105
+ "Subclasses must implement read() or override create_resource()"
106
+ )
107
+
108
+ async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
109
+ """Create a resource from the template with the given parameters."""
110
+
111
+ async def resource_read_fn() -> str | bytes:
112
+ # Call function and check if result is a coroutine
113
+ result = await self.read(arguments=params)
114
+ return result
115
+
116
+ return Resource.from_function(
117
+ fn=resource_read_fn,
118
+ uri=uri,
119
+ name=self.name,
120
+ description=self.description,
121
+ mime_type=self.mime_type,
122
+ tags=self.tags,
123
+ )
124
+
125
+ def __eq__(self, other: object) -> bool:
126
+ if type(self) is not type(other):
127
+ return False
128
+ assert isinstance(other, type(self))
129
+ return self.model_dump() == other.model_dump()
130
+
131
+ def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
132
+ """Convert the resource template to an MCPResourceTemplate."""
133
+ kwargs = {
134
+ "uriTemplate": self.uri_template,
135
+ "name": self.name,
136
+ "description": self.description,
137
+ "mimeType": self.mime_type,
138
+ }
139
+ return MCPResourceTemplate(**kwargs | overrides)
140
+
141
+
142
+ class FunctionResourceTemplate(ResourceTemplate):
143
+ """A template for dynamically creating resources."""
144
+
145
+ fn: Callable[..., Any]
146
+
147
+ async def read(self, arguments: dict[str, Any]) -> str | bytes:
148
+ """Read the resource content."""
149
+ from fastmcp.server.context import Context
150
+
151
+ # Add context to parameters if needed
152
+ kwargs = arguments.copy()
153
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
154
+ if context_kwarg and context_kwarg not in kwargs:
155
+ kwargs[context_kwarg] = get_context()
156
+
157
+ result = self.fn(**kwargs)
158
+ if inspect.iscoroutine(result):
159
+ result = await result
160
+ return result
161
+
162
  @classmethod
163
  def from_function(
164
  cls,
 
168
  description: str | None = None,
169
  mime_type: str | None = None,
170
  tags: set[str] | None = None,
171
+ ) -> FunctionResourceTemplate:
172
  """Create a template from a function."""
173
  from fastmcp.server.context import Context
174
 
 
247
  parameters=parameters,
248
  tags=tags or set(),
249
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/fastmcp/server/openapi.py CHANGED
@@ -605,7 +605,6 @@ class OpenAPIResourceTemplate(ResourceTemplate):
605
  uri_template=uri_template,
606
  name=name,
607
  description=description,
608
- fn=lambda **kwargs: None,
609
  parameters=parameters,
610
  tags=tags,
611
  )
 
605
  uri_template=uri_template,
606
  name=name,
607
  description=description,
 
608
  parameters=parameters,
609
  tags=tags,
610
  )
src/fastmcp/server/proxy.py CHANGED
@@ -32,10 +32,6 @@ if TYPE_CHECKING:
32
  logger = get_logger(__name__)
33
 
34
 
35
- def _proxy_passthrough():
36
- pass
37
-
38
-
39
  class ProxyTool(Tool):
40
  def __init__(self, client: Client, **kwargs):
41
  super().__init__(**kwargs)
@@ -116,7 +112,6 @@ class ProxyTemplate(ResourceTemplate):
116
  uri_template=template.uriTemplate,
117
  name=template.name,
118
  description=template.description,
119
- fn=_proxy_passthrough,
120
  parameters={},
121
  )
122
 
 
32
  logger = get_logger(__name__)
33
 
34
 
 
 
 
 
35
  class ProxyTool(Tool):
36
  def __init__(self, client: Client, **kwargs):
37
  super().__init__(**kwargs)
 
112
  uri_template=template.uriTemplate,
113
  name=template.name,
114
  description=template.description,
 
115
  parameters={},
116
  )
117
 
src/fastmcp/tools/tool.py CHANGED
@@ -86,8 +86,9 @@ class Tool(FastMCPBaseModel, ABC):
86
  )
87
 
88
  def __eq__(self, other: object) -> bool:
89
- if not isinstance(other, Tool):
90
  return False
 
91
  return self.model_dump() == other.model_dump()
92
 
93
  @abstractmethod
 
86
  )
87
 
88
  def __eq__(self, other: object) -> bool:
89
+ if type(self) is not type(other):
90
  return False
91
+ assert isinstance(other, type(self))
92
  return self.model_dump() == other.model_dump()
93
 
94
  @abstractmethod
tests/resources/test_resource_template.py CHANGED
@@ -634,9 +634,9 @@ class TestContextHandling:
634
  {"x": 42},
635
  )
636
 
637
- assert isinstance(resource, FunctionResource)
638
- content = await resource.read()
639
- assert content == "42"
640
 
641
  async def test_context_optional(self):
642
  """Test that context is optional when creating resources."""
@@ -662,6 +662,6 @@ class TestContextHandling:
662
  {"x": 42},
663
  )
664
 
665
- assert isinstance(resource, FunctionResource)
666
- content = await resource.read()
667
- assert content == "42"
 
634
  {"x": 42},
635
  )
636
 
637
+ assert isinstance(resource, FunctionResource)
638
+ content = await resource.read()
639
+ assert content == "42"
640
 
641
  async def test_context_optional(self):
642
  """Test that context is optional when creating resources."""
 
662
  {"x": 42},
663
  )
664
 
665
+ assert isinstance(resource, FunctionResource)
666
+ content = await resource.read()
667
+ assert content == "42"