fastmcp 2.3.3__py3-none-any.whl → 2.3.5__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/tools/tool.py CHANGED
@@ -11,9 +11,8 @@ from mcp.types import Tool as MCPTool
11
11
  from pydantic import BaseModel, BeforeValidator, Field
12
12
 
13
13
  import fastmcp
14
- from fastmcp.exceptions import ToolError
15
14
  from fastmcp.server.dependencies import get_context
16
- from fastmcp.utilities.json_schema import prune_params
15
+ from fastmcp.utilities.json_schema import compress_schema
17
16
  from fastmcp.utilities.logging import get_logger
18
17
  from fastmcp.utilities.types import (
19
18
  Image,
@@ -82,7 +81,11 @@ class Tool(BaseModel):
82
81
 
83
82
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
84
83
  if context_kwarg:
85
- schema = prune_params(schema, params=[context_kwarg])
84
+ prune_params = [context_kwarg]
85
+ else:
86
+ prune_params = None
87
+
88
+ schema = compress_schema(schema, prune_params=prune_params)
86
89
 
87
90
  return cls(
88
91
  fn=fn,
@@ -102,48 +105,45 @@ class Tool(BaseModel):
102
105
 
103
106
  arguments = arguments.copy()
104
107
 
105
- try:
106
- context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
107
- if context_kwarg and context_kwarg not in arguments:
108
- arguments[context_kwarg] = get_context()
109
-
110
- if fastmcp.settings.settings.tool_attempt_parse_json_args:
111
- # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
112
- # being passed in as JSON inside a string rather than an actual list.
113
- #
114
- # Claude desktop is prone to this - in fact it seems incapable of NOT doing
115
- # this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
116
- # which can be pre-parsed here.
117
- signature = inspect.signature(self.fn)
118
- for param_name in self.parameters["properties"]:
119
- arg = arguments.get(param_name, None)
120
- # if not in signature, we won't have annotations, so skip logic
121
- if param_name not in signature.parameters:
122
- continue
123
- # if not a string, we won't have a JSON to parse, so skip logic
124
- if not isinstance(arg, str):
125
- continue
126
- # skip if the type is a simple type (int, float, bool)
127
- if signature.parameters[param_name].annotation in (
128
- int,
129
- float,
130
- bool,
131
- ):
132
- continue
133
- try:
134
- arguments[param_name] = json.loads(arg)
135
-
136
- except json.JSONDecodeError:
137
- pass
138
-
139
- type_adapter = get_cached_typeadapter(self.fn)
140
- result = type_adapter.validate_python(arguments)
141
- if inspect.isawaitable(result):
142
- result = await result
143
-
144
- return _convert_to_content(result, serializer=self.serializer)
145
- except Exception as e:
146
- raise ToolError(f"Error executing tool {self.name}: {e}") from e
108
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
109
+ if context_kwarg and context_kwarg not in arguments:
110
+ arguments[context_kwarg] = get_context()
111
+
112
+ if fastmcp.settings.settings.tool_attempt_parse_json_args:
113
+ # Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
114
+ # being passed in as JSON inside a string rather than an actual list.
115
+ #
116
+ # Claude desktop is prone to this - in fact it seems incapable of NOT doing
117
+ # this. For sub-models, it tends to pass dicts (JSON objects) as JSON strings,
118
+ # which can be pre-parsed here.
119
+ signature = inspect.signature(self.fn)
120
+ for param_name in self.parameters["properties"]:
121
+ arg = arguments.get(param_name, None)
122
+ # if not in signature, we won't have annotations, so skip logic
123
+ if param_name not in signature.parameters:
124
+ continue
125
+ # if not a string, we won't have a JSON to parse, so skip logic
126
+ if not isinstance(arg, str):
127
+ continue
128
+ # skip if the type is a simple type (int, float, bool)
129
+ if signature.parameters[param_name].annotation in (
130
+ int,
131
+ float,
132
+ bool,
133
+ ):
134
+ continue
135
+ try:
136
+ arguments[param_name] = json.loads(arg)
137
+
138
+ except json.JSONDecodeError:
139
+ pass
140
+
141
+ type_adapter = get_cached_typeadapter(self.fn)
142
+ result = type_adapter.validate_python(arguments)
143
+ if inspect.isawaitable(result):
144
+ result = await result
145
+
146
+ return _convert_to_content(result, serializer=self.serializer)
147
147
 
148
148
  def to_mcp_tool(self, **overrides: Any) -> MCPTool:
149
149
  kwargs = {
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any
5
5
 
6
6
  from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
7
7
 
8
- from fastmcp.exceptions import NotFoundError
8
+ from fastmcp.exceptions import NotFoundError, ToolError
9
9
  from fastmcp.settings import DuplicateBehavior
10
10
  from fastmcp.tools.tool import Tool
11
11
  from fastmcp.utilities.logging import get_logger
@@ -94,6 +94,20 @@ class ToolManager:
94
94
  self._tools[key] = tool
95
95
  return tool
96
96
 
97
+ def remove_tool(self, key: str) -> None:
98
+ """Remove a tool from the server.
99
+
100
+ Args:
101
+ key: The key of the tool to remove
102
+
103
+ Raises:
104
+ NotFoundError: If the tool is not found
105
+ """
106
+ if key in self._tools:
107
+ del self._tools[key]
108
+ else:
109
+ raise NotFoundError(f"Unknown tool: {key}")
110
+
97
111
  async def call_tool(
98
112
  self, key: str, arguments: dict[str, Any]
99
113
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
@@ -102,4 +116,15 @@ class ToolManager:
102
116
  if not tool:
103
117
  raise NotFoundError(f"Unknown tool: {key}")
104
118
 
105
- return await tool.run(arguments)
119
+ try:
120
+ return await tool.run(arguments)
121
+
122
+ # raise ToolErrors as-is
123
+ except ToolError as e:
124
+ logger.exception(f"Error calling tool {key!r}: {e}")
125
+ raise e
126
+
127
+ # raise other exceptions as ToolErrors without revealing internal details
128
+ except Exception as e:
129
+ logger.exception(f"Error calling tool {key!r}: {e}")
130
+ raise ToolError(f"Error calling tool {key!r}") from e
@@ -0,0 +1,49 @@
1
+ from collections.abc import Callable, Iterable, Mapping
2
+ from typing import Any
3
+
4
+ import httpx
5
+ import mcp.types
6
+ from exceptiongroup import BaseExceptionGroup
7
+ from mcp import McpError
8
+
9
+ import fastmcp
10
+
11
+
12
+ def iter_exc(group: BaseExceptionGroup):
13
+ for exc in group.exceptions:
14
+ if isinstance(exc, BaseExceptionGroup):
15
+ yield from iter_exc(exc)
16
+ else:
17
+ yield exc
18
+
19
+
20
+ def _exception_handler(group: BaseExceptionGroup):
21
+ for leaf in iter_exc(group):
22
+ if isinstance(leaf, httpx.ConnectTimeout):
23
+ raise McpError(
24
+ error=mcp.types.ErrorData(
25
+ code=httpx.codes.REQUEST_TIMEOUT,
26
+ message="Timed out while waiting for response.",
27
+ )
28
+ )
29
+ raise leaf
30
+
31
+
32
+ # this catch handler is used to catch taskgroup exception groups and raise the
33
+ # first exception. This allows more sane debugging.
34
+ _catch_handlers: Mapping[
35
+ type[BaseException] | Iterable[type[BaseException]],
36
+ Callable[[BaseExceptionGroup[Any]], Any],
37
+ ] = {
38
+ Exception: _exception_handler,
39
+ }
40
+
41
+
42
+ def get_catch_handlers() -> Mapping[
43
+ type[BaseException] | Iterable[type[BaseException]],
44
+ Callable[[BaseExceptionGroup[Any]], Any],
45
+ ]:
46
+ if fastmcp.settings.settings.client_raise_first_exceptiongroup_error:
47
+ return _catch_handlers
48
+ else:
49
+ return {}
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import copy
4
- from collections.abc import Mapping, Sequence
5
4
 
6
5
 
7
6
  def _prune_param(schema: dict, param: str) -> dict:
@@ -14,6 +13,7 @@ def _prune_param(schema: dict, param: str) -> dict:
14
13
  removed = props.pop(param, None)
15
14
  if removed is None: # nothing to do
16
15
  return schema
16
+
17
17
  # Keep empty properties object rather than removing it entirely
18
18
  schema["properties"] = props
19
19
  if param in schema.get("required", []):
@@ -21,39 +21,100 @@ def _prune_param(schema: dict, param: str) -> dict:
21
21
  if not schema["required"]:
22
22
  schema.pop("required")
23
23
 
24
- # ── 2. collect all remaining local $ref targets ───────────────────
24
+ return schema
25
+
26
+
27
+ def _walk_and_prune(
28
+ schema: dict,
29
+ prune_defs: bool = False,
30
+ prune_titles: bool = False,
31
+ prune_additional_properties: bool = False,
32
+ ) -> dict:
33
+ """Walk the schema and optionally prune titles, unused definitions, and additionalProperties: false."""
34
+
35
+ # Will only be used if prune_defs is True
25
36
  used_defs: set[str] = set()
26
37
 
27
- def walk(node: object) -> None: # depth-first traversal
28
- if isinstance(node, Mapping):
29
- ref = node.get("$ref")
30
- if isinstance(ref, str) and ref.startswith("#/$defs/"):
31
- used_defs.add(ref.split("/")[-1])
38
+ def walk(node: object) -> None:
39
+ if isinstance(node, dict):
40
+ # Process $ref for definition tracking
41
+ if prune_defs:
42
+ ref = node.get("$ref")
43
+ if isinstance(ref, str) and ref.startswith("#/$defs/"):
44
+ used_defs.add(ref.split("/")[-1])
45
+
46
+ # Remove title if requested
47
+ if prune_titles and "title" in node:
48
+ node.pop("title")
49
+
50
+ # Remove additionalProperties: false at any level if requested
51
+ if (
52
+ prune_additional_properties
53
+ and node.get("additionalProperties", None) is False
54
+ ):
55
+ node.pop("additionalProperties")
56
+
57
+ # Walk children
32
58
  for v in node.values():
33
59
  walk(v)
34
- elif isinstance(node, Sequence) and not isinstance(node, str | bytes):
60
+
61
+ elif isinstance(node, list):
35
62
  for v in node:
36
63
  walk(v)
37
64
 
65
+ # Traverse the schema once
38
66
  walk(schema)
39
67
 
40
- # ── 3. remove orphaned definitions ────────────────────────────────
41
- defs = schema.get("$defs", {})
42
- for def_name in list(defs):
43
- if def_name not in used_defs:
44
- defs.pop(def_name)
45
- if not defs:
46
- schema.pop("$defs", None)
68
+ # Remove orphaned definitions if requested
69
+ if prune_defs:
70
+ defs = schema.get("$defs", {})
71
+ for def_name in list(defs):
72
+ if def_name not in used_defs:
73
+ defs.pop(def_name)
74
+ if not defs:
75
+ schema.pop("$defs", None)
76
+
77
+ return schema
78
+
47
79
 
80
+ def _prune_additional_properties(schema: dict) -> dict:
81
+ """Remove additionalProperties from the schema if it is False."""
82
+ if schema.get("additionalProperties", None) is False:
83
+ schema.pop("additionalProperties")
48
84
  return schema
49
85
 
50
86
 
51
- def prune_params(schema: dict, params: list[str]) -> dict:
87
+ def compress_schema(
88
+ schema: dict,
89
+ prune_params: list[str] | None = None,
90
+ prune_defs: bool = True,
91
+ prune_additional_properties: bool = True,
92
+ prune_titles: bool = False,
93
+ ) -> dict:
52
94
  """
53
95
  Remove the given parameters from the schema.
54
96
 
97
+ Args:
98
+ schema: The schema to compress
99
+ prune_params: List of parameter names to remove from properties
100
+ prune_defs: Whether to remove unused definitions
101
+ prune_additional_properties: Whether to remove additionalProperties: false
102
+ prune_titles: Whether to remove title fields from the schema
55
103
  """
104
+ # Make a copy so we don't modify the original
56
105
  schema = copy.deepcopy(schema)
57
- for param in params:
106
+
107
+ # Remove specific parameters if requested
108
+ for param in prune_params or []:
58
109
  schema = _prune_param(schema, param=param)
110
+
111
+ # Do a single walk to handle pruning operations
112
+ if prune_defs or prune_titles or prune_additional_properties:
113
+ schema = _walk_and_prune(
114
+ schema,
115
+ prune_defs=prune_defs,
116
+ prune_titles=prune_titles,
117
+ prune_additional_properties=prune_additional_properties,
118
+ )
119
+
59
120
  return schema
@@ -21,22 +21,27 @@ def get_logger(name: str) -> logging.Logger:
21
21
 
22
22
  def configure_logging(
23
23
  level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | int = "INFO",
24
+ logger: logging.Logger | None = None,
24
25
  ) -> None:
25
- """Configure logging for FastMCP.
26
+ """
27
+ Configure logging for FastMCP.
26
28
 
27
29
  Args:
30
+ logger: the logger to configure
28
31
  level: the log level to use
29
32
  """
33
+ if logger is None:
34
+ logger = logging.getLogger("FastMCP")
35
+
30
36
  # Only configure the FastMCP logger namespace
31
37
  handler = RichHandler(console=Console(stderr=True), rich_tracebacks=True)
32
38
  formatter = logging.Formatter("%(message)s")
33
39
  handler.setFormatter(formatter)
34
40
 
35
- fastmcp_logger = logging.getLogger("FastMCP")
36
- fastmcp_logger.setLevel(level)
41
+ logger.setLevel(level)
37
42
 
38
43
  # Remove any existing handlers to avoid duplicates on reconfiguration
39
- for hdlr in fastmcp_logger.handlers[:]:
40
- fastmcp_logger.removeHandler(hdlr)
44
+ for hdlr in logger.handlers[:]:
45
+ logger.removeHandler(hdlr)
41
46
 
42
- fastmcp_logger.addHandler(handler)
47
+ logger.addHandler(handler)
@@ -84,6 +84,9 @@ class HTTPRoute(BaseModel):
84
84
  responses: dict[str, ResponseInfo] = Field(
85
85
  default_factory=dict
86
86
  ) # Key: status code str
87
+ schema_definitions: dict[str, JsonSchema] = Field(
88
+ default_factory=dict
89
+ ) # Store component schemas
87
90
 
88
91
 
89
92
  # Export public symbols
@@ -221,6 +224,27 @@ class OpenAPI31Parser(BaseOpenAPIParser):
221
224
  logger.warning("OpenAPI schema has no paths defined.")
222
225
  return []
223
226
 
227
+ # Extract component schemas to add to each route
228
+ schema_definitions = {}
229
+ if hasattr(self.openapi, "components") and self.openapi.components:
230
+ components = self.openapi.components
231
+ if hasattr(components, "schemas") and components.schemas:
232
+ for name, schema in components.schemas.items():
233
+ try:
234
+ if isinstance(schema, Reference):
235
+ resolved_schema = self._resolve_ref(schema)
236
+ schema_definitions[name] = self._extract_schema_as_dict(
237
+ resolved_schema
238
+ )
239
+ else:
240
+ schema_definitions[name] = self._extract_schema_as_dict(
241
+ schema
242
+ )
243
+ except Exception as e:
244
+ logger.warning(
245
+ f"Failed to extract schema definition '{name}': {e}"
246
+ )
247
+
224
248
  for path_str, path_item_obj in self.openapi.paths.items():
225
249
  if not isinstance(path_item_obj, PathItem):
226
250
  logger.warning(
@@ -269,6 +293,7 @@ class OpenAPI31Parser(BaseOpenAPIParser):
269
293
  parameters=parameters,
270
294
  request_body=request_body_info,
271
295
  responses=responses,
296
+ schema_definitions=schema_definitions,
272
297
  )
273
298
  routes.append(route)
274
299
  logger.info(
@@ -386,16 +411,36 @@ class OpenAPI31Parser(BaseOpenAPIParser):
386
411
 
387
412
  param_schema_dict = {}
388
413
  if param_schema_obj: # Check if schema exists
414
+ # Resolve the schema if it's a reference
415
+ resolved_schema = self._resolve_ref(param_schema_obj)
389
416
  param_schema_dict = self._extract_schema_as_dict(param_schema_obj)
417
+
418
+ # Ensure default value is preserved from resolved schema
419
+ if (
420
+ not isinstance(resolved_schema, Reference)
421
+ and hasattr(resolved_schema, "default")
422
+ and resolved_schema.default is not None
423
+ ):
424
+ param_schema_dict["default"] = resolved_schema.default
390
425
  elif parameter.content:
391
426
  # Handle complex parameters with 'content'
392
427
  first_media_type = next(iter(parameter.content.values()), None)
393
428
  if (
394
429
  first_media_type and first_media_type.media_type_schema
395
430
  ): # CORRECTED: Use 'media_type_schema'
396
- param_schema_dict = self._extract_schema_as_dict(
397
- first_media_type.media_type_schema
398
- )
431
+ # Resolve the schema if it's a reference
432
+ media_schema = first_media_type.media_type_schema
433
+ resolved_media_schema = self._resolve_ref(media_schema)
434
+ param_schema_dict = self._extract_schema_as_dict(media_schema)
435
+
436
+ # Ensure default value is preserved from resolved schema
437
+ if (
438
+ not isinstance(resolved_media_schema, Reference)
439
+ and hasattr(resolved_media_schema, "default")
440
+ and resolved_media_schema.default is not None
441
+ ):
442
+ param_schema_dict["default"] = resolved_media_schema.default
443
+
399
444
  logger.debug(
400
445
  f"Parameter '{parameter.name}' using schema from 'content' field."
401
446
  )
@@ -543,6 +588,27 @@ class OpenAPI30Parser(BaseOpenAPIParser):
543
588
  logger.warning("OpenAPI schema has no paths defined.")
544
589
  return []
545
590
 
591
+ # Extract component schemas to add to each route
592
+ schema_definitions = {}
593
+ if hasattr(self.openapi, "components") and self.openapi.components:
594
+ components = self.openapi.components
595
+ if hasattr(components, "schemas") and components.schemas:
596
+ for name, schema in components.schemas.items():
597
+ try:
598
+ if isinstance(schema, Reference_30):
599
+ resolved_schema = self._resolve_ref(schema)
600
+ schema_definitions[name] = self._extract_schema_as_dict(
601
+ resolved_schema
602
+ )
603
+ else:
604
+ schema_definitions[name] = self._extract_schema_as_dict(
605
+ schema
606
+ )
607
+ except Exception as e:
608
+ logger.warning(
609
+ f"Failed to extract schema definition '{name}': {e}"
610
+ )
611
+
546
612
  for path_str, path_item_obj in self.openapi.paths.items():
547
613
  if not isinstance(path_item_obj, PathItem_30):
548
614
  logger.warning(
@@ -593,6 +659,7 @@ class OpenAPI30Parser(BaseOpenAPIParser):
593
659
  parameters=parameters,
594
660
  request_body=request_body_info,
595
661
  responses=responses,
662
+ schema_definitions=schema_definitions,
596
663
  )
597
664
  routes.append(route)
598
665
  logger.info(
@@ -711,14 +778,34 @@ class OpenAPI30Parser(BaseOpenAPIParser):
711
778
 
712
779
  param_schema_dict = {}
713
780
  if param_schema_obj: # Check if schema exists
781
+ # Resolve the schema if it's a reference
782
+ resolved_schema = self._resolve_ref(param_schema_obj)
714
783
  param_schema_dict = self._extract_schema_as_dict(param_schema_obj)
784
+
785
+ # Ensure default value is preserved from resolved schema
786
+ if (
787
+ not isinstance(resolved_schema, Reference_30)
788
+ and hasattr(resolved_schema, "default")
789
+ and resolved_schema.default is not None
790
+ ):
791
+ param_schema_dict["default"] = resolved_schema.default
715
792
  elif parameter.content:
716
793
  # Handle complex parameters with 'content'
717
794
  first_media_type = next(iter(parameter.content.values()), None)
718
795
  if first_media_type and first_media_type.media_type_schema:
719
- param_schema_dict = self._extract_schema_as_dict(
720
- first_media_type.media_type_schema
721
- )
796
+ # Resolve the schema if it's a reference
797
+ media_schema = first_media_type.media_type_schema
798
+ resolved_media_schema = self._resolve_ref(media_schema)
799
+ param_schema_dict = self._extract_schema_as_dict(media_schema)
800
+
801
+ # Ensure default value is preserved from resolved schema
802
+ if (
803
+ not isinstance(resolved_media_schema, Reference_30)
804
+ and hasattr(resolved_media_schema, "default")
805
+ and resolved_media_schema.default is not None
806
+ ):
807
+ param_schema_dict["default"] = resolved_media_schema.default
808
+
722
809
  logger.debug(
723
810
  f"Parameter '{parameter.name}' using schema from 'content' field."
724
811
  )
@@ -1173,6 +1260,23 @@ def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
1173
1260
  # Copy the schema and add description if available
1174
1261
  param_schema = param.schema_.copy() if isinstance(param.schema_, dict) else {}
1175
1262
 
1263
+ # Convert #/components/schemas references to #/$defs references
1264
+ if isinstance(param_schema, dict) and "$ref" in param_schema:
1265
+ ref_path = param_schema["$ref"]
1266
+ if ref_path.startswith("#/components/schemas/"):
1267
+ schema_name = ref_path.split("/")[-1]
1268
+ param_schema["$ref"] = f"#/$defs/{schema_name}"
1269
+
1270
+ # Also handle anyOf, allOf, oneOf references
1271
+ for section in ["anyOf", "allOf", "oneOf"]:
1272
+ if section in param_schema and isinstance(param_schema[section], list):
1273
+ for i, item in enumerate(param_schema[section]):
1274
+ if isinstance(item, dict) and "$ref" in item:
1275
+ ref_path = item["$ref"]
1276
+ if ref_path.startswith("#/components/schemas/"):
1277
+ schema_name = ref_path.split("/")[-1]
1278
+ param_schema[section][i]["$ref"] = f"#/$defs/{schema_name}"
1279
+
1176
1280
  # Add parameter description to schema if available and not already present
1177
1281
  if param.description and not param_schema.get("description"):
1178
1282
  param_schema["description"] = param.description
@@ -1193,8 +1297,19 @@ def _combine_schemas(route: openapi.HTTPRoute) -> dict[str, Any]:
1193
1297
  if route.request_body.required:
1194
1298
  required.extend(body_schema.get("required", []))
1195
1299
 
1196
- return {
1300
+ result = {
1197
1301
  "type": "object",
1198
1302
  "properties": properties,
1199
1303
  "required": required,
1200
1304
  }
1305
+
1306
+ # Add schema definitions if available
1307
+ if route.schema_definitions:
1308
+ result["$defs"] = route.schema_definitions
1309
+
1310
+ # Use compress_schema to remove unused definitions
1311
+ from fastmcp.utilities.json_schema import compress_schema
1312
+
1313
+ result = compress_schema(result)
1314
+
1315
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastmcp
3
- Version: 2.3.3
3
+ Version: 2.3.5
4
4
  Summary: The fast, Pythonic way to build MCP servers.
5
5
  Project-URL: Homepage, https://gofastmcp.com
6
6
  Project-URL: Repository, https://github.com/jlowin/fastmcp
@@ -19,7 +19,7 @@ Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
20
  Requires-Dist: exceptiongroup>=1.2.2
21
21
  Requires-Dist: httpx>=0.28.1
22
- Requires-Dist: mcp<2.0.0,>=1.8.0
22
+ Requires-Dist: mcp<2.0.0,>=1.9.0
23
23
  Requires-Dist: openapi-pydantic>=0.5.1
24
24
  Requires-Dist: python-dotenv>=1.1.0
25
25
  Requires-Dist: rich>=13.9.4
@@ -290,7 +290,7 @@ FastMCP introduces powerful ways to structure and deploy your MCP applications.
290
290
 
291
291
  ### Proxy Servers
292
292
 
293
- Create a FastMCP server that acts as an intermediary for another local or remote MCP server using `FastMCP.from_client()`. This is especially useful for bridging transports (e.g., remote SSE to local Stdio) or adding a layer of logic to a server you don't control.
293
+ Create a FastMCP server that acts as an intermediary for another local or remote MCP server using `FastMCP.as_proxy()`. This is especially useful for bridging transports (e.g., remote SSE to local Stdio) or adding a layer of logic to a server you don't control.
294
294
 
295
295
  Learn more in the [**Proxying Documentation**](https://gofastmcp.com/patterns/proxy).
296
296