fastmcp 2.12.5__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 (108) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +11 -11
  3. fastmcp/cli/install/claude_code.py +6 -6
  4. fastmcp/cli/install/claude_desktop.py +3 -3
  5. fastmcp/cli/install/cursor.py +18 -12
  6. fastmcp/cli/install/gemini_cli.py +3 -3
  7. fastmcp/cli/install/mcp_json.py +3 -3
  8. fastmcp/cli/run.py +13 -8
  9. fastmcp/client/__init__.py +9 -9
  10. fastmcp/client/auth/oauth.py +115 -217
  11. fastmcp/client/client.py +105 -39
  12. fastmcp/client/logging.py +18 -14
  13. fastmcp/client/oauth_callback.py +85 -171
  14. fastmcp/client/sampling.py +1 -1
  15. fastmcp/client/transports.py +80 -25
  16. fastmcp/contrib/component_manager/__init__.py +1 -1
  17. fastmcp/contrib/component_manager/component_manager.py +2 -2
  18. fastmcp/contrib/component_manager/component_service.py +6 -6
  19. fastmcp/contrib/mcp_mixin/README.md +32 -1
  20. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  21. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  22. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  23. fastmcp/experimental/server/openapi/__init__.py +5 -8
  24. fastmcp/experimental/server/openapi/components.py +11 -7
  25. fastmcp/experimental/server/openapi/routing.py +2 -2
  26. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  27. fastmcp/experimental/utilities/openapi/director.py +14 -15
  28. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  29. fastmcp/experimental/utilities/openapi/models.py +3 -3
  30. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  31. fastmcp/experimental/utilities/openapi/schemas.py +2 -2
  32. fastmcp/mcp_config.py +3 -4
  33. fastmcp/prompts/__init__.py +1 -1
  34. fastmcp/prompts/prompt.py +22 -19
  35. fastmcp/prompts/prompt_manager.py +16 -101
  36. fastmcp/resources/__init__.py +5 -5
  37. fastmcp/resources/resource.py +14 -9
  38. fastmcp/resources/resource_manager.py +9 -168
  39. fastmcp/resources/template.py +107 -17
  40. fastmcp/resources/types.py +30 -24
  41. fastmcp/server/__init__.py +1 -1
  42. fastmcp/server/auth/__init__.py +9 -5
  43. fastmcp/server/auth/auth.py +70 -43
  44. fastmcp/server/auth/handlers/authorize.py +326 -0
  45. fastmcp/server/auth/jwt_issuer.py +236 -0
  46. fastmcp/server/auth/middleware.py +96 -0
  47. fastmcp/server/auth/oauth_proxy.py +1510 -289
  48. fastmcp/server/auth/oidc_proxy.py +84 -20
  49. fastmcp/server/auth/providers/auth0.py +40 -21
  50. fastmcp/server/auth/providers/aws.py +29 -3
  51. fastmcp/server/auth/providers/azure.py +312 -131
  52. fastmcp/server/auth/providers/bearer.py +1 -1
  53. fastmcp/server/auth/providers/debug.py +114 -0
  54. fastmcp/server/auth/providers/descope.py +86 -29
  55. fastmcp/server/auth/providers/discord.py +308 -0
  56. fastmcp/server/auth/providers/github.py +29 -8
  57. fastmcp/server/auth/providers/google.py +48 -9
  58. fastmcp/server/auth/providers/in_memory.py +27 -3
  59. fastmcp/server/auth/providers/introspection.py +281 -0
  60. fastmcp/server/auth/providers/jwt.py +48 -31
  61. fastmcp/server/auth/providers/oci.py +233 -0
  62. fastmcp/server/auth/providers/scalekit.py +238 -0
  63. fastmcp/server/auth/providers/supabase.py +188 -0
  64. fastmcp/server/auth/providers/workos.py +35 -17
  65. fastmcp/server/context.py +177 -51
  66. fastmcp/server/dependencies.py +39 -12
  67. fastmcp/server/elicitation.py +1 -1
  68. fastmcp/server/http.py +56 -17
  69. fastmcp/server/low_level.py +121 -2
  70. fastmcp/server/middleware/__init__.py +1 -1
  71. fastmcp/server/middleware/caching.py +476 -0
  72. fastmcp/server/middleware/error_handling.py +14 -10
  73. fastmcp/server/middleware/logging.py +50 -39
  74. fastmcp/server/middleware/middleware.py +29 -16
  75. fastmcp/server/middleware/rate_limiting.py +3 -3
  76. fastmcp/server/middleware/tool_injection.py +116 -0
  77. fastmcp/server/openapi.py +10 -6
  78. fastmcp/server/proxy.py +22 -11
  79. fastmcp/server/server.py +725 -242
  80. fastmcp/settings.py +24 -10
  81. fastmcp/tools/__init__.py +1 -1
  82. fastmcp/tools/tool.py +70 -23
  83. fastmcp/tools/tool_manager.py +30 -112
  84. fastmcp/tools/tool_transform.py +12 -10
  85. fastmcp/utilities/cli.py +67 -28
  86. fastmcp/utilities/components.py +7 -2
  87. fastmcp/utilities/inspect.py +79 -23
  88. fastmcp/utilities/json_schema.py +4 -4
  89. fastmcp/utilities/json_schema_type.py +4 -4
  90. fastmcp/utilities/logging.py +118 -8
  91. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  92. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  93. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  94. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +4 -4
  95. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  96. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  97. fastmcp/utilities/openapi.py +11 -11
  98. fastmcp/utilities/tests.py +85 -4
  99. fastmcp/utilities/types.py +78 -16
  100. fastmcp/utilities/ui.py +626 -0
  101. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/METADATA +22 -14
  102. fastmcp-2.13.2.dist-info/RECORD +144 -0
  103. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  104. fastmcp/cli/claude.py +0 -135
  105. fastmcp/utilities/storage.py +0 -204
  106. fastmcp-2.12.5.dist-info/RECORD +0 -134
  107. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  108. {fastmcp-2.12.5.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/utilities/cli.py CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  import os
5
- from importlib.metadata import version
6
5
  from pathlib import Path
7
6
  from typing import TYPE_CHECKING, Any, Literal
8
7
 
@@ -138,7 +137,7 @@ def load_and_merge_config(
138
137
  return new_config, resolved_spec
139
138
 
140
139
 
141
- LOGO_ASCII = r"""
140
+ LOGO_ASCII_1 = r"""
142
141
  _ __ ___ _____ __ __ _____________ ____ ____
143
142
  _ __ ___ .'____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
144
143
  _ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
@@ -147,6 +146,56 @@ _ __ ___ /_/ \____/____/\__/_/ /_/\____/_/ /_____(*)____/
147
146
 
148
147
  """.lstrip("\n")
149
148
 
149
+ # This prints the below in a blue gradient
150
+ # █▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
151
+ # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
152
+ LOGO_ASCII_2 = (
153
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m▀\x1b[38;2;0;186;255m "
154
+ "\x1b[38;2;0;184;255m▄\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
155
+ "\x1b[38;2;0;172;255m█\x1b[38;2;0;169;255m▀\x1b[38;2;0;166;255m▀\x1b[38;2;0;163;255m "
156
+ "\x1b[38;2;0;160;255m▀\x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m▀\x1b[38;2;0;152;255m "
157
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m▀\x1b[38;2;0;143;255m▄\x1b[38;2;0;140;255m▀\x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
158
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▀\x1b[38;2;0;126;255m▀\x1b[38;2;0;123;255m "
159
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m█\x1b[39m\n"
160
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m \x1b[38;2;0;186;255m "
161
+ "\x1b[38;2;0;184;255m█\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
162
+ "\x1b[38;2;0;172;255m▄\x1b[38;2;0;169;255m▄\x1b[38;2;0;166;255m█\x1b[38;2;0;163;255m "
163
+ "\x1b[38;2;0;160;255m \x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m \x1b[38;2;0;152;255m "
164
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m \x1b[38;2;0;143;255m▀\x1b[38;2;0;140;255m \x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
165
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▄\x1b[38;2;0;126;255m▄\x1b[38;2;0;123;255m "
166
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m▀\x1b[39m"
167
+ ).strip()
168
+
169
+ # Prints the below in a blue gradient - sylized F
170
+ # ▄▀▀▀
171
+ # █▀▀
172
+ # ▀
173
+ LOGO_ASCII_3 = (
174
+ " \x1b[38;2;0;170;255m▄\x1b[38;2;0;142;255m▀\x1b[38;2;0;114;255m▀\x1b[38;2;0;86;255m▀\x1b[39m\n"
175
+ " \x1b[38;2;0;170;255m█\x1b[38;2;0;142;255m▀\x1b[38;2;0;114;255m▀\x1b[39m\n"
176
+ "\x1b[38;2;0;170;255m▀\x1b[39m\n"
177
+ "\x1b[0m"
178
+ )
179
+
180
+ # Prints the below in a blue gradient - block logo with slightly stylized F
181
+ # ▄▀▀ ▄▀█ █▀▀ ▀█▀ █▀▄▀█ █▀▀ █▀█
182
+ # █▀ █▀█ ▄▄█ █ █ ▀ █ █▄▄ █▀▀
183
+
184
+ LOGO_ASCII_4 = (
185
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m▄\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m▀\x1b[38;2;0;186;255m \x1b[38;2;0;184;255m▄\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
186
+ "\x1b[38;2;0;172;255m█\x1b[38;2;0;169;255m▀\x1b[38;2;0;166;255m▀\x1b[38;2;0;163;255m "
187
+ "\x1b[38;2;0;160;255m▀\x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m▀\x1b[38;2;0;152;255m "
188
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m▀\x1b[38;2;0;143;255m▄\x1b[38;2;0;140;255m▀\x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
189
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▀\x1b[38;2;0;126;255m▀\x1b[38;2;0;123;255m "
190
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m█\x1b[39m\n"
191
+ "\x1b[38;2;0;198;255m \x1b[38;2;0;195;255m█\x1b[38;2;0;192;255m▀\x1b[38;2;0;189;255m \x1b[38;2;0;186;255m \x1b[38;2;0;184;255m█\x1b[38;2;0;181;255m▀\x1b[38;2;0;178;255m█\x1b[38;2;0;175;255m "
192
+ "\x1b[38;2;0;172;255m▄\x1b[38;2;0;169;255m▄\x1b[38;2;0;166;255m█\x1b[38;2;0;163;255m "
193
+ "\x1b[38;2;0;160;255m \x1b[38;2;0;157;255m█\x1b[38;2;0;155;255m \x1b[38;2;0;152;255m "
194
+ "\x1b[38;2;0;149;255m█\x1b[38;2;0;146;255m \x1b[38;2;0;143;255m▀\x1b[38;2;0;140;255m \x1b[38;2;0;137;255m█\x1b[38;2;0;134;255m "
195
+ "\x1b[38;2;0;131;255m█\x1b[38;2;0;128;255m▄\x1b[38;2;0;126;255m▄\x1b[38;2;0;123;255m "
196
+ "\x1b[38;2;0;120;255m█\x1b[38;2;0;117;255m▀\x1b[38;2;0;114;255m▀\x1b[39m\n"
197
+ )
198
+
150
199
 
151
200
  def log_server_banner(
152
201
  server: FastMCP[Any],
@@ -167,10 +216,11 @@ def log_server_banner(
167
216
  """
168
217
 
169
218
  # Create the logo text
170
- logo_text = Text(LOGO_ASCII, style="bold green")
219
+ # Use Text with no_wrap and markup disabled to preserve ANSI escape codes
220
+ logo_text = Text.from_ansi(LOGO_ASCII_4, no_wrap=True)
171
221
 
172
222
  # Create the main title
173
- title_text = Text("FastMCP 2.0", style="bold blue")
223
+ title_text = Text(f"FastMCP {fastmcp.__version__}", style="bold blue")
174
224
 
175
225
  # Create the information table
176
226
  info_table = Table.grid(padding=(0, 1))
@@ -180,44 +230,31 @@ def log_server_banner(
180
230
 
181
231
  match transport:
182
232
  case "http" | "streamable-http":
183
- display_transport = "Streamable-HTTP"
233
+ display_transport = "HTTP"
184
234
  case "sse":
185
235
  display_transport = "SSE"
186
236
  case "stdio":
187
237
  display_transport = "STDIO"
188
238
 
189
- info_table.add_row("🖥️", "Server name:", server.name)
239
+ info_table.add_row("🖥", "Server name:", Text(server.name + "\n", style="bold blue"))
190
240
  info_table.add_row("📦", "Transport:", display_transport)
191
241
 
192
242
  # Show connection info based on transport
193
- if transport in ("http", "streamable-http", "sse"):
194
- if host and port:
195
- server_url = f"http://{host}:{port}"
196
- if path:
197
- server_url += f"/{path.lstrip('/')}"
198
- info_table.add_row("🔗", "Server URL:", server_url)
199
-
200
- # Add version information with explicit style overrides
201
- info_table.add_row("", "", "")
202
- info_table.add_row(
203
- "🏎️",
204
- "FastMCP version:",
205
- Text(fastmcp.__version__, style="dim white", no_wrap=True),
206
- )
207
- info_table.add_row(
208
- "🤝",
209
- "MCP SDK version:",
210
- Text(version("mcp"), style="dim white", no_wrap=True),
211
- )
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)
212
248
 
213
249
  # Add documentation link
214
250
  info_table.add_row("", "", "")
215
251
  info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
216
- info_table.add_row("🚀", "Deploy:", "https://fastmcp.cloud")
252
+ info_table.add_row("🚀", "Hosting:", "https://fastmcp.cloud")
217
253
 
218
254
  # Create panel with logo, title, and information using Group
219
255
  panel_content = Group(
220
256
  Align.center(logo_text),
257
+ "",
221
258
  Align.center(title_text),
222
259
  "",
223
260
  "",
@@ -228,8 +265,10 @@ def log_server_banner(
228
265
  panel_content,
229
266
  border_style="dim",
230
267
  padding=(1, 4),
231
- expand=False,
268
+ # expand=False,
269
+ width=80, # Set max width for the panel
232
270
  )
233
271
 
234
272
  console = Console(stderr=True)
235
- console.print(Group("\n", panel, "\n"))
273
+ # Center the panel itself
274
+ console.print(Group("\n", Align.center(panel), "\n"))
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from collections.abc import Sequence
4
- from typing import Annotated, Any, TypedDict
4
+ from typing import Annotated, Any, TypedDict, cast
5
5
 
6
+ from mcp.types import Icon
6
7
  from pydantic import BeforeValidator, Field, PrivateAttr
7
8
  from typing_extensions import Self, TypeVar
8
9
 
@@ -39,6 +40,10 @@ class FastMCPComponent(FastMCPBaseModel):
39
40
  default=None,
40
41
  description="The description of the component.",
41
42
  )
43
+ icons: list[Icon] | None = Field(
44
+ default=None,
45
+ description="Optional list of icons for this component to display in user interfaces.",
46
+ )
42
47
  tags: Annotated[set[str], BeforeValidator(_convert_set_default_none)] = Field(
43
48
  default_factory=set,
44
49
  description="Tags for the component.",
@@ -112,7 +117,7 @@ class FastMCPComponent(FastMCPBaseModel):
112
117
  copy = super().model_copy(update=update, deep=deep)
113
118
  if key is not None:
114
119
  copy._key = key
115
- return copy
120
+ return cast(Self, copy)
116
121
 
117
122
  def __eq__(self, other: object) -> bool:
118
123
  if type(self) is not type(other):
@@ -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:
@@ -127,13 +127,13 @@ def _single_pass_optimize(
127
127
  "required",
128
128
  ]
129
129
  ):
130
- node.pop("title")
130
+ node.pop("title") # type: ignore
131
131
 
132
132
  if (
133
133
  prune_additional_properties
134
- and node.get("additionalProperties") is False
134
+ and node.get("additionalProperties") is False # type: ignore
135
135
  ):
136
- node.pop("additionalProperties")
136
+ node.pop("additionalProperties") # type: ignore
137
137
 
138
138
  # Recursive traversal
139
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
@@ -6,6 +6,7 @@ from typing import Any, Literal, cast
6
6
 
7
7
  from rich.console import Console
8
8
  from rich.logging import RichHandler
9
+ from typing_extensions import override
9
10
 
10
11
  import fastmcp
11
12
 
@@ -19,7 +20,10 @@ def get_logger(name: str) -> logging.Logger:
19
20
  Returns:
20
21
  a configured logger instance
21
22
  """
22
- 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}")
23
27
 
24
28
 
25
29
  def configure_logging(
@@ -47,25 +51,52 @@ def configure_logging(
47
51
  if logger is None:
48
52
  logger = logging.getLogger("fastmcp")
49
53
 
50
- # Only configure the FastMCP logger namespace
54
+ formatter = logging.Formatter("%(message)s")
55
+
56
+ # Don't propagate to the root logger
57
+ logger.propagate = False
58
+ logger.setLevel(level)
59
+
60
+ # Configure the handler for normal logs
51
61
  handler = RichHandler(
52
62
  console=Console(stderr=True),
53
- rich_tracebacks=enable_rich_tracebacks,
54
63
  **rich_kwargs,
55
64
  )
56
- formatter = logging.Formatter("%(message)s")
57
65
  handler.setFormatter(formatter)
58
66
 
59
- 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)
60
93
 
61
94
  # Remove any existing handlers to avoid duplicates on reconfiguration
62
95
  for hdlr in logger.handlers[:]:
63
96
  logger.removeHandler(hdlr)
64
97
 
65
98
  logger.addHandler(handler)
66
-
67
- # Don't propagate to the root logger
68
- logger.propagate = False
99
+ logger.addHandler(traceback_handler)
69
100
 
70
101
 
71
102
  @contextlib.contextmanager
@@ -118,3 +149,82 @@ def temporary_log_level(
118
149
  )
119
150
  else:
120
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)