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.
Files changed (69) hide show
  1. fastmcp/__init__.py +5 -4
  2. fastmcp/cli/claude.py +22 -18
  3. fastmcp/cli/cli.py +472 -136
  4. fastmcp/cli/install/claude_code.py +37 -40
  5. fastmcp/cli/install/claude_desktop.py +37 -42
  6. fastmcp/cli/install/cursor.py +148 -38
  7. fastmcp/cli/install/mcp_json.py +38 -43
  8. fastmcp/cli/install/shared.py +64 -7
  9. fastmcp/cli/run.py +122 -215
  10. fastmcp/client/auth/oauth.py +69 -13
  11. fastmcp/client/client.py +46 -9
  12. fastmcp/client/oauth_callback.py +91 -91
  13. fastmcp/client/sampling.py +12 -4
  14. fastmcp/client/transports.py +139 -64
  15. fastmcp/experimental/sampling/__init__.py +0 -0
  16. fastmcp/experimental/sampling/handlers/__init__.py +3 -0
  17. fastmcp/experimental/sampling/handlers/base.py +21 -0
  18. fastmcp/experimental/sampling/handlers/openai.py +163 -0
  19. fastmcp/experimental/server/openapi/routing.py +0 -2
  20. fastmcp/experimental/server/openapi/server.py +0 -2
  21. fastmcp/experimental/utilities/openapi/parser.py +5 -1
  22. fastmcp/mcp_config.py +40 -20
  23. fastmcp/prompts/prompt_manager.py +2 -0
  24. fastmcp/resources/resource_manager.py +4 -0
  25. fastmcp/server/auth/__init__.py +2 -0
  26. fastmcp/server/auth/auth.py +2 -1
  27. fastmcp/server/auth/oauth_proxy.py +1047 -0
  28. fastmcp/server/auth/providers/azure.py +270 -0
  29. fastmcp/server/auth/providers/github.py +287 -0
  30. fastmcp/server/auth/providers/google.py +305 -0
  31. fastmcp/server/auth/providers/jwt.py +24 -12
  32. fastmcp/server/auth/providers/workos.py +256 -2
  33. fastmcp/server/auth/redirect_validation.py +65 -0
  34. fastmcp/server/context.py +91 -41
  35. fastmcp/server/elicitation.py +60 -1
  36. fastmcp/server/http.py +3 -3
  37. fastmcp/server/middleware/logging.py +66 -28
  38. fastmcp/server/proxy.py +2 -0
  39. fastmcp/server/sampling/handler.py +19 -0
  40. fastmcp/server/server.py +76 -15
  41. fastmcp/settings.py +16 -1
  42. fastmcp/tools/tool.py +22 -9
  43. fastmcp/tools/tool_manager.py +2 -0
  44. fastmcp/tools/tool_transform.py +39 -10
  45. fastmcp/utilities/auth.py +34 -0
  46. fastmcp/utilities/cli.py +148 -15
  47. fastmcp/utilities/components.py +2 -1
  48. fastmcp/utilities/inspect.py +166 -37
  49. fastmcp/utilities/json_schema_type.py +4 -2
  50. fastmcp/utilities/logging.py +4 -1
  51. fastmcp/utilities/mcp_config.py +47 -18
  52. fastmcp/utilities/mcp_server_config/__init__.py +25 -0
  53. fastmcp/utilities/mcp_server_config/v1/__init__.py +0 -0
  54. fastmcp/utilities/mcp_server_config/v1/environments/__init__.py +6 -0
  55. fastmcp/utilities/mcp_server_config/v1/environments/base.py +30 -0
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +306 -0
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +446 -0
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +361 -0
  59. fastmcp/utilities/mcp_server_config/v1/sources/__init__.py +0 -0
  60. fastmcp/utilities/mcp_server_config/v1/sources/base.py +30 -0
  61. fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py +216 -0
  62. fastmcp/utilities/tests.py +7 -2
  63. fastmcp/utilities/types.py +15 -2
  64. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/METADATA +2 -1
  65. fastmcp-2.12.0.dist-info/RECORD +129 -0
  66. fastmcp-2.11.3.dist-info/RECORD +0 -108
  67. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/WHEEL +0 -0
  68. {fastmcp-2.11.3.dist-info → fastmcp-2.12.0.dist-info}/entry_points.txt +0 -0
  69. {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="bold cyan", justify="left") # Label column
51
- info_table.add_column(style="white", justify="left") # Value column
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
- # Create panel with logo and information using Group
90
- panel_content = Group(logo_text, "", info_table)
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,
@@ -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
- assert isinstance(other, type(self))
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:
@@ -4,8 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import importlib.metadata
6
6
  from dataclasses import dataclass
7
- from typing import Any
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
- fastmcp_version: str
74
- mcp_version: str
75
- server_version: str | None
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
- server_version=(
175
- mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version
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, # For 1.x, key and name are the same
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
- annotations=annotations,
228
- tags=None, # 1.x doesn't have tags
229
- enabled=None, # 1.x doesn't have enabled field
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, # For 1.x, key and name are the same
265
+ key=mcp_prompt.name,
244
266
  name=mcp_prompt.name,
245
267
  description=mcp_prompt.description,
246
268
  arguments=arguments,
247
- tags=None, # 1.x doesn't have tags
248
- enabled=None, # 1.x doesn't have enabled field
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), # For 1.x, key and uri are the same
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
- tags=None, # 1.x doesn't have tags
263
- enabled=None, # 1.x doesn't have enabled field
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
- tags=None, # 1.x doesn't have tags
280
- enabled=None, # 1.x doesn't have enabled field
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=importlib.metadata.version("mcp"),
324
+ fastmcp_version=fastmcp.__version__, # Version generating this manifest
296
325
  mcp_version=importlib.metadata.version("mcp"),
297
- server_version=mcp._mcp_server.version,
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, # FastMCP1x does have templates
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
- assert name is not None # Should not be None after the or operation
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
- assert name is not None # Should not be None after the or operation
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)
@@ -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)