fastmcp 2.3.3__py3-none-any.whl → 2.3.5__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.
@@ -6,7 +6,7 @@ from typing import Any
6
6
 
7
7
  from pydantic import AnyUrl
8
8
 
9
- from fastmcp.exceptions import NotFoundError
9
+ from fastmcp.exceptions import NotFoundError, ResourceError
10
10
  from fastmcp.resources import FunctionResource
11
11
  from fastmcp.resources.resource import Resource
12
12
  from fastmcp.resources.template import (
@@ -244,11 +244,32 @@ class ResourceManager:
244
244
  uri_str,
245
245
  params=params,
246
246
  )
247
+ except ResourceError as e:
248
+ logger.error(f"Error creating resource from template: {e}")
249
+ raise e
247
250
  except Exception as e:
251
+ logger.error(f"Error creating resource from template: {e}")
248
252
  raise ValueError(f"Error creating resource from template: {e}")
249
253
 
250
254
  raise NotFoundError(f"Unknown resource: {uri_str}")
251
255
 
256
+ async def read_resource(self, uri: AnyUrl | str) -> str | bytes:
257
+ """Read a resource contents."""
258
+ resource = await self.get_resource(uri)
259
+
260
+ try:
261
+ return await resource.read()
262
+
263
+ # raise ResourceErrors as-is
264
+ except ResourceError as e:
265
+ logger.error(f"Error reading resource {uri!r}: {e}")
266
+ raise e
267
+
268
+ # raise other exceptions as ResourceErrors without revealing internal details
269
+ except Exception as e:
270
+ logger.error(f"Error reading resource {uri!r}: {e}")
271
+ raise ResourceError(f"Error reading resource {uri!r}") from e
272
+
252
273
  def get_resources(self) -> dict[str, Resource]:
253
274
  """Get all registered resources, keyed by URI."""
254
275
  return self._resources
@@ -21,6 +21,7 @@ from pydantic import (
21
21
 
22
22
  from fastmcp.resources.types import FunctionResource, Resource
23
23
  from fastmcp.server.dependencies import get_context
24
+ from fastmcp.utilities.json_schema import compress_schema
24
25
  from fastmcp.utilities.types import (
25
26
  _convert_set_defaults,
26
27
  find_kwarg_by_type,
@@ -150,6 +151,10 @@ class ResourceTemplate(BaseModel):
150
151
  # Get schema from TypeAdapter - will fail if function isn't properly typed
151
152
  parameters = TypeAdapter(fn).json_schema()
152
153
 
154
+ # compress the schema
155
+ prune_params = [context_kwarg] if context_kwarg else None
156
+ parameters = compress_schema(parameters, prune_params=prune_params)
157
+
153
158
  # ensure the arguments are properly cast
154
159
  fn = validate_call(fn)
155
160
 
@@ -171,28 +176,27 @@ class ResourceTemplate(BaseModel):
171
176
  """Create a resource from the template with the given parameters."""
172
177
  from fastmcp.server.context import Context
173
178
 
174
- try:
175
- # Add context to parameters if needed
176
- kwargs = params.copy()
177
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
178
- if context_kwarg and context_kwarg not in kwargs:
179
- kwargs[context_kwarg] = get_context()
179
+ # Add context to parameters if needed
180
+ kwargs = params.copy()
181
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
182
+ if context_kwarg and context_kwarg not in kwargs:
183
+ kwargs[context_kwarg] = get_context()
180
184
 
185
+ async def resource_read_fn() -> str | bytes:
181
186
  # Call function and check if result is a coroutine
182
187
  result = self.fn(**kwargs)
183
188
  if inspect.iscoroutine(result):
184
189
  result = await result
185
-
186
- return FunctionResource(
187
- uri=AnyUrl(uri), # Explicitly convert to AnyUrl
188
- name=self.name,
189
- description=self.description,
190
- mime_type=self.mime_type,
191
- fn=lambda **kwargs: result, # Capture result in closure
192
- tags=self.tags,
193
- )
194
- except Exception as e:
195
- raise ValueError(f"Error creating resource from template: {e}")
190
+ return result
191
+
192
+ return FunctionResource(
193
+ uri=AnyUrl(uri), # Explicitly convert to AnyUrl
194
+ name=self.name,
195
+ description=self.description,
196
+ mime_type=self.mime_type,
197
+ fn=resource_read_fn,
198
+ tags=self.tags,
199
+ )
196
200
 
197
201
  def __eq__(self, other: object) -> bool:
198
202
  if not isinstance(other, ResourceTemplate):
@@ -6,7 +6,7 @@ import inspect
6
6
  import json
7
7
  from collections.abc import Callable
8
8
  from pathlib import Path
9
- from typing import TYPE_CHECKING, Any
9
+ from typing import Any
10
10
 
11
11
  import anyio
12
12
  import anyio.to_thread
@@ -15,12 +15,13 @@ import pydantic.json
15
15
  import pydantic_core
16
16
  from pydantic import Field, ValidationInfo
17
17
 
18
+ from fastmcp.exceptions import ResourceError
18
19
  from fastmcp.resources.resource import Resource
19
20
  from fastmcp.server.dependencies import get_context
21
+ from fastmcp.utilities.logging import get_logger
20
22
  from fastmcp.utilities.types import find_kwarg_by_type
21
23
 
22
- if TYPE_CHECKING:
23
- pass
24
+ logger = get_logger(__name__)
24
25
 
25
26
 
26
27
  class TextResource(Resource):
@@ -62,26 +63,23 @@ class FunctionResource(Resource):
62
63
  """Read the resource by calling the wrapped function."""
63
64
  from fastmcp.server.context import Context
64
65
 
65
- try:
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
- except Exception as e:
84
- raise ValueError(f"Error reading resource {self.uri}: {e}")
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()
85
83
 
86
84
 
87
85
  class FileResource(Resource):
@@ -124,7 +122,7 @@ class FileResource(Resource):
124
122
  return await anyio.to_thread.run_sync(self.path.read_bytes)
125
123
  return await anyio.to_thread.run_sync(self.path.read_text)
126
124
  except Exception as e:
127
- raise ValueError(f"Error reading file {self.path}: {e}")
125
+ raise ResourceError(f"Error reading file {self.path}") from e
128
126
 
129
127
 
130
128
  class HttpResource(Resource):
@@ -185,7 +183,7 @@ class DirectoryResource(Resource):
185
183
  else list(self.path.rglob("*"))
186
184
  )
187
185
  except Exception as e:
188
- raise ValueError(f"Error listing directory {self.path}: {e}")
186
+ raise ResourceError(f"Error listing directory {self.path}: {e}")
189
187
 
190
188
  async def read(self) -> str: # Always returns JSON string
191
189
  """Read the directory listing."""
@@ -193,5 +191,5 @@ class DirectoryResource(Resource):
193
191
  files = await anyio.to_thread.run_sync(self.list_files)
194
192
  file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]
