fastmcp 2.6.1__py3-none-any.whl → 2.7.0__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.
@@ -10,18 +10,17 @@ from urllib.parse import unquote
10
10
 
11
11
  from mcp.types import ResourceTemplate as MCPResourceTemplate
12
12
  from pydantic import (
13
- AnyUrl,
14
- BaseModel,
15
13
  BeforeValidator,
16
14
  Field,
17
15
  field_validator,
18
16
  validate_call,
19
17
  )
20
18
 
21
- from fastmcp.resources.types import FunctionResource, Resource
19
+ from fastmcp.resources.types import Resource
22
20
  from fastmcp.server.dependencies import get_context
23
21
  from fastmcp.utilities.json_schema import compress_schema
24
22
  from fastmcp.utilities.types import (
23
+ FastMCPBaseModel,
25
24
  _convert_set_defaults,
26
25
  find_kwarg_by_type,
27
26
  get_cached_typeadapter,
@@ -52,12 +51,7 @@ def match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None:
52
51
  return None
53
52
 
54
53
 
55
- class MyModel(BaseModel):
56
- key: str
57
- value: int
58
-
59
-
60
- class ResourceTemplate(BaseModel):
54
+ class ResourceTemplate(FastMCPBaseModel):
61
55
  """A template for dynamically creating resources."""
62
56
 
63
57
  uri_template: str = Field(
@@ -71,11 +65,28 @@ class ResourceTemplate(BaseModel):
71
65
  mime_type: str = Field(
72
66
  default="text/plain", description="MIME type of the resource content"
73
67
  )
74
- fn: Callable[..., Any]
75
68
  parameters: dict[str, Any] = Field(
76
69
  description="JSON schema for function parameters"
77
70
  )
78
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
+
79
90
  @field_validator("mime_type", mode="before")
80
91
  @classmethod
81
92
  def set_default_mime_type(cls, mime_type: str | None) -> str:
@@ -84,6 +95,70 @@ class ResourceTemplate(BaseModel):
84
95
  return mime_type
85
96
  return "text/plain"
86
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
+
87
162
  @classmethod
88
163
  def from_function(
89
164
  cls,
@@ -93,7 +168,7 @@ class ResourceTemplate(BaseModel):
93
168
  description: str | None = None,
94
169
  mime_type: str | None = None,
95
170
  tags: set[str] | None = None,
96
- ) -> ResourceTemplate:
171
+ ) -> FunctionResourceTemplate:
97
172
  """Create a template from a function."""
98
173
  from fastmcp.server.context import Context
99
174
 
@@ -150,8 +225,12 @@ class ResourceTemplate(BaseModel):
150
225
 
151
226
  description = description or fn.__doc__
152
227
 
228
+ # if the fn is a callable class, we need to get the __call__ method from here out
153
229
  if not inspect.isroutine(fn):
154
230
  fn = fn.__call__
231
+ # if the fn is a staticmethod, we need to work with the underlying function
232
+ if isinstance(fn, staticmethod):
233
+ fn = fn.__func__
155
234
 
156
235
  type_adapter = get_cached_typeadapter(fn)
157
236
  parameters = type_adapter.json_schema()
@@ -172,48 +251,3 @@ class ResourceTemplate(BaseModel):
172
251
  parameters=parameters,
173
252
  tags=tags or set(),
174
253
  )
175
-
176
- def matches(self, uri: str) -> dict[str, Any] | None:
177
- """Check if URI matches template and extract parameters."""
178
- return match_uri_template(uri, self.uri_template)
179
-
180
- async def create_resource(self, uri: str, params: dict[str, Any]) -> Resource:
181
- """Create a resource from the template with the given parameters."""
182
- from fastmcp.server.context import Context
183
-
184
- # Add context to parameters if needed
185
- kwargs = params.copy()
186
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
187
- if context_kwarg and context_kwarg not in kwargs:
188
- kwargs[context_kwarg] = get_context()
189
-
190
- async def resource_read_fn() -> str | bytes:
191
- # Call function and check if result is a coroutine
192
- result = self.fn(**kwargs)
193
- if inspect.iscoroutine(result):
194
- result = await result
195
- return result
196
-
197
- return FunctionResource(
198
- uri=AnyUrl(uri), # Explicitly convert to AnyUrl
199
- name=self.name,
200
- description=self.description,
201
- mime_type=self.mime_type,
202
- fn=resource_read_fn,
203
- tags=self.tags,
204
- )
205
-
206
- def __eq__(self, other: object) -> bool:
207
- if not isinstance(other, ResourceTemplate):
208
- return False
209
- return self.model_dump() == other.model_dump()
210
-
211
- def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
212
- """Convert the resource template to an MCPResourceTemplate."""
213
- kwargs = {
214
- "uriTemplate": self.uri_template,
215
- "name": self.name,
216
- "description": self.description,
217
- "mimeType": self.mime_type,
218
- }
219
- return MCPResourceTemplate(**kwargs | overrides)
@@ -2,24 +2,18 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import inspect
6
5
  import json
