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.
- fastmcp/__init__.py +2 -2
- fastmcp/cli/cli.py +56 -36
- fastmcp/cli/install/__init__.py +2 -0
- fastmcp/cli/install/claude_code.py +7 -16
- fastmcp/cli/install/claude_desktop.py +4 -12
- fastmcp/cli/install/cursor.py +20 -30
- fastmcp/cli/install/gemini_cli.py +241 -0
- fastmcp/cli/install/mcp_json.py +4 -12
- fastmcp/cli/run.py +15 -94
- fastmcp/client/__init__.py +9 -9
- fastmcp/client/auth/oauth.py +117 -206
- fastmcp/client/client.py +123 -47
- fastmcp/client/elicitation.py +6 -1
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +85 -171
- fastmcp/client/sampling.py +1 -1
- fastmcp/client/transports.py +81 -26
- fastmcp/contrib/component_manager/__init__.py +1 -1
- fastmcp/contrib/component_manager/component_manager.py +2 -2
- fastmcp/contrib/component_manager/component_service.py +7 -7
- fastmcp/contrib/mcp_mixin/README.md +35 -4
- fastmcp/contrib/mcp_mixin/__init__.py +2 -2
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
- fastmcp/experimental/sampling/handlers/openai.py +2 -2
- fastmcp/experimental/server/openapi/__init__.py +5 -8
- fastmcp/experimental/server/openapi/components.py +11 -7
- fastmcp/experimental/server/openapi/routing.py +2 -2
- fastmcp/experimental/utilities/openapi/__init__.py +10 -15
- fastmcp/experimental/utilities/openapi/director.py +16 -10
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
- fastmcp/experimental/utilities/openapi/models.py +3 -3
- fastmcp/experimental/utilities/openapi/parser.py +37 -16
- fastmcp/experimental/utilities/openapi/schemas.py +33 -7
- fastmcp/mcp_config.py +3 -4
- fastmcp/prompts/__init__.py +1 -1
- fastmcp/prompts/prompt.py +32 -27
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/__init__.py +5 -5
- fastmcp/resources/resource.py +28 -20
- fastmcp/resources/resource_manager.py +9 -168
- fastmcp/resources/template.py +119 -27
- fastmcp/resources/types.py +30 -24
- fastmcp/server/__init__.py +1 -1
- fastmcp/server/auth/__init__.py +9 -5
- fastmcp/server/auth/auth.py +80 -47
- fastmcp/server/auth/handlers/authorize.py +326 -0
- fastmcp/server/auth/jwt_issuer.py +236 -0
- fastmcp/server/auth/middleware.py +96 -0
- fastmcp/server/auth/oauth_proxy.py +1556 -265
- fastmcp/server/auth/oidc_proxy.py +412 -0
- fastmcp/server/auth/providers/auth0.py +193 -0
- fastmcp/server/auth/providers/aws.py +263 -0
- fastmcp/server/auth/providers/azure.py +314 -129
- fastmcp/server/auth/providers/bearer.py +1 -1
- fastmcp/server/auth/providers/debug.py +114 -0
- fastmcp/server/auth/providers/descope.py +229 -0
- fastmcp/server/auth/providers/discord.py +308 -0
- fastmcp/server/auth/providers/github.py +31 -6
- fastmcp/server/auth/providers/google.py +50 -7
- fastmcp/server/auth/providers/in_memory.py +27 -3
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +48 -31
- fastmcp/server/auth/providers/oci.py +233 -0
- fastmcp/server/auth/providers/scalekit.py +238 -0
- fastmcp/server/auth/providers/supabase.py +188 -0
- fastmcp/server/auth/providers/workos.py +37 -15
- fastmcp/server/context.py +194 -67
- fastmcp/server/dependencies.py +56 -16
- fastmcp/server/elicitation.py +1 -1
- fastmcp/server/http.py +57 -18
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/__init__.py +1 -1
- fastmcp/server/middleware/caching.py +476 -0
- fastmcp/server/middleware/error_handling.py +14 -10
- fastmcp/server/middleware/logging.py +158 -116
- fastmcp/server/middleware/middleware.py +30 -16
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/middleware/tool_injection.py +116 -0
- fastmcp/server/openapi.py +15 -7
- fastmcp/server/proxy.py +22 -11
- fastmcp/server/server.py +744 -254
- fastmcp/settings.py +65 -15
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/tool.py +173 -108
- fastmcp/tools/tool_manager.py +30 -112
- fastmcp/tools/tool_transform.py +13 -11
- fastmcp/utilities/cli.py +67 -28
- fastmcp/utilities/components.py +7 -2
- fastmcp/utilities/inspect.py +79 -23
- fastmcp/utilities/json_schema.py +21 -4
- fastmcp/utilities/json_schema_type.py +4 -4
- fastmcp/utilities/logging.py +182 -10
- fastmcp/utilities/mcp_server_config/__init__.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
- fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
- fastmcp/utilities/openapi.py +11 -11
- fastmcp/utilities/tests.py +93 -10
- fastmcp/utilities/types.py +87 -21
- fastmcp/utilities/ui.py +626 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
- fastmcp-2.13.2.dist-info/RECORD +144 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
- fastmcp/cli/claude.py +0 -144
- fastmcp-2.12.1.dist-info/RECORD +0 -128
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
fastmcp/utilities/inspect.py
CHANGED
|
@@ -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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
116
|
-
|
|
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
|
|
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
|
|
164
|
+
for resource in resources_list:
|
|
154
165
|
resource_infos.append(
|
|
155
166
|
ResourceInfo(
|
|
156
|
-
key=key,
|
|
157
|
-
uri=key,
|
|
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
|
|
187
|
+
for template in templates_list:
|
|
174
188
|
template_infos.append(
|
|
175
189
|
TemplateInfo(
|
|
176
|
-
key=key,
|
|
177
|
-
uri_template=key,
|
|
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
|
-
|
|
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
|
|
504
|
+
return format_fastmcp_info(info)
|
|
449
505
|
else:
|
|
450
506
|
raise ValueError(f"Unknown format: {format}")
|
fastmcp/utilities/json_schema.py
CHANGED
|
@@ -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
|
|
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__ = ["
|
|
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[
|
|
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[
|
|
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
|
-
|
|
581
|
+
cls._apply_defaults = _apply_defaults # type: ignore[attr-defined]
|
|
582
582
|
|
|
583
583
|
# Store completed class
|
|
584
584
|
_classes[cache_key] = cls
|
fastmcp/utilities/logging.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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("
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
29
|
+
# Default no-op implementation
|