fastmcp 2.12.3__py3-none-any.whl → 2.12.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.
Files changed (34) hide show
  1. fastmcp/cli/install/gemini_cli.py +0 -1
  2. fastmcp/cli/run.py +2 -2
  3. fastmcp/client/auth/oauth.py +49 -36
  4. fastmcp/client/client.py +12 -2
  5. fastmcp/contrib/mcp_mixin/README.md +2 -2
  6. fastmcp/experimental/utilities/openapi/schemas.py +31 -5
  7. fastmcp/server/auth/auth.py +3 -3
  8. fastmcp/server/auth/oauth_proxy.py +42 -12
  9. fastmcp/server/auth/oidc_proxy.py +348 -0
  10. fastmcp/server/auth/providers/auth0.py +174 -0
  11. fastmcp/server/auth/providers/aws.py +237 -0
  12. fastmcp/server/auth/providers/azure.py +6 -2
  13. fastmcp/server/auth/providers/descope.py +172 -0
  14. fastmcp/server/auth/providers/github.py +6 -2
  15. fastmcp/server/auth/providers/google.py +6 -2
  16. fastmcp/server/auth/providers/workos.py +6 -2
  17. fastmcp/server/context.py +7 -6
  18. fastmcp/server/http.py +1 -1
  19. fastmcp/server/middleware/logging.py +147 -116
  20. fastmcp/server/middleware/middleware.py +3 -2
  21. fastmcp/server/openapi.py +5 -1
  22. fastmcp/server/server.py +36 -31
  23. fastmcp/settings.py +27 -5
  24. fastmcp/tools/tool.py +4 -2
  25. fastmcp/utilities/json_schema.py +18 -1
  26. fastmcp/utilities/logging.py +66 -4
  27. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +2 -1
  28. fastmcp/utilities/storage.py +204 -0
  29. fastmcp/utilities/tests.py +8 -6
  30. {fastmcp-2.12.3.dist-info → fastmcp-2.12.5.dist-info}/METADATA +121 -48
  31. {fastmcp-2.12.3.dist-info → fastmcp-2.12.5.dist-info}/RECORD +34 -29
  32. {fastmcp-2.12.3.dist-info → fastmcp-2.12.5.dist-info}/WHEEL +0 -0
  33. {fastmcp-2.12.3.dist-info → fastmcp-2.12.5.dist-info}/entry_points.txt +0 -0
  34. {fastmcp-2.12.3.dist-info → fastmcp-2.12.5.dist-info}/licenses/LICENSE +0 -0
@@ -16,7 +16,131 @@ def default_serializer(data: Any) -> str:
16
16
  return pydantic_core.to_json(data, fallback=str).decode()
17
17
 
18
18
 
19
- class LoggingMiddleware(Middleware):
19
+ class BaseLoggingMiddleware(Middleware):
20
+ """Base class for logging middleware."""
21
+
22
+ logger: Logger
23
+ log_level: int
24
+ include_payloads: bool
25
+ include_payload_length: bool
26
+ estimate_payload_tokens: bool
27
+ max_payload_length: int | None
28
+ methods: list[str] | None
29
+ structured_logging: bool
30
+ payload_serializer: Callable[[Any], str] | None
31
+
32
+ def _serialize_payload(self, context: MiddlewareContext[Any]) -> str:
33
+ payload: str
34
+
35
+ if not self.payload_serializer:
36
+ payload = default_serializer(context.message)
37
+ else:
38
+ try:
39
+ payload = self.payload_serializer(context.message)
40
+ except Exception as e:
41
+ self.logger.warning(
42
+ f"Failed to serialize payload due to {e}: {context.type} {context.method} {context.source}."
43
+ )
44
+ payload = default_serializer(context.message)
45
+
46
+ return payload
47
+
48
+ def _format_message(self, message: dict[str, str | int]) -> str:
49
+ """Format a message for logging."""
50
+ if self.structured_logging:
51
+ return json.dumps(message)
52
+ else:
53
+ return " ".join([f"{k}={v}" for k, v in message.items()])
54
+
55
+ def _get_timestamp_from_context(self, context: MiddlewareContext[Any]) -> str:
56
+ """Get a timestamp from the context."""
57
+ return context.timestamp.isoformat()
58
+
59
+ def _create_before_message(
60
+ self, context: MiddlewareContext[Any], event: str
61
+ ) -> dict[str, str | int]:
62
+ message = self._create_base_message(context, event)
63
+
64
+ if (
65
+ self.include_payloads
66
+ or self.include_payload_length
67
+ or self.estimate_payload_tokens
68
+ ):
69
+ payload = self._serialize_payload(context)
70
+
71
+ if self.include_payload_length or self.estimate_payload_tokens:
72
+ payload_length = len(payload)
73
+ payload_tokens = payload_length // 4
74
+ if self.estimate_payload_tokens:
75
+ message["payload_tokens"] = payload_tokens
76
+ if self.include_payload_length:
77
+ message["payload_length"] = payload_length
78
+
79
+ if self.max_payload_length and len(payload) > self.max_payload_length:
80
+ payload = payload[: self.max_payload_length] + "..."
81
+
82
+ if self.include_payloads:
83
+ message["payload"] = payload
84
+ message["payload_type"] = type(context.message).__name__
85
+
86
+ return message
87
+
88
+ def _create_after_message(
89
+ self, context: MiddlewareContext[Any], event: str
90
+ ) -> dict[str, str | int]:
91
+ return self._create_base_message(context, event)
92
+
93
+ def _create_base_message(
94
+ self,
95
+ context: MiddlewareContext[Any],
96
+ event: str,
97
+ ) -> dict[str, str | int]:
98
+ """Format a message for logging."""
99
+
100
+ parts: dict[str, str | int] = {
101
+ "event": event,
102
+ "timestamp": self._get_timestamp_from_context(context),
103
+ "method": context.method or "unknown",
104
+ "type": context.type,
105
+ "source": context.source,
106
+ }
107
+
108
+ return parts
109
+
110
+ async def on_message(
111
+ self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
112
+ ) -> Any:
113
+ """Log all messages."""
114
+
115
+ if self.methods and context.method not in self.methods:
116
+ return await call_next(context)
117
+
118
+ request_start_log_message = self._create_before_message(
119
+ context, "request_start"
120
+ )
121
+
122
+ formatted_message = self._format_message(request_start_log_message)
123
+ self.logger.log(self.log_level, f"Processing message: {formatted_message}")
124
+
125
+ try:
126
+ result = await call_next(context)
127
+
128
+ request_success_log_message = self._create_after_message(
129
+ context, "request_success"
130
+ )
131
+
132
+ formatted_message = self._format_message(request_success_log_message)
133
+ self.logger.log(self.log_level, f"Completed message: {formatted_message}")
134
+
135
+ return result
136
+ except Exception as e:
137
+ self.logger.log(
138
+ logging.ERROR, f"Failed message: {context.method or 'unknown'} - {e}"
139
+ )
140
+ raise
141
+
142
+
143
+ class LoggingMiddleware(BaseLoggingMiddleware):
20
144
  """Middleware that provides comprehensive request and response logging.
21
145
 
22
146
  Logs all MCP messages with configurable detail levels. Useful for debugging,
@@ -37,9 +161,12 @@ class LoggingMiddleware(Middleware):
37
161
 
38
162
  def __init__(
39
163
  self,
164
+ *,
40
165
  logger: logging.Logger | None = None,
41
166
  log_level: int = logging.INFO,
42
167
  include_payloads: bool = False,
168
+ include_payload_length: bool = False,
169
+ estimate_payload_tokens: bool = False,
43
170
  max_payload_length: int = 1000,
44
171
  methods: list[str] | None = None,
45
172
  payload_serializer: Callable[[Any], str] | None = None,
@@ -50,68 +177,25 @@ class LoggingMiddleware(Middleware):
50
177
  logger: Logger instance to use. If None, creates a logger named 'fastmcp.requests'
51
178
  log_level: Log level for messages (default: INFO)
52
179
  include_payloads: Whether to include message payloads in logs
180
+ include_payload_length: Whether to include response size in logs
181
+ estimate_payload_tokens: Whether to estimate response tokens
53
182
  max_payload_length: Maximum length of payload to log (prevents huge logs)
54
183
  methods: List of methods to log. If None, logs all methods.
184
+ payload_serializer: Callable that converts objects to a JSON string for the
185
+ payload. If not provided, uses FastMCP's default tool serializer.
55
186
  """
