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.
fastmcp/settings.py CHANGED
@@ -29,6 +29,17 @@ class Settings(BaseSettings):
29
29
 
30
30
  test_mode: bool = False
31
31
  log_level: LOG_LEVEL = "INFO"
32
+ enable_rich_tracebacks: Annotated[
33
+ bool,
34
+ Field(
35
+ description=inspect.cleandoc(
36
+ """
37
+ If True, will use rich tracebacks for logging.
38
+ """
39
+ )
40
+ ),
41
+ ] = True
42
+
32
43
  client_raise_first_exceptiongroup_error: Annotated[
33
44
  bool,
34
45
  Field(
@@ -44,6 +55,21 @@ class Settings(BaseSettings):
44
55
  ),
45
56
  ),
46
57
  ] = True
58
+
59
+ resource_prefix_format: Annotated[
60
+ Literal["protocol", "path"],
61
+ Field(
62
+ default="path",
63
+ description=inspect.cleandoc(
64
+ """
65
+ When perfixing a resource URI, either use path formatting (resource://prefix/path)
66
+ or protocol formatting (prefix+resource://path). Protocol formatting was the default in FastMCP < 2.4;
67
+ path formatting is current default.
68
+ """
69
+ ),
70
+ ),
71
+ ] = "path"
72
+
47
73
  tool_attempt_parse_json_args: Annotated[
48
74
  bool,
49
75
  Field(
@@ -66,7 +92,9 @@ class Settings(BaseSettings):
66
92
  """Finalize the settings."""
67
93
  from fastmcp.utilities.logging import configure_logging
68
94
 
69
- configure_logging(self.log_level)
95
+ configure_logging(
96
+ self.log_level, enable_rich_tracebacks=self.enable_rich_tracebacks
97
+ )
70
98
 
71
99
  return self
72
100
 
@@ -108,6 +136,23 @@ class ServerSettings(BaseSettings):
108
136
  # prompt settings
109
137
  on_duplicate_prompts: DuplicateBehavior = "warn"
110
138
 
139
+ # error handling
140
+ mask_error_details: Annotated[
141
+ bool,
142
+ Field(
143
+ default=False,
144
+ description=inspect.cleandoc(
145
+ """
146
+ If True, error details from user-supplied functions (tool, resource, prompt)
147
+ will be masked before being sent to clients. Only error messages from explicitly
148
+ raised ToolError, ResourceError, or PromptError will be included in responses.
149
+ If False (default), all error details will be included in responses, but prefixed
150
+ with appropriate context.
151
+ """
152
+ ),
153
+ ),
154
+ ] = False
155
+
111
156
  dependencies: Annotated[
112
157
  list[str],
113
158
  Field(
fastmcp/tools/tool.py CHANGED
@@ -69,13 +69,17 @@ class Tool(BaseModel):
69
69
  if param.kind == inspect.Parameter.VAR_KEYWORD:
70
70
  raise ValueError("Functions with **kwargs are not supported as tools")
71
71
 
72
- func_name = name or fn.__name__
72
+ func_name = name or getattr(fn, "__name__", None) or fn.__class__.__name__
73
73
 
74
74
  if func_name == "<lambda>":
75
75
  raise ValueError("You must provide a name for lambda functions")
76
76
 
77
77
  func_doc = description or fn.__doc__ or ""
78
78
 
79
+ # if the fn is a callable class, we need to get the __call__ method from here out
80
+ if not inspect.isroutine(fn):
81
+ fn = fn.__call__
82
+
79
83
  type_adapter = get_cached_typeadapter(fn)
80
84
  schema = type_adapter.json_schema()
81
85
 
@@ -23,9 +23,11 @@ class ToolManager:
23
23
  self,
24
24
  duplicate_behavior: DuplicateBehavior | None = None,
25
25
  serializer: Callable[[Any], str] | None = None,
26
+ mask_error_details: bool = False,
26
27
  ):
27
28
  self._tools: dict[str, Tool] = {}
28
29
  self._serializer = serializer
30
+ self.mask_error_details = mask_error_details
29
31
 
30
32
  # Default to "warn" if None is provided
31
33
  if duplicate_behavior is None:
@@ -124,7 +126,12 @@ class ToolManager:
124
126
  logger.exception(f"Error calling tool {key!r}: {e}")
125
127
  raise e
126
128
 
127
- # raise other exceptions as ToolErrors without revealing internal details
129
+ # Handle other exceptions
128
130
  except Exception as e:
129
131
  logger.exception(f"Error calling tool {key!r}: {e}")
130
- raise ToolError(f"Error calling tool {key!r}") from e
132
+ if self.mask_error_details:
133
+ # Mask internal details
134
+ raise ToolError(f"Error calling tool {key!r}") from e
135
+ else:
136
+ # Include original error details
137
+ raise ToolError(f"Error calling tool {key!r}: {e}") from e
@@ -22,6 +22,7 @@ def get_logger(name: str) -> logging.Logger:
22
22
  def configure_logging(
23
23
  level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
24
24
  logger: logging.Logger | None = None,
25
+ enable_rich_tracebacks: bool = True,
25
26
  ) -> None:
26
27
  """
27
28
  Configure logging for FastMCP.
@@ -30,11 +31,15 @@ def configure_logging(
30
31
  logger: the logger to configure
31
32
  level: the log level to use
32
33
  """
34
+
33
35
  if logger is None:
34
36
  logger = logging.getLogger("FastMCP")
35
37
 
36
38
  # Only configure the FastMCP logger namespace
37
- handler = RichHandler(console=Console(stderr=True), rich_tracebacks=True)
39
+ handler = RichHandler(
40
+ console=Console(stderr=True),
41
+ rich_tracebacks=enable_rich_tracebacks,
42
+ )
38
43
  formatter = logging.Formatter("%(message)s")
39
44
  handler.setFormatter(formatter)
40
45
 
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Literal
4
+ from urllib.parse import urlparse
5
+
6
+ from pydantic import AnyUrl, BaseModel, Field
7
+
8
+ if TYPE_CHECKING:
9
+ from fastmcp.client.transports import (
10
+ SSETransport,
11
+ StdioTransport,
12
+ StreamableHttpTransport,
13
+ )
14
+
15
+
16
+ def infer_transport_type_from_url(
17
+ url: str | AnyUrl,
18
+ ) -> Literal["streamable-http", "sse"]:
19
+ """
20
+ Infer the appropriate transport type from the given URL.
21
+ """
22
+ url = str(url)
23
+ if not url.startswith("http"):
24
+ raise ValueError(f"Invalid URL: {url}")
25
+
26
+ parsed_url = urlparse(url)
27
+ path = parsed_url.path
28
+
29
+ if "/sse/" in path or path.rstrip("/").endswith("/sse"):
30
+ return "sse"
31
+ else:
32
+ return "streamable-http"
33
+
34
+
35
+ class StdioMCPServer(BaseModel):
36
+ command: str
37
+ args: list[str] = Field(default_factory=list)
38
+ env: dict[str, Any] = Field(default_factory=dict)
39
+ cwd: str | None = None
40
+ transport: Literal["stdio"] = "stdio"
41
+
42
+ def to_transport(self) -> StdioTransport:
43
+ from fastmcp.client.transports import StdioTransport
44
+
45
+ return StdioTransport(
46
+ command=self.command,
47
+ args=self.args,
48
+ env=self.env,
49
+ cwd=self.cwd,
50
+ )
51
+
52
+
53
+ class RemoteMCPServer(BaseModel):
54
+ url: str
55
+ headers: dict[str, str] = Field(default_factory=dict)
56
+ transport: Literal["streamable-http", "sse", "http"] | None = None
57
+
58
+ def to_transport(self) -> StreamableHttpTransport | SSETransport:
59
+ from fastmcp.client.transports import SSETransport, StreamableHttpTransport
60
+
61
+ if self.transport is None:
62
+ transport = infer_transport_type_from_url(self.url)
63
+ else:
64
+ transport = self.transport
65
+
66
+ if transport == "sse":
67
+ return SSETransport(self.url, headers=self.headers)
68
+ else:
69
+ return StreamableHttpTransport(self.url, headers=self.headers)
70
+
71
+
72
+ class MCPConfig(BaseModel):
73
+ mcpServers: dict[str, StdioMCPServer | RemoteMCPServer]
74
+
75
+ @classmethod
76
+ def from_dict(cls, config: dict[str, Any]) -> MCPConfig:
77
+ return cls(mcpServers=config.get("mcpServers", config))