fastmcp 2.11.1__py3-none-any.whl → 2.11.3__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 (38) hide show
  1. fastmcp/cli/cli.py +5 -5
  2. fastmcp/cli/install/claude_code.py +2 -2
  3. fastmcp/cli/install/claude_desktop.py +2 -2
  4. fastmcp/cli/install/cursor.py +2 -2
  5. fastmcp/cli/install/mcp_json.py +2 -2
  6. fastmcp/cli/install/shared.py +2 -2
  7. fastmcp/cli/run.py +74 -24
  8. fastmcp/client/logging.py +25 -1
  9. fastmcp/client/transports.py +4 -3
  10. fastmcp/experimental/server/openapi/routing.py +1 -1
  11. fastmcp/experimental/server/openapi/server.py +10 -23
  12. fastmcp/experimental/utilities/openapi/__init__.py +2 -2
  13. fastmcp/experimental/utilities/openapi/formatters.py +34 -0
  14. fastmcp/experimental/utilities/openapi/models.py +5 -2
  15. fastmcp/experimental/utilities/openapi/parser.py +248 -70
  16. fastmcp/experimental/utilities/openapi/schemas.py +135 -106
  17. fastmcp/prompts/prompt_manager.py +2 -2
  18. fastmcp/resources/resource_manager.py +12 -6
  19. fastmcp/server/auth/__init__.py +9 -1
  20. fastmcp/server/auth/auth.py +17 -1
  21. fastmcp/server/auth/providers/jwt.py +3 -4
  22. fastmcp/server/auth/registry.py +1 -1
  23. fastmcp/server/dependencies.py +32 -2
  24. fastmcp/server/http.py +41 -34
  25. fastmcp/server/proxy.py +33 -15
  26. fastmcp/server/server.py +18 -11
  27. fastmcp/settings.py +6 -9
  28. fastmcp/tools/tool.py +7 -7
  29. fastmcp/tools/tool_manager.py +3 -1
  30. fastmcp/tools/tool_transform.py +41 -27
  31. fastmcp/utilities/components.py +19 -4
  32. fastmcp/utilities/inspect.py +12 -17
  33. fastmcp/utilities/openapi.py +4 -4
  34. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/METADATA +2 -2
  35. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/RECORD +38 -38
  36. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/WHEEL +0 -0
  37. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/entry_points.txt +0 -0
  38. {fastmcp-2.11.1.dist-info → fastmcp-2.11.3.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/proxy.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import inspect
3
4
  import warnings
4
- from collections.abc import Callable
5
+ from collections.abc import Awaitable, Callable
5
6
  from pathlib import Path
6
7
  from typing import TYPE_CHECKING, Any, cast
7
8
  from urllib.parse import quote
@@ -48,11 +49,27 @@ if TYPE_CHECKING:
48
49
 
49
50
  logger = get_logger(__name__)
50
51
 
52
+ # Type alias for client factory functions
53
+ ClientFactoryT = Callable[[], Client] | Callable[[], Awaitable[Client]]
51
54
 
52
- class ProxyToolManager(ToolManager):
55
+
56
+ class ProxyManagerMixin:
57
+ """A mixin for proxy managers to provide a unified client retrieval method."""
58
+
59
+ client_factory: ClientFactoryT
60
+
61
+ async def _get_client(self) -> Client:
62
+ """Gets a client instance by calling the sync or async factory."""
63
+ client = self.client_factory()
64
+ if inspect.isawaitable(client):
65
+ client = await client
66
+ return client
67
+
68
+
69
+ class ProxyToolManager(ToolManager, ProxyManagerMixin):
53
70
  """A ToolManager that sources its tools from a remote client in addition to local and mounted tools."""
54
71
 
55
- def __init__(self, client_factory: Callable[[], Client], **kwargs):
72
+ def __init__(self, client_factory: ClientFactoryT, **kwargs):
56
73
  super().__init__(**kwargs)
57
74
  self.client_factory = client_factory
58
75
 
@@ -63,7 +80,7 @@ class ProxyToolManager(ToolManager):
63
80
 
64
81
  # Then add proxy tools, but don't overwrite existing ones
65
82
  try:
66
- client = self.client_factory()
83
+ client = await self._get_client()
67
84
  async with client:
68
85
  client_tools = await client.list_tools()
69
86
  for tool in client_tools:
@@ -94,7 +111,7 @@ class ProxyToolManager(ToolManager):
94
111
  return await super().call_tool(key, arguments)
95
112
  except NotFoundError:
96
113
  # If not found locally, try proxy
97
- client = self.client_factory()
114
+ client = await self._get_client()
98
115
  async with client:
99
116
  result = await client.call_tool(key, arguments)
100
117
  return ToolResult(
@@ -103,10 +120,10 @@ class ProxyToolManager(ToolManager):
103
120
  )
104
121
 
105
122
 
106
- class ProxyResourceManager(ResourceManager):
123
+ class ProxyResourceManager(ResourceManager, ProxyManagerMixin):
107
124
  """A ResourceManager that sources its resources from a remote client in addition to local and mounted resources."""
108
125
 
109
- def __init__(self, client_factory: Callable[[], Client], **kwargs):
126
+ def __init__(self, client_factory: ClientFactoryT, **kwargs):
110
127
  super().__init__(**kwargs)
111
128
  self.client_factory = client_factory
112
129
 
@@ -117,7 +134,7 @@ class ProxyResourceManager(ResourceManager):
117
134
 
118
135
  # Then add proxy resources, but don't overwrite existing ones
119
136
  try:
120
- client = self.client_factory()
137
+ client = await self._get_client()
121
138
  async with client:
122
139
  client_resources = await client.list_resources()
123
140
  for resource in client_resources:
@@ -140,7 +157,7 @@ class ProxyResourceManager(ResourceManager):
140
157
 
141
158
  # Then add proxy templates, but don't overwrite existing ones
142
159
  try:
143
- client = self.client_factory()
160
+ client = await self._get_client()
144
161
  async with client:
145
162
  client_templates = await client.list_resource_templates()
146
163
  for template in client_templates:
@@ -173,7 +190,7 @@ class ProxyResourceManager(ResourceManager):
173
190
  return await super().read_resource(uri)
174
191
  except NotFoundError:
175
192
  # If not found locally, try proxy
176
- client = self.client_factory()
193
+ client = await self._get_client()
177
194
  async with client:
178
195
  result = await client.read_resource(uri)
179
196
  if isinstance(result[0], TextResourceContents):
@@ -184,10 +201,10 @@ class ProxyResourceManager(ResourceManager):
184
201
  raise ResourceError(f"Unsupported content type: {type(result[0])}")
185
202
 
186
203
 
187
- class ProxyPromptManager(PromptManager):
204
+ class ProxyPromptManager(PromptManager, ProxyManagerMixin):
188
205
  """A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts."""
189
206
 
190
- def __init__(self, client_factory: Callable[[], Client], **kwargs):
207
+ def __init__(self, client_factory: ClientFactoryT, **kwargs):
191
208
  super().__init__(**kwargs)
192
209
  self.client_factory = client_factory
193
210
 
@@ -198,7 +215,7 @@ class ProxyPromptManager(PromptManager):
198
215
 
199
216
  # Then add proxy prompts, but don't overwrite existing ones
200
217
  try:
201
- client = self.client_factory()
218
+ client = await self._get_client()
202
219
  async with client:
203
220
  client_prompts = await client.list_prompts()
204
221
  for prompt in client_prompts:
@@ -230,7 +247,7 @@ class ProxyPromptManager(PromptManager):
230
247
  return await super().render_prompt(name, arguments)
231
248
  except NotFoundError:
232
249
  # If not found locally, try proxy
233
- client = self.client_factory()
250
+ client = await self._get_client()
234
251
  async with client:
235
252
  result = await client.get_prompt(name, arguments)
236
253
  return result
@@ -444,7 +461,7 @@ class FastMCPProxy(FastMCP):
444
461
  self,
445
462
  client: Client | None = None,
446
463
  *,
447
- client_factory: Callable[[], Client] | None = None,
464
+ client_factory: ClientFactoryT | None = None,
448
465
  **kwargs,
449
466
  ):
450
467
  """
@@ -459,6 +476,7 @@ class FastMCPProxy(FastMCP):
459
476
  created that provides session isolation for backwards compatibility.
460
477
  client_factory: A callable that returns a Client instance when called.
461
478
  This gives you full control over session creation and reuse.
479
+ Can be either a synchronous or asynchronous function.
462
480
  **kwargs: Additional settings for the FastMCP server.
463
481
  """
464
482
 
fastmcp/server/server.py CHANGED
@@ -49,7 +49,7 @@ from fastmcp.prompts import Prompt, PromptManager
49
49
  from fastmcp.prompts.prompt import FunctionPrompt
50
50
  from fastmcp.resources import Resource, ResourceManager
51
51
  from fastmcp.resources.template import ResourceTemplate
52
- from fastmcp.server.auth.auth import AuthProvider
52
+ from fastmcp.server.auth import AuthProvider
53
53
  from fastmcp.server.auth.registry import get_registered_provider
54
54
  from fastmcp.server.http import (
55
55
  StarletteWithLifespan,
@@ -140,19 +140,18 @@ class FastMCP(Generic[LifespanResultT]):
140
140
  ]
141
141
  | None
142
142
  ) = None,
143
- tool_serializer: Callable[[Any], str] | None = None,
144
- cache_expiration_seconds: float | None = None,
145
- on_duplicate_tools: DuplicateBehavior | None = None,
146
- on_duplicate_resources: DuplicateBehavior | None = None,
147
- on_duplicate_prompts: DuplicateBehavior | None = None,
143
+ dependencies: list[str] | None = None,
148
144
  resource_prefix_format: Literal["protocol", "path"] | None = None,
149
145
  mask_error_details: bool | None = None,
150
146
  tools: list[Tool | Callable[..., Any]] | None = None,
151
147
  tool_transformations: dict[str, ToolTransformConfig] | None = None,
152
- dependencies: list[str] | None = None,
148
+ tool_serializer: Callable[[Any], str] | None = None,
153
149
  include_tags: set[str] | None = None,
154
150
  exclude_tags: set[str] | None = None,
155
151
  include_fastmcp_meta: bool | None = None,
152
+ on_duplicate_tools: DuplicateBehavior | None = None,
153
+ on_duplicate_resources: DuplicateBehavior | None = None,
154
+ on_duplicate_prompts: DuplicateBehavior | None = None,
156
155
  # ---
157
156
  # ---
158
157
  # --- The following arguments are DEPRECATED ---
@@ -304,6 +303,10 @@ class FastMCP(Generic[LifespanResultT]):
304
303
  def instructions(self) -> str | None:
305
304
  return self._mcp_server.instructions
306
305
 
306
+ @property
307
+ def version(self) -> str | None:
308
+ return self._mcp_server.version
309
+
307
310
  async def run_async(
308
311
  self,
309
312
  transport: Transport | None = None,
@@ -1888,7 +1891,7 @@ class FastMCP(Generic[LifespanResultT]):
1888
1891
  # Import tools from the server
1889
1892
  for key, tool in (await server.get_tools()).items():
1890
1893
  if prefix:
1891
- tool = tool.with_key(f"{prefix}_{key}")
1894
+ tool = tool.model_copy(key=f"{prefix}_{key}")
1892
1895
  self._tool_manager.add_tool(tool)
1893
1896
 
1894
1897
  # Import resources and templates from the server
@@ -1897,7 +1900,9 @@ class FastMCP(Generic[LifespanResultT]):
1897
1900
  resource_key = add_resource_prefix(
1898
1901
  key, prefix, self.resource_prefix_format
1899
1902
  )
1900
- resource = resource.with_key(resource_key)
1903
+ resource = resource.model_copy(
1904
+ update={"name": f"{prefix}_{resource.name}"}, key=resource_key
1905
+ )
1901
1906
  self._resource_manager.add_resource(resource)
1902
1907
 
1903
1908
  for key, template in (await server.get_resource_templates()).items():
@@ -1905,13 +1910,15 @@ class FastMCP(Generic[LifespanResultT]):
1905
1910
  template_key = add_resource_prefix(
1906
1911
  key, prefix, self.resource_prefix_format
1907
1912
  )
1908
- template = template.with_key(template_key)
1913
+ template = template.model_copy(
1914
+ update={"name": f"{prefix}_{template.name}"}, key=template_key
1915
+ )
1909
1916
  self._resource_manager.add_template(template)
1910
1917
 
1911
1918
  # Import prompts from the server
1912
1919
  for key, prompt in (await server.get_prompts()).items():
1913
1920
  if prefix:
1914
- prompt = prompt.with_key(f"{prefix}_{key}")
1921
+ prompt = prompt.model_copy(key=f"{prefix}_{key}")
1915
1922
  self._prompt_manager.add_prompt(prompt)
1916
1923
 
1917
1924
  if prefix:
fastmcp/settings.py CHANGED
@@ -222,9 +222,9 @@ class Settings(BaseSettings):
222
222
  # HTTP settings
223
223
  host: str = "127.0.0.1"
224
224
  port: int = 8000
225
- sse_path: str = "/sse/"
225
+ sse_path: str = "/sse"
226
226
  message_path: str = "/messages/"
227
- streamable_http_path: str = "/mcp/"
227
+ streamable_http_path: str = "/mcp"
228
228
  debug: bool = False
229
229
 
230
230
  # error handling
@@ -244,13 +244,10 @@ class Settings(BaseSettings):
244
244
  ),
245
245
  ] = False
246
246
 
247
- server_dependencies: Annotated[
248
- list[str],
249
- Field(
250
- default_factory=list,
251
- description="List of dependencies to install in the server environment",
252
- ),
253
- ] = []
247
+ server_dependencies: list[str] = Field(
248
+ default_factory=list,
249
+ description="List of dependencies to install in the server environment",
250
+ )
254
251
 
255
252
  # StreamableHTTP settings
256
253
  json_response: bool = False
fastmcp/tools/tool.py CHANGED
@@ -8,7 +8,6 @@ from typing import (
8
8
  Annotated,
9
9
  Any,
10
10
  Generic,
11
- Literal,
12
11
  TypeVar,
13
12
  get_type_hints,
14
13
  )
@@ -163,7 +162,7 @@ class Tool(FastMCPComponent):
163
162
  tags: set[str] | None = None,
164
163
  annotations: ToolAnnotations | None = None,
165
164
  exclude_args: list[str] | None = None,
166
- output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
165
+ output_schema: dict[str, Any] | None | NotSetT = NotSet,
167
166
  serializer: Callable[[Any], str] | None = None,
168
167
  meta: dict[str, Any] | None = None,
169
168
  enabled: bool | None = None,
@@ -199,17 +198,18 @@ class Tool(FastMCPComponent):
199
198
  def from_tool(
200
199
  cls,
201
200
  tool: Tool,
202
- transform_fn: Callable[..., Any] | None = None,
201
+ *,
203
202
  name: str | None = None,
204
203
  title: str | None | NotSetT = NotSet,
205
- transform_args: dict[str, ArgTransform] | None = None,
206
204
  description: str | None | NotSetT = NotSet,
207
205
  tags: set[str] | None = None,
208
- annotations: ToolAnnotations | None = None,
209
- output_schema: dict[str, Any] | None | Literal[False] = None,
206
+ annotations: ToolAnnotations | None | NotSetT = NotSet,
207
+ output_schema: dict[str, Any] | None | NotSetT = NotSet,
210
208
  serializer: Callable[[Any], str] | None = None,
211
209
  meta: dict[str, Any] | None | NotSetT = NotSet,
210
+ transform_args: dict[str, ArgTransform] | None = None,
212
211
  enabled: bool | None = None,
212
+ transform_fn: Callable[..., Any] | None = None,
213
213
  ) -> TransformedTool:
214
214
  from fastmcp.tools.tool_transform import TransformedTool
215
215
 
@@ -242,7 +242,7 @@ class FunctionTool(Tool):
242
242
  tags: set[str] | None = None,
243
243
  annotations: ToolAnnotations | None = None,
244
244
  exclude_args: list[str] | None = None,
245
- output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet,
245
+ output_schema: dict[str, Any] | None | NotSetT = NotSet,
246
246
  serializer: Callable[[Any], str] | None = None,
247
247
  meta: dict[str, Any] | None = None,
248
248
  enabled: bool | None = None,
@@ -75,7 +75,9 @@ class ToolManager:
75
75
  child_dict = {t.key: t for t in child_results}
76
76
  if mounted.prefix:
77
77
  for tool in child_dict.values():
78
- prefixed_tool = tool.with_key(f"{mounted.prefix}_{tool.key}")
78
+ prefixed_tool = tool.model_copy(
79
+ key=f"{mounted.prefix}_{tool.key}"
80
+ )
79
81
  all_tools[prefixed_tool.key] = prefixed_tool
80
82
  else:
81
83
  all_tools.update(child_dict)
@@ -6,6 +6,7 @@ from contextvars import ContextVar
6
6
  from dataclasses import dataclass
7
7
  from typing import Annotated, Any, Literal
8
8
 
9
+ import pydantic_core
9
10
  from mcp.types import ToolAnnotations
10
11
  from pydantic import ConfigDict
11
12
  from pydantic.fields import Field
@@ -312,11 +313,9 @@ class TransformedTool(Tool):
312
313
  # Custom function returns ToolResult - preserve its content
313
314
  return result
314
315
  else:
315
- # Forwarded call with disabled schema - strip structured content
316
- return ToolResult(
317
- content=result.content,
318
- structured_content=None,
319
- )
316
+ # Forwarded call with no explicit schema - preserve parent's structured content
317
+ # The parent tool may have generated structured content via its own fallback logic
318
+ return result
320
319
  elif self.output_schema.get(
321
320
  "type"
322
321
  ) != "object" and not self.output_schema.get("x-fastmcp-wrap-result"):
@@ -334,17 +333,23 @@ class TransformedTool(Tool):
334
333
  result, serializer=self.serializer
335
334
  )
336
335
 
337
- # Handle structured content based on output schema
336
+ structured_output = None
337
+ # First handle structured content based on output schema, if any
338
338
  if self.output_schema is not None:
339
339
  if self.output_schema.get("x-fastmcp-wrap-result"):
340
340
  # Schema says wrap - always wrap in result key
341
341
  structured_output = {"result": result}
342
342
  else:
343
- # Object schemas - use result directly
344
- # User is responsible for returning dict-compatible data
345
343
  structured_output = result
346
- else:
347
- structured_output = None
344
+ # If no output schema, try to serialize the result. If it is a dict, use
345
+ # it as structured content. If it is not a dict, ignore it.
346
+ if structured_output is None:
347
+ try:
348
+ structured_output = pydantic_core.to_jsonable_python(result)
349
+ if not isinstance(structured_output, dict):
350
+ structured_output = None
351
+ except Exception:
352
+ pass
348
353
 
349
354
  return ToolResult(
350
355
  content=unstructured_result,
@@ -363,9 +368,9 @@ class TransformedTool(Tool):
363
368
  tags: set[str] | None = None,
364
369
  transform_fn: Callable[..., Any] | None = None,
365
370
  transform_args: dict[str, ArgTransform] | None = None,
366
- annotations: ToolAnnotations | None = None,
367
- output_schema: dict[str, Any] | None | Literal[False] = None,
368
- serializer: Callable[[Any], str] | None = None,
371
+ annotations: ToolAnnotations | None | NotSetT = NotSet,
372
+ output_schema: dict[str, Any] | None | NotSetT = NotSet,
373
+ serializer: Callable[[Any], str] | None | NotSetT = NotSet,
369
374
  meta: dict[str, Any] | None | NotSetT = NotSet,
370
375
  enabled: bool | None = None,
371
376
  ) -> TransformedTool:
@@ -445,6 +450,11 @@ class TransformedTool(Tool):
445
450
  """
446
451
  transform_args = transform_args or {}
447
452
 
453
+ if transform_fn is not None:
454
+ parsed_fn = ParsedFunction.from_function(transform_fn, validate=False)
455
+ else:
456
+ parsed_fn = None
457
+
448
458
  # Validate transform_args
449
459
  parent_params = set(tool.parameters.get("properties", {}).keys())
450
460
  unknown_args = set(transform_args.keys()) - parent_params
@@ -457,16 +467,11 @@ class TransformedTool(Tool):
457
467
  # Always create the forwarding transform
458
468
  schema, forwarding_fn = cls._create_forwarding_transform(tool, transform_args)
459
469
 
460
- # Handle output schema with smart fallback
461
- if output_schema is False:
462
- final_output_schema = None
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
470
+ # Handle output schema
471
+ if output_schema is NotSet:
472
+ # Use smart fallback: try custom function, then parent
468
473
  if transform_fn is not None:
469
- parsed_fn = ParsedFunction.from_function(transform_fn, validate=False)
474
+ assert parsed_fn is not None
470
475
  final_output_schema = parsed_fn.output_schema
471
476
  if final_output_schema is None:
472
477
  # Check if function returns ToolResult - if so, don't fall back to parent
@@ -479,15 +484,17 @@ class TransformedTool(Tool):
479
484
  final_output_schema = tool.output_schema
480
485
  else:
481
486
  final_output_schema = tool.output_schema
487
+ else:
488
+ assert isinstance(output_schema, dict | None)
489
+ final_output_schema = output_schema
482
490
 
483
491
  if transform_fn is None:
484
492
  # User wants pure transformation - use forwarding_fn as the main function
485
493
  final_fn = forwarding_fn
486
494
  final_schema = schema
487
495
  else:
496
+ assert parsed_fn is not None
488
497
  # User provided custom function - merge schemas
489
- if "parsed_fn" not in locals():
490
- parsed_fn = ParsedFunction.from_function(transform_fn, validate=False)
491
498
  final_fn = transform_fn
492
499
 
493
500
  has_kwargs = cls._function_has_kwargs(transform_fn)
@@ -552,6 +559,13 @@ class TransformedTool(Tool):
552
559
  )
553
560
  final_title = title if not isinstance(title, NotSetT) else tool.title
554
561
  final_meta = meta if not isinstance(meta, NotSetT) else tool.meta
562
+ final_annotations = (
563
+ annotations if not isinstance(annotations, NotSetT) else tool.annotations
564
+ )
565
+ final_serializer = (
566
+ serializer if not isinstance(serializer, NotSetT) else tool.serializer
567
+ )
568
+ final_enabled = enabled if enabled is not None else tool.enabled
555
569
 
556
570
  transformed_tool = cls(
557
571
  fn=final_fn,
@@ -563,11 +577,11 @@ class TransformedTool(Tool):
563
577
  parameters=final_schema,
564
578
  output_schema=final_output_schema,
565
579
  tags=tags or tool.tags,
566
- annotations=annotations or tool.annotations,
567
- serializer=serializer or tool.serializer,
580
+ annotations=final_annotations,
581
+ serializer=final_serializer,
568
582
  meta=final_meta,
569
583
  transform_args=transform_args,
570
- enabled=enabled if enabled is not None else True,
584
+ enabled=final_enabled,
571
585
  )
572
586
 
573
587
  return transformed_tool
@@ -91,12 +91,27 @@ class FastMCPComponent(FastMCPBaseModel):
91
91
 
92
92
  return meta or None
93
93
 
94
- def with_key(self, key: str) -> Self:
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 = self.model_copy()
99
- copy._key = key
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:
@@ -9,6 +9,7 @@ from typing import Any
9
9
  from mcp.server.fastmcp import FastMCP as FastMCP1x
10
10
 
11
11
  import fastmcp
12
+ from fastmcp import Client
12
13
  from fastmcp.server.server import FastMCP
13
14
 
14
15
 
@@ -71,7 +72,7 @@ class FastMCPInfo:
71
72
  instructions: str | None
72
73
  fastmcp_version: str
73
74
  mcp_version: str
74
- server_version: str
75
+ server_version: str | None
75
76
  tools: list[ToolInfo]
76
77
  prompts: list[PromptInfo]
77
78
  resources: list[ResourceInfo]
@@ -170,7 +171,9 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
170
171
  instructions=mcp.instructions,
171
172
  fastmcp_version=fastmcp.__version__,
172
173
  mcp_version=importlib.metadata.version("mcp"),
173
- server_version=fastmcp.__version__, # v2.x uses FastMCP version
174
+ server_version=(
175
+ mcp.version if hasattr(mcp, "version") else mcp._mcp_server.version
176
+ ),
174
177
  tools=tool_infos,
175
178
  prompts=prompt_infos,
176
179
  resources=resource_infos,
@@ -179,7 +182,7 @@ async def inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo:
179
182
  )
180
183
 
181
184
 
182
- async def inspect_fastmcp_v1(mcp: Any) -> FastMCPInfo:
185
+ async def inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo:
183
186
  """Extract information from a FastMCP v1.x instance using a Client.
184
187
 
185
188
  Args:
@@ -188,7 +191,6 @@ async def inspect_fastmcp_v1(mcp: Any) -> FastMCPInfo:
188
191
  Returns:
189
192
  FastMCPInfo dataclass containing the extracted information