7
- from collections.abc import Callable
8
6
  from pathlib import Path
9
- from typing import Any
10
7
 
11
8
  import anyio
12
9
  import anyio.to_thread
13
10
  import httpx
14
11
  import pydantic.json
15
- import pydantic_core
16
12
  from pydantic import Field, ValidationInfo
17
13
 
18
14
  from fastmcp.exceptions import ResourceError
19
15
  from fastmcp.resources.resource import Resource
20
- from fastmcp.server.dependencies import get_context
21
16
  from fastmcp.utilities.logging import get_logger
22
- from fastmcp.utilities.types import find_kwarg_by_type
23
17
 
24
18
  logger = get_logger(__name__)
25
19
 
@@ -44,44 +38,6 @@ class BinaryResource(Resource):
44
38
  return self.data
45
39
 
46
40
 
47
- class FunctionResource(Resource):
48
- """A resource that defers data loading by wrapping a function.
49
-
50
- The function is only called when the resource is read, allowing for lazy loading
51
- of potentially expensive data. This is particularly useful when listing resources,
52
- as the function won't be called until the resource is actually accessed.
53
-
54
- The function can return:
55
- - str for text content (default)
56
- - bytes for binary content
57
- - other types will be converted to JSON
58
- """
59
-
60
- fn: Callable[[], Any]
61
-
62
- async def read(self) -> str | bytes:
63
- """Read the resource by calling the wrapped function."""
64
- from fastmcp.server.context import Context
65
-
66
- kwargs = {}
67
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
68
- if context_kwarg is not None:
69
- kwargs[context_kwarg] = get_context()
70
-
71
- result = self.fn(**kwargs)
72
- if inspect.iscoroutinefunction(self.fn):
73
- result = await result
74
-
75
- if isinstance(result, Resource):
76
- return await result.read()
77
- elif isinstance(result, bytes):
78
- return result
79
- elif isinstance(result, str):
80
- return result
81
- else:
82
- return pydantic_core.to_json(result, fallback=str, indent=2).decode()
83
-
84
-
85
41
  class FileResource(Resource):
86
42
  """A resource that reads from a file.
87
43
 
fastmcp/server/context.py CHANGED
@@ -49,7 +49,7 @@ class Context:
49
49
  To use context in a tool function, add a parameter with the Context type annotation:
50
50
 
51
51
  ```python
52
- @server.tool()
52
+ @server.tool
53
53
  def my_tool(x: int, ctx: Context) -> str:
54
54
  # Log messages to the client
55
55
  ctx.info(f"Processing {x}")
fastmcp/server/openapi.py CHANGED
@@ -233,7 +233,6 @@ class OpenAPITool(Tool):
233
233
  name=name,
234
234
  description=description,
235
235
  parameters=parameters,
236
- fn=self._execute_request, # We'll use an instance method instead of a global function
237
236
  tags=tags,
238
237
  annotations=annotations,
239
238
  exclude_args=exclude_args,
@@ -247,9 +246,10 @@ class OpenAPITool(Tool):
247
246
  """Custom representation to prevent recursion errors when printing."""
248
247
  return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
249
248
 
250
- async def _execute_request(self, *args, **kwargs):
249
+ async def run(
250
+ self, arguments: dict[str, Any]
251
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
251
252
  """Execute the HTTP request based on the route configuration."""
252
- context = kwargs.get("context")
253
253
 
254
254
  # Prepare URL
255
255
  path = self._route.path
@@ -258,11 +258,11 @@ class OpenAPITool(Tool):
258
258
  # Path parameters should never be None as they're typically required
259
259
  # but we'll handle that case anyway
260
260
  path_params = {
261
- p.name: kwargs.get(p.name)
261
+ p.name: arguments.get(p.name)
262
262
  for p in self._route.parameters
263
263
  if p.location == "path"
264
- and p.name in kwargs
265
- and kwargs.get(p.name) is not None
264
+ and p.name in arguments
265
+ and arguments.get(p.name) is not None
266
266
  }
267
267
 
268
268
  # Ensure all path parameters are provided
@@ -340,11 +340,11 @@ class OpenAPITool(Tool):
340
340
  for p in self._route.parameters:
341
341
  if (
342
342
  p.location == "query"
343
- and p.name in kwargs
344
- and kwargs.get(p.name) is not None
345
- and kwargs.get(p.name) != ""
343
+ and p.name in arguments
344
+ and arguments.get(p.name) is not None
345
+ and arguments.get(p.name) != ""
346
346
  ):
347
- param_value = kwargs.get(p.name)
347
+ param_value = arguments.get(p.name)
348
348
 
349
349
  # Format array query parameters as comma-separated strings
350
350
  # following OpenAPI form style (default for query parameters)
@@ -399,10 +399,10 @@ class OpenAPITool(Tool):
399
399
  for p in self._route.parameters:
400
400
  if (
401
401
  p.location == "header"
402
- and p.name in kwargs
403
- and kwargs[p.name] is not None
402
+ and p.name in arguments
403
+ and arguments[p.name] is not None
404
404
  ):
405
- openapi_headers[p.name.lower()] = str(kwargs[p.name])
405
+ openapi_headers[p.name.lower()] = str(arguments[p.name])
406
406
  headers.update(openapi_headers)
407
407
 
408
408
  # Add headers from the current MCP client HTTP request (these take precedence)
@@ -420,21 +420,13 @@ class OpenAPITool(Tool):
420
420
  }
421
421
  body_params = {
422
422
  k: v
423
- for k, v in kwargs.items()
423
+ for k, v in arguments.items()
424
424
  if k not in path_query_header_params and k != "context"
425
425
  }
426
426
 
427
427
  if body_params:
428
428
  json_data = body_params
429
429
 
430
- # Log the request details if a context is available
431
- if context:
432
- try:
433
- await context.info(f"Making {self._route.method} request to {path}")
434
- except (ValueError, AttributeError):
435
- # Silently continue if context logging is not available
436
- pass
437
-
438
430
  # Execute the request
439
431
  try:
440
432
  response = await self._client.request(
@@ -451,10 +443,11 @@ class OpenAPITool(Tool):
451
443
 
452
444
  # Try to parse as JSON first
453
445
  try:
454
- return response.json()
446
+ result = response.json()
455
447
  except (json.JSONDecodeError, ValueError):
456
448
  # Return text content if not JSON
457
- return response.text
449
+ result = response.text
450
+ return _convert_to_content(result)
458
451
 
459
452
  except httpx.HTTPStatusError as e:
460
453
  # Handle HTTP errors (4xx, 5xx)
@@ -474,13 +467,6 @@ class OpenAPITool(Tool):
474
467
  # Handle request errors (connection, timeout, etc.)
475
468
  raise ValueError(f"Request error: {str(e)}")
476
469
 
477
- async def run(
478
- self, arguments: dict[str, Any]
479
- ) -> list[TextContent | ImageContent | EmbeddedResource]:
480
- """Run the tool with arguments and optional context."""
481
- response = await self._execute_request(**arguments)
482
- return _convert_to_content(response)
483
-
484
470
 
485
471
  class OpenAPIResource(Resource):
486
472
  """Resource implementation for OpenAPI endpoints."""
@@ -619,7 +605,6 @@ class OpenAPIResourceTemplate(ResourceTemplate):
619
605
  uri_template=uri_template,
620
606
  name=name,
621
607
  description=description,
622
- fn=lambda **kwargs: None,
623
608
  parameters=parameters,
624
609
  tags=tags,
625
610
  )
fastmcp/server/proxy.py CHANGED
@@ -32,10 +32,6 @@ if TYPE_CHECKING:
32
32
  logger = get_logger(__name__)
33
33
 
34
34
 
35
- def _proxy_passthrough():
36
- pass
37
-
38
-
39
35
  class ProxyTool(Tool):
40
36
  def __init__(self, client: Client, **kwargs):
41
37
  super().__init__(**kwargs)
@@ -48,7 +44,6 @@ class ProxyTool(Tool):
48
44
  name=tool.name,
49
45
  description=tool.description,
50
46
  parameters=tool.inputSchema,
51
- fn=_proxy_passthrough,
52
47
  )
53
48
 
54
49
  async def run(
@@ -69,6 +64,9 @@ class ProxyTool(Tool):
69
64
 
70
65
 
71
66
  class ProxyResource(Resource):
67
+ _client: Client
68
+ _value: str | bytes | None = None
69
+
72
70
  def __init__(self, client: Client, *, _value: str | bytes | None = None, **kwargs):
73
71
  super().__init__(**kwargs)
74
72
  self._client = client
@@ -114,7 +112,6 @@ class ProxyTemplate(ResourceTemplate):
114
112
  uri_template=template.uriTemplate,
115
113
  name=template.name,
116
114
  description=template.description,
117
- fn=_proxy_passthrough,
118
115
  parameters={},
119
116
  )
120
117
 
@@ -146,12 +143,13 @@ class ProxyTemplate(ResourceTemplate):
146
143
  name=self.name,
147
144
  description=self.description,
148
145
  mime_type=result[0].mimeType,
149
- contents=result,
150
146
  _value=value,
151
147
  )
152
148
 
153
149
 
154
150
  class ProxyPrompt(Prompt):
151
+ _client: Client
152
+
155
153
  def __init__(self, client: Client, **kwargs):
156
154
  super().__init__(**kwargs)
157
155
  self._client = client
@@ -163,7 +161,6 @@ class ProxyPrompt(Prompt):
163
161
  name=prompt.name,
164
162
  description=prompt.description,
165
163
  arguments=[a.model_dump() for a in prompt.arguments or []],
166
- fn=_proxy_passthrough,
167
164
  )
168
165
 
169
166
  async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]: