fastmcp 2.14.0__py3-none-any.whl → 2.14.2__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.
Files changed (41) hide show
  1. fastmcp/client/client.py +79 -12
  2. fastmcp/client/sampling/__init__.py +69 -0
  3. fastmcp/client/sampling/handlers/__init__.py +0 -0
  4. fastmcp/client/sampling/handlers/anthropic.py +387 -0
  5. fastmcp/client/sampling/handlers/openai.py +399 -0
  6. fastmcp/client/tasks.py +0 -63
  7. fastmcp/client/transports.py +35 -16
  8. fastmcp/experimental/sampling/handlers/__init__.py +5 -0
  9. fastmcp/experimental/sampling/handlers/openai.py +4 -169
  10. fastmcp/prompts/prompt.py +5 -5
  11. fastmcp/prompts/prompt_manager.py +3 -4
  12. fastmcp/resources/resource.py +4 -4
  13. fastmcp/resources/resource_manager.py +9 -14
  14. fastmcp/resources/template.py +5 -5
  15. fastmcp/server/auth/auth.py +20 -5
  16. fastmcp/server/auth/oauth_proxy.py +73 -15
  17. fastmcp/server/auth/providers/supabase.py +11 -6
  18. fastmcp/server/context.py +448 -113
  19. fastmcp/server/dependencies.py +5 -0
  20. fastmcp/server/elicitation.py +7 -3
  21. fastmcp/server/middleware/error_handling.py +1 -1
  22. fastmcp/server/openapi/components.py +2 -4
  23. fastmcp/server/proxy.py +3 -3
  24. fastmcp/server/sampling/__init__.py +10 -0
  25. fastmcp/server/sampling/run.py +301 -0
  26. fastmcp/server/sampling/sampling_tool.py +108 -0
  27. fastmcp/server/server.py +84 -78
  28. fastmcp/server/tasks/converters.py +2 -1
  29. fastmcp/tools/tool.py +8 -6
  30. fastmcp/tools/tool_manager.py +5 -7
  31. fastmcp/utilities/cli.py +23 -43
  32. fastmcp/utilities/json_schema.py +40 -0
  33. fastmcp/utilities/openapi/schemas.py +4 -4
  34. {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/METADATA +8 -3
  35. {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/RECORD +38 -34
  36. fastmcp/client/sampling.py +0 -56
  37. fastmcp/experimental/sampling/handlers/base.py +0 -21
  38. fastmcp/server/sampling/handler.py +0 -19
  39. {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/WHEEL +0 -0
  40. {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/entry_points.txt +0 -0
  41. {fastmcp-2.14.0.dist-info → fastmcp-2.14.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py CHANGED
@@ -46,10 +46,10 @@ from mcp.types import (
46
46
  GetPromptResult,
47
47
  ToolAnnotations,
48
48
  )
49
- from mcp.types import Prompt as MCPPrompt
50
- from mcp.types import Resource as MCPResource
51
- from mcp.types import ResourceTemplate as MCPResourceTemplate
52
- from mcp.types import Tool as MCPTool
49
+ from mcp.types import Prompt as SDKPrompt
50
+ from mcp.types import Resource as SDKResource
51
+ from mcp.types import ResourceTemplate as SDKResourceTemplate
52
+ from mcp.types import Tool as SDKTool
53
53
  from pydantic import AnyUrl
54
54
  from starlette.middleware import Middleware as ASGIMiddleware
55
55
  from starlette.requests import Request
@@ -94,34 +94,17 @@ from fastmcp.utilities.types import NotSet, NotSetT
94
94
  if TYPE_CHECKING:
95
95
  from fastmcp.client import Client
96
96
  from fastmcp.client.client import FastMCP1Server
97
+ from fastmcp.client.sampling import SamplingHandler
97
98
  from fastmcp.client.transports import ClientTransport, ClientTransportT
98
99
  from fastmcp.server.openapi import ComponentFn as OpenAPIComponentFn
99
100
  from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
100
101
  from fastmcp.server.openapi import RouteMapFn as OpenAPIRouteMapFn
101
102
  from fastmcp.server.proxy import FastMCPProxy
102
- from fastmcp.server.sampling.handler import ServerSamplingHandler
103
103
  from fastmcp.tools.tool import ToolResultSerializerType
104
104
 
105
105
  logger = get_logger(__name__)
106
106
 
107
107
 
108
- def _create_named_fn_wrapper(fn: Callable[..., Any], name: str) -> Callable[..., Any]:
109
- """Create a wrapper function with a custom __name__ for Docket registration.
110
-
111
- Docket uses fn.__name__ as the key for function registration and lookup.
112
- When mounting servers, we need unique names to avoid collisions between
113
- mounted servers that have identically-named functions.
114
- """
115
- import functools
116
-
117
- @functools.wraps(fn)
118
- async def wrapper(*args: Any, **kwargs: Any) -> Any:
119
- return await fn(*args, **kwargs)
120
-
121
- wrapper.__name__ = name
122
- return wrapper
123
-
124
-
125
108
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
126
109
  Transport = Literal["stdio", "http", "sse", "streamable-http"]
127
110
 
@@ -208,7 +191,7 @@ class FastMCP(Generic[LifespanResultT]):
208
191
  streamable_http_path: str | None = None,
209
192
  json_response: bool | None = None,
210
193
  stateless_http: bool | None = None,
211
- sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None,
194
+ sampling_handler: SamplingHandler | None = None,
212
195
  sampling_handler_behavior: Literal["always", "fallback"] | None = None,
213
196
  ):
214
197
  # Resolve server default for background task support
@@ -288,9 +271,7 @@ class FastMCP(Generic[LifespanResultT]):
288
271
  # Set up MCP protocol handlers
289
272
  self._setup_handlers()
290
273
 
291
- self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = (
292
- sampling_handler
293
- )
274
+ self.sampling_handler: SamplingHandler | None = sampling_handler
294
275
  self.sampling_handler_behavior: Literal["always", "fallback"] = (
295
276
  sampling_handler_behavior or "fallback"
296
277
  )
@@ -439,7 +420,7 @@ class FastMCP(Generic[LifespanResultT]):
439
420
  isinstance(tool, FunctionTool)
440
421
  and tool.task_config.mode != "forbidden"
441
422
  ):
442
- docket.register(tool.fn)
423
+ docket.register(tool.fn, names=[tool.key])
443
424
 
444
425
  for prompt in self._prompt_manager._prompts.values():
445
426
  if (
@@ -447,27 +428,30 @@ class FastMCP(Generic[LifespanResultT]):
447
428
  and prompt.task_config.mode != "forbidden"
448
429
  ):
449
430
  # task execution requires async fn (validated at creation time)
450
- docket.register(cast(Callable[..., Awaitable[Any]], prompt.fn))
431
+ docket.register(
432
+ cast(Callable[..., Awaitable[Any]], prompt.fn),
433
+ names=[prompt.key],
434
+ )
451
435
 
452
436
  for resource in self._resource_manager._resources.values():
453
437
  if (
454
438
  isinstance(resource, FunctionResource)
455
439
  and resource.task_config.mode != "forbidden"
456
440
  ):
457
- docket.register(resource.fn)
441
+ docket.register(resource.fn, names=[resource.name])
458
442
 
459
443
  for template in self._resource_manager._templates.values():
460
444
  if (
461
445
  isinstance(template, FunctionResourceTemplate)
462
446
  and template.task_config.mode != "forbidden"
463
447
  ):
464
- docket.register(template.fn)
448
+ docket.register(template.fn, names=[template.name])
465
449
 
466
450
  # Also register functions from mounted servers so tasks can
467
451
  # execute in the parent's Docket context
468
452
  for mounted in self._mounted_servers:
469
453
  await self._register_mounted_server_functions(
470
- mounted.server, docket, mounted.prefix
454
+ mounted.server, docket, mounted.prefix, mounted.tool_names
471
455
  )
472
456
 
473
457
  # Set Docket in ContextVar so CurrentDocket can access it
@@ -491,12 +475,9 @@ class FastMCP(Generic[LifespanResultT]):
491
475
  try:
492
476
  yield
493
477
  finally:
494
- # Cancel worker task on exit with timeout to prevent hanging
495
478
  worker_task.cancel()
496
- with suppress(
497
- asyncio.CancelledError, asyncio.TimeoutError
498
- ):
499
- await asyncio.wait_for(worker_task, timeout=2.0)
479
+ with suppress(asyncio.CancelledError):
480
+ await worker_task
500
481
  finally:
501
482
  _current_worker.reset(worker_token)
502
483
  finally:
@@ -509,7 +490,11 @@ class FastMCP(Generic[LifespanResultT]):
509
490
  _current_server.reset(server_token)
510
491
 
511
492
  async def _register_mounted_server_functions(
512
- self, server: FastMCP, docket: Docket, prefix: str | None
493
+ self,
494
+ server: FastMCP,
495
+ docket: Docket,
496
+ prefix: str | None,
497
+ tool_names: dict[str, str] | None = None,
513
498
  ) -> None:
514
499
  """Register task-enabled functions from a mounted server with Docket.
515
500
 
@@ -521,14 +506,19 @@ class FastMCP(Generic[LifespanResultT]):
521
506
  docket: The Docket instance to register with
522
507
  prefix: The mount prefix to prepend to function names (matches
523
508
  client-facing tool/prompt names)
509
+ tool_names: Optional mapping of original tool names to custom names
524
510
  """
525
511
  # Register tools with prefixed names to avoid collisions
526
512
  for tool in server._tool_manager._tools.values():
527
513
  if isinstance(tool, FunctionTool) and tool.task_config.mode != "forbidden":
528
- # Use same naming as client-facing tool keys
529
- fn_name = f"{prefix}_{tool.key}" if prefix else tool.key
530
- named_fn = _create_named_fn_wrapper(tool.fn, fn_name)
531
- docket.register(named_fn)
514
+ # Apply tool_names override first, then prefix (matches get_tools logic)
515
+ if tool_names and tool.key in tool_names:
516
+ fn_name = tool_names[tool.key]
517
+ elif prefix:
518
+ fn_name = f"{prefix}_{tool.key}"
519
+ else:
520
+ fn_name = tool.key
521
+ docket.register(tool.fn, names=[fn_name])
532
522
 
533
523
  # Register prompts with prefixed names
534
524
  for prompt in server._prompt_manager._prompts.values():
@@ -537,10 +527,10 @@ class FastMCP(Generic[LifespanResultT]):
537
527
  and prompt.task_config.mode != "forbidden"
538
528
  ):
539
529
  fn_name = f"{prefix}_{prompt.key}" if prefix else prompt.key
540
- named_fn = _create_named_fn_wrapper(
541
- cast(Callable[..., Awaitable[Any]], prompt.fn), fn_name
530
+ docket.register(
531
+ cast(Callable[..., Awaitable[Any]], prompt.fn),
532
+ names=[fn_name],
542
533
  )
543
- docket.register(named_fn)
544
534
 
545
535
  # Register resources with prefixed names (use name, not key/URI)
546
536
  for resource in server._resource_manager._resources.values():
@@ -549,8 +539,7 @@ class FastMCP(Generic[LifespanResultT]):
549
539
  and resource.task_config.mode != "forbidden"
550
540
  ):
551
541
  fn_name = f"{prefix}_{resource.name}" if prefix else resource.name
552
- named_fn = _create_named_fn_wrapper(resource.fn, fn_name)
553
- docket.register(named_fn)
542
+ docket.register(resource.fn, names=[fn_name])
554
543
 
555
544
  # Register resource templates with prefixed names (use name, not key/URI)
556
545
  for template in server._resource_manager._templates.values():
@@ -559,8 +548,7 @@ class FastMCP(Generic[LifespanResultT]):
559
548
  and template.task_config.mode != "forbidden"
560
549
  ):
561
550
  fn_name = f"{prefix}_{template.name}" if prefix else template.name
562
- named_fn = _create_named_fn_wrapper(template.fn, fn_name)
563
- docket.register(named_fn)
551
+ docket.register(template.fn, names=[fn_name])
564
552
 
565
553
  # Recursively register from nested mounted servers with accumulated prefix
566
554
  for nested in server._mounted_servers:
@@ -570,7 +558,7 @@ class FastMCP(Generic[LifespanResultT]):
570
558
  else (prefix or nested.prefix)
571
559
  )
572
560
  await self._register_mounted_server_functions(
573
- nested.server, docket, nested_prefix
561
+ nested.server, docket, nested_prefix, nested.tool_names
574
562
  )
575
563
 
576
564
  @asynccontextmanager
@@ -938,7 +926,13 @@ class FastMCP(Generic[LifespanResultT]):
938
926
  try:
939
927
  child_tools = await mounted.server.get_tools()
940
928
  for key, tool in child_tools.items():
941
- new_key = f"{mounted.prefix}_{key}" if mounted.prefix else key
929
+ # Check for manual override first, then apply prefix
930
+ if mounted.tool_names and key in mounted.tool_names:
931
+ new_key = mounted.tool_names[key]
932
+ elif mounted.prefix:
933
+ new_key = f"{mounted.prefix}_{key}"
934
+ else:
935
+ new_key = key
942
936
  all_tools[new_key] = tool.model_copy(key=new_key)
943
937
  except Exception as e:
944
938
  logger.warning(
@@ -1153,7 +1147,7 @@ class FastMCP(Generic[LifespanResultT]):
1153
1147
 
1154
1148
  return routes
1155
1149
 
1156
- async def _list_tools_mcp(self) -> list[MCPTool]:
1150
+ async def _list_tools_mcp(self) -> list[SDKTool]:
1157
1151
  """
1158
1152
  List all available tools, in the format expected by the low-level MCP
1159
1153
  server.
@@ -1218,9 +1212,15 @@ class FastMCP(Generic[LifespanResultT]):
1218
1212
  if not self._should_enable_component(tool):
1219
1213
  continue
1220
1214
 
1221
- key = tool.key
1222
- if mounted.prefix:
1215
+ # Check for manual override first, then apply prefix
1216
+ if mounted.tool_names and tool.key in mounted.tool_names:
1217
+ key = mounted.tool_names[tool.key]
1218
+ elif mounted.prefix:
1223
1219
  key = f"{mounted.prefix}_{tool.key}"
1220
+ else:
1221
+ key = tool.key
1222
+
1223
+ if key != tool.key:
1224
1224
  tool = tool.model_copy(key=key)
1225
1225
  # Later mounted servers override earlier ones
1226
1226
  all_tools[key] = tool
@@ -1237,7 +1237,7 @@ class FastMCP(Generic[LifespanResultT]):
1237
1237
 
1238
1238
  return list(all_tools.values())
1239
1239
 
1240
- async def _list_resources_mcp(self) -> list[MCPResource]:
1240
+ async def _list_resources_mcp(self) -> list[SDKResource]:
1241
1241
  """
1242
1242
  List all available resources, in the format expected by the low-level MCP
1243
1243
  server.
@@ -1326,7 +1326,7 @@ class FastMCP(Generic[LifespanResultT]):
1326
1326
 
1327
1327
  return list(all_resources.values())
1328
1328
 
1329
- async def _list_resource_templates_mcp(self) -> list[MCPResourceTemplate]:
1329
+ async def _list_resource_templates_mcp(self) -> list[SDKResourceTemplate]:
1330
1330
  """
1331
1331
  List all available resource templates, in the format expected by the low-level MCP
1332
1332
  server.
@@ -1420,7 +1420,7 @@ class FastMCP(Generic[LifespanResultT]):
1420
1420
 
1421
1421
  return list(all_templates.values())
1422
1422
 
1423
- async def _list_prompts_mcp(self) -> list[MCPPrompt]:
1423
+ async def _list_prompts_mcp(self) -> list[SDKPrompt]:
1424
1424
  """
1425
1425
  List all available prompts, in the format expected by the low-level MCP
1426
1426
  server.
@@ -1490,9 +1490,13 @@ class FastMCP(Generic[LifespanResultT]):
1490
1490
  if not self._should_enable_component(prompt):
1491
1491
  continue
1492
1492
 
1493
- key = prompt.key
1493
+ # Apply prefix to prompt key
1494
1494
  if mounted.prefix:
1495
1495
  key = f"{mounted.prefix}_{prompt.key}"
1496
+ else:
1497
+ key = prompt.key
1498
+
1499
+ if key != prompt.key:
1496
1500
  prompt = prompt.model_copy(key=key)
1497
1501
  # Later mounted servers override earlier ones
1498
1502
  all_prompts[key] = prompt
@@ -1632,7 +1636,20 @@ class FastMCP(Generic[LifespanResultT]):
1632
1636
  # Try mounted servers in reverse order (later wins)
1633
1637
  for mounted in reversed(self._mounted_servers):
1634
1638
  try_name = tool_name
1635
- if mounted.prefix:
1639
+
1640
+ # First check if tool_name is an overridden name (reverse lookup)
1641
+ if mounted.tool_names:
1642
+ for orig_key, override_name in mounted.tool_names.items():
1643
+ if override_name == tool_name:
1644
+ try_name = orig_key
1645
+ break
1646
+ else:
1647
+ # Not an override, try standard prefix stripping
1648
+ if mounted.prefix:
1649
+ if not tool_name.startswith(f"{mounted.prefix}_"):
1650
+ continue
1651
+ try_name = tool_name[len(mounted.prefix) + 1 :]
1652
+ elif mounted.prefix:
1636
1653
  if not tool_name.startswith(f"{mounted.prefix}_"):
1637
1654
  continue
1638
1655
  try_name = tool_name[len(mounted.prefix) + 1 :]
@@ -1968,8 +1985,7 @@ class FastMCP(Generic[LifespanResultT]):
1968
1985
  output_schema: Optional JSON schema for the tool's output
1969
1986
  annotations: Optional annotations about the tool's behavior
1970
1987
  exclude_args: Optional list of argument names to exclude from the tool schema.
1971
- Note: `exclude_args` will be deprecated in FastMCP 2.14 in favor of dependency
1972
- injection with `Depends()` for better lifecycle management.
1988
+ Deprecated: Use `Depends()` for dependency injection instead.
1973
1989
  meta: Optional meta information about the tool
1974
1990
  enabled: Optional boolean to enable or disable the tool
1975
1991
 
@@ -2480,10 +2496,7 @@ class FastMCP(Generic[LifespanResultT]):
2480
2496
  """
2481
2497
  # Display server banner
2482
2498
  if show_banner:
2483
- log_server_banner(
2484
- server=self,
2485
- transport="stdio",
2486
- )
2499
+ log_server_banner(server=self)
2487
2500
 
2488
2501
  with temporary_log_level(log_level):
2489
2502
  async with self._lifespan_manager():
@@ -2546,22 +2559,9 @@ class FastMCP(Generic[LifespanResultT]):
2546
2559
  stateless_http=stateless_http,
2547
2560
  )
2548
2561
 
2549
- # Get the path for the server URL
2550
- server_path = (
2551
- app.state.path.lstrip("/")
2552
- if hasattr(app, "state") and hasattr(app.state, "path")
2553
- else path or ""
2554
- )
2555
-
2556
2562
  # Display server banner
2557
2563
  if show_banner:
2558
- log_server_banner(
2559
- server=self,
2560
- transport=transport,
2561
- host=host,
2562
- port=port,
2563
- path=server_path,
2564
- )
2564
+ log_server_banner(server=self)
2565
2565
  uvicorn_config_from_user = uvicorn_config or {}
2566
2566
 
2567
2567
  config_kwargs: dict[str, Any] = {
@@ -2651,6 +2651,7 @@ class FastMCP(Generic[LifespanResultT]):
2651
2651
  server: FastMCP[LifespanResultT],
2652
2652
  prefix: str | None = None,
2653
2653
  as_proxy: bool | None = None,
2654
+ tool_names: dict[str, str] | None = None,
2654
2655
  ) -> None:
2655
2656
  """Mount another FastMCP server on this server with an optional prefix.
2656
2657
 
@@ -2695,6 +2696,9 @@ class FastMCP(Generic[LifespanResultT]):
2695
2696
  as_proxy: Whether to treat the mounted server as a proxy. If None (default),
2696
2697
  automatically determined based on whether the server has a custom lifespan
2697
2698
  (True if it has a custom lifespan, False otherwise).
2699
+ tool_names: Optional mapping of original tool names to custom names. Use this
2700
+ to override prefixed names. Keys are the original tool names from the
2701
+ mounted server.
2698
2702
  """
2699
2703
  from fastmcp.server.proxy import FastMCPProxy
2700
2704
 
@@ -2715,6 +2719,7 @@ class FastMCP(Generic[LifespanResultT]):
2715
2719
  mounted_server = MountedServer(
2716
2720
  prefix=prefix,
2717
2721
  server=server,
2722
+ tool_names=tool_names,
2718
2723
  )
2719
2724
  self._mounted_servers.append(mounted_server)
2720
2725
 
@@ -2974,6 +2979,7 @@ class FastMCP(Generic[LifespanResultT]):
2974
2979
  class MountedServer:
2975
2980
  prefix: str | None
2976
2981
  server: FastMCP[Any]
2982
+ tool_names: dict[str, str] | None = None
2977
2983
 
2978
2984
 
2979
2985
  def add_resource_prefix(uri: str, prefix: str) -> str:
@@ -123,7 +123,8 @@ async def convert_prompt_result(
123
123
  messages: list[mcp.types.PromptMessage] = []
124
124
  for msg in raw_value:
125
125
  if isinstance(msg, PromptMessage):
126
- messages.append(msg.to_mcp())
126
+ # PromptMessage is imported from mcp.types - use directly
127
+ messages.append(msg)
127
128
  elif isinstance(msg, str):
128
129
  messages.append(
129
130
  mcp.types.PromptMessage(
fastmcp/tools/tool.py CHANGED
@@ -32,7 +32,7 @@ import fastmcp
32
32
  from fastmcp.server.dependencies import get_context, without_injected_parameters
33
33
  from fastmcp.server.tasks.config import TaskConfig
34
34
  from fastmcp.utilities.components import FastMCPComponent
35
- from fastmcp.utilities.json_schema import compress_schema
35
+ from fastmcp.utilities.json_schema import compress_schema, resolve_root_ref
36
36
  from fastmcp.utilities.logging import get_logger
37
37
  from fastmcp.utilities.types import (
38
38
  Audio,
@@ -321,11 +321,9 @@ class FunctionTool(Tool):
321
321
  """Create a Tool from a function."""
322
322
  if exclude_args and fastmcp.settings.deprecation_warnings:
323
323
  warnings.warn(
324
- "The `exclude_args` parameter will be deprecated in FastMCP 2.14. "
325
- "We recommend using dependency injection with `Depends()` instead, which provides "
326
- "better lifecycle management and is more explicit. "
327
- "`exclude_args` will continue to work until then. "
328
- "See https://gofastmcp.com/docs/servers/tools for examples.",
324
+ "The `exclude_args` parameter is deprecated as of FastMCP 2.14. "
325
+ "Use dependency injection with `Depends()` instead for better lifecycle management. "
326
+ "See https://gofastmcp.com/servers/dependencies for examples.",
329
327
  DeprecationWarning,
330
328
  stacklevel=2,
331
329
  )
@@ -561,6 +559,10 @@ class ParsedFunction:
561
559
 
562
560
  output_schema = compress_schema(output_schema, prune_titles=True)
563
561
 
562
+ # Resolve root-level $ref to meet MCP spec requirement for type: object
563
+ # Self-referential Pydantic models generate schemas with $ref at root
564
+ output_schema = resolve_root_ref(output_schema)
565
+
564
566
  except PydanticSchemaGenerationError as e:
565
567
  if "_UnserializableType" not in str(e):
566
568
  logger.debug(f"Unable to generate schema for type {output_type!r}")
@@ -8,7 +8,7 @@ from mcp.types import ToolAnnotations
8
8
  from pydantic import ValidationError
9
9
 
10
10
  from fastmcp import settings
11
- from fastmcp.exceptions import NotFoundError, ToolError
11
+ from fastmcp.exceptions import FastMCPError, NotFoundError, ToolError
12
12
  from fastmcp.settings import DuplicateBehavior
13
13
  from fastmcp.tools.tool import Tool, ToolResult
14
14
  from fastmcp.tools.tool_transform import (
@@ -158,12 +158,10 @@ class ToolManager:
158
158
  tool = await self.get_tool(key)
159
159
  try:
160
160
  return await tool.run(arguments)
161
- except ValidationError as e:
162
- logger.exception(f"Error validating tool {key!r}: {e}")
163
- raise e
164
- except ToolError as e:
165
- logger.exception(f"Error calling tool {key!r}")
166
- raise e
161
+ except FastMCPError:
162
+ raise
163
+ except ValidationError:
164
+ raise
167
165
  except Exception as e:
168
166
  logger.exception(f"Error calling tool {key!r}")
169
167
  if self.mask_error_details:
fastmcp/utilities/cli.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import os
5
5
  from pathlib import Path
6
- from typing import TYPE_CHECKING, Any, Literal
6
+ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  from pydantic import ValidationError
9
9
  from rich.align import Align
@@ -197,23 +197,8 @@ LOGO_ASCII_4 = (
197
197
  )
198
198
 
199
199
 
200
- def log_server_banner(
201
- server: FastMCP[Any],
202
- transport: Literal["stdio", "http", "sse", "streamable-http"],
203
- *,
204
- host: str | None = None,
205
- port: int | None = None,
206
- path: str | None = None,
207
- ) -> None:
208
- """Creates and logs a formatted banner with server information and logo.
209
-
210
- Args:
211
- transport: The transport protocol being used
212
- server_name: Optional server name to display
213
- host: Host address (for HTTP transports)
214
- port: Port number (for HTTP transports)
215
- path: Server path (for HTTP transports)
216
- """
200
+ def log_server_banner(server: FastMCP[Any]) -> None:
201
+ """Creates and logs a formatted banner with server information and logo."""
217
202
 
218
203
  # Create the logo text
219
204
  # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
@@ -228,39 +213,34 @@ def log_server_banner(
228
213
  info_table.add_column(style="cyan", justify="left") # Label column
229
214
  info_table.add_column(style="dim", justify="left") # Value column
230
215
 
231
- match transport:
232
- case "http" | "streamable-http":
233
- display_transport = "HTTP"
234
- case "sse":
235
- display_transport = "SSE"
236
- case "stdio":
237
- display_transport = "STDIO"
238
-
239
- info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
240
- info_table.add_row("📦", "Transport:", display_transport)
241
-
242
- # Show connection info based on transport
243
- if transport in ("http", "streamable-http", "sse") and host and port:
244
- server_url = f"http://{host}:{port}"
245
- if path:
246
- server_url += f"/{path.lstrip('/')}"
247
- info_table.add_row("🔗", "Server URL:", server_url)
248
-
249
- # Add documentation link
250
- info_table.add_row("", "", "")
251
- info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
252
- info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
216
+ info_table.add_row("🖥", "Server:", Text(server.name, style="dim"))
217
+ info_table.add_row("🚀", "Deploy free:", "https://fastmcp.cloud")
253
218
 
254
219
  # Create panel with logo, title, and information using Group
220
+ docs_url = Text("https://gofastmcp.com", style="dim")
255
221
  panel_content = Group(
222
+ "",
256
223
  Align.center(logo_text),
257
224
  "",
258
- Align.center(title_text),
259
225
  "",
226
+ Align.center(title_text),
227
+ Align.center(docs_url),
260
228
  "",
261
229
  Align.center(info_table),
262
230
  )
263
231
 
232
+ # v3 notice banner (shown below main panel)
233
+ v3_line1 = Text("✨ FastMCP 3.0 is coming!", style="bold")
234
+ v3_line2 = Text(
235
+ "Pin fastmcp<3 in production, then upgrade when you're ready.", style="dim"
236
+ )
237
+ v3_notice = Panel(
238
+ Group(Align.center(v3_line1), Align.center(v3_line2)),
239
+ border_style="blue",
240
+ padding=(0, 2),
241
+ width=80,
242
+ )
243
+
264
244
  panel = Panel(
265
245
  panel_content,
266
246
  border_style="dim",
@@ -270,5 +250,5 @@ def log_server_banner(
270
250
  )
271
251
 
272
252
  console = Console(stderr=True)
273
- # Center the panel itself
274
- console.print(Group("\n", Align.center(panel), "\n"))
253
+ # Center both panels
254
+ console.print(Group("\n", Align.center(panel), Align.center(v3_notice), "\n"))
@@ -1,6 +1,46 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections import defaultdict
4
+ from typing import Any
5
+
6
+
7
+ def resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]:
8
+ """Resolve $ref at root level to meet MCP spec requirements.
9
+
10
+ MCP specification requires outputSchema to have "type": "object" at the root level.
11
+ When Pydantic generates schemas for self-referential models, it uses $ref at the
12
+ root level pointing to $defs. This function resolves such references by inlining
13
+ the referenced definition while preserving $defs for nested references.
14
+
15
+ Args:
16
+ schema: JSON schema dict that may have $ref at root level
17
+
18
+ Returns:
19
+ A new schema dict with root-level $ref resolved, or the original schema
20
+ if no resolution is needed
21
+
22
+ Example:
23
+ >>> schema = {
24
+ ... "$defs": {"Node": {"type": "object", "properties": {...}}},
25
+ ... "$ref": "#/$defs/Node"
26
+ ... }
27
+ >>> resolved = resolve_root_ref(schema)
28
+ >>> # Result: {"type": "object", "properties": {...}, "$defs": {...}}
29
+ """
30
+ # Only resolve if we have $ref at root level with $defs but no explicit type
31
+ if "$ref" in schema and "$defs" in schema and "type" not in schema:
32
+ ref = schema["$ref"]
33
+ # Only handle local $defs references
34
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
35
+ def_name = ref.split("/")[-1]
36
+ defs = schema["$defs"]
37
+ if def_name in defs:
38
+ # Create a new schema by copying the referenced definition
39
+ resolved = dict(defs[def_name])
40
+ # Preserve $defs for nested references (other fields may still use them)
41
+ resolved["$defs"] = defs
42
+ return resolved
43
+ return schema
4
44
 
5
45
 
6
46
  def _prune_param(schema: dict, param: str) -> dict:
@@ -539,9 +539,9 @@ def extract_output_schema_from_responses(
539
539
  # Replace $ref with the actual schema definition
540
540
  output_schema = _replace_ref_with_defs(schema_definitions[schema_name])
541
541
 
542
- # Convert OpenAPI schema to JSON Schema format
543
- # Only needed for OpenAPI 3.0 - 3.1 uses standard JSON Schema null types
544
- if openapi_version and openapi_version.startswith("3.0"):
542
+ if openapi_version and openapi_version.startswith("3"):
543
+ # Convert OpenAPI 3.x schema to JSON Schema format for proper handling
544
+ # of constructs like oneOf, anyOf, and nullable fields
545
545
  from .json_schema_converter import convert_openapi_schema_to_json_schema
546
546
 
547
547
  output_schema = convert_openapi_schema_to_json_schema(
@@ -570,7 +570,7 @@ def extract_output_schema_from_responses(
570
570
  processed_defs[name] = _replace_ref_with_defs(schema)
571
571
 
572
572
  # Convert OpenAPI schema definitions to JSON Schema format if needed
573
- if openapi_version and openapi_version.startswith("3.0"):
573
+ if openapi_version and openapi_version.startswith("3"):
574
574
  from .json_schema_converter import convert_openapi_schema_to_json_schema
575
575
 
576
576
  for def_name in list(processed_defs.keys()):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.14.0
3
+ Version: 2.14.2
4
4
  Summary: The fast, Pythonic way to build MCP servers and clients.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -23,17 +23,19 @@ Requires-Dist: cyclopts>=4.0.0
23
23
  Requires-Dist: exceptiongroup>=1.2.2
24
24
  Requires-Dist: httpx>=0.28.1
25
25
  Requires-Dist: jsonschema-path>=0.3.4
26
- Requires-Dist: mcp>=1.23.1
26
+ Requires-Dist: mcp<2.0,>=1.24.0
27
27
  Requires-Dist: openapi-pydantic>=0.5.1
28
28
  Requires-Dist: platformdirs>=4.0.0
29
29
  Requires-Dist: py-key-value-aio[disk,keyring,memory]<0.4.0,>=0.3.0
30
30
  Requires-Dist: pydantic[email]>=2.11.7
31
- Requires-Dist: pydocket>=0.15.2
31
+ Requires-Dist: pydocket>=0.16.3
32
32
  Requires-Dist: pyperclip>=1.9.0
33
33
  Requires-Dist: python-dotenv>=1.1.0
34
34
  Requires-Dist: rich>=13.9.4
35
35
  Requires-Dist: uvicorn>=0.35
36
36
  Requires-Dist: websockets>=15.0.1
37
+ Provides-Extra: anthropic
38
+ Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
37
39
  Provides-Extra: openai
38
40
  Requires-Dist: openai>=1.102.0; extra == 'openai'
39
41
  Description-Content-Type: text/markdown
@@ -73,6 +75,9 @@ Description-Content-Type: text/markdown
73
75
  >
74
76
  > **For production MCP applications, install FastMCP:** `pip install fastmcp`
75
77
 
78
+ > [!Important]
79
+ > FastMCP 3.0 is in development and may include breaking changes. To avoid unexpected issues, pin your dependency to v2: `fastmcp<3`
80
+
76
81
  ---
77
82
 
78
83
  **FastMCP is the standard framework for building MCP applications**, providing the fastest path from idea to production.