190
193
  """
191
- from fastmcp import Client
192
194
 
193
195
  # Use a client to interact with the FastMCP1x server
194
196
  async with Client(mcp) as client:
@@ -288,11 +290,11 @@ async def inspect_fastmcp_v1(mcp: Any) -> FastMCPInfo:
288
290
  }
289
291
 
290
292
  return FastMCPInfo(
291
- name=mcp.name,
292
- instructions=getattr(mcp, "instructions", None),
293
- fastmcp_version=fastmcp.__version__, # Report current fastmcp version
293
+ name=mcp._mcp_server.name,
294
+ instructions=mcp._mcp_server.instructions,
295
+ fastmcp_version=importlib.metadata.version("mcp"),
294
296
  mcp_version=importlib.metadata.version("mcp"),
295
- server_version="1.0", # FastMCP 1.x version
297
+ server_version=mcp._mcp_server.version,
296
298
  tools=tool_infos,
297
299
  prompts=prompt_infos,
298
300
  resources=resource_infos,
@@ -301,14 +303,7 @@ async def inspect_fastmcp_v1(mcp: Any) -> FastMCPInfo:
301
303
  )
302
304
 
303
305
 
304
- def _is_fastmcp_v1(mcp: Any) -> bool:
305
- """Check if the given instance is a FastMCP v1.x instance."""
306
-
307
- # Check if it's an instance of FastMCP1x and not FastMCP2
308
- return isinstance(mcp, FastMCP1x) and not isinstance(mcp, FastMCP)
309
-
310
-
311
- async def inspect_fastmcp(mcp: FastMCP[Any] | Any) -> FastMCPInfo:
306
+ async def inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo:
312
307
  """Extract information from a FastMCP instance into a dataclass.
313
308
 
314
309
  This function automatically detects whether the instance is FastMCP v1.x or v2.x
@@ -320,7 +315,7 @@ async def inspect_fastmcp(mcp: FastMCP[Any] | Any) -> FastMCPInfo:
320
315
  Returns:
321
316
  FastMCPInfo dataclass containing the extracted information
322
317
  """
323
- if _is_fastmcp_v1(mcp):
318
+ if isinstance(mcp, FastMCP1x):
324
319
  return await inspect_fastmcp_v1(mcp)
325
320
  else:
326
321
  return await inspect_fastmcp_v2(mcp)
@@ -212,7 +212,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
212
212
  if openapi_version.startswith("3.0"):
213
213
  # Use OpenAPI 3.0 models
214
214
  openapi_30 = OpenAPI_30.model_validate(openapi_dict)
215
- logger.info(
215
+ logger.debug(
216
216
  f"Successfully parsed OpenAPI 3.0 schema version: {openapi_30.openapi}"
217
217
  )
218
218
  parser = OpenAPIParser(
@@ -230,7 +230,7 @@ def parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute
230
230
  else:
231
231
  # Default to OpenAPI 3.1 models
232
232
  openapi_31 = OpenAPI.model_validate(openapi_dict)
233
- logger.info(
233
+ logger.debug(
234
234
  f"Successfully parsed OpenAPI 3.1 schema version: {openapi_31.openapi}"
235
235
  )
236
236
  parser = OpenAPIParser(
@@ -713,7 +713,7 @@ class OpenAPIParser(
713
713
  openapi_version=self.openapi_version,
714
714
  )
715
715
  routes.append(route)
716
- logger.info(
716
+ logger.debug(
717
717
  f"Successfully extracted route: {method_upper} {path_str}"
718
718
  )
719
719
  except ValueError as op_error:
@@ -734,7 +734,7 @@ class OpenAPIParser(
734
734
  exc_info=True,
735
735
  )
736
736
 
737
- logger.info(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
737
+ logger.debug(f"Finished parsing. Extracted {len(routes)} HTTP routes.")
738
738
  return routes
739
739
 
740
740
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.11.1
3
+ Version: 2.11.3
4
4
  Summary: The fast, Pythonic way to build MCP servers and clients.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -21,7 +21,7 @@ Requires-Dist: authlib>=1.5.2
21
21
  Requires-Dist: cyclopts>=3.0.0
22
22
  Requires-Dist: exceptiongroup>=1.2.2
23
23
  Requires-Dist: httpx>=0.28.1
24
- Requires-Dist: mcp>=1.10.0
24
+ Requires-Dist: mcp<2.0.0,>=1.12.4
25
25
  Requires-Dist: openapi-core>=0.19.5
26
26
  Requires-Dist: openapi-pydantic>=0.5.1
27
27
  Requires-Dist: pydantic[email]>=2.11.7