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
fastmcp/tools/tool_transform.py
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
|
+
import warnings
|
|
4
5
|
from collections.abc import Callable
|
|
5
6
|
from contextvars import ContextVar
|
|
6
7
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Annotated, Any, Literal
|
|
8
|
+
from typing import Annotated, Any, Literal, cast
|
|
8
9
|
|
|
10
|
+
import pydantic_core
|
|
9
11
|
from mcp.types import ToolAnnotations
|
|
10
12
|
from pydantic import ConfigDict
|
|
11
13
|
from pydantic.fields import Field
|
|
12
14
|
from pydantic.functional_validators import BeforeValidator
|
|
13
15
|
|
|
16
|
+
import fastmcp
|
|
14
17
|
from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content
|
|
15
18
|
from fastmcp.utilities.components import _convert_set_default_none
|
|
16
19
|
from fastmcp.utilities.json_schema import compress_schema
|
|
@@ -26,7 +29,7 @@ logger = get_logger(__name__)
|
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
# Context variable to store current transformed tool
|
|
29
|
-
_current_tool: ContextVar[TransformedTool | None] = ContextVar(
|
|
32
|
+
_current_tool: ContextVar[TransformedTool | None] = ContextVar( # type: ignore[assignment]
|
|
30
33
|
"_current_tool", default=None
|
|
31
34
|
)
|
|
32
35
|
|
|
@@ -312,11 +315,9 @@ class TransformedTool(Tool):
|
|
|
312
315
|
# Custom function returns ToolResult - preserve its content
|
|
313
316
|
return result
|
|
314
317
|
else:
|
|
315
|
-
# Forwarded call with
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
structured_content=None,
|
|
319
|
-
)
|
|
318
|
+
# Forwarded call with no explicit schema - preserve parent's structured content
|
|
319
|
+
# The parent tool may have generated structured content via its own fallback logic
|
|
320
|
+
return result
|
|
320
321
|
elif self.output_schema.get(
|
|
321
322
|
"type"
|
|
322
323
|
) != "object" and not self.output_schema.get("x-fastmcp-wrap-result"):
|
|
@@ -334,17 +335,23 @@ class TransformedTool(Tool):
|
|
|
334
335
|
result, serializer=self.serializer
|
|
335
336
|
)
|
|
336
337
|
|
|
337
|
-
|
|
338
|
+
structured_output = None
|
|
339
|
+
# First handle structured content based on output schema, if any
|
|
338
340
|
if self.output_schema is not None:
|
|
339
341
|
if self.output_schema.get("x-fastmcp-wrap-result"):
|
|
340
342
|
# Schema says wrap - always wrap in result key
|
|
341
343
|
structured_output = {"result": result}
|
|
342
344
|
else:
|
|
343
|
-
# Object schemas - use result directly
|
|
344
|
-
# User is responsible for returning dict-compatible data
|
|
345
345
|
structured_output = result
|
|
346
|
-
|
|
347
|
-
|
|
346
|
+
# If no output schema, try to serialize the result. If it is a dict, use
|
|
347
|
+
# it as structured content. If it is not a dict, ignore it.
|
|
348
|
+
if structured_output is None:
|
|
349
|
+
try:
|
|
350
|
+
structured_output = pydantic_core.to_jsonable_python(result)
|
|
351
|
+
if not isinstance(structured_output, dict):
|
|
352
|
+
structured_output = None
|
|
353
|
+
except Exception:
|
|
354
|
+
pass
|
|
348
355
|
|
|
349
356
|
return ToolResult(
|
|
350
357
|
content=unstructured_result,
|
|
@@ -363,9 +370,9 @@ class TransformedTool(Tool):
|
|
|
363
370
|
tags: set[str] | None = None,
|
|
364
371
|
transform_fn: Callable[..., Any] | None = None,
|
|
365
372
|
transform_args: dict[str, ArgTransform] | None = None,
|
|
366
|
-
annotations: ToolAnnotations | None =
|
|
367
|
-
output_schema: dict[str, Any] | None | Literal[False] =
|
|
368
|
-
serializer: Callable[[Any], str] | None =
|
|
373
|
+
annotations: ToolAnnotations | None | NotSetT = NotSet,
|
|
374
|
+
output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
|
|
375
|
+
serializer: Callable[[Any], str] | None | NotSetT = NotSet,
|
|
369
376
|
meta: dict[str, Any] | None | NotSetT = NotSet,
|
|
370
377
|
enabled: bool | None = None,
|
|
371
378
|
) -> TransformedTool:
|
|
@@ -432,7 +439,7 @@ class TransformedTool(Tool):
|
|
|
432
439
|
})
|
|
433
440
|
|
|
434
441
|
# Disable structured outputs
|
|
435
|
-
Tool.from_tool(parent, output_schema=
|
|
442
|
+
Tool.from_tool(parent, output_schema=None)
|
|
436
443
|
|
|
437
444
|
# Return ToolResult for full control
|
|
438
445
|
async def custom_output(**kwargs) -> ToolResult:
|
|
@@ -445,6 +452,11 @@ class TransformedTool(Tool):
|
|
|
445
452
|
"""
|
|
446
453
|
transform_args = transform_args or {}
|
|
447
454
|
|
|
455
|
+
if transform_fn is not None:
|
|
456
|
+
parsed_fn = ParsedFunction.from_function(transform_fn, validate=False)
|
|
457
|
+
else:
|
|
458
|
+
parsed_fn = None
|
|
459
|
+
|
|
448
460
|
# Validate transform_args
|
|
449
461
|
parent_params = set(tool.parameters.get("properties", {}).keys())
|
|
450
462
|
unknown_args = set(transform_args.keys()) - parent_params
|
|
@@ -457,17 +469,12 @@ class TransformedTool(Tool):
|
|
|
457
469
|
# Always create the forwarding transform
|
|
458
470
|
schema, forwarding_fn = cls._create_forwarding_transform(tool, transform_args)
|
|
459
471
|
|
|
460
|
-
# Handle output schema
|
|
461
|
-
if output_schema is
|
|
462
|
-
|
|
463
|
-
elif output_schema is not None:
|
|
464
|
-
# Explicit schema provided - use as-is
|
|
465
|
-
final_output_schema = output_schema
|
|
466
|
-
else:
|
|
467
|
-
# Smart fallback: try custom function, then parent, then None
|
|
472
|
+
# Handle output schema
|
|
473
|
+
if output_schema is NotSet:
|
|
474
|
+
# Use smart fallback: try custom function, then parent
|
|
468
475
|
if transform_fn is not None:
|
|
469
|
-
|
|
470
|
-
final_output_schema = parsed_fn.output_schema
|
|
476
|
+
# parsed fn is not none here
|
|
477
|
+
final_output_schema = cast(ParsedFunction, parsed_fn).output_schema
|
|
471
478
|
if final_output_schema is None:
|
|
472
479
|
# Check if function returns ToolResult - if so, don't fall back to parent
|
|
473
480
|
return_annotation = inspect.signature(
|
|
@@ -479,15 +486,26 @@ class TransformedTool(Tool):
|
|
|
479
486
|
final_output_schema = tool.output_schema
|
|
480
487
|
else:
|
|
481
488
|
final_output_schema = tool.output_schema
|
|
489
|
+
elif output_schema is False:
|
|
490
|
+
# Handle False as deprecated synonym for None (deprecated in 2.11.4)
|
|
491
|
+
if fastmcp.settings.deprecation_warnings:
|
|
492
|
+
warnings.warn(
|
|
493
|
+
"Passing output_schema=False is deprecated. Use output_schema=None instead.",
|
|
494
|
+
DeprecationWarning,
|
|
495
|
+
stacklevel=2,
|
|
496
|
+
)
|
|
497
|
+
final_output_schema = None
|
|
498
|
+
else:
|
|
499
|
+
final_output_schema = cast(dict | None, output_schema)
|
|
482
500
|
|
|
483
501
|
if transform_fn is None:
|
|
484
502
|
# User wants pure transformation - use forwarding_fn as the main function
|
|
485
503
|
final_fn = forwarding_fn
|
|
486
504
|
final_schema = schema
|
|
487
505
|
else:
|
|
506
|
+
# parsed fn is not none here
|
|
507
|
+
parsed_fn = cast(ParsedFunction, parsed_fn)
|
|
488
508
|
# User provided custom function - merge schemas
|
|
489
|
-
if "parsed_fn" not in locals():
|
|
490
|
-
parsed_fn = ParsedFunction.from_function(transform_fn, validate=False)
|
|
491
509
|
final_fn = transform_fn
|
|
492
510
|
|
|
493
511
|
has_kwargs = cls._function_has_kwargs(transform_fn)
|
|
@@ -552,6 +570,13 @@ class TransformedTool(Tool):
|
|
|
552
570
|
)
|
|
553
571
|
final_title = title if not isinstance(title, NotSetT) else tool.title
|
|
554
572
|
final_meta = meta if not isinstance(meta, NotSetT) else tool.meta
|
|
573
|
+
final_annotations = (
|
|
574
|
+
annotations if not isinstance(annotations, NotSetT) else tool.annotations
|
|
575
|
+
)
|
|
576
|
+
final_serializer = (
|
|
577
|
+
serializer if not isinstance(serializer, NotSetT) else tool.serializer
|
|
578
|
+
)
|
|
579
|
+
final_enabled = enabled if enabled is not None else tool.enabled
|
|
555
580
|
|
|
556
581
|
transformed_tool = cls(
|
|
557
582
|
fn=final_fn,
|
|
@@ -563,11 +588,11 @@ class TransformedTool(Tool):
|
|
|
563
588
|
parameters=final_schema,
|
|
564
589
|
output_schema=final_output_schema,
|
|
565
590
|
tags=tags or tool.tags,
|
|
566
|
-
annotations=
|
|
567
|
-
serializer=
|
|
591
|
+
annotations=final_annotations,
|
|
592
|
+
serializer=final_serializer,
|
|
568
593
|
meta=final_meta,
|
|
569
594
|
transform_args=transform_args,
|
|
570
|
-
enabled=
|
|
595
|
+
enabled=final_enabled,
|
|
571
596
|
)
|
|
572
597
|
|
|
573
598
|
return transformed_tool
|
|
@@ -816,12 +841,30 @@ class TransformedTool(Tool):
|
|
|
816
841
|
if "default" in param_schema:
|
|
817
842
|
final_required.discard(param_name)
|
|
818
843
|
|
|
819
|
-
|
|
844
|
+
# Merge $defs from both schemas, with override taking precedence
|
|
845
|
+
merged_defs = base_schema.get("$defs", {}).copy()
|
|
846
|
+
override_defs = override_schema.get("$defs", {})
|
|
847
|
+
|
|
848
|
+
for def_name, def_schema in override_defs.items():
|
|
849
|
+
if def_name in merged_defs:
|
|
850
|
+
base_def = merged_defs[def_name].copy()
|
|
851
|
+
base_def.update(def_schema)
|
|
852
|
+
merged_defs[def_name] = base_def
|
|
853
|
+
else:
|
|
854
|
+
merged_defs[def_name] = def_schema.copy()
|
|
855
|
+
|
|
856
|
+
result = {
|
|
820
857
|
"type": "object",
|
|
821
858
|
"properties": merged_props,
|
|
822
859
|
"required": list(final_required),
|
|
823
860
|
}
|
|
824
861
|
|
|
862
|
+
if merged_defs:
|
|
863
|
+
result["$defs"] = merged_defs
|
|
864
|
+
result = compress_schema(result, prune_defs=True)
|
|
865
|
+
|
|
866
|
+
return result
|
|
867
|
+
|
|
825
868
|
@staticmethod
|
|
826
869
|
def _function_has_kwargs(fn: Callable[..., Any]) -> bool:
|
|
827
870
|
"""Check if function accepts **kwargs.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Authentication utility helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def parse_scopes(value: Any) -> list[str] | None:
|
|
10
|
+
"""Parse scopes from environment variables or settings values.
|
|
11
|
+
|
|
12
|
+
Accepts either a JSON array string, a comma- or space-separated string,
|
|
13
|
+
a list of strings, or ``None``. Returns a list of scopes or ``None`` if
|
|
14
|
+
no value is provided.
|
|
15
|
+
"""
|
|
16
|
+
if value is None or value == "":
|
|
17
|
+
return None if value is None else []
|
|
18
|
+
if isinstance(value, list):
|
|
19
|
+
return [str(v).strip() for v in value if str(v).strip()]
|
|
20
|
+
if isinstance(value, str):
|
|
21
|
+
value = value.strip()
|
|
22
|
+
if not value:
|
|
23
|
+
return []
|
|
24
|
+
# Try JSON array first
|
|
25
|
+
if value.startswith("["):
|
|
26
|
+
try:
|
|
27
|
+
data = json.loads(value)
|
|
28
|
+
if isinstance(data, list):
|
|
29
|
+
return [str(v).strip() for v in data if str(v).strip()]
|
|
30
|
+
except Exception:
|
|
31
|
+
pass
|
|
32
|
+
# Fallback to comma/space separated list
|
|
33
|
+
return [s.strip() for s in value.replace(",", " ").split() if s.strip()]
|
|
34
|
+
return value
|
fastmcp/utilities/cli.py
CHANGED
|
@@ -1,24 +1,149 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
3
5
|
from importlib.metadata import version
|
|
6
|
+
from pathlib import Path
|
|
4
7
|
from typing import TYPE_CHECKING, Any, Literal
|
|
5
8
|
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
from rich.align import Align
|
|
6
11
|
from rich.console import Console, Group
|
|
7
12
|
from rich.panel import Panel
|
|
8
13
|
from rich.table import Table
|
|
9
14
|
from rich.text import Text
|
|
10
15
|
|
|
11
16
|
import fastmcp
|
|
17
|
+
from fastmcp.utilities.logging import get_logger
|
|
18
|
+
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
|
19
|
+
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
|
20
|
+
from fastmcp.utilities.types import get_cached_typeadapter
|
|
12
21
|
|
|
13
22
|
if TYPE_CHECKING:
|
|
14
23
|
from fastmcp import FastMCP
|
|
15
24
|
|
|
25
|
+
logger = get_logger("cli.config")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_already_in_uv_subprocess() -> bool:
|
|
29
|
+
"""Check if we're already running in a FastMCP uv subprocess."""
|
|
30
|
+
return bool(os.environ.get("FASTMCP_UV_SPAWNED"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_and_merge_config(
|
|
34
|
+
server_spec: str | None,
|
|
35
|
+
**cli_overrides,
|
|
36
|
+
) -> tuple[MCPServerConfig, str]:
|
|
37
|
+
"""Load config from server_spec and apply CLI overrides.
|
|
38
|
+
|
|
39
|
+
This consolidates the config parsing logic that was duplicated across
|
|
40
|
+
run, inspect, and dev commands.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
server_spec: Python file, config file, URL, or None to auto-detect
|
|
44
|
+
cli_overrides: CLI arguments that override config values
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Tuple of (MCPServerConfig, resolved_server_spec)
|
|
48
|
+
"""
|
|
49
|
+
config = None
|
|
50
|
+
config_path = None
|
|
51
|
+
|
|
52
|
+
# Auto-detect fastmcp.json if no server_spec provided
|
|
53
|
+
if server_spec is None:
|
|
54
|
+
config_path = Path("fastmcp.json")
|
|
55
|
+
if not config_path.exists():
|
|
56
|
+
found_config = MCPServerConfig.find_config()
|
|
57
|
+
if found_config:
|
|
58
|
+
config_path = found_config
|
|
59
|
+
else:
|
|
60
|
+
logger.error(
|
|
61
|
+
"No server specification provided and no fastmcp.json found in current directory.\n"
|
|
62
|
+
"Please specify a server file or create a fastmcp.json configuration."
|
|
63
|
+
)
|
|
64
|
+
raise FileNotFoundError("No server specification or fastmcp.json found")
|
|
65
|
+
|
|
66
|
+
resolved_spec = str(config_path)
|
|
67
|
+
logger.info(f"Using configuration from {config_path}")
|
|
68
|
+
else:
|
|
69
|
+
resolved_spec = server_spec
|
|
70
|
+
|
|
71
|
+
# Load config if server_spec is a .json file
|
|
72
|
+
if resolved_spec.endswith(".json"):
|
|
73
|
+
config_path = Path(resolved_spec)
|
|
74
|
+
if config_path.exists():
|
|
75
|
+
try:
|
|
76
|
+
with open(config_path) as f:
|
|
77
|
+
data = json.load(f)
|
|
78
|
+
|
|
79
|
+
# Check if it's an MCPConfig first (has canonical mcpServers key)
|
|
80
|
+
if "mcpServers" in data:
|
|
81
|
+
# MCPConfig - we don't process these here, just pass through
|
|
82
|
+
pass
|
|
83
|
+
else:
|
|
84
|
+
# Try to parse as MCPServerConfig
|
|
85
|
+
try:
|
|
86
|
+
adapter = get_cached_typeadapter(MCPServerConfig)
|
|
87
|
+
config = adapter.validate_python(data)
|
|
88
|
+
|
|
89
|
+
# Apply deployment settings
|
|
90
|
+
if config.deployment:
|
|
91
|
+
config.deployment.apply_runtime_settings(config_path)
|
|
92
|
+
|
|
93
|
+
except ValidationError:
|
|
94
|
+
# Not a valid MCPServerConfig, just pass through
|
|
95
|
+
pass
|
|
96
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
97
|
+
# Not a valid JSON file, just pass through
|
|
98
|
+
pass
|
|
99
|
+
|
|
100
|
+
# If we don't have a config object yet, create one from filesystem source
|
|
101
|
+
if config is None:
|
|
102
|
+
source = FileSystemSource(path=resolved_spec)
|
|
103
|
+
config = MCPServerConfig(source=source)
|
|
104
|
+
|
|
105
|
+
# Convert to dict for immutable transformation
|
|
106
|
+
config_dict = config.model_dump()
|
|
107
|
+
|
|
108
|
+
# Apply CLI overrides to config's environment (always exists due to default_factory)
|
|
109
|
+
if python_override := cli_overrides.get("python"):
|
|
110
|
+
config_dict["environment"]["python"] = python_override
|
|
111
|
+
if packages_override := cli_overrides.get("with_packages"):
|
|
112
|
+
# Merge packages - CLI packages are added to config packages
|
|
113
|
+
existing = config_dict["environment"].get("dependencies") or []
|
|
114
|
+
config_dict["environment"]["dependencies"] = packages_override + existing
|
|
115
|
+
if requirements_override := cli_overrides.get("with_requirements"):
|
|
116
|
+
config_dict["environment"]["requirements"] = str(requirements_override)
|
|
117
|
+
if project_override := cli_overrides.get("project"):
|
|
118
|
+
config_dict["environment"]["project"] = str(project_override)
|
|
119
|
+
if editable_override := cli_overrides.get("editable"):
|
|
120
|
+
config_dict["environment"]["editable"] = editable_override
|
|
121
|
+
|
|
122
|
+
# Apply deployment CLI overrides (always exists due to default_factory)
|
|
123
|
+
if transport_override := cli_overrides.get("transport"):
|
|
124
|
+
config_dict["deployment"]["transport"] = transport_override
|
|
125
|
+
if host_override := cli_overrides.get("host"):
|
|
126
|
+
config_dict["deployment"]["host"] = host_override
|
|
127
|
+
if port_override := cli_overrides.get("port"):
|
|
128
|
+
config_dict["deployment"]["port"] = port_override
|
|
129
|
+
if path_override := cli_overrides.get("path"):
|
|
130
|
+
config_dict["deployment"]["path"] = path_override
|
|
131
|
+
if log_level_override := cli_overrides.get("log_level"):
|
|
132
|
+
config_dict["deployment"]["log_level"] = log_level_override
|
|
133
|
+
if server_args_override := cli_overrides.get("server_args"):
|
|
134
|
+
config_dict["deployment"]["args"] = server_args_override
|
|
135
|
+
|
|
136
|
+
# Create new config from modified dict
|
|
137
|
+
new_config = MCPServerConfig(**config_dict)
|
|
138
|
+
return new_config, resolved_spec
|
|
139
|
+
|
|
140
|
+
|
|
16
141
|
LOGO_ASCII = r"""
|
|
17
|
-
_ __ ___
|
|
18
|
-
_ __ ___
|
|
142
|
+
_ __ ___ _____ __ __ _____________ ____ ____
|
|
143
|
+
_ __ ___ .'____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
|
|
19
144
|
_ __ ___ / /_ / __ `/ ___/ __/ /|_/ / / / /_/ / ___/ / / / / /
|
|
20
145
|
_ __ ___ / __/ / /_/ (__ ) /_/ / / / /___/ ____/ / __/_/ /_/ /
|
|
21
|
-
_ __ ___ /_/ \
|
|
146
|
+
_ __ ___ /_/ \____/____/\__/_/ /_/\____/_/ /_____(*)____/
|
|
22
147
|
|
|
23
148
|
""".lstrip("\n")
|
|
24
149
|
|
|
@@ -44,11 +169,14 @@ def log_server_banner(
|
|
|
44
169
|
# Create the logo text
|
|
45
170
|
logo_text = Text(LOGO_ASCII, style="bold green")
|
|
46
171
|
|
|
172
|
+
# Create the main title
|
|
173
|
+
title_text = Text("FastMCP 2.0", style="bold blue")
|
|
174
|
+
|
|
47
175
|
# Create the information table
|
|
48
176
|
info_table = Table.grid(padding=(0, 1))
|
|
49
177
|
info_table.add_column(style="bold", justify="center") # Emoji column
|
|
50
|
-
info_table.add_column(style="
|
|
51
|
-
info_table.add_column(style="
|
|
178
|
+
info_table.add_column(style="cyan", justify="left") # Label column
|
|
179
|
+
info_table.add_column(style="dim", justify="left") # Value column
|
|
52
180
|
|
|
53
181
|
match transport:
|
|
54
182
|
case "http" | "streamable-http":
|
|
@@ -69,11 +197,6 @@ def log_server_banner(
|
|
|
69
197
|
server_url += f"/{path.lstrip('/')}"
|
|
70
198
|
info_table.add_row("🔗", "Server URL:", server_url)
|
|
71
199
|
|
|
72
|
-
# Add documentation link
|
|
73
|
-
info_table.add_row("", "", "")
|
|
74
|
-
info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
|
|
75
|
-
info_table.add_row("🚀", "Deploy:", "https://fastmcp.cloud")
|
|
76
|
-
|
|
77
200
|
# Add version information with explicit style overrides
|
|
78
201
|
info_table.add_row("", "", "")
|
|
79
202
|
info_table.add_row(
|
|
@@ -83,16 +206,26 @@ def log_server_banner(
|
|
|
83
206
|
)
|
|
84
207
|
info_table.add_row(
|
|
85
208
|
"🤝",
|
|
86
|
-
"MCP version:",
|
|
209
|
+
"MCP SDK version:",
|
|
87
210
|
Text(version("mcp"), style="dim white", no_wrap=True),
|
|
88
211
|
)
|
|
89
|
-
|
|
90
|
-
|
|
212
|
+
|
|
213
|
+
# Add documentation link
|
|
214
|
+
info_table.add_row("", "", "")
|
|
215
|
+
info_table.add_row("📚", "Docs:", "https://gofastmcp.com")
|
|
216
|
+
info_table.add_row("🚀", "Deploy:", "https://fastmcp.cloud")
|
|
217
|
+
|
|
218
|
+
# Create panel with logo, title, and information using Group
|
|
219
|
+
panel_content = Group(
|
|
220
|
+
Align.center(logo_text),
|
|
221
|
+
Align.center(title_text),
|
|
222
|
+
"",
|
|
223
|
+
"",
|
|
224
|
+
Align.center(info_table),
|
|
225
|
+
)
|
|
91
226
|
|
|
92
227
|
panel = Panel(
|
|
93
228
|
panel_content,
|
|
94
|
-
title="FastMCP 2.0",
|
|
95
|
-
title_align="left",
|
|
96
229
|
border_style="dim",
|
|
97
230
|
padding=(1, 4),
|
|
98
231
|
expand=False,
|
fastmcp/utilities/components.py
CHANGED
|
@@ -91,18 +91,34 @@ class FastMCPComponent(FastMCPBaseModel):
|
|
|
91
91
|
|
|
92
92
|
return meta or None
|
|
93
93
|
|
|
94
|
-
def
|
|
94
|
+
def model_copy(
|
|
95
|
+
self,
|
|
96
|
+
*,
|
|
97
|
+
update: dict[str, Any] | None = None,
|
|
98
|
+
deep: bool = False,
|
|
99
|
+
key: str | None = None,
|
|
100
|
+
) -> Self:
|
|
101
|
+
"""
|
|
102
|
+
Create a copy of the component.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
update: A dictionary of fields to update.
|
|
106
|
+
deep: Whether to deep copy the component.
|
|
107
|
+
key: The key to use for the copy.
|
|
108
|
+
"""
|
|
95
109
|
# `model_copy` has an `update` parameter but it doesn't work for certain private attributes
|
|
96
110
|
# https://github.com/pydantic/pydantic/issues/12116
|
|
97
|
-
# So we manually set the private attribute here instead
|
|
98
|
-
copy =
|
|
99
|
-
|
|
111
|
+
# So we manually set the private attribute here instead, such as _key
|
|
112
|
+
copy = super().model_copy(update=update, deep=deep)
|
|
113
|
+
if key is not None:
|
|
114
|
+
copy._key = key
|
|
100
115
|
return copy
|
|
101
116
|
|
|
102
117
|
def __eq__(self, other: object) -> bool:
|
|
103
118
|
if type(self) is not type(other):
|
|
104
119
|
return False
|
|
105
|
-
|
|
120
|
+
if not isinstance(other, type(self)):
|
|
121
|
+
return False
|
|
106
122
|
return self.model_dump() == other.model_dump()
|
|
107
123
|
|
|
108
124
|
def __repr__(self) -> str:
|