fastmcp 2.14.4__py3-none-any.whl → 3.0.0b1__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/_vendor/__init__.py +1 -0
- fastmcp/_vendor/docket_di/README.md +7 -0
- fastmcp/_vendor/docket_di/__init__.py +163 -0
- fastmcp/cli/cli.py +112 -28
- fastmcp/cli/install/claude_code.py +1 -5
- fastmcp/cli/install/claude_desktop.py +1 -5
- fastmcp/cli/install/cursor.py +1 -5
- fastmcp/cli/install/gemini_cli.py +1 -5
- fastmcp/cli/install/mcp_json.py +1 -6
- fastmcp/cli/run.py +146 -5
- fastmcp/client/__init__.py +7 -9
- fastmcp/client/auth/oauth.py +18 -17
- fastmcp/client/client.py +100 -870
- fastmcp/client/elicitation.py +1 -1
- fastmcp/client/mixins/__init__.py +13 -0
- fastmcp/client/mixins/prompts.py +295 -0
- fastmcp/client/mixins/resources.py +325 -0
- fastmcp/client/mixins/task_management.py +157 -0
- fastmcp/client/mixins/tools.py +397 -0
- fastmcp/client/sampling/handlers/anthropic.py +2 -2
- fastmcp/client/sampling/handlers/openai.py +1 -1
- fastmcp/client/tasks.py +3 -3
- fastmcp/client/telemetry.py +47 -0
- fastmcp/client/transports/__init__.py +38 -0
- fastmcp/client/transports/base.py +82 -0
- fastmcp/client/transports/config.py +170 -0
- fastmcp/client/transports/http.py +145 -0
- fastmcp/client/transports/inference.py +154 -0
- fastmcp/client/transports/memory.py +90 -0
- fastmcp/client/transports/sse.py +89 -0
- fastmcp/client/transports/stdio.py +543 -0
- fastmcp/contrib/component_manager/README.md +4 -10
- fastmcp/contrib/component_manager/__init__.py +1 -2
- fastmcp/contrib/component_manager/component_manager.py +95 -160
- fastmcp/contrib/component_manager/example.py +1 -1
- fastmcp/contrib/mcp_mixin/example.py +4 -4
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +11 -4
- fastmcp/decorators.py +41 -0
- fastmcp/dependencies.py +12 -1
- fastmcp/exceptions.py +4 -0
- fastmcp/experimental/server/openapi/__init__.py +18 -15
- fastmcp/mcp_config.py +13 -4
- fastmcp/prompts/__init__.py +6 -3
- fastmcp/prompts/function_prompt.py +465 -0
- fastmcp/prompts/prompt.py +321 -271
- fastmcp/resources/__init__.py +5 -3
- fastmcp/resources/function_resource.py +335 -0
- fastmcp/resources/resource.py +325 -115
- fastmcp/resources/template.py +215 -43
- fastmcp/resources/types.py +27 -12
- fastmcp/server/__init__.py +2 -2
- fastmcp/server/auth/__init__.py +14 -0
- fastmcp/server/auth/auth.py +30 -10
- fastmcp/server/auth/authorization.py +190 -0
- fastmcp/server/auth/oauth_proxy/__init__.py +14 -0
- fastmcp/server/auth/oauth_proxy/consent.py +361 -0
- fastmcp/server/auth/oauth_proxy/models.py +178 -0
- fastmcp/server/auth/{oauth_proxy.py → oauth_proxy/proxy.py} +24 -778
- fastmcp/server/auth/oauth_proxy/ui.py +277 -0
- fastmcp/server/auth/oidc_proxy.py +2 -2
- fastmcp/server/auth/providers/auth0.py +24 -94
- fastmcp/server/auth/providers/aws.py +26 -95
- fastmcp/server/auth/providers/azure.py +41 -129
- fastmcp/server/auth/providers/descope.py +18 -49
- fastmcp/server/auth/providers/discord.py +25 -86
- fastmcp/server/auth/providers/github.py +23 -87
- fastmcp/server/auth/providers/google.py +24 -87
- fastmcp/server/auth/providers/introspection.py +60 -79
- fastmcp/server/auth/providers/jwt.py +30 -67
- fastmcp/server/auth/providers/oci.py +47 -110
- fastmcp/server/auth/providers/scalekit.py +23 -61
- fastmcp/server/auth/providers/supabase.py +18 -47
- fastmcp/server/auth/providers/workos.py +34 -127
- fastmcp/server/context.py +372 -419
- fastmcp/server/dependencies.py +541 -251
- fastmcp/server/elicitation.py +20 -18
- fastmcp/server/event_store.py +3 -3
- fastmcp/server/http.py +16 -6
- fastmcp/server/lifespan.py +198 -0
- fastmcp/server/low_level.py +92 -2
- fastmcp/server/middleware/__init__.py +5 -1
- fastmcp/server/middleware/authorization.py +312 -0
- fastmcp/server/middleware/caching.py +101 -54
- fastmcp/server/middleware/middleware.py +6 -9
- fastmcp/server/middleware/ping.py +70 -0
- fastmcp/server/middleware/tool_injection.py +2 -2
- fastmcp/server/mixins/__init__.py +7 -0
- fastmcp/server/mixins/lifespan.py +217 -0
- fastmcp/server/mixins/mcp_operations.py +392 -0
- fastmcp/server/mixins/transport.py +342 -0
- fastmcp/server/openapi/__init__.py +41 -21
- fastmcp/server/openapi/components.py +16 -339
- fastmcp/server/openapi/routing.py +34 -118
- fastmcp/server/openapi/server.py +67 -392
- fastmcp/server/providers/__init__.py +71 -0
- fastmcp/server/providers/aggregate.py +261 -0
- fastmcp/server/providers/base.py +578 -0
- fastmcp/server/providers/fastmcp_provider.py +674 -0
- fastmcp/server/providers/filesystem.py +226 -0
- fastmcp/server/providers/filesystem_discovery.py +327 -0
- fastmcp/server/providers/local_provider/__init__.py +11 -0
- fastmcp/server/providers/local_provider/decorators/__init__.py +15 -0
- fastmcp/server/providers/local_provider/decorators/prompts.py +256 -0
- fastmcp/server/providers/local_provider/decorators/resources.py +240 -0
- fastmcp/server/providers/local_provider/decorators/tools.py +315 -0
- fastmcp/server/providers/local_provider/local_provider.py +465 -0
- fastmcp/server/providers/openapi/__init__.py +39 -0
- fastmcp/server/providers/openapi/components.py +332 -0
- fastmcp/server/providers/openapi/provider.py +405 -0
- fastmcp/server/providers/openapi/routing.py +109 -0
- fastmcp/server/providers/proxy.py +867 -0
- fastmcp/server/providers/skills/__init__.py +59 -0
- fastmcp/server/providers/skills/_common.py +101 -0
- fastmcp/server/providers/skills/claude_provider.py +44 -0
- fastmcp/server/providers/skills/directory_provider.py +153 -0
- fastmcp/server/providers/skills/skill_provider.py +432 -0
- fastmcp/server/providers/skills/vendor_providers.py +142 -0
- fastmcp/server/providers/wrapped_provider.py +140 -0
- fastmcp/server/proxy.py +34 -700
- fastmcp/server/sampling/run.py +341 -2
- fastmcp/server/sampling/sampling_tool.py +4 -3
- fastmcp/server/server.py +1214 -2171
- fastmcp/server/tasks/__init__.py +2 -1
- fastmcp/server/tasks/capabilities.py +13 -1
- fastmcp/server/tasks/config.py +66 -3
- fastmcp/server/tasks/handlers.py +65 -273
- fastmcp/server/tasks/keys.py +4 -6
- fastmcp/server/tasks/requests.py +474 -0
- fastmcp/server/tasks/routing.py +76 -0
- fastmcp/server/tasks/subscriptions.py +20 -11
- fastmcp/server/telemetry.py +131 -0
- fastmcp/server/transforms/__init__.py +244 -0
- fastmcp/server/transforms/namespace.py +193 -0
- fastmcp/server/transforms/prompts_as_tools.py +175 -0
- fastmcp/server/transforms/resources_as_tools.py +190 -0
- fastmcp/server/transforms/tool_transform.py +96 -0
- fastmcp/server/transforms/version_filter.py +124 -0
- fastmcp/server/transforms/visibility.py +526 -0
- fastmcp/settings.py +34 -96
- fastmcp/telemetry.py +122 -0
- fastmcp/tools/__init__.py +10 -3
- fastmcp/tools/function_parsing.py +201 -0
- fastmcp/tools/function_tool.py +467 -0
- fastmcp/tools/tool.py +215 -362
- fastmcp/tools/tool_transform.py +38 -21
- fastmcp/utilities/async_utils.py +69 -0
- fastmcp/utilities/components.py +152 -91
- fastmcp/utilities/inspect.py +8 -20
- fastmcp/utilities/json_schema.py +12 -5
- fastmcp/utilities/json_schema_type.py +17 -15
- fastmcp/utilities/lifespan.py +56 -0
- fastmcp/utilities/logging.py +12 -4
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/openapi/parser.py +3 -3
- fastmcp/utilities/pagination.py +80 -0
- fastmcp/utilities/skills.py +253 -0
- fastmcp/utilities/tests.py +0 -16
- fastmcp/utilities/timeout.py +47 -0
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/versions.py +285 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/METADATA +8 -5
- fastmcp-3.0.0b1.dist-info/RECORD +228 -0
- fastmcp/client/transports.py +0 -1170
- fastmcp/contrib/component_manager/component_service.py +0 -209
- fastmcp/prompts/prompt_manager.py +0 -117
- fastmcp/resources/resource_manager.py +0 -338
- fastmcp/server/tasks/converters.py +0 -206
- fastmcp/server/tasks/protocol.py +0 -359
- fastmcp/tools/tool_manager.py +0 -170
- fastmcp/utilities/mcp_config.py +0 -56
- fastmcp-2.14.4.dist-info/RECORD +0 -161
- /fastmcp/server/{openapi → providers/openapi}/README.md +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/WHEEL +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.14.4.dist-info → fastmcp-3.0.0b1.dist-info}/licenses/LICENSE +0 -0
fastmcp/utilities/inspect.py
CHANGED
|
@@ -26,7 +26,6 @@ class ToolInfo:
|
|
|
26
26
|
output_schema: dict[str, Any] | None = None
|
|
27
27
|
annotations: dict[str, Any] | None = None
|
|
28
28
|
tags: list[str] | None = None
|
|
29
|
-
enabled: bool | None = None
|
|
30
29
|
title: str | None = None
|
|
31
30
|
icons: list[dict[str, Any]] | None = None
|
|
32
31
|
meta: dict[str, Any] | None = None
|
|
@@ -41,7 +40,6 @@ class PromptInfo:
|
|
|
41
40
|
description: str | None
|
|
42
41
|
arguments: list[dict[str, Any]] | None = None
|
|
43
42
|
tags: list[str] | None = None
|
|
44
|
-
enabled: bool | None = None
|
|
45
43
|
title: str | None = None
|
|
46
44
|
icons: list[dict[str, Any]] | None = None
|
|
47
45
|
meta: dict[str, Any] | None = None
|
|
@@ -58,7 +56,6 @@ class ResourceInfo:
|
|
|
58
56
|
mime_type: str | None = None
|
|
59
57
|
annotations: dict[str, Any] | None = None
|
|
60
58
|
tags: list[str] | None = None
|
|
61
|
-
enabled: bool | None = None
|
|
62
59
|
title: str | None = None
|
|
63
60
|
icons: list[dict[str, Any]] | None = None
|
|
64
61
|
meta: dict[str, Any] | None = None
|
|
@@ -76,7 +73,6 @@ class TemplateInfo:
|
|
|
76
73
|
parameters: dict[str, Any] | None = None
|
|
77
74
|
annotations: dict[str, Any] | None = None
|
|
78
75
|
tags: list[str] | None = None
|
|
79
|
-
enabled: bool | None = None
|
|
80
76
|
title: str | None = None
|
|
81
77
|
icons: list[dict[str, Any]] | None = None
|
|
82
78
|
meta: dict[str, Any] | None = None
|
|
@@ -110,16 +106,16 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
110
106
|
Returns:
|
|
111
107
|
FastMCPInfo dataclass containing the extracted information
|
|
112
108
|
"""
|
|
113
|
-
# Get all components
|
|
114
|
-
tools_list = await mcp.
|
|
115
|
-
prompts_list = await mcp.
|
|
116
|
-
resources_list = await mcp.
|
|
117
|
-
templates_list = await mcp.
|
|
109
|
+
# Get all components (list_* includes middleware, enabled/auth filtering)
|
|
110
|
+
tools_list = await mcp.list_tools()
|
|
111
|
+
prompts_list = await mcp.list_prompts()
|
|
112
|
+
resources_list = await mcp.list_resources()
|
|
113
|
+
templates_list = await mcp.list_resource_templates()
|
|
118
114
|
|
|
119
115
|
# Extract detailed tool information
|
|
120
116
|
tool_infos = []
|
|
121
117
|
for tool in tools_list:
|
|
122
|
-
mcp_tool = tool.to_mcp_tool(name=tool.
|
|
118
|
+
mcp_tool = tool.to_mcp_tool(name=tool.name)
|
|
123
119
|
tool_infos.append(
|
|
124
120
|
ToolInfo(
|
|
125
121
|
key=tool.key,
|
|
@@ -129,7 +125,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
129
125
|
output_schema=tool.output_schema,
|
|
130
126
|
annotations=tool.annotations.model_dump() if tool.annotations else None,
|
|
131
127
|
tags=list(tool.tags) if tool.tags else None,
|
|
132
|
-
enabled=tool.enabled,
|
|
133
128
|
title=tool.title,
|
|
134
129
|
icons=[icon.model_dump() for icon in tool.icons]
|
|
135
130
|
if tool.icons
|
|
@@ -150,7 +145,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
150
145
|
if prompt.arguments
|
|
151
146
|
else None,
|
|
152
147
|
tags=list(prompt.tags) if prompt.tags else None,
|
|
153
|
-
enabled=prompt.enabled,
|
|
154
148
|
title=prompt.title,
|
|
155
149
|
icons=[icon.model_dump() for icon in prompt.icons]
|
|
156
150
|
if prompt.icons
|
|
@@ -165,7 +159,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
165
159
|
resource_infos.append(
|
|
166
160
|
ResourceInfo(
|
|
167
161
|
key=resource.key,
|
|
168
|
-
uri=resource.
|
|
162
|
+
uri=str(resource.uri),
|
|
169
163
|
name=resource.name,
|
|
170
164
|
description=resource.description,
|
|
171
165
|
mime_type=resource.mime_type,
|
|
@@ -173,7 +167,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
173
167
|
if resource.annotations
|
|
174
168
|
else None,
|
|
175
169
|
tags=list(resource.tags) if resource.tags else None,
|
|
176
|
-
enabled=resource.enabled,
|
|
177
170
|
title=resource.title,
|
|
178
171
|
icons=[icon.model_dump() for icon in resource.icons]
|
|
179
172
|
if resource.icons
|
|
@@ -188,7 +181,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
188
181
|
template_infos.append(
|
|
189
182
|
TemplateInfo(
|
|
190
183
|
key=template.key,
|
|
191
|
-
uri_template=template.
|
|
184
|
+
uri_template=template.uri_template,
|
|
192
185
|
name=template.name,
|
|
193
186
|
description=template.description,
|
|
194
187
|
mime_type=template.mime_type,
|
|
@@ -197,7 +190,6 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
197
190
|
if template.annotations
|
|
198
191
|
else None,
|
|
199
192
|
tags=list(template.tags) if template.tags else None,
|
|
200
|
-
enabled=template.enabled,
|
|
201
193
|
title=template.title,
|
|
202
194
|
icons=[icon.model_dump() for icon in template.icons]
|
|
203
195
|
if template.icons
|
|
@@ -275,7 +267,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
275
267
|
output_schema=None, # v1 doesn't have output_schema
|
|
276
268
|
annotations=None, # v1 doesn't have annotations
|
|
277
269
|
tags=None, # v1 doesn't have tags
|
|
278
|
-
enabled=None, # v1 doesn't have enabled field
|
|
279
270
|
title=None, # v1 doesn't have title
|
|
280
271
|
icons=[icon.model_dump() for icon in mcp_tool.icons]
|
|
281
272
|
if hasattr(mcp_tool, "icons") and mcp_tool.icons
|
|
@@ -299,7 +290,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
299
290
|
description=mcp_prompt.description,
|
|
300
291
|
arguments=arguments,
|
|
301
292
|
tags=None, # v1 doesn't have tags
|
|
302
|
-
enabled=None, # v1 doesn't have enabled field
|
|
303
293
|
title=None, # v1 doesn't have title
|
|
304
294
|
icons=[icon.model_dump() for icon in mcp_prompt.icons]
|
|
305
295
|
if hasattr(mcp_prompt, "icons") and mcp_prompt.icons
|
|
@@ -320,7 +310,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
320
310
|
mime_type=mcp_resource.mimeType,
|
|
321
311
|
annotations=None, # v1 doesn't have annotations
|
|
322
312
|
tags=None, # v1 doesn't have tags
|
|
323
|
-
enabled=None, # v1 doesn't have enabled field
|
|
324
313
|
title=None, # v1 doesn't have title
|
|
325
314
|
icons=[icon.model_dump() for icon in mcp_resource.icons]
|
|
326
315
|
if hasattr(mcp_resource, "icons") and mcp_resource.icons
|
|
@@ -342,7 +331,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
342
331
|
parameters=None, # v1 doesn't expose template parameters
|
|
343
332
|
annotations=None, # v1 doesn't have annotations
|
|
344
333
|
tags=None, # v1 doesn't have tags
|
|
345
|
-
enabled=None, # v1 doesn't have enabled field
|
|
346
334
|
title=None, # v1 doesn't have title
|
|
347
335
|
icons=[icon.model_dump() for icon in mcp_template.icons]
|
|
348
336
|
if hasattr(mcp_template, "icons") and mcp_template.icons
|
fastmcp/utilities/json_schema.py
CHANGED
|
@@ -364,31 +364,38 @@ def _single_pass_optimize(
|
|
|
364
364
|
def compress_schema(
|
|
365
365
|
schema: dict[str, Any],
|
|
366
366
|
prune_params: list[str] | None = None,
|
|
367
|
-
prune_defs: bool = True,
|
|
368
367
|
prune_additional_properties: bool = True,
|
|
369
368
|
prune_titles: bool = False,
|
|
370
369
|
) -> dict[str, Any]:
|
|
371
370
|
"""
|
|
372
|
-
|
|
371
|
+
Compress and optimize a JSON schema for MCP compatibility.
|
|
372
|
+
|
|
373
|
+
This function dereferences all $ref entries (inlining definitions) to ensure
|
|
374
|
+
compatibility with MCP clients that don't properly handle $ref in schemas
|
|
375
|
+
(e.g., VS Code Copilot). It also applies various optimizations to reduce
|
|
376
|
+
schema size.
|
|
373
377
|
|
|
374
378
|
Args:
|
|
375
379
|
schema: The schema to compress
|
|
376
380
|
prune_params: List of parameter names to remove from properties
|
|
377
|
-
prune_defs: Whether to remove unused definitions
|
|
378
381
|
prune_additional_properties: Whether to remove additionalProperties: false
|
|
379
382
|
prune_titles: Whether to remove title fields from the schema
|
|
380
383
|
"""
|
|
384
|
+
# Dereference $ref - this inlines all definitions and removes $defs
|
|
385
|
+
# Required for MCP client compatibility
|
|
386
|
+
schema = dereference_refs(schema)
|
|
387
|
+
|
|
381
388
|
# Remove specific parameters if requested
|
|
382
389
|
for param in prune_params or []:
|
|
383
390
|
schema = _prune_param(schema, param=param)
|
|
384
391
|
|
|
385
392
|
# Apply combined optimizations in a single tree traversal
|
|
386
|
-
if prune_titles or prune_additional_properties
|
|
393
|
+
if prune_titles or prune_additional_properties:
|
|
387
394
|
schema = _single_pass_optimize(
|
|
388
395
|
schema,
|
|
389
396
|
prune_titles=prune_titles,
|
|
390
397
|
prune_additional_properties=prune_additional_properties,
|
|
391
|
-
prune_defs=
|
|
398
|
+
prune_defs=False,
|
|
392
399
|
)
|
|
393
400
|
|
|
394
401
|
return schema
|
|
@@ -47,6 +47,7 @@ from typing import (
|
|
|
47
47
|
ForwardRef,
|
|
48
48
|
Literal,
|
|
49
49
|
Union,
|
|
50
|
+
cast,
|
|
50
51
|
)
|
|
51
52
|
|
|
52
53
|
from pydantic import (
|
|
@@ -171,14 +172,15 @@ def json_schema_to_type(
|
|
|
171
172
|
if not schema.get("properties") and schema.get("additionalProperties"):
|
|
172
173
|
additional_props = schema["additionalProperties"]
|
|
173
174
|
if additional_props is True:
|
|
174
|
-
return dict[str, Any]
|
|
175
|
+
return dict[str, Any]
|
|
175
176
|
else:
|
|
176
177
|
# Handle typed dictionaries like dict[str, str]
|
|
177
178
|
value_type = _schema_to_type(additional_props, schemas=schema)
|
|
178
|
-
|
|
179
|
+
# value_type might be ForwardRef or type - cast to Any for dynamic type construction
|
|
180
|
+
return cast(type[Any], dict[str, value_type]) # type: ignore[valid-type]
|
|
179
181
|
# If no properties and no additionalProperties, default to dict[str, Any] for safety
|
|
180
182
|
elif not schema.get("properties") and not schema.get("additionalProperties"):
|
|
181
|
-
return dict[str, Any]
|
|
183
|
+
return dict[str, Any]
|
|
182
184
|
# If has properties AND additionalProperties is True, use Pydantic BaseModel
|
|
183
185
|
elif schema.get("properties") and schema.get("additionalProperties") is True:
|
|
184
186
|
return _create_pydantic_model(schema, name, schemas=schema)
|
|
@@ -265,13 +267,13 @@ def _create_array_type(
|
|
|
265
267
|
if isinstance(items, list):
|
|
266
268
|
# Handle positional item schemas
|
|
267
269
|
item_types = [_schema_to_type(s, schemas) for s in items]
|
|
268
|
-
combined = Union[tuple(item_types)] #
|
|
270
|
+
combined = Union[tuple(item_types)] # noqa: UP007
|
|
269
271
|
base = list[combined] # type: ignore[valid-type]
|
|
270
272
|
else:
|
|
271
273
|
# Handle single item schema
|
|
272
274
|
item_type = _schema_to_type(items, schemas)
|
|
273
275
|
base_class = set if schema.get("uniqueItems") else list
|
|
274
|
-
base = base_class[item_type]
|
|
276
|
+
base = base_class[item_type]
|
|
275
277
|
|
|
276
278
|
constraints = {
|
|
277
279
|
k: v
|
|
@@ -295,17 +297,17 @@ def _get_from_type_handler(
|
|
|
295
297
|
"""Get the appropriate type handler for the schema."""
|
|
296
298
|
|
|
297
299
|
type_handlers: dict[str, Callable[..., Any]] = { # TODO
|
|
298
|
-
"string": lambda s: _create_string_type(s),
|
|
299
|
-
"integer": lambda s: _create_numeric_type(int, s),
|
|
300
|
-
"number": lambda s: _create_numeric_type(float, s),
|
|
301
|
-
"boolean": lambda _: bool,
|
|
302
|
-
"null": lambda _: type(None),
|
|
303
|
-
"array": lambda s: _create_array_type(s, schemas),
|
|
300
|
+
"string": lambda s: _create_string_type(s),
|
|
301
|
+
"integer": lambda s: _create_numeric_type(int, s),
|
|
302
|
+
"number": lambda s: _create_numeric_type(float, s),
|
|
303
|
+
"boolean": lambda _: bool,
|
|
304
|
+
"null": lambda _: type(None),
|
|
305
|
+
"array": lambda s: _create_array_type(s, schemas),
|
|
304
306
|
"object": lambda s: (
|
|
305
307
|
_create_pydantic_model(s, s.get("title"), schemas)
|
|
306
308
|
if s.get("properties") and s.get("additionalProperties") is True
|
|
307
309
|
else _create_dataclass(s, s.get("title"), schemas)
|
|
308
|
-
),
|
|
310
|
+
),
|
|
309
311
|
}
|
|
310
312
|
return type_handlers.get(schema.get("type", None), _return_Any)
|
|
311
313
|
|
|
@@ -326,7 +328,7 @@ def _schema_to_type(
|
|
|
326
328
|
ref = schema["$ref"]
|
|
327
329
|
# Handle self-reference
|
|
328
330
|
if ref == "#":
|
|
329
|
-
return ForwardRef(schema.get("title", "Root"))
|
|
331
|
+
return ForwardRef(schema.get("title", "Root"))
|
|
330
332
|
return _schema_to_type(_resolve_ref(ref, schemas), schemas)
|
|
331
333
|
|
|
332
334
|
if "const" in schema:
|
|
@@ -348,7 +350,7 @@ def _schema_to_type(
|
|
|
348
350
|
# This is a dict type, handle it directly
|
|
349
351
|
additional_props = subschema["additionalProperties"]
|
|
350
352
|
if additional_props is True:
|
|
351
|
-
types.append(dict[str, Any])
|
|
353
|
+
types.append(dict[str, Any])
|
|
352
354
|
else:
|
|
353
355
|
value_type = _schema_to_type(additional_props, schemas)
|
|
354
356
|
types.append(dict[str, value_type]) # type: ignore
|
|
@@ -374,7 +376,7 @@ def _schema_to_type(
|
|
|
374
376
|
|
|
375
377
|
schema_type = schema.get("type")
|
|
376
378
|
if not schema_type:
|
|
377
|
-
return Any
|
|
379
|
+
return Any
|
|
378
380
|
|
|
379
381
|
if isinstance(schema_type, list):
|
|
380
382
|
# Create a copy of the schema for each type, but keep all constraints
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Lifespan utilities for combining async context manager lifespans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import AsyncIterator, Callable
|
|
6
|
+
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
AppT = TypeVar("AppT")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def combine_lifespans(
|
|
13
|
+
*lifespans: Callable[[AppT], AbstractAsyncContextManager[dict[str, Any] | None]],
|
|
14
|
+
) -> Callable[[AppT], AbstractAsyncContextManager[dict[str, Any]]]:
|
|
15
|
+
"""Combine multiple lifespans into a single lifespan.
|
|
16
|
+
|
|
17
|
+
Useful when mounting FastMCP into FastAPI and you need to run
|
|
18
|
+
both your app's lifespan and the MCP server's lifespan.
|
|
19
|
+
|
|
20
|
+
Works with both FastAPI-style lifespans (yield None) and FastMCP-style
|
|
21
|
+
lifespans (yield dict). Results are merged; later lifespans override
|
|
22
|
+
earlier ones on key conflicts.
|
|
23
|
+
|
|
24
|
+
Lifespans are entered in order and exited in reverse order (LIFO).
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
from fastmcp import FastMCP
|
|
29
|
+
from fastmcp.utilities.lifespan import combine_lifespans
|
|
30
|
+
from fastapi import FastAPI
|
|
31
|
+
|
|
32
|
+
mcp = FastMCP("Tools")
|
|
33
|
+
mcp_app = mcp.http_app()
|
|
34
|
+
|
|
35
|
+
app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))
|
|
36
|
+
app.mount("/mcp", mcp_app) # MCP endpoint at /mcp
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
*lifespans: Lifespan context manager factories to combine.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
A combined lifespan context manager factory.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@asynccontextmanager
|
|
47
|
+
async def combined(app: AppT) -> AsyncIterator[dict[str, Any]]:
|
|
48
|
+
merged: dict[str, Any] = {}
|
|
49
|
+
async with AsyncExitStack() as stack:
|
|
50
|
+
for ls in lifespans:
|
|
51
|
+
result = await stack.enter_async_context(ls(app))
|
|
52
|
+
if result is not None:
|
|
53
|
+
merged.update(result)
|
|
54
|
+
yield merged
|
|
55
|
+
|
|
56
|
+
return combined
|
fastmcp/utilities/logging.py
CHANGED
|
@@ -57,6 +57,18 @@ def configure_logging(
|
|
|
57
57
|
logger.propagate = False
|
|
58
58
|
logger.setLevel(level)
|
|
59
59
|
|
|
60
|
+
# Remove any existing handlers to avoid duplicates on reconfiguration
|
|
61
|
+
for hdlr in logger.handlers[:]:
|
|
62
|
+
logger.removeHandler(hdlr)
|
|
63
|
+
|
|
64
|
+
# Use standard logging handlers if rich logging is disabled
|
|
65
|
+
if not fastmcp.settings.enable_rich_logging:
|
|
66
|
+
# Create a standard StreamHandler for stderr
|
|
67
|
+
handler = logging.StreamHandler()
|
|
68
|
+
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
|
|
69
|
+
logger.addHandler(handler)
|
|
70
|
+
return
|
|
71
|
+
|
|
60
72
|
# Configure the handler for normal logs
|
|
61
73
|
handler = RichHandler(
|
|
62
74
|
console=Console(stderr=True),
|
|
@@ -91,10 +103,6 @@ def configure_logging(
|
|
|
91
103
|
|
|
92
104
|
traceback_handler.addFilter(lambda record: record.exc_info is not None)
|
|
93
105
|
|
|
94
|
-
# Remove any existing handlers to avoid duplicates on reconfiguration
|
|
95
|
-
for hdlr in logger.handlers[:]:
|
|
96
|
-
logger.removeHandler(hdlr)
|
|
97
|
-
|
|
98
106
|
logger.addHandler(handler)
|
|
99
107
|
logger.addHandler(traceback_handler)
|
|
100
108
|
|
|
@@ -216,7 +216,7 @@ class MCPServerConfig(BaseModel):
|
|
|
216
216
|
|
|
217
217
|
"""
|
|
218
218
|
if isinstance(v, dict):
|
|
219
|
-
return Deployment(**v)
|
|
219
|
+
return Deployment(**v)
|
|
220
220
|
return cast(Deployment, v) # type: ignore[return-value]
|
|
221
221
|
|
|
222
222
|
@classmethod
|
|
@@ -301,9 +301,9 @@ class MCPServerConfig(BaseModel):
|
|
|
301
301
|
if any([transport, host, port, path, log_level, env, cwd, args]):
|
|
302
302
|
# Convert streamable-http to http for backward compatibility
|
|
303
303
|
if transport == "streamable-http":
|
|
304
|
-
transport = "http"
|
|
304
|
+
transport = "http"
|
|
305
305
|
deployment = Deployment(
|
|
306
|
-
transport=transport,
|
|
306
|
+
transport=transport,
|
|
307
307
|
host=host,
|
|
308
308
|
port=port,
|
|
309
309
|
path=path,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""OpenAPI parsing logic for converting OpenAPI specs to HTTPRoute objects."""
|
|
2
2
|
|
|
3
|
-
from typing import Any, Generic, TypeVar
|
|
3
|
+
from typing import Any, Generic, TypeVar, cast
|
|
4
4
|
|
|
5
5
|
from openapi_pydantic import (
|
|
6
6
|
OpenAPI,
|
|
@@ -146,9 +146,9 @@ class OpenAPIParser(
|
|
|
146
146
|
def _convert_to_parameter_location(self, param_in: str) -> ParameterLocation:
|
|
147
147
|
"""Convert string parameter location to our ParameterLocation type."""
|
|
148
148
|
if param_in in ["path", "query", "header", "cookie"]:
|
|
149
|
-
return
|
|
149
|
+
return cast(ParameterLocation, param_in)
|
|
150
150
|
logger.warning(f"Unknown parameter location: {param_in}, defaulting to 'query'")
|
|
151
|
-
return "query"
|
|
151
|
+
return cast(ParameterLocation, "query")
|
|
152
152
|
|
|
153
153
|
def _resolve_ref(self, item: Any) -> Any:
|
|
154
154
|
"""Resolves a reference to its target definition."""
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Pagination utilities for MCP list operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import binascii
|
|
7
|
+
import json
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TypeVar
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CursorState:
|
|
17
|
+
"""Internal representation of pagination cursor state.
|
|
18
|
+
|
|
19
|
+
The cursor encodes the offset into the result set. This is opaque to clients
|
|
20
|
+
per the MCP spec - they should not parse or modify cursors.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
offset: int
|
|
24
|
+
|
|
25
|
+
def encode(self) -> str:
|
|
26
|
+
"""Encode cursor state to an opaque string."""
|
|
27
|
+
data = json.dumps({"o": self.offset})
|
|
28
|
+
return base64.urlsafe_b64encode(data.encode()).decode()
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def decode(cls, cursor: str) -> CursorState:
|
|
32
|
+
"""Decode cursor from an opaque string.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If the cursor is invalid or malformed.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
data = json.loads(base64.urlsafe_b64decode(cursor.encode()).decode())
|
|
39
|
+
return cls(offset=data["o"])
|
|
40
|
+
except (
|
|
41
|
+
json.JSONDecodeError,
|
|
42
|
+
KeyError,
|
|
43
|
+
ValueError,
|
|
44
|
+
TypeError,
|
|
45
|
+
binascii.Error,
|
|
46
|
+
) as e:
|
|
47
|
+
raise ValueError(f"Invalid cursor: {cursor}") from e
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def paginate_sequence(
|
|
51
|
+
items: Sequence[T],
|
|
52
|
+
cursor: str | None,
|
|
53
|
+
page_size: int,
|
|
54
|
+
) -> tuple[list[T], str | None]:
|
|
55
|
+
"""Paginate a sequence of items.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
items: The full sequence to paginate.
|
|
59
|
+
cursor: Optional cursor from a previous request. None for first page.
|
|
60
|
+
page_size: Maximum number of items per page.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (page_items, next_cursor). next_cursor is None if no more pages.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If the cursor is invalid.
|
|
67
|
+
"""
|
|
68
|
+
offset = 0
|
|
69
|
+
if cursor:
|
|
70
|
+
state = CursorState.decode(cursor)
|
|
71
|
+
offset = state.offset
|
|
72
|
+
|
|
73
|
+
end = offset + page_size
|
|
74
|
+
page = list(items[offset:end])
|
|
75
|
+
|
|
76
|
+
next_cursor = None
|
|
77
|
+
if end < len(items):
|
|
78
|
+
next_cursor = CursorState(offset=end).encode()
|
|
79
|
+
|
|
80
|
+
return page, next_cursor
|