fastmcp 2.12.1__py3-none-any.whl → 2.13.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 (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -28,6 +28,7 @@ class ToolInfo:
28
28
  tags: list[str] | None = None
29
29
  enabled: bool | None = None
30
30
  title: str | None = None
31
+ icons: list[dict[str, Any]] | None = None
31
32
  meta: dict[str, Any] | None = None
32
33
 
33
34
 
@@ -42,6 +43,7 @@ class PromptInfo:
42
43
  tags: list[str] | None = None
43
44
  enabled: bool | None = None
44
45
  title: str | None = None
46
+ icons: list[dict[str, Any]] | None = None
45
47
  meta: dict[str, Any] | None = None
46
48
 
47
49
 
@@ -58,6 +60,7 @@ class ResourceInfo:
58
60
  tags: list[str] | None = None
59
61
  enabled: bool | None = None
60
62
  title: str | None = None
63
+ icons: list[dict[str, Any]] | None = None
61
64
  meta: dict[str, Any] | None = None
62
65
 
63
66
 
@@ -75,6 +78,7 @@ class TemplateInfo:
75
78
  tags: list[str] | None = None
76
79
  enabled: bool | None = None
77
80
  title: str | None = None
81
+ icons: list[dict[str, Any]] | None = None
78
82
  meta: dict[str, Any] | None = None
79
83
 
80
84
 
@@ -85,6 +89,8 @@ class FastMCPInfo:
85
89
  name: str
86
90
  instructions: str | None
87
91
  version: str | None # The server's own version string (if specified)
92
+ website_url: str | None
93
+ icons: list[dict[str, Any]] | None
88
94
  fastmcp_version: str # Version of FastMCP generating this manifest
89
95
  mcp_version: str # Version of MCP protocol library
90
96
  server_generation: int # Server generation: 1 (mcp package) or 2 (fastmcp)
@@ -104,21 +110,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
104
110
  Returns:
105
111
  FastMCPInfo dataclass containing the extracted information
106
112
  """
107
- # Get all the components using FastMCP2's direct methods
108
- tools_dict = await mcp.get_tools()
109
- prompts_dict = await mcp.get_prompts()
110
- resources_dict = await mcp.get_resources()
111
- templates_dict = await mcp.get_resource_templates()
113
+ # Get all components via middleware to respect filtering and preserve metadata
114
+ tools_list = await mcp._list_tools_middleware()
115
+ prompts_list = await mcp._list_prompts_middleware()
116
+ resources_list = await mcp._list_resources_middleware()
117
+ templates_list = await mcp._list_resource_templates_middleware()
112
118
 
113
119
  # Extract detailed tool information
114
120
  tool_infos = []
115
- for key, tool in tools_dict.items():
116
- # Convert to MCP tool to get input schema
117
- mcp_tool = tool.to_mcp_tool(name=key)
121
+ for tool in tools_list:
122
+ mcp_tool = tool.to_mcp_tool(name=tool.key)
118
123
  tool_infos.append(
119
124
  ToolInfo(
120
- key=key,
121
- name=tool.name or key,
125
+ key=tool.key,
126
+ name=tool.name or tool.key,
122
127
  description=tool.description,
123
128
  input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
124
129
  output_schema=tool.output_schema,
@@ -126,17 +131,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
126
131
  tags=list(tool.tags) if tool.tags else None,
127
132
  enabled=tool.enabled,
128
133
  title=tool.title,
134
+ icons=[icon.model_dump() for icon in tool.icons]
135
+ if tool.icons
136
+ else None,
129
137
  meta=tool.meta,
130
138
  )
131
139
  )
132
140
 
133
141
  # Extract detailed prompt information
134
142
  prompt_infos = []
135
- for key, prompt in prompts_dict.items():
143
+ for prompt in prompts_list:
136
144
  prompt_infos.append(
137
145
  PromptInfo(
138
- key=key,
139
- name=prompt.name or key,
146
+ key=prompt.key,
147
+ name=prompt.name or prompt.key,
140
148
  description=prompt.description,
141
149
  arguments=[arg.model_dump() for arg in prompt.arguments]
142
150
  if prompt.arguments
@@ -144,17 +152,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
144
152
  tags=list(prompt.tags) if prompt.tags else None,
145
153
  enabled=prompt.enabled,
146
154
  title=prompt.title,
155
+ icons=[icon.model_dump() for icon in prompt.icons]
156
+ if prompt.icons
157
+ else None,
147
158
  meta=prompt.meta,
148
159
  )
149
160
  )
150
161
 
151
162
  # Extract detailed resource information
152
163
  resource_infos = []
153
- for key, resource in resources_dict.items():
164
+ for resource in resources_list:
154
165
  resource_infos.append(
155
166
  ResourceInfo(
156
- key=key,
157
- uri=key, # For v2, key is the URI
167
+ key=resource.key,
168
+ uri=resource.key,
158
169
  name=resource.name,
159
170
  description=resource.description,
160
171
  mime_type=resource.mime_type,
@@ -164,17 +175,20 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
164
175
  tags=list(resource.tags) if resource.tags else None,
165
176
  enabled=resource.enabled,
166
177
  title=resource.title,
178
+ icons=[icon.model_dump() for icon in resource.icons]
179
+ if resource.icons
180
+ else None,
167
181
  meta=resource.meta,
168
182
  )
169
183
  )
170
184
 
171
185
  # Extract detailed template information
172
186
  template_infos = []
173
- for key, template in templates_dict.items():
187
+ for template in templates_list:
174
188
  template_infos.append(
175
189
  TemplateInfo(
176
- key=key,
177
- uri_template=key, # For v2, key is the URI template
190
+ key=template.key,
191
+ uri_template=template.key,
178
192
  name=template.name,
179
193
  description=template.description,
180
194
  mime_type=template.mime_type,
@@ -185,6 +199,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
185
199
  tags=list(template.tags) if template.tags else None,
186
200
  enabled=template.enabled,
187
201
  title=template.title,
202
+ icons=[icon.model_dump() for icon in template.icons]
203
+ if template.icons
204
+ else None,
188
205
  meta=template.meta,
189
206
  )
190
207
  )
@@ -197,13 +214,25 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
197
214
  "logging": {},
198
215
  }
199
216
 
217
+ # Extract server-level icons and website_url
218
+ server_icons = (
219
+ [icon.model_dump() for icon in mcp._mcp_server.icons]
220
+ if hasattr(mcp._mcp_server, "icons") and mcp._mcp_server.icons
221
+ else None
222
+ )
223
+ server_website_url = (
224
+ mcp._mcp_server.website_url if hasattr(mcp._mcp_server, "website_url") else None
225
+ )
226
+
200
227
  return FastMCPInfo(
201
228
  name=mcp.name,
202
229
  instructions=mcp.instructions,
230
+ version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
231
+ website_url=server_website_url,
232
+ icons=server_icons,
203
233
  fastmcp_version=fastmcp.__version__,
204
234
  mcp_version=importlib.metadata.version("mcp"),
205
235
  server_generation=2, # FastMCP v2
206
- version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
207
236
  tools=tool_infos,
208
237
  prompts=prompt_infos,
209
238
  resources=resource_infos,
@@ -248,6 +277,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
248
277
  tags=None, # v1 doesn't have tags
249
278
  enabled=None, # v1 doesn't have enabled field
250
279
  title=None, # v1 doesn't have title
280
+ icons=[icon.model_dump() for icon in mcp_tool.icons]
281
+ if hasattr(mcp_tool, "icons") and mcp_tool.icons
282
+ else None,
251
283
  meta=None, # v1 doesn't have meta field
252
284
  )
253
285
  )
@@ -269,6 +301,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
269
301
  tags=None, # v1 doesn't have tags
270
302
  enabled=None, # v1 doesn't have enabled field
271
303
  title=None, # v1 doesn't have title
304
+ icons=[icon.model_dump() for icon in mcp_prompt.icons]
305
+ if hasattr(mcp_prompt, "icons") and mcp_prompt.icons
306
+ else None,
272
307
  meta=None, # v1 doesn't have meta field
273
308
  )
274
309
  )
@@ -287,6 +322,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
287
322
  tags=None, # v1 doesn't have tags
288
323
  enabled=None, # v1 doesn't have enabled field
289
324
  title=None, # v1 doesn't have title
325
+ icons=[icon.model_dump() for icon in mcp_resource.icons]
326
+ if hasattr(mcp_resource, "icons") and mcp_resource.icons
327
+ else None,
290
328
  meta=None, # v1 doesn't have meta field
291
329
  )
292
330
  )
@@ -306,6 +344,9 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
306
344
  tags=None, # v1 doesn't have tags
307
345
  enabled=None, # v1 doesn't have enabled field
308
346
  title=None, # v1 doesn't have title
347
+ icons=[icon.model_dump() for icon in mcp_template.icons]
348
+ if hasattr(mcp_template, "icons") and mcp_template.icons
349
+ else None,
309
350
  meta=None, # v1 doesn't have meta field
310
351
  )
311
352
  )
@@ -318,13 +359,26 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
318
359
  "logging": {},
319
360
  }
320
361
 
362
+ # Extract server-level icons and website_url from serverInfo
363
+ server_info = client.initialize_result.serverInfo
364
+ server_icons = (
365
+ [icon.model_dump() for icon in server_info.icons]
366
+ if hasattr(server_info, "icons") and server_info.icons
367
+ else None
368
+ )
369
+ server_website_url = (
370
+ server_info.websiteUrl if hasattr(server_info, "websiteUrl") else None
371
+ )
372
+
321
373
  return FastMCPInfo(
322
374
  name=mcp._mcp_server.name,
323
375
  instructions=mcp._mcp_server.instructions,
376
+ version=mcp._mcp_server.version,
377
+ website_url=server_website_url,
378
+ icons=server_icons,
324
379
  fastmcp_version=fastmcp.__version__, # Version generating this manifest
325
380
  mcp_version=importlib.metadata.version("mcp"),
326
381
  server_generation=1, # MCP v1
327
- version=mcp._mcp_server.version,
328
382
  tools=tool_infos,
329
383
  prompts=prompt_infos,
330
384
  resources=resource_infos,
@@ -358,7 +412,7 @@ class InspectFormat(str, Enum):
358
412
  MCP = "mcp"
359
413
 
360
414
 
361
- async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
415
+ def format_fastmcp_info(info: FastMCPInfo) -> bytes:
362
416
  """Format FastMCPInfo as FastMCP-specific JSON.
363
417
 
364
418
  This includes FastMCP-specific fields like tags, enabled, annotations, etc.
@@ -369,6 +423,8 @@ async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
369
423
  "name": info.name,
370
424
  "instructions": info.instructions,
371
425
  "version": info.version,
426
+ "website_url": info.website_url,
427
+ "icons": info.icons,
372
428
  "generation": info.server_generation,
373
429
  "capabilities": info.capabilities,
374
430
  },
@@ -445,6 +501,6 @@ async def format_info(
445
501
  # This works for both v1 and v2 servers
446
502
  if info is None:
447
503
  info = await inspect_fastmcp(mcp)
448
- return await format_fastmcp_info(info)
504
+ return format_fastmcp_info(info)
449
505
  else:
450
506
  raise ValueError(f"Unknown format: {format}")
@@ -98,7 +98,7 @@ def _single_pass_optimize(
98
98
  if isinstance(node, dict):
99
99
  # Collect $ref references for unused definition removal
100
100
  if prune_defs:
101
- ref = node.get("$ref")
101
+ ref = node.get("$ref") # type: ignore
102
102
  if isinstance(ref, str) and ref.startswith("#/$defs/"):
103
103
  referenced_def = ref.split("/")[-1]
104
104
  if current_def_name:
@@ -109,14 +109,31 @@ 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") # type: ignore
114
131
 
115
132
  if (
116
133
  prune_additional_properties
117
- and node.get("additionalProperties") is False
134
+ and node.get("additionalProperties") is False # type: ignore
118
135
  ):
119
- node.pop("additionalProperties")
136
+ node.pop("additionalProperties") # type: ignore
120
137
 
121
138
  # Recursive traversal
122
139
  for key, value in node.items():
@@ -61,7 +61,7 @@ from pydantic import (
61
61
  )
62
62
  from typing_extensions import NotRequired, TypedDict
63
63
 
64
- __all__ = ["json_schema_to_type", "JSONSchema"]
64
+ __all__ = ["JSONSchema", "json_schema_to_type"]
65
65
 
66
66
 
67
67
  FORMAT_TYPES: dict[str, Any] = {
@@ -368,7 +368,7 @@ def _schema_to_type(
368
368
  return types[0]
369
369
  else:
370
370
  if has_null:
371
- return Union[tuple(types + [type(None)])] # type: ignore # noqa: UP007
371
+ return Union[(*types, type(None))] # type: ignore
372
372
  else:
373
373
  return Union[tuple(types)] # type: ignore # noqa: UP007
374
374
 
@@ -389,7 +389,7 @@ def _schema_to_type(
389
389
  if len(types) == 1:
390
390
  return types[0] | None # type: ignore
391
391
  else:
392
- return Union[tuple(types + [type(None)])] # type: ignore # noqa: UP007
392
+ return Union[(*types, type(None))] # type: ignore
393
393
  return Union[tuple(types)] # type: ignore # noqa: UP007
394
394
 
395
395
  return _get_from_type_handler(schema, schemas)(schema)
@@ -578,7 +578,7 @@ def _create_dataclass(
578
578
  return _merge_defaults(data, original_schema)
579
579
  return data
580
580
 
581
- setattr(cls, "_apply_defaults", _apply_defaults)
581
+ cls._apply_defaults = _apply_defaults # type: ignore[attr-defined]
582
582
 
583
583
  # Store completed class
584
584
  _classes[cache_key] = cls
@@ -1,10 +1,14 @@
1
1
  """Logging utilities for FastMCP."""
2
2
 
3
+ import contextlib
3
4
  import logging
4
- from typing import Any, Literal
5
+ from typing import Any, Literal, cast
5
6
 
6
7
  from rich.console import Console
7
8
  from rich.logging import RichHandler
9
+ from typing_extensions import override
10
+
11
+ import fastmcp
8
12
 
9
13
 
10
14
  def get_logger(name: str) -> logging.Logger:
@@ -16,13 +20,16 @@ def get_logger(name: str) -> logging.Logger:
16
20
  Returns:
17
21
  a configured logger instance
18
22
  """
19
- return logging.getLogger(f"FastMCP.{name}")
23
+ if name.startswith("fastmcp."):
24
+ return logging.getLogger(name=name)
25
+
26
+ return logging.getLogger(name=f"fastmcp.{name}")
20
27
 
21
28
 
22
29
  def configure_logging(
23
30
  level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
24
31
  logger: logging.Logger | None = None,
25
- enable_rich_tracebacks: bool = True,
32
+ enable_rich_tracebacks: bool | None = None,
26
33
  **rich_kwargs: Any,
27
34
  ) -> None:
28
35
  """
@@ -33,26 +40,191 @@ def configure_logging(
33
40
  level: the log level to use
34
41
  rich_kwargs: the parameters to use for creating RichHandler
35
42
  """
43
+ # Check if logging is disabled in settings
44
+ if not fastmcp.settings.log_enabled:
45
+ return
46
+
47
+ # Use settings default if not specified
48
+ if enable_rich_tracebacks is None:
49
+ enable_rich_tracebacks = fastmcp.settings.enable_rich_tracebacks
36
50
 
37
51
  if logger is None:
38
- logger = logging.getLogger("FastMCP")
52
+ logger = logging.getLogger("fastmcp")
53
+
54
+ formatter = logging.Formatter("%(message)s")
55
+
56
+ # Don't propagate to the root logger
57
+ logger.propagate = False
58
+ logger.setLevel(level)
39
59
 
40
- # Only configure the FastMCP logger namespace
60
+ # Configure the handler for normal logs
41
61
  handler = RichHandler(
42
62
  console=Console(stderr=True),
43
- rich_tracebacks=enable_rich_tracebacks,
44
63
  **rich_kwargs,
45
64
  )
46
- formatter = logging.Formatter("%(message)s")
47
65
  handler.setFormatter(formatter)
48
66
 
49
- logger.setLevel(level)
67
+ # filter to exclude tracebacks
68
+ handler.addFilter(lambda record: record.exc_info is None)
69
+
70
+ # Configure the handler for tracebacks, for tracebacks we use a compressed format:
71
+ # no path or level name to maximize width available for the traceback
72
+ # suppress framework frames and limit the number of frames to 3
73
+
74
+ import mcp
75
+ import pydantic
76
+
77
+ # Build traceback kwargs with defaults that can be overridden
78
+ traceback_kwargs = {
79
+ "console": Console(stderr=True),
80
+ "show_path": False,
81
+ "show_level": False,
82
+ "rich_tracebacks": enable_rich_tracebacks,
83
+ "tracebacks_max_frames": 3,
84
+ "tracebacks_suppress": [fastmcp, mcp, pydantic],
85
+ }
86
+ # Override defaults with user-provided values
87
+ traceback_kwargs.update(rich_kwargs)
88
+
89
+ traceback_handler = RichHandler(**traceback_kwargs) # type: ignore[arg-type]
90
+ traceback_handler.setFormatter(formatter)
91
+
92
+ traceback_handler.addFilter(lambda record: record.exc_info is not None)
50
93
 
51
94
  # Remove any existing handlers to avoid duplicates on reconfiguration
52
95
  for hdlr in logger.handlers[:]:
53
96
  logger.removeHandler(hdlr)
54
97
 
55
98
  logger.addHandler(handler)
99
+ logger.addHandler(traceback_handler)
56
100
 
57
- # Don't propagate to the root logger
58
- logger.propagate = False
101
+
102
+ @contextlib.contextmanager
103
+ def temporary_log_level(
104
+ level: str | None,
105
+ logger: logging.Logger | None = None,
106
+ enable_rich_tracebacks: bool | None = None,
107
+ **rich_kwargs: Any,
108
+ ):
109
+ """Context manager to temporarily set log level and restore it afterwards.
110
+
111
+ Args:
112
+ level: The temporary log level to set (e.g., "DEBUG", "INFO")
113
+ logger: Optional logger to configure (defaults to FastMCP logger)
114
+ enable_rich_tracebacks: Whether to enable rich tracebacks
115
+ **rich_kwargs: Additional parameters for RichHandler
116
+
117
+ Usage:
118
+ with temporary_log_level("DEBUG"):
119
+ # Code that runs with DEBUG logging
120
+ pass
121
+ # Original log level is restored here
122
+ """
123
+ if level:
124
+ # Get the original log level from settings
125
+ original_level = fastmcp.settings.log_level
126
+
127
+ # Configure with new level
128
+ # Cast to proper type for type checker
129
+ log_level_literal = cast(
130
+ Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
131
+ level.upper(),
132
+ )
133
+ configure_logging(
134
+ level=log_level_literal,
135
+ logger=logger,
136
+ enable_rich_tracebacks=enable_rich_tracebacks,
137
+ **rich_kwargs,
138
+ )
139
+ try:
140
+ yield
141
+ finally:
142
+ # Restore original configuration using configure_logging
143
+ # This will respect the log_enabled setting
144
+ configure_logging(
145
+ level=original_level,
146
+ logger=logger,
147
+ enable_rich_tracebacks=enable_rich_tracebacks,
148
+ **rich_kwargs,
149
+ )
150
+ else:
151
+ yield
152
+
153
+
154
+ _level_to_no: dict[
155
+ Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None, int | None
156
+ ] = {
157
+ "DEBUG": logging.DEBUG,
158
+ "INFO": logging.INFO,
159
+ "WARNING": logging.WARNING,
160
+ "ERROR": logging.ERROR,
161
+ "CRITICAL": logging.CRITICAL,
162
+ None: None,
163
+ }
164
+
165
+
166
+ class _ClampedLogFilter(logging.Filter):
167
+ min_level: tuple[int, str] | None
168
+ max_level: tuple[int, str] | None
169
+
170
+ def __init__(
171
+ self,
172
+ min_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
173
+ | None = None,
174
+ max_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
175
+ | None = None,
176
+ ):
177
+ self.min_level = None
178
+ self.max_level = None
179
+
180
+ if min_level_no := _level_to_no.get(min_level):
181
+ self.min_level = (min_level_no, str(min_level))
182
+ if max_level_no := _level_to_no.get(max_level):
183
+ self.max_level = (max_level_no, str(max_level))
184
+
185
+ super().__init__()
186
+
187
+ @override
188
+ def filter(self, record: logging.LogRecord) -> bool:
189
+ if self.max_level:
190
+ max_level_no, max_level_name = self.max_level
191
+
192
+ if record.levelno > max_level_no:
193
+ record.levelno = max_level_no
194
+ record.levelname = max_level_name
195
+ return True
196
+
197
+ if self.min_level:
198
+ min_level_no, min_level_name = self.min_level
199
+ if record.levelno < min_level_no:
200
+ record.levelno = min_level_no
201
+ record.levelname = min_level_name
202
+ return True
203
+
204
+ return True
205
+
206
+
207
+ def _clamp_logger(
208
+ logger: logging.Logger,
209
+ min_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
210
+ max_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
211
+ ) -> None:
212
+ """Clamp the logger to a minimum and maximum level.
213
+
214
+ If min_level is provided, messages logged at a lower level than `min_level` will have their level increased to `min_level`.
215
+ If max_level is provided, messages logged at a higher level than `max_level` will have their level decreased to `max_level`.
216
+
217
+ Args:
218
+ min_level: The lower bound of the clamp
219
+ max_level: The upper bound of the clamp
220
+ """
221
+ _unclamp_logger(logger=logger)
222
+
223
+ logger.addFilter(filter=_ClampedLogFilter(min_level=min_level, max_level=max_level))
224
+
225
+
226
+ def _unclamp_logger(logger: logging.Logger) -> None:
227
+ """Remove all clamped log filters from the logger."""
228
+ for filter in logger.filters[:]:
229
+ if isinstance(filter, _ClampedLogFilter):
230
+ logger.removeFilter(filter)
@@ -15,11 +15,11 @@ from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
15
15
  from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
16
16
 
17
17
  __all__ = [
18
- "Source",
19
18
  "Deployment",
20
19
  "Environment",
21
- "UVEnvironment",
22
- "MCPServerConfig",
23
20
  "FileSystemSource",
21
+ "MCPServerConfig",
22
+ "Source",
23
+ "UVEnvironment",
24
24
  "generate_schema",
25
25
  ]
@@ -19,7 +19,6 @@ class Environment(BaseModel, ABC):
19
19
  Returns:
20
20
  Full command ready for subprocess execution
21
21
  """
22
- pass
23
22
 
24
23
  async def prepare(self, output_dir: Path | None = None) -> None:
25
24
  """Prepare the environment (optional, can be no-op).
@@ -27,4 +26,4 @@ class Environment(BaseModel, ABC):
27
26
  Args:
28
27
  output_dir: Directory for persistent environment setup
29
28
  """
30
- pass # Default no-op implementation
29
+ # Default no-op implementation