fastmcp 2.11.2__py3-none-any.whl → 2.12.0__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 +5 -4
- fastmcp/cli/claude.py +22 -18
- fastmcp/cli/cli.py +472 -136
- fastmcp/cli/install/claude_code.py +37 -40
- fastmcp/cli/install/claude_desktop.py +37 -42
- fastmcp/cli/install/cursor.py +148 -38
- fastmcp/cli/install/mcp_json.py +38 -43
- fastmcp/cli/install/shared.py +64 -7
- fastmcp/cli/run.py +122 -215
- fastmcp/client/auth/oauth.py +69 -13
- fastmcp/client/client.py +46 -9
- fastmcp/client/logging.py +25 -1
- fastmcp/client/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +143 -67
- fastmcp/experimental/sampling/__init__.py +0 -0
- fastmcp/experimental/sampling/handlers/__init__.py +3 -0
- fastmcp/experimental/sampling/handlers/base.py +21 -0
- fastmcp/experimental/sampling/handlers/openai.py +163 -0
- fastmcp/experimental/server/openapi/routing.py +1 -3
- fastmcp/experimental/server/openapi/server.py +10 -25
- fastmcp/experimental/utilities/openapi/__init__.py +2 -2
- fastmcp/experimental/utilities/openapi/formatters.py +34 -0
- fastmcp/experimental/utilities/openapi/models.py +5 -2
- fastmcp/experimental/utilities/openapi/parser.py +252 -70
- fastmcp/experimental/utilities/openapi/schemas.py +135 -106
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +4 -2
- fastmcp/resources/resource_manager.py +16 -6
- fastmcp/server/auth/__init__.py +11 -1
- fastmcp/server/auth/auth.py +19 -2
- fastmcp/server/auth/oauth_proxy.py +1047 -0
- fastmcp/server/auth/providers/azure.py +270 -0
- fastmcp/server/auth/providers/github.py +287 -0
- fastmcp/server/auth/providers/google.py +305 -0
- fastmcp/server/auth/providers/jwt.py +27 -16
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/auth/registry.py +1 -1
- fastmcp/server/context.py +91 -41
- fastmcp/server/dependencies.py +32 -2
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +44 -37
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +85 -20
- fastmcp/settings.py +18 -3
- fastmcp/tools/tool.py +23 -10
- fastmcp/tools/tool_manager.py +5 -1
- fastmcp/tools/tool_transform.py +75 -32
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +21 -5
- fastmcp/utilities/inspect.py +166 -37
- fastmcp/utilities/json_schema_type.py +4 -2
- fastmcp/utilities/logging.py +4 -1
- fastmcp/utilities/mcp_config.py +47 -18
- fastmcp/utilities/mcp_server_config/__init__.py +25 -0
- fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
- fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
- fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
- fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
- fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
- fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
- fastmcp/utilities/openapi.py +4 -4
- fastmcp/utilities/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/METADATA +3 -2
- fastmcp-2.12.0.dist-info/RECORD +129 -0
- fastmcp-2.11.2.dist-info/RECORD +0 -108
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.2.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -75,7 +75,7 @@ def _replace_ref_with_defs(
|
|
|
75
75
|
info: dict[str, Any], description: str | None = None
|
|
76
76
|
) -> dict[str, Any]:
|
|
77
77
|
"""
|
|
78
|
-
Replace openapi $ref with jsonschema $defs
|
|
78
|
+
Replace openapi $ref with jsonschema $defs recursively.
|
|
79
79
|
|
|
80
80
|
Examples:
|
|
81
81
|
- {"type": "object", "properties": {"$ref": "#/components/schemas/..."}}
|
|
@@ -202,6 +202,7 @@ def _make_optional_parameter_nullable(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
202
202
|
|
|
203
203
|
def _combine_schemas_and_map_params(
|
|
204
204
|
route: HTTPRoute,
|
|
205
|
+
convert_refs: bool = True,
|
|
205
206
|
) -> tuple[dict[str, Any], dict[str, dict[str, str]]]:
|
|
206
207
|
"""
|
|
207
208
|
Combines parameter and request body schemas into a single schema.
|
|
@@ -233,10 +234,43 @@ def _combine_schemas_and_map_params(
|
|
|
233
234
|
|
|
234
235
|
if route.request_body and route.request_body.content_schema:
|
|
235
236
|
content_type = next(iter(route.request_body.content_schema))
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
|
|
238
|
+
# Convert refs if needed
|
|
239
|
+
if convert_refs:
|
|
240
|
+
body_schema = _replace_ref_with_defs(
|
|
241
|
+
route.request_body.content_schema[content_type]
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
body_schema = route.request_body.content_schema[content_type]
|
|
245
|
+
|
|
246
|
+
if route.request_body.description and not body_schema.get("description"):
|
|
247
|
+
body_schema["description"] = route.request_body.description
|
|
248
|
+
|
|
249
|
+
# Handle allOf at the top level by merging all schemas
|
|
250
|
+
if "allOf" in body_schema and isinstance(body_schema["allOf"], list):
|
|
251
|
+
merged_props = {}
|
|
252
|
+
merged_required = []
|
|
253
|
+
|
|
254
|
+
for sub_schema in body_schema["allOf"]:
|
|
255
|
+
if isinstance(sub_schema, dict):
|
|
256
|
+
# Merge properties
|
|
257
|
+
if "properties" in sub_schema:
|
|
258
|
+
merged_props.update(sub_schema["properties"])
|
|
259
|
+
# Merge required fields
|
|
260
|
+
if "required" in sub_schema:
|
|
261
|
+
merged_required.extend(sub_schema["required"])
|
|
262
|
+
|
|
263
|
+
# Update body_schema with merged properties
|
|
264
|
+
body_schema["properties"] = merged_props
|
|
265
|
+
if merged_required:
|
|
266
|
+
# Remove duplicates while preserving order
|
|
267
|
+
seen = set()
|
|
268
|
+
body_schema["required"] = [
|
|
269
|
+
x for x in merged_required if not (x in seen or seen.add(x))
|
|
270
|
+
]
|
|
271
|
+
# Remove the allOf since we've merged it
|
|
272
|
+
body_schema.pop("allOf", None)
|
|
273
|
+
|
|
240
274
|
body_props = body_schema.get("properties", {})
|
|
241
275
|
|
|
242
276
|
# Detect collisions: parameters that exist in both body and path/query/header
|
|
@@ -261,10 +295,11 @@ def _combine_schemas_and_map_params(
|
|
|
261
295
|
"openapi_name": param.name,
|
|
262
296
|
}
|
|
263
297
|
|
|
264
|
-
#
|
|
265
|
-
|
|
266
|
-
param.schema_
|
|
267
|
-
|
|
298
|
+
# Convert refs if needed
|
|
299
|
+
if convert_refs:
|
|
300
|
+
param_schema = _replace_ref_with_defs(param.schema_)
|
|
301
|
+
else:
|
|
302
|
+
param_schema = param.schema_
|
|
268
303
|
original_desc = param_schema.get("description", "")
|
|
269
304
|
location_desc = f"({param.location.capitalize()} parameter)"
|
|
270
305
|
if original_desc:
|
|
@@ -287,9 +322,11 @@ def _combine_schemas_and_map_params(
|
|
|
287
322
|
"openapi_name": param.name,
|
|
288
323
|
}
|
|
289
324
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
325
|
+
# Convert refs if needed
|
|
326
|
+
if convert_refs:
|
|
327
|
+
param_schema = _replace_ref_with_defs(param.schema_)
|
|
328
|
+
else:
|
|
329
|
+
param_schema = param.schema_
|
|
293
330
|
|
|
294
331
|
# Don't make optional parameters nullable - they can simply be omitted
|
|
295
332
|
# The OpenAPI specification doesn't require optional parameters to accept null values
|
|
@@ -298,14 +335,28 @@ def _combine_schemas_and_map_params(
|
|
|
298
335
|
|
|
299
336
|
# Add request body properties (no suffixes for body parameters)
|
|
300
337
|
if route.request_body and route.request_body.content_schema:
|
|
301
|
-
|
|
302
|
-
|
|
338
|
+
# If body is just a $ref, we need to handle it differently
|
|
339
|
+
if "$ref" in body_schema and not body_props:
|
|
340
|
+
# The entire body is a reference to a schema
|
|
341
|
+
# We need to expand this inline or keep the ref
|
|
342
|
+
# For simplicity, we'll keep it as a single property
|
|
343
|
+
properties["body"] = body_schema
|
|
344
|
+
if route.request_body.required:
|
|
345
|
+
required.append("body")
|
|
346
|
+
parameter_map["body"] = {"location": "body", "openapi_name": "body"}
|
|
347
|
+
else:
|
|
348
|
+
# Normal case: body has properties
|
|
349
|
+
for prop_name, prop_schema in body_props.items():
|
|
350
|
+
properties[prop_name] = prop_schema
|
|
303
351
|
|
|
304
|
-
|
|
305
|
-
|
|
352
|
+
# Track parameter mapping for body properties
|
|
353
|
+
parameter_map[prop_name] = {
|
|
354
|
+
"location": "body",
|
|
355
|
+
"openapi_name": prop_name,
|
|
356
|
+
}
|
|
306
357
|
|
|
307
|
-
|
|
308
|
-
|
|
358
|
+
if route.request_body.required:
|
|
359
|
+
required.extend(body_schema.get("required", []))
|
|
309
360
|
|
|
310
361
|
result = {
|
|
311
362
|
"type": "object",
|
|
@@ -313,43 +364,53 @@ def _combine_schemas_and_map_params(
|
|
|
313
364
|
"required": required,
|
|
314
365
|
}
|
|
315
366
|
# Add schema definitions if available
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
schema_defs = route.request_schemas
|
|
368
|
+
if schema_defs:
|
|
369
|
+
if convert_refs:
|
|
370
|
+
# Need to convert refs and prune
|
|
371
|
+
all_defs = schema_defs.copy()
|
|
372
|
+
# Convert each schema definition recursively
|
|
373
|
+
for name, schema in all_defs.items():
|
|
374
|
+
if isinstance(schema, dict):
|
|
375
|
+
all_defs[name] = _replace_ref_with_defs(schema)
|
|
376
|
+
|
|
377
|
+
# Prune to only needed schemas
|
|
378
|
+
used_refs = set()
|
|
379
|
+
|
|
380
|
+
def find_refs_in_value(value):
|
|
381
|
+
"""Recursively find all $ref references."""
|
|
382
|
+
if isinstance(value, dict):
|
|
383
|
+
if "$ref" in value and isinstance(value["$ref"], str):
|
|
384
|
+
ref = value["$ref"]
|
|
385
|
+
if ref.startswith("#/$defs/"):
|
|
386
|
+
used_refs.add(ref.split("/")[-1])
|
|
387
|
+
for v in value.values():
|
|
388
|
+
find_refs_in_value(v)
|
|
389
|
+
elif isinstance(value, list):
|
|
390
|
+
for item in value:
|
|
391
|
+
find_refs_in_value(item)
|
|
392
|
+
|
|
393
|
+
# Find refs in properties
|
|
394
|
+
find_refs_in_value(properties)
|
|
395
|
+
|
|
396
|
+
# Collect transitive dependencies
|
|
397
|
+
if used_refs:
|
|
398
|
+
collected_all = False
|
|
399
|
+
while not collected_all:
|
|
400
|
+
initial_count = len(used_refs)
|
|
401
|
+
for name in list(used_refs):
|
|
402
|
+
if name in all_defs:
|
|
403
|
+
find_refs_in_value(all_defs[name])
|
|
404
|
+
collected_all = len(used_refs) == initial_count
|
|
405
|
+
|
|
406
|
+
result["$defs"] = {
|
|
407
|
+
name: def_schema
|
|
408
|
+
for name, def_schema in all_defs.items()
|
|
409
|
+
if name in used_refs
|
|
410
|
+
}
|
|
351
411
|
else:
|
|
352
|
-
|
|
412
|
+
# From parser - already converted and pruned
|
|
413
|
+
result["$defs"] = schema_defs
|
|
353
414
|
|
|
354
415
|
return result, parameter_map
|
|
355
416
|
|
|
@@ -440,17 +501,17 @@ def extract_output_schema_from_responses(
|
|
|
440
501
|
if not schema or not isinstance(schema, dict):
|
|
441
502
|
return None
|
|
442
503
|
|
|
443
|
-
#
|
|
444
|
-
output_schema = schema
|
|
504
|
+
# Convert refs if needed
|
|
505
|
+
output_schema = _replace_ref_with_defs(schema)
|
|
445
506
|
|
|
446
507
|
# If schema has a $ref, resolve it first before processing nullable fields
|
|
447
508
|
if "$ref" in output_schema and schema_definitions:
|
|
448
509
|
ref_path = output_schema["$ref"]
|
|
449
|
-
if ref_path.startswith("
|
|
510
|
+
if ref_path.startswith("#/$defs/"):
|
|
450
511
|
schema_name = ref_path.split("/")[-1]
|
|
451
512
|
if schema_name in schema_definitions:
|
|
452
513
|
# Replace $ref with the actual schema definition
|
|
453
|
-
output_schema = schema_definitions[schema_name]
|
|
514
|
+
output_schema = _replace_ref_with_defs(schema_definitions[schema_name])
|
|
454
515
|
|
|
455
516
|
# Convert OpenAPI schema to JSON Schema format
|
|
456
517
|
# Only needed for OpenAPI 3.0 - 3.1 uses standard JSON Schema null types
|
|
@@ -473,56 +534,25 @@ def extract_output_schema_from_responses(
|
|
|
473
534
|
}
|
|
474
535
|
output_schema = wrapped_schema
|
|
475
536
|
|
|
476
|
-
# Add schema definitions if available
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
processed_defs =
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if
|
|
483
|
-
|
|
484
|
-
|
|
537
|
+
# Add schema definitions if available
|
|
538
|
+
if schema_definitions:
|
|
539
|
+
# Convert refs if needed
|
|
540
|
+
processed_defs = schema_definitions.copy()
|
|
541
|
+
# Convert each schema definition recursively
|
|
542
|
+
for name, schema in processed_defs.items():
|
|
543
|
+
if isinstance(schema, dict):
|
|
544
|
+
processed_defs[name] = _replace_ref_with_defs(schema)
|
|
545
|
+
|
|
546
|
+
# Convert OpenAPI schema definitions to JSON Schema format if needed
|
|
547
|
+
if openapi_version and openapi_version.startswith("3.0"):
|
|
548
|
+
from .json_schema_converter import convert_openapi_schema_to_json_schema
|
|
549
|
+
|
|
550
|
+
for def_name in list(processed_defs.keys()):
|
|
485
551
|
processed_defs[def_name] = convert_openapi_schema_to_json_schema(
|
|
486
|
-
|
|
552
|
+
processed_defs[def_name], openapi_version
|
|
487
553
|
)
|
|
488
|
-
else:
|
|
489
|
-
processed_defs[def_name] = def_schema
|
|
490
|
-
output_schema["$defs"] = processed_defs
|
|
491
554
|
|
|
492
|
-
|
|
493
|
-
if output_schema.get("additionalProperties") is False:
|
|
494
|
-
output_schema.pop("additionalProperties")
|
|
495
|
-
|
|
496
|
-
# Remove unused definitions (lightweight approach - just check direct $ref usage)
|
|
497
|
-
if "$defs" in output_schema:
|
|
498
|
-
used_refs = set()
|
|
499
|
-
|
|
500
|
-
def find_refs_in_value(value):
|
|
501
|
-
if isinstance(value, dict):
|
|
502
|
-
if "$ref" in value and isinstance(value["$ref"], str):
|
|
503
|
-
ref = value["$ref"]
|
|
504
|
-
if ref.startswith("#/$defs/"):
|
|
505
|
-
used_refs.add(ref.split("/")[-1])
|
|
506
|
-
for v in value.values():
|
|
507
|
-
find_refs_in_value(v)
|
|
508
|
-
elif isinstance(value, list):
|
|
509
|
-
for item in value:
|
|
510
|
-
find_refs_in_value(item)
|
|
511
|
-
|
|
512
|
-
# Find refs in the main schema (excluding $defs section)
|
|
513
|
-
for key, value in output_schema.items():
|
|
514
|
-
if key != "$defs":
|
|
515
|
-
find_refs_in_value(value)
|
|
516
|
-
|
|
517
|
-
# Remove unused definitions
|
|
518
|
-
if used_refs:
|
|
519
|
-
output_schema["$defs"] = {
|
|
520
|
-
name: def_schema
|
|
521
|
-
for name, def_schema in output_schema["$defs"].items()
|
|
522
|
-
if name in used_refs
|
|
523
|
-
}
|
|
524
|
-
else:
|
|
525
|
-
output_schema.pop("$defs")
|
|
555
|
+
output_schema["$defs"] = processed_defs
|
|
526
556
|
|
|
527
557
|
return output_schema
|
|
528
558
|
|
|
@@ -533,6 +563,5 @@ __all__ = [
|
|
|
533
563
|
"_combine_schemas",
|
|
534
564
|
"_combine_schemas_and_map_params",
|
|
535
565
|
"extract_output_schema_from_responses",
|
|
536
|
-
"_replace_ref_with_defs",
|
|
537
566
|
"_make_optional_parameter_nullable",
|
|
538
567
|
]
|
fastmcp/mcp_config.py
CHANGED
|
@@ -27,7 +27,7 @@ from __future__ import annotations
|
|
|
27
27
|
import datetime
|
|
28
28
|
import re
|
|
29
29
|
from pathlib import Path
|
|
30
|
-
from typing import TYPE_CHECKING, Annotated, Any, Literal
|
|
30
|
+
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
|
|
31
31
|
from urllib.parse import urlparse
|
|
32
32
|
|
|
33
33
|
import httpx
|
|
@@ -36,7 +36,6 @@ from pydantic import (
|
|
|
36
36
|
BaseModel,
|
|
37
37
|
ConfigDict,
|
|
38
38
|
Field,
|
|
39
|
-
ValidationInfo,
|
|
40
39
|
model_validator,
|
|
41
40
|
)
|
|
42
41
|
from typing_extensions import Self, override
|
|
@@ -47,11 +46,11 @@ from fastmcp.utilities.types import FastMCPBaseModel
|
|
|
47
46
|
if TYPE_CHECKING:
|
|
48
47
|
from fastmcp.client.transports import (
|
|
49
48
|
ClientTransport,
|
|
50
|
-
FastMCPTransport,
|
|
51
49
|
SSETransport,
|
|
52
50
|
StdioTransport,
|
|
53
51
|
StreamableHttpTransport,
|
|
54
52
|
)
|
|
53
|
+
from fastmcp.server.server import FastMCP
|
|
55
54
|
|
|
56
55
|
|
|
57
56
|
def infer_transport_type_from_url(
|
|
@@ -90,21 +89,38 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
|
|
|
90
89
|
description="The tags to exclude in the proxy.",
|
|
91
90
|
)
|
|
92
91
|
|
|
93
|
-
def
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
92
|
+
def _to_server_and_underlying_transport(
|
|
93
|
+
self,
|
|
94
|
+
server_name: str | None = None,
|
|
95
|
+
client_name: str | None = None,
|
|
96
|
+
) -> tuple[FastMCP[Any], ClientTransport]:
|
|
97
|
+
"""Turn the Transforming MCPServer into a FastMCP Server and also return the underlying transport."""
|
|
98
|
+
from fastmcp import FastMCP
|
|
99
|
+
from fastmcp.client import Client
|
|
100
|
+
from fastmcp.client.transports import (
|
|
101
|
+
ClientTransport, # pyright: ignore[reportUnusedImport]
|
|
102
|
+
)
|
|
97
103
|
|
|
98
104
|
transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
|
|
105
|
+
transport = cast(ClientTransport, transport)
|
|
106
|
+
|
|
107
|
+
client: Client[ClientTransport] = Client(transport=transport, name=client_name)
|
|
99
108
|
|
|
100
109
|
wrapped_mcp_server = FastMCP.as_proxy(
|
|
101
|
-
|
|
110
|
+
name=server_name,
|
|
111
|
+
backend=client,
|
|
102
112
|
tool_transformations=self.tools,
|
|
103
113
|
include_tags=self.include_tags,
|
|
104
114
|
exclude_tags=self.exclude_tags,
|
|
105
115
|
)
|
|
106
116
|
|
|
107
|
-
return
|
|
117
|
+
return wrapped_mcp_server, transport
|
|
118
|
+
|
|
119
|
+
def to_transport(self) -> ClientTransport:
|
|
120
|
+
"""Get the transport for the transforming MCP server."""
|
|
121
|
+
from fastmcp.client.transports import FastMCPTransport
|
|
122
|
+
|
|
123
|
+
return FastMCPTransport(mcp=self._to_server_and_underlying_transport()[0])
|
|
108
124
|
|
|
109
125
|
|
|
110
126
|
class StdioMCPServer(BaseModel):
|
|
@@ -232,20 +248,24 @@ class MCPConfig(BaseModel):
|
|
|
232
248
|
For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
|
|
233
249
|
"""
|
|
234
250
|
|
|
235
|
-
mcpServers: dict[str, MCPServerTypes]
|
|
251
|
+
mcpServers: dict[str, MCPServerTypes] = Field(default_factory=dict)
|
|
236
252
|
|
|
237
253
|
model_config = ConfigDict(extra="allow") # Preserve unknown top-level fields
|
|
238
254
|
|
|
239
255
|
@model_validator(mode="before")
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
256
|
+
@classmethod
|
|
257
|
+
def wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
258
|
+
"""If there's no mcpServers key but there are server configs at root, wrap them."""
|
|
259
|
+
if "mcpServers" not in values:
|
|
260
|
+
# Check if any values look like server configs
|
|
261
|
+
has_servers = any(
|
|
262
|
+
isinstance(v, dict) and ("command" in v or "url" in v)
|
|
263
|
+
for v in values.values()
|
|
264
|
+
)
|
|
265
|
+
if has_servers:
|
|
266
|
+
# Move all server-like configs under mcpServers
|
|
267
|
+
return {"mcpServers": values}
|
|
268
|
+
return values
|
|
249
269
|
|
|
250
270
|
def add_server(self, name: str, server: MCPServerTypes) -> None:
|
|
251
271
|
"""Add or update a server in the configuration."""
|
|
@@ -282,7 +302,7 @@ class CanonicalMCPConfig(MCPConfig):
|
|
|
282
302
|
The format is designed to be client-agnostic and extensible for future use cases.
|
|
283
303
|
"""
|
|
284
304
|
|
|
285
|
-
mcpServers: dict[str, CanonicalMCPServerTypes]
|
|
305
|
+
mcpServers: dict[str, CanonicalMCPServerTypes] = Field(default_factory=dict)
|
|
286
306
|
|
|
287
307
|
@override
|
|
288
308
|
def add_server(self, name: str, server: CanonicalMCPServerTypes) -> None:
|
|
@@ -69,8 +69,8 @@ class PromptManager:
|
|
|
69
69
|
child_dict = {p.key: p for p in child_results}
|
|
70
70
|
if mounted.prefix:
|
|
71
71
|
for prompt in child_dict.values():
|
|
72
|
-
prefixed_prompt = prompt.
|
|
73
|
-
f"{mounted.prefix}_{prompt.key}"
|
|
72
|
+
prefixed_prompt = prompt.model_copy(
|
|
73
|
+
key=f"{mounted.prefix}_{prompt.key}"
|
|
74
74
|
)
|
|
75
75
|
all_prompts[prefixed_prompt.key] = prefixed_prompt
|
|
76
76
|
else:
|
|
@@ -80,6 +80,8 @@ class PromptManager:
|
|
|
80
80
|
logger.warning(
|
|
81
81
|
f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
82
82
|
)
|
|
83
|
+
if settings.mounted_components_raise_on_load_error:
|
|
84
|
+
raise
|
|
83
85
|
continue
|
|
84
86
|
|
|
85
87
|
# Finally, add local prompts, which always take precedence
|
|
@@ -101,8 +101,11 @@ class ResourceManager:
|
|
|
101
101
|
prefixed_uri = add_resource_prefix(
|
|
102
102
|
uri, mounted.prefix, mounted.resource_prefix_format
|
|
103
103
|
)
|
|
104
|
-
# Create a copy of the resource with the prefixed key
|
|
105
|
-
prefixed_resource = resource.
|
|
104
|
+
# Create a copy of the resource with the prefixed key and name
|
|
105
|
+
prefixed_resource = resource.model_copy(
|
|
106
|
+
update={"name": f"{mounted.prefix}_{resource.name}"},
|
|
107
|
+
key=prefixed_uri,
|
|
108
|
+
)
|
|
106
109
|
all_resources[prefixed_uri] = prefixed_resource
|
|
107
110
|
else:
|
|
108
111
|
all_resources.update(child_resources)
|
|
@@ -111,6 +114,8 @@ class ResourceManager:
|
|
|
111
114
|
logger.warning(
|
|
112
115
|
f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
113
116
|
)
|
|
117
|
+
if settings.mounted_components_raise_on_load_error:
|
|
118
|
+
raise
|
|
114
119
|
continue
|
|
115
120
|
|
|
116
121
|
# Finally, add local resources, which always take precedence
|
|
@@ -149,8 +154,11 @@ class ResourceManager:
|
|
|
149
154
|
prefixed_uri_template = add_resource_prefix(
|
|
150
155
|
uri_template, mounted.prefix, mounted.resource_prefix_format
|
|
151
156
|
)
|
|
152
|
-
# Create a copy of the template with the prefixed key
|
|
153
|
-
prefixed_template = template.
|
|
157
|
+
# Create a copy of the template with the prefixed key and name
|
|
158
|
+
prefixed_template = template.model_copy(
|
|
159
|
+
update={"name": f"{mounted.prefix}_{template.name}"},
|
|
160
|
+
key=prefixed_uri_template,
|
|
161
|
+
)
|
|
154
162
|
all_templates[prefixed_uri_template] = prefixed_template
|
|
155
163
|
else:
|
|
156
164
|
all_templates.update(child_dict)
|
|
@@ -159,6 +167,8 @@ class ResourceManager:
|
|
|
159
167
|
logger.warning(
|
|
160
168
|
f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
|
|
161
169
|
)
|
|
170
|
+
if settings.mounted_components_raise_on_load_error:
|
|
171
|
+
raise
|
|
162
172
|
continue
|
|
163
173
|
|
|
164
174
|
# Finally, add local templates, which always take precedence
|
|
@@ -273,7 +283,7 @@ class ResourceManager:
|
|
|
273
283
|
Args:
|
|
274
284
|
resource: A Resource instance to add. The resource's .key attribute
|
|
275
285
|
will be used as the storage key. To overwrite it, call
|
|
276
|
-
Resource.
|
|
286
|
+
Resource.model_copy(key=new_key) before calling this method.
|
|
277
287
|
"""
|
|
278
288
|
existing = self._resources.get(resource.key)
|
|
279
289
|
if existing:
|
|
@@ -322,7 +332,7 @@ class ResourceManager:
|
|
|
322
332
|
Args:
|
|
323
333
|
template: A ResourceTemplate instance to add. The template's .key attribute
|
|
324
334
|
will be used as the storage key. To overwrite it, call
|
|
325
|
-
ResourceTemplate.
|
|
335
|
+
ResourceTemplate.model_copy(key=new_key) before calling this method.
|
|
326
336
|
|
|
327
337
|
Returns:
|
|
328
338
|
The added template. If a template with the same URI already exists,
|
fastmcp/server/auth/__init__.py
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
from .auth import
|
|
1
|
+
from .auth import (
|
|
2
|
+
OAuthProvider,
|
|
3
|
+
TokenVerifier,
|
|
4
|
+
RemoteAuthProvider,
|
|
5
|
+
AccessToken,
|
|
6
|
+
AuthProvider,
|
|
7
|
+
)
|
|
2
8
|
from .providers.jwt import JWTVerifier, StaticTokenVerifier
|
|
9
|
+
from .oauth_proxy import OAuthProxy
|
|
3
10
|
|
|
4
11
|
|
|
5
12
|
__all__ = [
|
|
13
|
+
"AuthProvider",
|
|
6
14
|
"OAuthProvider",
|
|
7
15
|
"TokenVerifier",
|
|
8
16
|
"JWTVerifier",
|
|
9
17
|
"StaticTokenVerifier",
|
|
10
18
|
"RemoteAuthProvider",
|
|
19
|
+
"AccessToken",
|
|
20
|
+
"OAuthProxy",
|
|
11
21
|
]
|
|
12
22
|
|
|
13
23
|
|
fastmcp/server/auth/auth.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.auth.provider import (
|
|
6
|
+
AccessToken as _SDKAccessToken,
|
|
7
|
+
)
|
|
3
8
|
from mcp.server.auth.provider import (
|
|
4
|
-
AccessToken,
|
|
5
9
|
AuthorizationCode,
|
|
6
10
|
OAuthAuthorizationServerProvider,
|
|
7
11
|
RefreshToken,
|
|
@@ -21,6 +25,12 @@ from pydantic import AnyHttpUrl
|
|
|
21
25
|
from starlette.routing import Route
|
|
22
26
|
|
|
23
27
|
|
|
28
|
+
class AccessToken(_SDKAccessToken):
|
|
29
|
+
"""AccessToken that includes all JWT claims."""
|
|
30
|
+
|
|
31
|
+
claims: dict[str, Any] = {}
|
|
32
|
+
|
|
33
|
+
|
|
24
34
|
class AuthProvider(TokenVerifierProtocol):
|
|
25
35
|
"""Base class for all FastMCP authentication providers.
|
|
26
36
|
|
|
@@ -125,11 +135,15 @@ class RemoteAuthProvider(AuthProvider):
|
|
|
125
135
|
the authorization servers that issue valid tokens.
|
|
126
136
|
"""
|
|
127
137
|
|
|
138
|
+
resource_server_url: AnyHttpUrl
|
|
139
|
+
|
|
128
140
|
def __init__(
|
|
129
141
|
self,
|
|
130
142
|
token_verifier: TokenVerifier,
|
|
131
143
|
authorization_servers: list[AnyHttpUrl],
|
|
132
144
|
resource_server_url: AnyHttpUrl | str,
|
|
145
|
+
resource_name: str | None = None,
|
|
146
|
+
resource_documentation: AnyHttpUrl | None = None,
|
|
133
147
|
):
|
|
134
148
|
"""Initialize the remote auth provider.
|
|
135
149
|
|
|
@@ -143,6 +157,8 @@ class RemoteAuthProvider(AuthProvider):
|
|
|
143
157
|
super().__init__(resource_server_url=resource_server_url)
|
|
144
158
|
self.token_verifier = token_verifier
|
|
145
159
|
self.authorization_servers = authorization_servers
|
|
160
|
+
self.resource_name = resource_name
|
|
161
|
+
self.resource_documentation = resource_documentation
|
|
146
162
|
|
|
147
163
|
async def verify_token(self, token: str) -> AccessToken | None:
|
|
148
164
|
"""Verify token using the configured token verifier."""
|
|
@@ -155,12 +171,13 @@ class RemoteAuthProvider(AuthProvider):
|
|
|
155
171
|
Subclasses can override this method to add additional routes by calling
|
|
156
172
|
super().get_routes() and extending the returned list.
|
|
157
173
|
"""
|
|
158
|
-
assert self.resource_server_url is not None
|
|
159
174
|
|
|
160
175
|
return create_protected_resource_routes(
|
|
161
176
|
resource_url=self.resource_server_url,
|
|
162
177
|
authorization_servers=self.authorization_servers,
|
|
163
178
|
scopes_supported=self.token_verifier.required_scopes,
|
|
179
|
+
resource_name=self.resource_name,
|
|
180
|
+
resource_documentation=self.resource_documentation,
|
|
164
181
|
)
|
|
165
182
|
|
|
166
183
|
|