fastmcp 2.3.5__py3-none-any.whl → 2.5.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.
@@ -14,7 +14,6 @@ from pydantic import (
14
14
  BaseModel,
15
15
  BeforeValidator,
16
16
  Field,
17
- TypeAdapter,
18
17
  field_validator,
19
18
  validate_call,
20
19
  )
@@ -25,6 +24,7 @@ from fastmcp.utilities.json_schema import compress_schema
25
24
  from fastmcp.utilities.types import (
26
25
  _convert_set_defaults,
27
26
  find_kwarg_by_type,
27
+ get_cached_typeadapter,
28
28
  )
29
29
 
30
30
 
@@ -97,7 +97,7 @@ class ResourceTemplate(BaseModel):
97
97
  """Create a template from a function."""
98
98
  from fastmcp.server.context import Context
99
99
 
100
- func_name = name or fn.__name__
100
+ func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
101
101
  if func_name == "<lambda>":
102
102
  raise ValueError("You must provide a name for lambda functions")
103
103
 
@@ -148,8 +148,13 @@ class ResourceTemplate(BaseModel):
148
148
  f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
149
149
  )
150
150
 
151
- # Get schema from TypeAdapter - will fail if function isn't properly typed
152
- parameters = TypeAdapter(fn).json_schema()
151
+ description = description or fn.__doc__ or ""
152
+
153
+ if not inspect.isroutine(fn):
154
+ fn = fn.__call__
155
+
156
+ type_adapter = get_cached_typeadapter(fn)
157
+ parameters = type_adapter.json_schema()
153
158
 
154
159
  # compress the schema
155
160
  prune_params = [context_kwarg] if context_kwarg else None
@@ -161,7 +166,7 @@ class ResourceTemplate(BaseModel):
161
166
  return cls(
162
167
  uri_template=uri_template,
163
168
  name=func_name,
164
- description=description or fn.__doc__ or "",
169
+ description=description,
165
170
  mime_type=mime_type or "text/plain",
166
171
  fn=fn,
167
172
  parameters=parameters,
fastmcp/server/context.py CHANGED
@@ -12,6 +12,8 @@ from mcp.shared.context import RequestContext
12
12
  from mcp.types import (
13
13
  CreateMessageResult,
14
14
  ImageContent,
15
+ ModelHint,
16
+ ModelPreferences,
15
17
  Root,
16
18
  SamplingMessage,
17
19
  TextContent,
@@ -200,6 +202,7 @@ class Context:
200
202
  system_prompt: str | None = None,
201
203
  temperature: float | None = None,
202
204
  max_tokens: int | None = None,
205
+ model_preferences: ModelPreferences | str | list[str] | None = None,
203
206
  ) -> TextContent | ImageContent:
204
207
  """
205
208
  Send a sampling request to the client and await the response.
@@ -231,6 +234,7 @@ class Context:
231
234
  system_prompt=system_prompt,
232
235
  temperature=temperature,
233
236
  max_tokens=max_tokens,
237
+ model_preferences=self._parse_model_preferences(model_preferences),
234
238
  )
235
239
 
236
240
  return result.content
@@ -248,3 +252,45 @@ class Context:
248
252
  )
249
253
 
250
254
  return fastmcp.server.dependencies.get_http_request()
255
+
256
+ def _parse_model_preferences(
257
+ self, model_preferences: ModelPreferences | str | list[str] | None
258
+ ) -> ModelPreferences | None:
259
+ """
260
+ Validates and converts user input for model_preferences into a ModelPreferences object.
261
+
262
+ Args:
263
+ model_preferences (ModelPreferences | str | list[str] | None):
264
+ The model preferences to use. Accepts:
265
+ - ModelPreferences (returns as-is)
266
+ - str (single model hint)
267
+ - list[str] (multiple model hints)
268
+ - None (no preferences)
269
+
270
+ Returns:
271
+ ModelPreferences | None: The parsed ModelPreferences object, or None if not provided.
272
+
273
+ Raises:
274
+ ValueError: If the input is not a supported type or contains invalid values.
275
+ """
276
+ if model_preferences is None:
277
+ return None
278
+ elif isinstance(model_preferences, ModelPreferences):
279
+ return model_preferences
280
+ elif isinstance(model_preferences, str):
281
+ # Single model hint
282
+ return ModelPreferences(hints=[ModelHint(name=model_preferences)])
283
+ elif isinstance(model_preferences, list):
284
+ # List of model hints (strings)
285
+ if not all(isinstance(h, str) for h in model_preferences):
286
+ raise ValueError(
287
+ "All elements of model_preferences list must be"
288
+ " strings (model name hints)."
289
+ )
290
+ return ModelPreferences(
291
+ hints=[ModelHint(name=h) for h in model_preferences]
292
+ )
293
+ else:
294
+ raise ValueError(
295
+ "model_preferences must be one of: ModelPreferences, str, list[str], or None."
296
+ )
fastmcp/server/http.py CHANGED
@@ -241,6 +241,7 @@ def create_sse_app(
241
241
  # Add custom routes with lowest precedence
242
242
  if routes:
243
243
  server_routes.extend(routes)
244
+ server_routes.extend(server._additional_http_routes)
244
245
 
245
246
  # Add middleware
246
247
  if middleware:
@@ -306,7 +307,29 @@ def create_streamable_http_app(
306
307
  async def handle_streamable_http(
307
308
  scope: Scope, receive: Receive, send: Send
308
309
  ) -> None:
309
- await session_manager.handle_request(scope, receive, send)
310
+ try:
311
+ await session_manager.handle_request(scope, receive, send)
312
+ except RuntimeError as e:
313
+ if str(e) == "Task group is not initialized. Make sure to use run().":
314
+ logger.error(
315
+ f"Original RuntimeError from mcp library: {e}", exc_info=True
316
+ )
317
+ new_error_message = (
318
+ "FastMCP's StreamableHTTPSessionManager task group was not initialized. "
319
+ "This commonly occurs when the FastMCP application's lifespan is not "
320
+ "passed to the parent ASGI application (e.g., FastAPI or Starlette). "
321
+ "Please ensure you are setting `lifespan=mcp_app.lifespan` in your "
322
+ "parent app's constructor, where `mcp_app` is the application instance "
323
+ "returned by `fastmcp_instance.http_app()`. \\n"
324
+ "For more details, see the FastMCP ASGI integration documentation: "
325
+ "https://gofastmcp.com/deployment/asgi"
326
+ )
327
+ # Raise a new RuntimeError that includes the original error's message
328
+ # for full context, but leads with the more helpful guidance.
329
+ raise RuntimeError(f"{new_error_message}\\nOriginal error: {e}") from e
330
+ else:
331
+ # Re-raise other RuntimeErrors if they don't match the specific message
332
+ raise
310
333
 
311
334
  # Get auth middleware and routes
312
335
  auth_middleware, auth_routes, required_scopes = setup_auth_middleware_and_routes(
@@ -337,6 +360,7 @@ def create_streamable_http_app(
337
360
  # Add custom routes with lowest precedence
338
361
  if routes:
339
362
  server_routes.extend(routes)
363
+ server_routes.extend(server._additional_http_routes)
340
364
 
341
365
  # Add middleware
342
366
  if middleware: