fastmcp 2.11.3__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/oauth_callback.py +91 -91
- fastmcp/client/sampling.py +12 -4
- fastmcp/client/transports.py +139 -64
- 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 +0 -2
- fastmcp/experimental/server/openapi/server.py +0 -2
- fastmcp/experimental/utilities/openapi/parser.py +5 -1
- fastmcp/mcp_config.py +40 -20
- fastmcp/prompts/prompt_manager.py +2 -0
- fastmcp/resources/resource_manager.py +4 -0
- fastmcp/server/auth/__init__.py +2 -0
- fastmcp/server/auth/auth.py +2 -1
- 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 +24 -12
- fastmcp/server/auth/providers/workos.py +256 -2
- fastmcp/server/auth/redirect_validation.py +65 -0
- fastmcp/server/context.py +91 -41
- fastmcp/server/elicitation.py +60 -1
- fastmcp/server/http.py +3 -3
- fastmcp/server/middleware/logging.py +66 -28
- fastmcp/server/proxy.py +2 -0
- fastmcp/server/sampling/handler.py +19 -0
- fastmcp/server/server.py +76 -15
- fastmcp/settings.py +16 -1
- fastmcp/tools/tool.py +22 -9
- fastmcp/tools/tool_manager.py +2 -0
- fastmcp/tools/tool_transform.py +39 -10
- fastmcp/utilities/auth.py +34 -0
- fastmcp/utilities/cli.py +148 -15
- fastmcp/utilities/components.py +2 -1
- 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/tests.py +7 -2
- fastmcp/utilities/types.py +15 -2
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
- fastmcp-2.12.0.dist-info/RECORD +129 -0
- fastmcp-2.11.3.dist-info/RECORD +0 -108
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/licenses/LICENSE +0 -0
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
|
@@ -117,7 +117,8 @@ class FastMCPComponent(FastMCPBaseModel):
|
|
|
117
117
|
def __eq__(self, other: object) -> bool:
|
|
118
118
|
if type(self) is not type(other):
|
|
119
119
|
return False
|
|
120
|
-
|
|
120
|
+
if not isinstance(other, type(self)):
|
|
121
|
+
return False
|
|
121
122
|
return self.model_dump() == other.model_dump()
|
|
122
123
|
|
|
123
124
|
def __repr__(self) -> str:
|
fastmcp/utilities/inspect.py
CHANGED
|
@@ -4,8 +4,10 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import importlib.metadata
|
|
6
6
|
from dataclasses import dataclass
|
|
7
|
-
from
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Literal, cast
|
|
8
9
|
|
|
10
|
+
import pydantic_core
|
|
9
11
|
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
|
10
12
|
|
|
11
13
|
import fastmcp
|
|
@@ -21,9 +23,12 @@ class ToolInfo:
|
|
|
21
23
|
name: str
|
|
22
24
|
description: str | None
|
|
23
25
|
input_schema: dict[str, Any]
|
|
26
|
+
output_schema: dict[str, Any] | None = None
|
|
24
27
|
annotations: dict[str, Any] | None = None
|
|
25
28
|
tags: list[str] | None = None
|
|
26
29
|
enabled: bool | None = None
|
|
30
|
+
title: str | None = None
|
|
31
|
+
meta: dict[str, Any] | None = None
|
|
27
32
|
|
|
28
33
|
|
|
29
34
|
@dataclass
|
|
@@ -36,6 +41,8 @@ class PromptInfo:
|
|
|
36
41
|
arguments: list[dict[str, Any]] | None = None
|
|
37
42
|
tags: list[str] | None = None
|
|
38
43
|
enabled: bool | None = None
|
|
44
|
+
title: str | None = None
|
|
45
|
+
meta: dict[str, Any] | None = None
|
|
39
46
|
|
|
40
47
|
|
|
41
48
|
@dataclass
|
|
@@ -47,8 +54,11 @@ class ResourceInfo:
|
|
|
47
54
|
name: str | None
|
|
48
55
|
description: str | None
|
|
49
56
|
mime_type: str | None = None
|
|
57
|
+
annotations: dict[str, Any] | None = None
|
|
50
58
|
tags: list[str] | None = None
|
|
51
59
|
enabled: bool | None = None
|
|
60
|
+
title: str | None = None
|
|
61
|
+
meta: dict[str, Any] | None = None
|
|
52
62
|
|
|
53
63
|
|
|
54
64
|
@dataclass
|
|
@@ -60,8 +70,12 @@ class TemplateInfo:
|
|
|
60
70
|
name: str | None
|
|
61
71
|
description: str | None
|
|
62
72
|
mime_type: str | None = None
|
|
73
|
+
parameters: dict[str, Any] | None = None
|
|
74
|
+
annotations: dict[str, Any] | None = None
|
|
63
75
|
tags: list[str] | None = None
|
|
64
76
|
enabled: bool | None = None
|
|
77
|
+
title: str | None = None
|
|
78
|
+
meta: dict[str, Any] | None = None
|
|
65
79
|
|
|
66
80
|
|
|
67
81
|
@dataclass
|
|
@@ -70,9 +84,10 @@ class FastMCPInfo:
|
|
|
70
84
|
|
|
71
85
|
name: str
|
|
72
86
|
instructions: str | None
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
version: str | None # The server's own version string (if specified)
|
|
88
|
+
fastmcp_version: str # Version of FastMCP generating this manifest
|
|
89
|
+
mcp_version: str # Version of MCP protocol library
|
|
90
|
+
server_generation: int # Server generation: 1 (mcp package) or 2 (fastmcp)
|
|
76
91
|
tools: list[ToolInfo]
|
|
77
92
|
prompts: list[PromptInfo]
|
|
78
93
|
resources: list[ResourceInfo]
|
|
@@ -106,9 +121,12 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
106
121
|
name=tool.name or key,
|
|
107
122
|
description=tool.description,
|
|
108
123
|
input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
|
|
124
|
+
output_schema=tool.output_schema,
|
|
109
125
|
annotations=tool.annotations.model_dump() if tool.annotations else None,
|
|
110
126
|
tags=list(tool.tags) if tool.tags else None,
|
|
111
127
|
enabled=tool.enabled,
|
|
128
|
+
title=tool.title,
|
|
129
|
+
meta=tool.meta,
|
|
112
130
|
)
|
|
113
131
|
)
|
|
114
132
|
|
|
@@ -125,6 +143,8 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
125
143
|
else None,
|
|
126
144
|
tags=list(prompt.tags) if prompt.tags else None,
|
|
127
145
|
enabled=prompt.enabled,
|
|
146
|
+
title=prompt.title,
|
|
147
|
+
meta=prompt.meta,
|
|
128
148
|
)
|
|
129
149
|
)
|
|
130
150
|
|
|
@@ -138,8 +158,13 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
138
158
|
name=resource.name,
|
|
139
159
|
description=resource.description,
|
|
140
160
|
mime_type=resource.mime_type,
|
|
161
|
+
annotations=resource.annotations.model_dump()
|
|
162
|
+
if resource.annotations
|
|
163
|
+
else None,
|
|
141
164
|
tags=list(resource.tags) if resource.tags else None,
|
|
142
165
|
enabled=resource.enabled,
|
|
166
|
+
title=resource.title,
|
|
167
|
+
meta=resource.meta,
|
|
143
168
|
)
|
|
144
169
|
)
|
|
145
170
|
|
|
@@ -153,8 +178,14 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
153
178
|
name=template.name,
|
|
154
179
|
description=template.description,
|
|
155
180
|
mime_type=template.mime_type,
|
|
181
|
+
parameters=template.parameters,
|
|
182
|
+
annotations=template.annotations.model_dump()
|
|
183
|
+
if template.annotations
|
|
184
|
+
else None,
|
|
156
185
|
tags=list(template.tags) if template.tags else None,
|
|
157
186
|
enabled=template.enabled,
|
|
187
|
+
title=template.title,
|
|
188
|
+
meta=template.meta,
|
|
158
189
|
)
|
|
159
190
|
)
|
|
160
191
|
|
|
@@ -171,9 +202,8 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
|
|
|
171
202
|
instructions=mcp.instructions,
|
|
172
203
|
fastmcp_version=fastmcp.__version__,
|
|
173
204
|
mcp_version=importlib.metadata.version("mcp"),
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
),
|
|
205
|
+
server_generation=2, # FastMCP v2
|
|
206
|
+
version=(mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version),
|
|
177
207
|
tools=tool_infos,
|
|
178
208
|
prompts=prompt_infos,
|
|
179
209
|
resources=resource_infos,
|
|
@@ -191,7 +221,6 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
191
221
|
Returns:
|
|
192
222
|
FastMCPInfo dataclass containing the extracted information
|
|
193
223
|
"""
|
|
194
|
-
|
|
195
224
|
# Use a client to interact with the FastMCP1x server
|
|
196
225
|
async with Client(mcp) as client:
|
|
197
226
|
# Get components via client calls (these return MCP objects)
|
|
@@ -208,25 +237,18 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
208
237
|
# Extract detailed tool information from MCP Tool objects
|
|
209
238
|
tool_infos = []
|
|
210
239
|
for mcp_tool in mcp_tools:
|
|
211
|
-
# Extract annotations if they exist
|
|
212
|
-
annotations = None
|
|
213
|
-
if hasattr(mcp_tool, "annotations") and mcp_tool.annotations:
|
|
214
|
-
if hasattr(mcp_tool.annotations, "model_dump"):
|
|
215
|
-
annotations = mcp_tool.annotations.model_dump()
|
|
216
|
-
elif isinstance(mcp_tool.annotations, dict):
|
|
217
|
-
annotations = mcp_tool.annotations
|
|
218
|
-
else:
|
|
219
|
-
annotations = None
|
|
220
|
-
|
|
221
240
|
tool_infos.append(
|
|
222
241
|
ToolInfo(
|
|
223
|
-
key=mcp_tool.name,
|
|
242
|
+
key=mcp_tool.name,
|
|
224
243
|
name=mcp_tool.name,
|
|
225
244
|
description=mcp_tool.description,
|
|
226
245
|
input_schema=mcp_tool.inputSchema if mcp_tool.inputSchema else {},
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
246
|
+
output_schema=None, # v1 doesn't have output_schema
|
|
247
|
+
annotations=None, # v1 doesn't have annotations
|
|
248
|
+
tags=None, # v1 doesn't have tags
|
|
249
|
+
enabled=None, # v1 doesn't have enabled field
|
|
250
|
+
title=None, # v1 doesn't have title
|
|
251
|
+
meta=None, # v1 doesn't have meta field
|
|
230
252
|
)
|
|
231
253
|
)
|
|
232
254
|
|
|
@@ -240,12 +262,14 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
240
262
|
|
|
241
263
|
prompt_infos.append(
|
|
242
264
|
PromptInfo(
|
|
243
|
-
key=mcp_prompt.name,
|
|
265
|
+
key=mcp_prompt.name,
|
|
244
266
|
name=mcp_prompt.name,
|
|
245
267
|
description=mcp_prompt.description,
|
|
246
268
|
arguments=arguments,
|
|
247
|
-
tags=None, #
|
|
248
|
-
enabled=None, #
|
|
269
|
+
tags=None, # v1 doesn't have tags
|
|
270
|
+
enabled=None, # v1 doesn't have enabled field
|
|
271
|
+
title=None, # v1 doesn't have title
|
|
272
|
+
meta=None, # v1 doesn't have meta field
|
|
249
273
|
)
|
|
250
274
|
)
|
|
251
275
|
|
|
@@ -254,13 +278,16 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
254
278
|
for mcp_resource in mcp_resources:
|
|
255
279
|
resource_infos.append(
|
|
256
280
|
ResourceInfo(
|
|
257
|
-
key=str(mcp_resource.uri),
|
|
281
|
+
key=str(mcp_resource.uri),
|
|
258
282
|
uri=str(mcp_resource.uri),
|
|
259
283
|
name=mcp_resource.name,
|
|
260
284
|
description=mcp_resource.description,
|
|
261
285
|
mime_type=mcp_resource.mimeType,
|
|
262
|
-
|
|
263
|
-
|
|
286
|
+
annotations=None, # v1 doesn't have annotations
|
|
287
|
+
tags=None, # v1 doesn't have tags
|
|
288
|
+
enabled=None, # v1 doesn't have enabled field
|
|
289
|
+
title=None, # v1 doesn't have title
|
|
290
|
+
meta=None, # v1 doesn't have meta field
|
|
264
291
|
)
|
|
265
292
|
)
|
|
266
293
|
|
|
@@ -269,15 +296,17 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
269
296
|
for mcp_template in mcp_templates:
|
|
270
297
|
template_infos.append(
|
|
271
298
|
TemplateInfo(
|
|
272
|
-
key=str(
|
|
273
|
-
mcp_template.uriTemplate
|
|
274
|
-
), # For 1.x, key and uriTemplate are the same
|
|
299
|
+
key=str(mcp_template.uriTemplate),
|
|
275
300
|
uri_template=str(mcp_template.uriTemplate),
|
|
276
301
|
name=mcp_template.name,
|
|
277
302
|
description=mcp_template.description,
|
|
278
303
|
mime_type=mcp_template.mimeType,
|
|
279
|
-
|
|
280
|
-
|
|
304
|
+
parameters=None, # v1 doesn't expose template parameters
|
|
305
|
+
annotations=None, # v1 doesn't have annotations
|
|
306
|
+
tags=None, # v1 doesn't have tags
|
|
307
|
+
enabled=None, # v1 doesn't have enabled field
|
|
308
|
+
title=None, # v1 doesn't have title
|
|
309
|
+
meta=None, # v1 doesn't have meta field
|
|
281
310
|
)
|
|
282
311
|
)
|
|
283
312
|
|
|
@@ -292,13 +321,14 @@ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
|
|
|
292
321
|
return FastMCPInfo(
|
|
293
322
|
name=mcp._mcp_server.name,
|
|
294
323
|
instructions=mcp._mcp_server.instructions,
|
|
295
|
-
fastmcp_version=
|
|
324
|
+
fastmcp_version=fastmcp.__version__, # Version generating this manifest
|
|
296
325
|
mcp_version=importlib.metadata.version("mcp"),
|
|
297
|
-
|
|
326
|
+
server_generation=1, # MCP v1
|
|
327
|
+
version=mcp._mcp_server.version,
|
|
298
328
|
tools=tool_infos,
|
|
299
329
|
prompts=prompt_infos,
|
|
300
330
|
resources=resource_infos,
|
|
301
|
-
templates=template_infos,
|
|
331
|
+
templates=template_infos,
|
|
302
332
|
capabilities=capabilities,
|
|
303
333
|
)
|
|
304
334
|
|
|
@@ -318,4 +348,103 @@ async def inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo:
|
|
|
318
348
|
if isinstance(mcp, FastMCP1x):
|
|
319
349
|
return await inspect_fastmcp_v1(mcp)
|
|
320
350
|
else:
|
|
321
|
-
return await inspect_fastmcp_v2(mcp)
|
|
351
|
+
return await inspect_fastmcp_v2(cast(FastMCP[Any], mcp))
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class InspectFormat(str, Enum):
|
|
355
|
+
"""Output format for inspect command."""
|
|
356
|
+
|
|
357
|
+
FASTMCP = "fastmcp"
|
|
358
|
+
MCP = "mcp"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
async def format_fastmcp_info(info: FastMCPInfo) -> bytes:
|
|
362
|
+
"""Format FastMCPInfo as FastMCP-specific JSON.
|
|
363
|
+
|
|
364
|
+
This includes FastMCP-specific fields like tags, enabled, annotations, etc.
|
|
365
|
+
"""
|
|
366
|
+
# Build the output dict with nested structure
|
|
367
|
+
result = {
|
|
368
|
+
"server": {
|
|
369
|
+
"name": info.name,
|
|
370
|
+
"instructions": info.instructions,
|
|
371
|
+
"version": info.version,
|
|
372
|
+
"generation": info.server_generation,
|
|
373
|
+
"capabilities": info.capabilities,
|
|
374
|
+
},
|
|
375
|
+
"environment": {
|
|
376
|
+
"fastmcp": info.fastmcp_version,
|
|
377
|
+
"mcp": info.mcp_version,
|
|
378
|
+
},
|
|
379
|
+
"tools": info.tools,
|
|
380
|
+
"prompts": info.prompts,
|
|
381
|
+
"resources": info.resources,
|
|
382
|
+
"templates": info.templates,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return pydantic_core.to_json(result, indent=2)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
async def format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes:
|
|
389
|
+
"""Format server info as standard MCP protocol JSON.
|
|
390
|
+
|
|
391
|
+
Uses Client to get the standard MCP protocol format with camelCase fields.
|
|
392
|
+
Includes version metadata at the top level.
|
|
393
|
+
"""
|
|
394
|
+
async with Client(mcp) as client:
|
|
395
|
+
# Get all the MCP protocol objects
|
|
396
|
+
tools_result = await client.list_tools_mcp()
|
|
397
|
+
prompts_result = await client.list_prompts_mcp()
|
|
398
|
+
resources_result = await client.list_resources_mcp()
|
|
399
|
+
templates_result = await client.list_resource_templates_mcp()
|
|
400
|
+
|
|
401
|
+
# Get server info from the initialize result
|
|
402
|
+
server_info = client.initialize_result.serverInfo
|
|
403
|
+
|
|
404
|
+
# Combine into MCP protocol structure with environment metadata
|
|
405
|
+
result = {
|
|
406
|
+
"environment": {
|
|
407
|
+
"fastmcp": fastmcp.__version__, # Version generating this manifest
|
|
408
|
+
"mcp": importlib.metadata.version("mcp"), # MCP protocol version
|
|
409
|
+
},
|
|
410
|
+
"serverInfo": server_info,
|
|
411
|
+
"capabilities": {}, # MCP format doesn't include capabilities at top level
|
|
412
|
+
"tools": tools_result.tools,
|
|
413
|
+
"prompts": prompts_result.prompts,
|
|
414
|
+
"resources": resources_result.resources,
|
|
415
|
+
"resourceTemplates": templates_result.resourceTemplates,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return pydantic_core.to_json(result, indent=2)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
async def format_info(
|
|
422
|
+
mcp: FastMCP[Any] | FastMCP1x,
|
|
423
|
+
format: InspectFormat | Literal["fastmcp", "mcp"],
|
|
424
|
+
info: FastMCPInfo | None = None,
|
|
425
|
+
) -> bytes:
|
|
426
|
+
"""Format server information according to the specified format.
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
mcp: The FastMCP instance
|
|
430
|
+
format: Output format ("fastmcp" or "mcp")
|
|
431
|
+
info: Pre-extracted FastMCPInfo (optional, will be extracted if not provided)
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
JSON bytes in the requested format
|
|
435
|
+
"""
|
|
436
|
+
# Convert string to enum if needed
|
|
437
|
+
if isinstance(format, str):
|
|
438
|
+
format = InspectFormat(format)
|
|
439
|
+
|
|
440
|
+
if format == InspectFormat.MCP:
|
|
441
|
+
# MCP format doesn't need FastMCPInfo, it uses Client directly
|
|
442
|
+
return await format_mcp_info(mcp)
|
|
443
|
+
elif format == InspectFormat.FASTMCP:
|
|
444
|
+
# For FastMCP format, we need the FastMCPInfo
|
|
445
|
+
# This works for both v1 and v2 servers
|
|
446
|
+
if info is None:
|
|
447
|
+
info = await inspect_fastmcp(mcp)
|
|
448
|
+
return await format_fastmcp_info(info)
|
|
449
|
+
else:
|
|
450
|
+
raise ValueError(f"Unknown format: {format}")
|
|
@@ -449,7 +449,8 @@ def _create_pydantic_model(
|
|
|
449
449
|
) -> type:
|
|
450
450
|
"""Create Pydantic BaseModel from object schema with additionalProperties."""
|
|
451
451
|
name = name or schema.get("title", "Root")
|
|
452
|
-
|
|
452
|
+
if name is None:
|
|
453
|
+
raise ValueError("Name is required")
|
|
453
454
|
sanitized_name = _sanitize_name(name)
|
|
454
455
|
schema_hash = _hash_schema(schema)
|
|
455
456
|
cache_key = (schema_hash, sanitized_name)
|
|
@@ -507,7 +508,8 @@ def _create_dataclass(
|
|
|
507
508
|
"""Create dataclass from object schema."""
|
|
508
509
|
name = name or schema.get("title", "Root")
|
|
509
510
|
# Sanitize name for class creation
|
|
510
|
-
|
|
511
|
+
if name is None:
|
|
512
|
+
raise ValueError("Name is required")
|
|
511
513
|
sanitized_name = _sanitize_name(name)
|
|
512
514
|
schema_hash = _hash_schema(schema)
|
|
513
515
|
cache_key = (schema_hash, sanitized_name)
|
fastmcp/utilities/logging.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Logging utilities for FastMCP."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
-
from typing import Literal
|
|
4
|
+
from typing import Any, Literal
|
|
5
5
|
|
|
6
6
|
from rich.console import Console
|
|
7
7
|
from rich.logging import RichHandler
|
|
@@ -23,6 +23,7 @@ def configure_logging(
|
|
|
23
23
|
level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
|
|
24
24
|
logger: logging.Logger | None = None,
|
|
25
25
|
enable_rich_tracebacks: bool = True,
|
|
26
|
+
**rich_kwargs: Any,
|
|
26
27
|
) -> None:
|
|
27
28
|
"""
|
|
28
29
|
Configure logging for FastMCP.
|
|
@@ -30,6 +31,7 @@ def configure_logging(
|
|
|
30
31
|
Args:
|
|
31
32
|
logger: the logger to configure
|
|
32
33
|
level: the log level to use
|
|
34
|
+
rich_kwargs: the parameters to use for creating RichHandler
|
|
33
35
|
"""
|
|
34
36
|
|
|
35
37
|
if logger is None:
|
|
@@ -39,6 +41,7 @@ def configure_logging(
|
|
|
39
41
|
handler = RichHandler(
|
|
40
42
|
console=Console(stderr=True),
|
|
41
43
|
rich_tracebacks=enable_rich_tracebacks,
|
|
44
|
+
**rich_kwargs,
|
|
42
45
|
)
|
|
43
46
|
formatter = logging.Formatter("%(message)s")
|
|
44
47
|
handler.setFormatter(formatter)
|