56
187
  self.logger: Logger = logger or logging.getLogger("fastmcp.requests")
57
- self.log_level: int = log_level
188
+ self.log_level = log_level
58
189
  self.include_payloads: bool = include_payloads
190
+ self.include_payload_length: bool = include_payload_length
191
+ self.estimate_payload_tokens: bool = estimate_payload_tokens
59
192
  self.max_payload_length: int = max_payload_length
60
193
  self.methods: list[str] | None = methods
61
194
  self.payload_serializer: Callable[[Any], str] | None = payload_serializer
195
+ self.structured_logging: bool = False
62
196
 
63
- def _format_message(self, context: MiddlewareContext[Any]) -> str:
64
- """Format a message for logging."""
65
- parts = [
66
- f"source={context.source}",
67
- f"type={context.type}",
68
- f"method={context.method or 'unknown'}",
69
- ]
70
-
71
- if self.include_payloads:
72
- payload: str
73
-
74
- if not self.payload_serializer:
75
- payload = default_serializer(context.message)
76
- else:
77
- try:
78
- payload = self.payload_serializer(context.message)
79
- except Exception as e:
80
- self.logger.warning(
81
- f"Failed {e} to serialize payload: {context.type} {context.method} {context.source}."
82
- )
83
- payload = default_serializer(context.message)
84
-
85
- if len(payload) > self.max_payload_length:
86
- payload = payload[: self.max_payload_length] + "..."
87
197
 
88
- parts.append(f"payload={payload}")
89
- return " ".join(parts)
90
-
91
- async def on_message(
92
- self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
93
- ) -> Any:
94
- """Log all messages."""
95
- message_info = self._format_message(context)
96
- if self.methods and context.method not in self.methods:
97
- return await call_next(context)
98
-
99
- self.logger.log(self.log_level, f"Processing message: {message_info}")
100
-
101
- try:
102
- result = await call_next(context)
103
- self.logger.log(
104
- self.log_level, f"Completed message: {context.method or 'unknown'}"
105
- )
106
- return result
107
- except Exception as e:
108
- self.logger.log(
109
- logging.ERROR, f"Failed message: {context.method or 'unknown'} - {e}"
110
- )
111
- raise
112
-
113
-
114
- class StructuredLoggingMiddleware(Middleware):
198
+ class StructuredLoggingMiddleware(BaseLoggingMiddleware):
115
199
  """Middleware that provides structured JSON logging for better log analysis.
116
200
 
117
201
  Outputs structured logs that are easier to parse and analyze with log
@@ -129,9 +213,12 @@ class StructuredLoggingMiddleware(Middleware):
129
213
 
130
214
  def __init__(
131
215
  self,
216
+ *,
132
217
  logger: logging.Logger | None = None,
133
218
  log_level: int = logging.INFO,
134
219
  include_payloads: bool = False,
220
+ include_payload_length: bool = False,
221
+ estimate_payload_tokens: bool = False,
135
222
  methods: list[str] | None = None,
136
223
  payload_serializer: Callable[[Any], str] | None = None,
137
224
  ):
@@ -141,74 +228,18 @@ class StructuredLoggingMiddleware(Middleware):
141
228
  logger: Logger instance to use. If None, creates a logger named 'fastmcp.structured'
142
229
  log_level: Log level for messages (default: INFO)
143
230
  include_payloads: Whether to include message payloads in logs
231
+ include_payload_length: Whether to include payload size in logs
232
+ estimate_payload_tokens: Whether to estimate token count using length // 4
144
233
  methods: List of methods to log. If None, logs all methods.
145
- serializer: Callable that converts objects to a JSON string for the
234
+ payload_serializer: Callable that converts objects to a JSON string for the
146
235
  payload. If not provided, uses FastMCP's default tool serializer.
147
236
  """
148
237
  self.logger: Logger = logger or logging.getLogger("fastmcp.structured")
149
238
  self.log_level: int = log_level
150
239
  self.include_payloads: bool = include_payloads
240
+ self.include_payload_length: bool = include_payload_length
241
+ self.estimate_payload_tokens: bool = estimate_payload_tokens
151
242
  self.methods: list[str] | None = methods
152
243
  self.payload_serializer: Callable[[Any], str] | None = payload_serializer
153
-
154
- def _create_log_entry(
155
- self, context: MiddlewareContext[Any], event: str, **extra_fields: Any
156
- ) -> dict[str, Any]:
157
- """Create a structured log entry."""
158
- entry = {
159
- "event": event,
160
- "timestamp": context.timestamp.isoformat(),
161
- "source": context.source,
162
- "type": context.type,
163
- "method": context.method,
164
- **extra_fields,
165
- }
166
-
167
- if self.include_payloads:
168
- payload: str
169
-
170
- if not self.payload_serializer:
171
- payload = default_serializer(context.message)
172
- else:
173
- try:
174
- payload = self.payload_serializer(context.message)
175
- except Exception as e:
176
- self.logger.warning(
177
- f"Failed {str(e)} to serialize payload: {context.type} {context.method} {context.source}."
178
- )
179
- payload = default_serializer(context.message)
180
-
181
- entry["payload"] = payload
182
-
183
- return entry
184
-
185
- async def on_message(
186
- self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
187
- ) -> Any:
188
- """Log structured message information."""
189
- start_entry = self._create_log_entry(context, "request_start")
190
- if self.methods and context.method not in self.methods:
191
- return await call_next(context)
192
-
193
- self.logger.log(self.log_level, json.dumps(start_entry))
194
-
195
- try:
196
- result = await call_next(context)
197
-
198
- success_entry = self._create_log_entry(
199
- context,
200
- "request_success",
201
- result_type=type(result).__name__ if result else None,
202
- )
203
- self.logger.log(self.log_level, json.dumps(success_entry))
204
-
205
- return result
206
- except Exception as e:
207
- error_entry = self._create_log_entry(
208
- context,
209
- "request_error",
210
- error_type=type(e).__name__,
211
- error_message=str(e),
212
- )
213
- self.logger.log(logging.ERROR, json.dumps(error_entry))
214
- raise
244
+ self.max_payload_length: int | None = None
245
+ self.structured_logging: bool = True
@@ -15,6 +15,7 @@ from typing import (
15
15
  )
16
16
 
17
17
  import mcp.types as mt
18
+ from mcp.server.lowlevel.helper_types import ReadResourceContents
18
19
  from typing_extensions import TypeVar
19
20
 
20
21
  from fastmcp.prompts.prompt import Prompt
@@ -154,8 +155,8 @@ class Middleware:
154
155
  async def on_read_resource(
155
156
  self,
156
157
  context: MiddlewareContext[mt.ReadResourceRequestParams],
157
- call_next: CallNext[mt.ReadResourceRequestParams, mt.ReadResourceResult],
158
- ) -> mt.ReadResourceResult:
158
+ call_next: CallNext[mt.ReadResourceRequestParams, list[ReadResourceContents]],
159
+ ) -> list[ReadResourceContents]:
159
160
  return await call_next(context)
