fastmcp 2.12.2__py3-none-any.whl → 2.12.4__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 (54) hide show
  1. fastmcp/cli/claude.py +1 -10
  2. fastmcp/cli/cli.py +45 -25
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +1 -10
  5. fastmcp/cli/install/claude_desktop.py +1 -9
  6. fastmcp/cli/install/cursor.py +2 -18
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +1 -9
  9. fastmcp/cli/run.py +2 -86
  10. fastmcp/client/auth/oauth.py +50 -37
  11. fastmcp/client/client.py +18 -8
  12. fastmcp/client/elicitation.py +6 -1
  13. fastmcp/client/transports.py +1 -1
  14. fastmcp/contrib/component_manager/component_service.py +1 -1
  15. fastmcp/contrib/mcp_mixin/README.md +3 -3
  16. fastmcp/contrib/mcp_mixin/mcp_mixin.py +41 -6
  17. fastmcp/experimental/utilities/openapi/director.py +8 -1
  18. fastmcp/experimental/utilities/openapi/schemas.py +31 -5
  19. fastmcp/prompts/prompt.py +10 -8
  20. fastmcp/resources/resource.py +14 -11
  21. fastmcp/resources/template.py +12 -10
  22. fastmcp/server/auth/auth.py +10 -4
  23. fastmcp/server/auth/oauth_proxy.py +93 -23
  24. fastmcp/server/auth/oidc_proxy.py +348 -0
  25. fastmcp/server/auth/providers/auth0.py +174 -0
  26. fastmcp/server/auth/providers/aws.py +237 -0
  27. fastmcp/server/auth/providers/azure.py +6 -2
  28. fastmcp/server/auth/providers/descope.py +172 -0
  29. fastmcp/server/auth/providers/github.py +6 -2
  30. fastmcp/server/auth/providers/google.py +6 -2
  31. fastmcp/server/auth/providers/workos.py +6 -2
  32. fastmcp/server/context.py +17 -16
  33. fastmcp/server/dependencies.py +18 -5
  34. fastmcp/server/http.py +1 -1
  35. fastmcp/server/middleware/logging.py +147 -116
  36. fastmcp/server/middleware/middleware.py +3 -2
  37. fastmcp/server/openapi.py +5 -1
  38. fastmcp/server/server.py +43 -36
  39. fastmcp/settings.py +42 -6
  40. fastmcp/tools/tool.py +105 -87
  41. fastmcp/tools/tool_transform.py +1 -1
  42. fastmcp/utilities/json_schema.py +18 -1
  43. fastmcp/utilities/logging.py +66 -4
  44. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +4 -39
  45. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -2
  46. fastmcp/utilities/mcp_server_config/v1/schema.json +2 -1
  47. fastmcp/utilities/storage.py +204 -0
  48. fastmcp/utilities/tests.py +8 -6
  49. fastmcp/utilities/types.py +9 -5
  50. {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/METADATA +121 -48
  51. {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/RECORD +54 -48
  52. {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/WHEEL +0 -0
  53. {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/entry_points.txt +0 -0
  54. {fastmcp-2.12.2.dist-info → fastmcp-2.12.4.dist-info}/licenses/LICENSE +0 -0
@@ -32,6 +32,7 @@ from fastmcp.server.auth.auth import AccessToken
32
32
  from fastmcp.server.auth.oauth_proxy import OAuthProxy
33
33
  from fastmcp.utilities.auth import parse_scopes
34
34
  from fastmcp.utilities.logging import get_logger
35
+ from fastmcp.utilities.storage import KVStorage
35
36
  from fastmcp.utilities.types import NotSet, NotSetT
36
37
 
37
38
  logger = get_logger(__name__)
@@ -217,6 +218,7 @@ class GoogleProvider(OAuthProxy):
217
218
  required_scopes: list[str] | NotSetT = NotSet,
218
219
  timeout_seconds: int | NotSetT = NotSet,
219
220
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
221
+ client_storage: KVStorage | None = None,
220
222
  ):
221
223
  """Initialize Google OAuth provider.
222
224
 
@@ -232,6 +234,8 @@ class GoogleProvider(OAuthProxy):
232
234
  timeout_seconds: HTTP request timeout for Google API calls
233
235
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
234
236
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
237
+ client_storage: Storage implementation for OAuth client registrations.
238
+ Defaults to file-based storage if not specified.
235
239
  """
236
240
 
237
241
  settings = GoogleProviderSettings.model_validate(
@@ -261,7 +265,6 @@ class GoogleProvider(OAuthProxy):
261
265
  )
262
266
 
263
267
  # Apply defaults
264
- redirect_path_final = settings.redirect_path or "/auth/callback"
265
268
  timeout_seconds_final = settings.timeout_seconds or 10
266
269
  # Google requires at least one scope - openid is the minimal OIDC scope
267
270
  required_scopes_final = settings.required_scopes or ["openid"]
@@ -286,9 +289,10 @@ class GoogleProvider(OAuthProxy):
286
289
  upstream_client_secret=client_secret_str,
287
290
  token_verifier=token_verifier,
288
291
  base_url=settings.base_url,
289
- redirect_path=redirect_path_final,
292
+ redirect_path=settings.redirect_path,
290
293
  issuer_url=settings.base_url, # We act as the issuer for client registration
291
294
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
295
+ client_storage=client_storage,
292
296
  )
293
297
 
294
298
  logger.info(
@@ -23,6 +23,7 @@ from fastmcp.server.auth.oauth_proxy import OAuthProxy
23
23
  from fastmcp.server.auth.providers.jwt import JWTVerifier
24
24
  from fastmcp.utilities.auth import parse_scopes
25
25
  from fastmcp.utilities.logging import get_logger
26
+ from fastmcp.utilities.storage import KVStorage
26
27
  from fastmcp.utilities.types import NotSet, NotSetT
27
28
 
28
29
  logger = get_logger(__name__)
@@ -169,6 +170,7 @@ class WorkOSProvider(OAuthProxy):
169
170
  required_scopes: list[str] | None | NotSetT = NotSet,
170
171
  timeout_seconds: int | NotSetT = NotSet,
171
172
  allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
173
+ client_storage: KVStorage | None = None,
172
174
  ):
173
175
  """Initialize WorkOS OAuth provider.
174
176
 
@@ -182,6 +184,8 @@ class WorkOSProvider(OAuthProxy):
182
184
  timeout_seconds: HTTP request timeout for WorkOS API calls
183
185
  allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
184
186
  If None (default), all URIs are allowed. If empty list, no URIs are allowed.
187
+ client_storage: Storage implementation for OAuth client registrations.
188
+ Defaults to file-based storage if not specified.
185
189
  """
186
190
 
187
191
  settings = WorkOSProviderSettings.model_validate(
@@ -220,7 +224,6 @@ class WorkOSProvider(OAuthProxy):
220
224
  if not authkit_domain_str.startswith(("http://", "https://")):
221
225
  authkit_domain_str = f"https://{authkit_domain_str}"
222
226
  authkit_domain_final = authkit_domain_str.rstrip("/")
223
- redirect_path_final = settings.redirect_path or "/auth/callback"
224
227
  timeout_seconds_final = settings.timeout_seconds or 10
225
228
  scopes_final = settings.required_scopes or []
226
229
  allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
@@ -245,9 +248,10 @@ class WorkOSProvider(OAuthProxy):
245
248
  upstream_client_secret=client_secret_str,
246
249
  token_verifier=token_verifier,
247
250
  base_url=settings.base_url,
248
- redirect_path=redirect_path_final,
251
+ redirect_path=settings.redirect_path,
249
252
  issuer_url=settings.base_url,
250
253
  allowed_client_redirect_uris=allowed_client_redirect_uris_final,
254
+ client_storage=client_storage,
251
255
  )
252
256
 
253
257
  logger.info(
fastmcp/server/context.py CHANGED
@@ -5,7 +5,7 @@ import copy
5
5
  import inspect
6
6
  import warnings
7
7
  import weakref
8
- from collections.abc import Generator, Mapping
8
+ from collections.abc import Generator, Mapping, Sequence
9
9
  from contextlib import contextmanager
10
10
  from contextvars import ContextVar, Token
11
11
  from dataclasses import dataclass
@@ -17,9 +17,10 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
17
17
  from mcp.server.lowlevel.server import request_ctx
18
18
  from mcp.shared.context import RequestContext
19
19
  from mcp.types import (
20
+ AudioContent,
20
21
  ClientCapabilities,
21
- ContentBlock,
22
22
  CreateMessageResult,
23
+ ImageContent,
23
24
  IncludeContext,
24
25
  ModelHint,
25
26
  ModelPreferences,
@@ -85,18 +86,18 @@ class Context:
85
86
 
86
87
  ```python
87
88
  @server.tool
88
- def my_tool(x: int, ctx: Context) -> str:
89
+ async def my_tool(x: int, ctx: Context) -> str:
89
90
  # Log messages to the client
90
- ctx.info(f"Processing {x}")
91
- ctx.debug("Debug info")
92
- ctx.warning("Warning message")
93
- ctx.error("Error message")
91
+ await ctx.info(f"Processing {x}")
92
+ await ctx.debug("Debug info")
93
+ await ctx.warning("Warning message")
94
+ await ctx.error("Error message")
94
95
 
95
96
  # Report progress
96
- ctx.report_progress(50, 100, "Processing")
97
+ await ctx.report_progress(50, 100, "Processing")
97
98
 
98
99
  # Access resources
99
- data = ctx.read_resource("resource://data")
100
+ data = await ctx.read_resource("resource://data")
100
101
 
101
102
  # Get request info
102
103
  request_id = ctx.request_id
@@ -359,13 +360,13 @@ class Context:
359
360
 
360
361
  async def sample(
361
362
  self,
362
- messages: str | list[str | SamplingMessage],
363
+ messages: str | Sequence[str | SamplingMessage],
363
364
  system_prompt: str | None = None,
364
365
  include_context: IncludeContext | None = None,
365
366
  temperature: float | None = None,
366
367
  max_tokens: int | None = None,
367
368
  model_preferences: ModelPreferences | str | list[str] | None = None,
368
- ) -> ContentBlock:
369
+ ) -> TextContent | ImageContent | AudioContent:
369
370
  """
370
371
  Send a sampling request to the client and await the response.
371
372
 
@@ -383,7 +384,7 @@ class Context:
383
384
  content=TextContent(text=messages, type="text"), role="user"
384
385
  )
385
386
  ]
386
- elif isinstance(messages, list):
387
+ elif isinstance(messages, Sequence):
387
388
  sampling_messages = [
388
389
  SamplingMessage(content=TextContent(text=m, type="text"), role="user")
389
390
  if isinstance(m, str)
@@ -449,7 +450,7 @@ class Context:
449
450
  AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
450
451
  ): ...
451
452
 
452
- """When response_type is None, the accepted elicitaiton will contain an
453
+ """When response_type is None, the accepted elicitation will contain an
453
454
  empty dict"""
454
455
 
455
456
  @overload
@@ -459,7 +460,7 @@ class Context:
459
460
  response_type: type[T],
460
461
  ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ...
461
462
 
462
- """When response_type is not None, the accepted elicitaiton will contain the
463
+ """When response_type is not None, the accepted elicitation will contain the
463
464
  response data"""
464
465
 
465
466
  @overload
@@ -469,7 +470,7 @@ class Context:
469
470
  response_type: list[str],
470
471
  ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ...
471
472
 
472
- """When response_type is a list of strings, the accepted elicitaiton will
473
+ """When response_type is a list of strings, the accepted elicitation will
473
474
  contain the selected string response"""
474
475
 
475
476
  async def elicit(
@@ -573,7 +574,7 @@ class Context:
573
574
  warnings.warn(
574
575
  "Context.get_http_request() is deprecated and will be removed in a future version. "
575
576
  "Use get_http_request() from fastmcp.server.dependencies instead. "
576
- "See https://gofastmcp.com/patterns/http-requests for more details.",
577
+ "See https://gofastmcp.com/servers/context#http-requests for more details.",
577
578
  DeprecationWarning,
578
579
  stacklevel=2,
579
580
  )
@@ -5,6 +5,9 @@ from typing import TYPE_CHECKING
5
5
  from mcp.server.auth.middleware.auth_context import (
6
6
  get_access_token as _sdk_get_access_token,
7
7
  )
8
+ from mcp.server.auth.provider import (
9
+ AccessToken as _SDKAccessToken,
10
+ )
8
11
  from starlette.requests import Request
9
12
 
10
13
  from fastmcp.server.auth import AccessToken
@@ -107,17 +110,27 @@ def get_access_token() -> AccessToken | None:
107
110
  The access token if an authenticated user is available, None otherwise.
108
111
  """
109
112
  #
110
- obj = _sdk_get_access_token()
111
- if obj is None or isinstance(obj, AccessToken):
112
- return obj
113
+ access_token: _SDKAccessToken | None = _sdk_get_access_token()
114
+
115
+ if access_token is None or isinstance(access_token, AccessToken):
116
+ return access_token
113
117
 
114
118
  # If the object is not a FastMCP AccessToken, convert it to one if the fields are compatible
115
119
  # This is a workaround for the case where the SDK returns a different type
116
120
  # If it fails, it will raise a TypeError
117
121
  try:
118
- return AccessToken(**obj.model_dump())
122
+ access_token_as_dict = access_token.model_dump()
123
+ return AccessToken(
124
+ token=access_token_as_dict["token"],
125
+ client_id=access_token_as_dict["client_id"],
126
+ scopes=access_token_as_dict["scopes"],
127
+ # Optional fields
128
+ expires_at=access_token_as_dict.get("expires_at"),
129
+ resource_owner=access_token_as_dict.get("resource_owner"),
130
+ claims=access_token_as_dict.get("claims"),
131
+ )
119
132
  except Exception as e:
120
133
  raise TypeError(
121
- f"Expected fastmcp.server.auth.auth.AccessToken, got {type(obj).__name__}. "
134
+ f"Expected fastmcp.server.auth.auth.AccessToken, got {type(access_token).__name__}. "
122
135
  "Ensure the SDK is using the correct AccessToken type."
123
136
  ) from e
fastmcp/server/http.py CHANGED
@@ -113,7 +113,7 @@ def create_base_app(
113
113
  A Starlette application
114
114
  """
115
115
  # Always add RequestContextMiddleware as the outermost middleware
116
- middleware.append(Middleware(RequestContextMiddleware))
116
+ middleware.insert(0, Middleware(RequestContextMiddleware))
117
117
 
118
118
  return StarletteWithLifespan(
119
119
  routes=routes,
@@ -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