195
193
  return json.dumps({"files": file_list}, indent=2)
196
- except Exception as e:
197
- raise ValueError(f"Error reading directory {self.path}: {e}")
194
+ except Exception:
195
+ raise ResourceError(f"Error reading directory {self.path}")
fastmcp/server/context.py CHANGED
@@ -56,7 +56,7 @@ class Context:
56
56
  ctx.error("Error message")
57
57
 
58
58
  # Report progress
59
- ctx.report_progress(50, 100)
59
+ ctx.report_progress(50, 100, "Processing")
60
60
 
61
61
  # Access resources
62
62
  data = ctx.read_resource("resource://data")
@@ -96,7 +96,7 @@ class Context:
96
96
  return self.fastmcp._mcp_server.request_context
97
97
 
98
98
  async def report_progress(
99
- self, progress: float, total: float | None = None
99
+ self, progress: float, total: float | None = None, message: str | None = None
100
100
  ) -> None:
101
101
  """Report progress for the current operation.
102
102
 
@@ -115,7 +115,10 @@ class Context:
115
115
  return
116
116
 
117
117
  await self.request_context.session.send_progress_notification(
118
- progress_token=progress_token, progress=progress, total=total
118
+ progress_token=progress_token,
119
+ progress=progress,
120
+ total=total,
121
+ message=message,
119
122
  )
120
123
 
121
124
  async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
fastmcp/server/http.py CHANGED
@@ -10,9 +10,16 @@ from mcp.server.auth.middleware.bearer_auth import (
10
10
  BearerAuthBackend,
11
11
  RequireAuthMiddleware,
12
12
  )
13
- from mcp.server.auth.provider import OAuthAuthorizationServerProvider
13
+ from mcp.server.auth.provider import (
14
+ AccessTokenT,
15
+ AuthorizationCodeT,
16
+ OAuthAuthorizationServerProvider,
17
+ RefreshTokenT,
18
+ )
14
19
  from mcp.server.auth.routes import create_auth_routes
15
20
  from mcp.server.auth.settings import AuthSettings
21
+ from mcp.server.lowlevel.server import LifespanResultT
22
+ from mcp.server.sse import SseServerTransport
16
23
  from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
17
24
  from starlette.applications import Starlette
18
25
  from starlette.middleware import Middleware
@@ -20,9 +27,8 @@ from starlette.middleware.authentication import AuthenticationMiddleware
20
27
  from starlette.requests import Request
21
28
  from starlette.responses import Response
22
29
  from starlette.routing import BaseRoute, Mount, Route
23
- from starlette.types import Receive, Scope, Send
30
+ from starlette.types import Lifespan, Receive, Scope, Send
24
31
 
25
- from fastmcp.low_level.sse_server_transport import SseServerTransport
26
32
  from fastmcp.utilities.logging import get_logger
27
33
 
28
34
  if TYPE_CHECKING:
@@ -30,12 +36,19 @@ if TYPE_CHECKING:
30
36
 
31
37
  logger = get_logger(__name__)
32
38
 
39
+
33
40
  _current_http_request: ContextVar[Request | None] = ContextVar(
34
41
  "http_request",
35
42
  default=None,
36
43
  )
37
44
 
38
45
 
46
+ class StarletteWithLifespan(Starlette):
47
+ @property
48
+ def lifespan(self) -> Lifespan:
49
+ return self.router.lifespan_context
50
+
51
+
39
52
  @contextmanager
40
53
  def set_http_request(request: Request) -> Generator[Request, None, None]:
41
54
  token = _current_http_request.set(request)
@@ -62,7 +75,10 @@ class RequestContextMiddleware:
62
75
 
63
76
 
64
77
  def setup_auth_middleware_and_routes(
65
- auth_server_provider: OAuthAuthorizationServerProvider | None,
78
+ auth_server_provider: OAuthAuthorizationServerProvider[
79
+ AuthorizationCodeT, RefreshTokenT, AccessTokenT
80
+ ]
81
+ | None,
66
82
  auth_settings: AuthSettings | None,
67
83
  ) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
68
84
  """Set up authentication middleware and routes if auth is enabled.