160
161
 
161
162
  async def on_get_prompt(
fastmcp/server/openapi.py CHANGED
@@ -785,6 +785,7 @@ class FastMCPOpenAPI(FastMCP):
785
785
  http_routes = openapi.parse_openapi_to_http_routes(openapi_spec)
786
786
 
787
787
  # Process routes
788
+ num_excluded = 0
788
789
  route_maps = (route_maps or []) + DEFAULT_ROUTE_MAPPINGS
789
790
  for route in http_routes:
790
791
  # Determine route type based on mappings or default rules
@@ -823,8 +824,11 @@ class FastMCPOpenAPI(FastMCP):
823
824
  self._create_openapi_template(route, component_name, tags=route_tags)
824
825
  elif route_type == MCPType.EXCLUDE:
825
826
  logger.info(f"Excluding route: {route.method} {route.path}")
827
+ num_excluded += 1
826
828
 
827
- logger.info(f"Created FastMCP OpenAPI server with {len(http_routes)} routes")
829
+ logger.info(
830
+ f"Created FastMCP OpenAPI server with {len(http_routes) - num_excluded} routes"
831
+ )
828
832
 
829
833
  def _generate_default_name(
830
834
  self, route: openapi.HTTPRoute, mcp_names_map: dict[str, str] | None = None
fastmcp/server/server.py CHANGED
@@ -8,11 +8,7 @@ import re
8
8
  import secrets
9
9
  import warnings
10
10
  from collections.abc import AsyncIterator, Awaitable, Callable
11
- from contextlib import (
12
- AbstractAsyncContextManager,
13
- AsyncExitStack,
14
- asynccontextmanager,
15
- )
11
+ from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
16
12
  from dataclasses import dataclass
17
13
  from functools import partial
18
14
  from pathlib import Path
@@ -65,7 +61,7 @@ from fastmcp.tools.tool import FunctionTool, Tool, ToolResult
65
61
  from fastmcp.tools.tool_transform import ToolTransformConfig
66
62
  from fastmcp.utilities.cli import log_server_banner
67
63
  from fastmcp.utilities.components import FastMCPComponent
68
- from fastmcp.utilities.logging import get_logger
64
+ from fastmcp.utilities.logging import get_logger, temporary_log_level
69
65
  from fastmcp.utilities.types import NotSet, NotSetT
70
66
 
71
67
  if TYPE_CHECKING:
@@ -208,8 +204,8 @@ class FastMCP(Generic[LifespanResultT]):
208
204
  # if auth is `NotSet`, try to create a provider from the environment
209
205
  if auth is NotSet:
210
206
  if fastmcp.settings.server_auth is not None:
211
- # ImportString returns the class itself
212
- auth = fastmcp.settings.server_auth()
207
+ # server_auth_class returns the class itself
208
+ auth = fastmcp.settings.server_auth_class()
213
209
  else:
214
210
  auth = None
215
211
  self.auth = cast(AuthProvider | None, auth)
@@ -329,6 +325,10 @@ class FastMCP(Generic[LifespanResultT]):
329
325
  def instructions(self) -> str | None:
330
326
  return self._mcp_server.instructions
331
327
 
328
+ @instructions.setter
329
+ def instructions(self, value: str | None) -> None:
330
+ self._mcp_server.instructions = value
331
+
332
332
  @property
333
333
  def version(self) -> str | None:
334
334
  return self._mcp_server.version
@@ -1481,9 +1481,15 @@ class FastMCP(Generic[LifespanResultT]):
1481
1481
  meta=meta,
1482
1482
  )
1483
1483
 
1484
- async def run_stdio_async(self, show_banner: bool = True) -> None:
1485
- """Run the server using stdio transport."""
1484
+ async def run_stdio_async(
1485
+ self, show_banner: bool = True, log_level: str | None = None
1486
+ ) -> None:
1487
+ """Run the server using stdio transport.
1486
1488
 
1489
+ Args:
1490
+ show_banner: Whether to display the server banner
1491
+ log_level: Log level for the server
1492
+ """
1487
1493
  # Display server banner
1488
1494
  if show_banner:
1489
1495
  log_server_banner(
@@ -1491,15 +1497,16 @@ class FastMCP(Generic[LifespanResultT]):
1491
1497
  transport="stdio",
1492
1498
  )
1493
1499
 
1494
- async with stdio_server() as (read_stream, write_stream):
1495
- logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1496
- await self._mcp_server.run(
1497
- read_stream,
1498
- write_stream,
1499
- self._mcp_server.create_initialization_options(
1500
- NotificationOptions(tools_changed=True)
1501
- ),
1502
- )
1500
+ with temporary_log_level(log_level):
1501
+ async with stdio_server() as (read_stream, write_stream):
1502
+ logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
1503
+ await self._mcp_server.run(
1504
+ read_stream,
1505
+ write_stream,
1506
+ self._mcp_server.create_initialization_options(
1507
+ NotificationOptions(tools_changed=True)
1508
+ ),
1509
+ )
1503
1510
 
1504
1511
  async def run_http_async(
1505
1512
  self,
@@ -1525,7 +1532,6 @@ class FastMCP(Generic[LifespanResultT]):
1525
1532
  middleware: A list of middleware to apply to the app
1526
1533
  stateless_http: Whether to use stateless HTTP (defaults to settings.stateless_http)
1527
1534
  """
1528
-
1529
1535
  host = host or self._deprecated_settings.host
1530
1536
  port = port or self._deprecated_settings.port
1531
1537
  default_log_level_to_use = (
@@ -1566,14 +1572,15 @@ class FastMCP(Generic[LifespanResultT]):
1566
1572
  if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
1567
1573
  config_kwargs["log_level"] = default_log_level_to_use
1568
1574
 
1569
- config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
1570
- server = uvicorn.Server(config)
1571
- path = app.state.path.lstrip("/") # type: ignore
1572
- logger.info(
1573
- f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1574
- )
1575
+ with temporary_log_level(log_level):
1576
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
1577
+ server = uvicorn.Server(config)
1578
+ path = app.state.path.lstrip("/") # type: ignore
1579
+ logger.info(
1580
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
1581
+ )
1575
1582
 
1576
- await server.serve()
1583
+ await server.serve()
1577
1584
 
1578
1585
  async def run_sse_async(
1579
1586
  self,
@@ -2122,10 +2129,8 @@ class FastMCP(Generic[LifespanResultT]):
2122
2129
  # - Connected clients: reuse existing session for all requests
2123
2130
  # - Disconnected clients: create fresh sessions per request for isolation
2124
2131
  if client.is_connected():
2125
- from fastmcp.utilities.logging import get_logger
2126
-
2127
- logger = get_logger(__name__)
2128
- logger.info(
2132
+ _proxy_logger = get_logger(__name__)
2133
+ _proxy_logger.info(
2129
2134
  "Proxy detected connected client - reusing existing session for all requests. "
2130
2135
  "This may cause context mixing in concurrent scenarios."
2131
2136
  )
fastmcp/settings.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations as _annotations
3
3
  import inspect
4
4
  import warnings
5
5
  from pathlib import Path
6
- from typing import Annotated, Any, Literal
6
+ from typing import TYPE_CHECKING, Annotated, Any, Literal
7
7
 
8
8
  from pydantic import Field, ImportString, field_validator
9
9
  from pydantic.fields import FieldInfo
@@ -23,6 +23,9 @@ LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
23
23
 
24
24
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
25
25
 
26
+ if TYPE_CHECKING:
27
+ from fastmcp.server.auth.auth import AuthProvider
28
+
26
29
 
27
30
  class ExtendedEnvSettingsSource(EnvSettingsSource):
28
31
  """
@@ -258,7 +261,7 @@ class Settings(BaseSettings):
258
261
 
259
262
  # Auth settings
260
263
  server_auth: Annotated[
261
- ImportString | None,
264
+ str | None,
262
265
  Field(
263
266
  description=inspect.cleandoc(
264
267
  """
@@ -266,9 +269,9 @@ class Settings(BaseSettings):
266
269
  the full module path to an AuthProvider class (e.g.,
267
270
  'fastmcp.server.auth.providers.google.GoogleProvider').
268
271
 
269
- The specified class will be imported and instantiated automatically.
270
- Any class that inherits from AuthProvider can be used, including
271
- custom implementations.
272
+ The specified class will be imported and instantiated automatically
273
+ during FastMCP server creation. Any class that inherits from AuthProvider
274
+ can be used, including custom implementations.
272
275
 
273
276
  If None, no automatic configuration will take place.
274
277
 
@@ -357,6 +360,25 @@ class Settings(BaseSettings):
357
360
  ),
358
361
  ] = True
359
362
 
363
+ @property
364
+ def server_auth_class(self) -> AuthProvider | None:
365
+ from fastmcp.utilities.types import get_cached_typeadapter
366
+
367
+ if not self.server_auth:
368
+ return None
369
+
370
+ # https://github.com/jlowin/fastmcp/issues/1749
371
+ # Pydantic imports the module in an ImportString during model validation, but we don't want the server
372
+ # auth module imported during settings creation as it imports dependencies we aren't ready for yet.
373
+ # To fix this while limiting breaking changes, we delay the import by only creating the ImportString
374
+ # when the class is actually needed
375
+
376
+ type_adapter = get_cached_typeadapter(ImportString)
377
+
378
+ auth_class = type_adapter.validate_python(self.server_auth)
379
+
380
+ return auth_class
381
+
360
382
 
361
383
  def __getattr__(name: str):
362
384
  """
fastmcp/tools/tool.py CHANGED
@@ -413,7 +413,9 @@ class ParsedFunction:
413
413
 
414
414
  input_type_adapter = get_cached_typeadapter(fn)
415
415
  input_schema = input_type_adapter.json_schema()
416
- input_schema = compress_schema(input_schema, prune_params=prune_params)
416
+ input_schema = compress_schema(
417
+ input_schema, prune_params=prune_params, prune_titles=True
418
+ )
417
419
 
418
420
  output_schema = None
419
421
  # Get the return annotation from the signature
@@ -473,7 +475,7 @@ class ParsedFunction:
473
475
  else:
474
476
  output_schema = base_schema
475
477
 
476
- output_schema = compress_schema(output_schema)
478
+ output_schema = compress_schema(output_schema, prune_titles=True)
477
479
 
478
480
  except PydanticSchemaGenerationError as e:
479
481
  if "_UnserializableType" not in str(e):
@@ -109,8 +109,25 @@ def _single_pass_optimize(
109
109
  root_refs.add(referenced_def)
110
110
 
111
111
  # Apply cleanups
112
+ # Only remove "title" if it's a schema metadata field
113
+ # Schema objects have keywords like "type", "properties", "$ref", etc.
114
+ # If we see these, then "title" is metadata, not a property name
112
115
  if prune_titles and "title" in node:
113
- node.pop("title")
116
+ # Check if this looks like a schema node
117
+ if any(
118
+ k in node
119
+ for k in [
120
+ "type",
121
+ "properties",
122
+ "$ref",
123
+ "items",
124
+ "allOf",
125
+ "oneOf",
126
+ "anyOf",
127
+ "required",
128
+ ]
129
+ ):
130
+ node.pop("title")
114
131
 
115
132
  if (
116
133
  prune_additional_properties