@@ -112,7 +128,7 @@ def create_base_app(
112
128
  middleware: list[Middleware],
113
129
  debug: bool = False,
114
130
  lifespan: Callable | None = None,
115
- ) -> Starlette:
131
+ ) -> StarletteWithLifespan:
116
132
  """Create a base Starlette app with common middleware and routes.
117
133
 
118
134
  Args:
@@ -127,7 +143,7 @@ def create_base_app(
127
143
  # Always add RequestContextMiddleware as the outermost middleware
128
144
  middleware.append(Middleware(RequestContextMiddleware))
129
145
 
130
- return Starlette(
146
+ return StarletteWithLifespan(
131
147
  routes=routes,
132
148
  middleware=middleware,
133
149
  debug=debug,
@@ -136,15 +152,18 @@ def create_base_app(
136
152
 
137
153
 
138
154
  def create_sse_app(
139
- server: FastMCP,
155
+ server: FastMCP[LifespanResultT],
140
156
  message_path: str,
141
157
  sse_path: str,
142
- auth_server_provider: OAuthAuthorizationServerProvider | None = None,
158
+ auth_server_provider: OAuthAuthorizationServerProvider[
159
+ AuthorizationCodeT, RefreshTokenT, AccessTokenT
160
+ ]
161
+ | None = None,
143
162
  auth_settings: AuthSettings | None = None,
144
163
  debug: bool = False,
145
164
  routes: list[BaseRoute] | None = None,
146
165
  middleware: list[Middleware] | None = None,
147
- ) -> Starlette:
166
+ ) -> StarletteWithLifespan:
148
167
  """Return an instance of the SSE server app.
149
168
 
150
169
  Args:
@@ -228,25 +247,33 @@ def create_sse_app(
228
247
  server_middleware.extend(middleware)
229
248
 
230
249
  # Create and return the app
231
- return create_base_app(
250
+ app = create_base_app(
232
251
  routes=server_routes,
233
252
  middleware=server_middleware,
234
253
  debug=debug,
235
254
  )
255
+ # Store the FastMCP server instance on the Starlette app state
256
+ app.state.fastmcp_server = server
257
+ app.state.path = sse_path
258
+
259
+ return app
236
260
 
237
261
 
238
262
  def create_streamable_http_app(
239
- server: FastMCP,
263
+ server: FastMCP[LifespanResultT],
240
264
  streamable_http_path: str,
241
265
  event_store: None = None,
242
- auth_server_provider: OAuthAuthorizationServerProvider | None = None,
266
+ auth_server_provider: OAuthAuthorizationServerProvider[
267
+ AuthorizationCodeT, RefreshTokenT, AccessTokenT
268
+ ]
269
+ | None = None,
243
270
  auth_settings: AuthSettings | None = None,
244
271
  json_response: bool = False,
245
272
  stateless_http: bool = False,
246
273
  debug: bool = False,
247
274
  routes: list[BaseRoute] | None = None,
248
275
  middleware: list[Middleware] | None = None,
249
- ) -> Starlette:
276
+ ) -> StarletteWithLifespan:
250
277
  """Return an instance of the StreamableHTTP server app.
251
278
 
252
279
  Args:
@@ -322,9 +349,15 @@ def create_streamable_http_app(
322
349
  yield
323
350
 
324
351
  # Create and return the app with lifespan
325
- return create_base_app(
352
+ app = create_base_app(
326
353
  routes=server_routes,
327
354
  middleware=server_middleware,
328
355
  debug=debug,
329
356
  lifespan=lifespan,
330
357
  )
358
+ # Store the FastMCP server instance on the Starlette app state
359
+ app.state.fastmcp_server = server
360
+
361
+ app.state.path = streamable_http_path
362
+
363
+ return app
fastmcp/server/openapi.py CHANGED
@@ -14,6 +14,7 @@ import httpx
14
14
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
15
15
  from pydantic.networks import AnyUrl
16
16
 
17
+ from fastmcp.exceptions import ToolError
17
18
  from fastmcp.resources import Resource, ResourceTemplate
18
19
  from fastmcp.server.server import FastMCP
19
20
  from fastmcp.tools.tool import Tool, _convert_to_content
@@ -137,6 +138,10 @@ class OpenAPITool(Tool):
137
138
  self._route = route
138
139
  self._timeout = timeout
139
140
 
141
+ def __repr__(self) -> str:
142
+ """Custom representation to prevent recursion errors when printing."""
143
+ return f"OpenAPITool(name={self.name!r}, method={self._route.method}, path={self._route.path})"
144
+
140
145
  async def _execute_request(self, *args, **kwargs):
141
146
  """Execute the HTTP request based on the route configuration."""
142
147
  context = kwargs.get("context")
@@ -163,7 +168,7 @@ class OpenAPITool(Tool):
163
168
  }
164
169
  missing_params = required_path_params - path_params.keys()
165
170
  if missing_params:
166
- raise ValueError(f"Missing required path parameters: {missing_params}")
171
+ raise ToolError(f"Missing required path parameters: {missing_params}")
167
172
 
168
173
  for param_name, param_value in path_params.items():
169
174
  path = path.replace(f"{{{param_name}}}", str(param_value))
@@ -286,6 +291,10 @@ class OpenAPIResource(Resource):
286
291
  self._route = route
287
292
  self._timeout = timeout
288
293
 
294
+ def __repr__(self) -> str:
295
+ """Custom representation to prevent recursion errors when printing."""
296
+ return f"OpenAPIResource(name={self.name!r}, uri={self.uri!r}, path={self._route.path})"
297
+
289
298
  async def read(self) -> str | bytes:
290
299
  """Fetch the resource data by making an HTTP request."""
291
300
  try:
@@ -396,6 +405,10 @@ class OpenAPIResourceTemplate(ResourceTemplate):
396
405
  self._route = route
397
406
  self._timeout = timeout
398
407
 
408
+ def __repr__(self) -> str:
409
+ """Custom representation to prevent recursion errors when printing."""
410
+ return f"OpenAPIResourceTemplate(name={self.name!r}, uri_template={self.uri_template!r}, path={self._route.path})"
411
+
399
412
  async def create_resource(
400
413
  self,
401
414
  uri: str,
fastmcp/server/proxy.py CHANGED
@@ -18,7 +18,7 @@ from mcp.types import (
18
18
  from pydantic.networks import AnyUrl
19
19
 
20
20
  from fastmcp.client import Client
21
- from fastmcp.exceptions import NotFoundError
21
+ from fastmcp.exceptions import NotFoundError, ResourceError, ToolError
22
22
  from fastmcp.prompts import Prompt, PromptMessage
23
23
  from fastmcp.resources import Resource, ResourceTemplate
24
24
  from fastmcp.server.context import Context
@@ -64,7 +64,7 @@ class ProxyTool(Tool):
64
64
  arguments=arguments,
65
65
  )
66
66
  if result.isError:
67
- raise ValueError(cast(mcp.types.TextContent, result.content[0]).text)
67
+ raise ToolError(cast(mcp.types.TextContent, result.content[0]).text)
68
68
  return result.content
69
69
 
70
70
 
@@ -97,7 +97,7 @@ class ProxyResource(Resource):
97
97
  elif isinstance(result[0], BlobResourceContents):
98
98
  return result[0].blob
99
99
  else:
100
- raise ValueError(f"Unsupported content type: {type(result[0])}")
100
+ raise ResourceError(f"Unsupported content type: {type(result[0])}")
101
101
 
102
102
 
103
103
  class ProxyTemplate(ResourceTemplate):
@@ -138,7 +138,7 @@ class ProxyTemplate(ResourceTemplate):
138
138
  elif isinstance(result[0], BlobResourceContents):
139
139
  value = result[0].blob
140
140
  else:
141
- raise ValueError(f"Unsupported content type: {type(result[0])}")
141
+ raise ResourceError(f"Unsupported content type: {type(result[0])}")
142
142
 
143
143
  return ProxyResource(
144
144
  client=self._client,