fastmcp 2.3.4__py3-none-any.whl → 2.4.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/server/server.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import datetime
6
+ import re
6
7
  import warnings
7
8
  from collections.abc import AsyncIterator, Awaitable, Callable
8
9
  from contextlib import (
@@ -11,11 +12,11 @@ from contextlib import (
11
12
  asynccontextmanager,
12
13
  )
13
14
  from functools import partial
15
+ from pathlib import Path
14
16
  from typing import TYPE_CHECKING, Any, Generic, Literal
15
17
 
16
18
  import anyio
17
19
  import httpx
18
- import pydantic
19
20
  import uvicorn
20
21
  from mcp.server.auth.provider import OAuthAuthorizationServerProvider
21
22
  from mcp.server.lowlevel.helper_types import ReadResourceContents
@@ -35,7 +36,6 @@ from mcp.types import Resource as MCPResource
35
36
  from mcp.types import ResourceTemplate as MCPResourceTemplate
36
37
  from mcp.types import Tool as MCPTool
37
38
  from pydantic import AnyUrl
38
- from starlette.applications import Starlette
39
39
  from starlette.middleware import Middleware
40
40
  from starlette.requests import Request
41
41
  from starlette.responses import Response
@@ -48,25 +48,33 @@ from fastmcp.prompts import Prompt, PromptManager
48
48
  from fastmcp.prompts.prompt import PromptResult
49
49
  from fastmcp.resources import Resource, ResourceManager
50
50
  from fastmcp.resources.template import ResourceTemplate
51
- from fastmcp.server.http import create_sse_app
51
+ from fastmcp.server.http import (
52
+ StarletteWithLifespan,
53
+ create_sse_app,
54
+ create_streamable_http_app,
55
+ )
52
56
  from fastmcp.tools import ToolManager
53
57
  from fastmcp.tools.tool import Tool
54
58
  from fastmcp.utilities.cache import TimedCache
55
59
  from fastmcp.utilities.decorators import DecoratedFunction
56
60
  from fastmcp.utilities.logging import get_logger
61
+ from fastmcp.utilities.mcp_config import MCPConfig
57
62
 
58
63
  if TYPE_CHECKING:
59
64
  from fastmcp.client import Client
60
- from fastmcp.server.openapi import FastMCPOpenAPI
65
+ from fastmcp.client.transports import ClientTransport
66
+ from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap
61
67
  from fastmcp.server.proxy import FastMCPProxy
62
-
63
68
  logger = get_logger(__name__)
64
69
 
65
70
  DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
66
71
 
72
+ # Compiled URI parsing regex to split a URI into protocol and path components
73
+ URI_PATTERN = re.compile(r"^([^:]+://)(.*?)$")
74
+
67
75
 
68
76
  @asynccontextmanager
69
- async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
77
+ async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
70
78
  """Default lifespan context manager that does nothing.
71
79
 
72
80
  Args:
@@ -79,8 +87,10 @@ async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
79
87
 
80
88
 
81
89
  def _lifespan_wrapper(
82
- app: FastMCP,
83
- lifespan: Callable[[FastMCP], AbstractAsyncContextManager[LifespanResultT]],
90
+ app: FastMCP[LifespanResultT],
91
+ lifespan: Callable[
92
+ [FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
93
+ ],
84
94
  ) -> Callable[
85
95
  [MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
86
96
  ]:
@@ -114,6 +124,7 @@ class FastMCP(Generic[LifespanResultT]):
114
124
  on_duplicate_tools: DuplicateBehavior | None = None,
115
125
  on_duplicate_resources: DuplicateBehavior | None = None,
116
126
  on_duplicate_prompts: DuplicateBehavior | None = None,
127
+ resource_prefix_format: Literal["protocol", "path"] | None = None,
117
128
  **settings: Any,
118
129
  ):
119
130
  if settings:
@@ -128,6 +139,14 @@ class FastMCP(Generic[LifespanResultT]):
128
139
  )
129
140
  self.settings = fastmcp.settings.ServerSettings(**settings)
130
141
 
142
+ self.resource_prefix_format: Literal["protocol", "path"]
143
+ if resource_prefix_format is None:
144
+ self.resource_prefix_format = (
145
+ fastmcp.settings.settings.resource_prefix_format
146
+ )
147
+ else:
148
+ self.resource_prefix_format = resource_prefix_format
149
+
131
150
  self.tags: set[str] = tags or set()
132
151
  self.dependencies = dependencies
133
152
  self._cache = TimedCache(
@@ -189,15 +208,13 @@ class FastMCP(Generic[LifespanResultT]):
189
208
  """
190
209
  if transport is None:
191
210
  transport = "stdio"
192
- if transport not in ["stdio", "streamable-http", "sse"]:
211
+ if transport not in {"stdio", "streamable-http", "sse"}:
193
212
  raise ValueError(f"Unknown transport: {transport}")
194
213
 
195
214
  if transport == "stdio":
196
215
  await self.run_stdio_async(**transport_kwargs)
197
- elif transport == "streamable-http":
198
- await self.run_http_async(transport="streamable-http", **transport_kwargs)
199
- elif transport == "sse":
200
- await self.run_http_async(transport="sse", **transport_kwargs)
216
+ elif transport in {"streamable-http", "sse"}:
217
+ await self.run_http_async(transport=transport, **transport_kwargs)
201
218
  else:
202
219
  raise ValueError(f"Unknown transport: {transport}")
203
220
 
@@ -211,7 +228,6 @@ class FastMCP(Generic[LifespanResultT]):
211
228
  Args:
212
229
  transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
213
230
  """
214
- logger.info(f'Starting server "{self.name}"...')
215
231
 
216
232
  anyio.run(partial(self.run_async, transport, **transport_kwargs))
217
233
 
@@ -228,7 +244,7 @@ class FastMCP(Generic[LifespanResultT]):
228
244
  async def get_tools(self) -> dict[str, Tool]:
229
245
  """Get all registered tools, indexed by registered key."""
230
246
  if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
231
- tools = {}
247
+ tools: dict[str, Tool] = {}
232
248
  for server in self._mounted_servers.values():
233
249
  server_tools = await server.get_tools()
234
250
  tools.update(server_tools)
@@ -239,7 +255,7 @@ class FastMCP(Generic[LifespanResultT]):
239
255
  async def get_resources(self) -> dict[str, Resource]:
240
256
  """Get all registered resources, indexed by registered key."""
241
257
  if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
242
- resources = {}
258
+ resources: dict[str, Resource] = {}
243
259
  for server in self._mounted_servers.values():
244
260
  server_resources = await server.get_resources()
245
261
  resources.update(server_resources)
@@ -252,7 +268,7 @@ class FastMCP(Generic[LifespanResultT]):
252
268
  if (
253
269
  templates := self._cache.get("resource_templates")
254
270
  ) is self._cache.NOT_FOUND:
255
- templates = {}
271
+ templates: dict[str, ResourceTemplate] = {}
256
272
  for server in self._mounted_servers.values():
257
273
  server_templates = await server.get_resource_templates()
258
274
  templates.update(server_templates)
@@ -265,7 +281,7 @@ class FastMCP(Generic[LifespanResultT]):
265
281
  List all available prompts.
266
282
  """
267
283
  if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
268
- prompts = {}
284
+ prompts: dict[str, Prompt] = {}
269
285
  for server in self._mounted_servers.values():
270
286
  server_prompts = await server.get_prompts()
271
287
  prompts.update(server_prompts)
@@ -728,6 +744,7 @@ class FastMCP(Generic[LifespanResultT]):
728
744
  async def run_stdio_async(self) -> None:
729
745
  """Run the server using stdio transport."""
730
746
  async with stdio_server() as (read_stream, write_stream):
747
+ logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
731
748
  await self._mcp_server.run(
732
749
  read_stream,
733
750
  write_stream,
@@ -743,7 +760,8 @@ class FastMCP(Generic[LifespanResultT]):
743
760
  port: int | None = None,
744
761
  log_level: str | None = None,
745
762
  path: str | None = None,
746
- uvicorn_config: dict | None = None,
763
+ uvicorn_config: dict[str, Any] | None = None,
764
+ middleware: list[Middleware] | None = None,
747
765
  ) -> None:
748
766
  """Run the server using HTTP transport.
749
767
 
@@ -755,21 +773,29 @@ class FastMCP(Generic[LifespanResultT]):
755
773
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
756
774
  uvicorn_config: Additional configuration for the Uvicorn server
757
775
  """
758
- uvicorn_config = uvicorn_config or {}
759
- uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
760
- # lifespan is required for streamable http
761
- uvicorn_config["lifespan"] = "on"
762
-
763
- app = self.http_app(path=path, transport=transport)
764
-
765
- config = uvicorn.Config(
766
- app,
767
- host=host or self.settings.host,
768
- port=port or self.settings.port,
769
- log_level=log_level or self.settings.log_level.lower(),
770
- **uvicorn_config,
771
- )
776
+ host = host or self.settings.host
777
+ port = port or self.settings.port
778
+ default_log_level_to_use = log_level or self.settings.log_level.lower()
779
+
780
+ app = self.http_app(path=path, transport=transport, middleware=middleware)
781
+
782
+ _uvicorn_config_from_user = uvicorn_config or {}
783
+
784
+ config_kwargs: dict[str, Any] = {
785
+ "timeout_graceful_shutdown": 0,
786
+ "lifespan": "on",
787
+ }
788
+ config_kwargs.update(_uvicorn_config_from_user)
789
+
790
+ if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
791
+ config_kwargs["log_level"] = default_log_level_to_use
792
+
793
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
772
794
  server = uvicorn.Server(config)
795
+ path = app.state.path.lstrip("/") # type: ignore
796
+ logger.info(
797
+ f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
798
+ )
773
799
  await server.serve()
774
800
 
775
801
  async def run_sse_async(
@@ -779,7 +805,7 @@ class FastMCP(Generic[LifespanResultT]):
779
805
  log_level: str | None = None,
780
806
  path: str | None = None,
781
807
  message_path: str | None = None,
782
- uvicorn_config: dict | None = None,
808
+ uvicorn_config: dict[str, Any] | None = None,
783
809
  ) -> None:
784
810
  """Run the server using SSE transport."""
785
811
 
@@ -805,7 +831,7 @@ class FastMCP(Generic[LifespanResultT]):
805
831
  path: str | None = None,
806
832
  message_path: str | None = None,
807
833
  middleware: list[Middleware] | None = None,
808
- ) -> Starlette:
834
+ ) -> StarletteWithLifespan:
809
835
  """
810
836
  Create a Starlette app for the SSE server.
811
837
 
@@ -836,7 +862,7 @@ class FastMCP(Generic[LifespanResultT]):
836
862
  self,
837
863
  path: str | None = None,
838
864
  middleware: list[Middleware] | None = None,
839
- ) -> Starlette:
865
+ ) -> StarletteWithLifespan:
840
866
  """
841
867
  Create a Starlette app for the StreamableHTTP server.
842
868
 
@@ -857,7 +883,7 @@ class FastMCP(Generic[LifespanResultT]):
857
883
  path: str | None = None,
858
884
  middleware: list[Middleware] | None = None,
859
885
  transport: Literal["streamable-http", "sse"] = "streamable-http",
860
- ) -> Starlette:
886
+ ) -> StarletteWithLifespan:
861
887
  """Create a Starlette app using the specified HTTP transport.
862
888
 
863
889
  Args:
@@ -868,7 +894,6 @@ class FastMCP(Generic[LifespanResultT]):
868
894
  Returns:
869
895
  A Starlette application configured with the specified transport
870
896
  """
871
- from fastmcp.server.http import create_streamable_http_app
872
897
 
873
898
  if transport == "streamable-http":
874
899
  return create_streamable_http_app(
@@ -901,7 +926,7 @@ class FastMCP(Generic[LifespanResultT]):
901
926
  port: int | None = None,
902
927
  log_level: str | None = None,
903
928
  path: str | None = None,
904
- uvicorn_config: dict | None = None,
929
+ uvicorn_config: dict[str, Any] | None = None,
905
930
  ) -> None:
906
931
  # Deprecated since 2.3.2
907
932
  warnings.warn(
@@ -923,10 +948,11 @@ class FastMCP(Generic[LifespanResultT]):
923
948
  self,
924
949
  prefix: str,
925
950
  server: FastMCP[LifespanResultT],
951
+ as_proxy: bool | None = None,
952
+ *,
926
953
  tool_separator: str | None = None,
927
954
  resource_separator: str | None = None,
928
955
  prompt_separator: str | None = None,
929
- as_proxy: bool | None = None,
930
956
  ) -> None:
931
957
  """Mount another FastMCP server on this server with the given prefix.
932
958
 
@@ -937,15 +963,15 @@ class FastMCP(Generic[LifespanResultT]):
937
963
  through the parent.
938
964
 
939
965
  When a server is mounted:
940
- - Tools from the mounted server are accessible with prefixed names using the tool_separator.
966
+ - Tools from the mounted server are accessible with prefixed names.
941
967
  Example: If server has a tool named "get_weather", it will be available as "prefix_get_weather".
942
- - Resources are accessible with prefixed URIs using the resource_separator.
968
+ - Resources are accessible with prefixed URIs.
943
969
  Example: If server has a resource with URI "weather://forecast", it will be available as
944
- "prefix+weather://forecast".
945
- - Templates are accessible with prefixed URI templates using the resource_separator.
970
+ "weather://prefix/forecast".
971
+ - Templates are accessible with prefixed URI templates.
946
972
  Example: If server has a template with URI "weather://location/{id}", it will be available
947
- as "prefix+weather://location/{id}".
948
- - Prompts are accessible with prefixed names using the prompt_separator.
973
+ as "weather://prefix/location/{id}".
974
+ - Prompts are accessible with prefixed names.
949
975
  Example: If server has a prompt named "weather_prompt", it will be available as
950
976
  "prefix_weather_prompt".
951
977
 
@@ -963,17 +989,44 @@ class FastMCP(Generic[LifespanResultT]):
963
989
  Args:
964
990
  prefix: Prefix to use for the mounted server's objects.
965
991
  server: The FastMCP server to mount.
966
- tool_separator: Separator character for tool names (defaults to "_").
967
- resource_separator: Separator character for resource URIs (defaults to "+").
968
- prompt_separator: Separator character for prompt names (defaults to "_").
969
992
  as_proxy: Whether to treat the mounted server as a proxy. If None (default),
970
993
  automatically determined based on whether the server has a custom lifespan
971
994
  (True if it has a custom lifespan, False otherwise).
995
+ tool_separator: Deprecated. Separator character for tool names.
996
+ resource_separator: Deprecated. Separator character for resource URIs.
997
+ prompt_separator: Deprecated. Separator character for prompt names.
972
998
  """
973
999
  from fastmcp import Client
974
1000
  from fastmcp.client.transports import FastMCPTransport
975
1001
  from fastmcp.server.proxy import FastMCPProxy
976
1002
 
1003
+ if tool_separator is not None:
1004
+ # Deprecated since 2.3.6
1005
+ warnings.warn(
1006
+ "The tool_separator parameter is deprecated and will be removed in a future version. "
1007
+ "Tools are now prefixed using 'prefix_toolname' format.",
1008
+ DeprecationWarning,
1009
+ stacklevel=2,
1010
+ )
1011
+
1012
+ if resource_separator is not None:
1013
+ # Deprecated since 2.3.6
1014
+ warnings.warn(
1015
+ "The resource_separator parameter is deprecated and ignored. "
1016
+ "Resource prefixes are now added using the protocol://prefix/path format.",
1017
+ DeprecationWarning,
1018
+ stacklevel=2,
1019
+ )
1020
+
1021
+ if prompt_separator is not None:
1022
+ # Deprecated since 2.3.6
1023
+ warnings.warn(
1024
+ "The prompt_separator parameter is deprecated and will be removed in a future version. "
1025
+ "Prompts are now prefixed using 'prefix_promptname' format.",
1026
+ DeprecationWarning,
1027
+ stacklevel=2,
1028
+ )
1029
+
977
1030
  # if as_proxy is not specified and the server has a custom lifespan,
978
1031
  # we should treat it as a proxy
979
1032
  if as_proxy is None:
@@ -985,9 +1038,6 @@ class FastMCP(Generic[LifespanResultT]):
985
1038
  mounted_server = MountedServer(
986
1039
  server=server,
987
1040
  prefix=prefix,
988
- tool_separator=tool_separator,
989
- resource_separator=resource_separator,
990
- prompt_separator=prompt_separator,
991
1041
  )
992
1042
  self._mounted_servers[prefix] = mounted_server
993
1043
  self._cache.clear()
@@ -1013,84 +1063,136 @@ class FastMCP(Generic[LifespanResultT]):
1013
1063
  future changes to the imported server will not be reflected in the
1014
1064
  importing server. Server-level configurations and lifespans are not imported.
1015
1065
 
1016
- When a server is mounted: - The tools are imported with prefixed names
1017
- using the tool_separator
1066
+ When a server is imported:
1067
+ - The tools are imported with prefixed names
1018
1068
  Example: If server has a tool named "get_weather", it will be
1019
- available as "weatherget_weather"
1020
- - The resources are imported with prefixed URIs using the
1021
- resource_separator Example: If server has a resource with URI
1022
- "weather://forecast", it will be available as
1023
- "weather+weather://forecast"
1024
- - The templates are imported with prefixed URI templates using the
1025
- resource_separator Example: If server has a template with URI
1026
- "weather://location/{id}", it will be available as
1027
- "weather+weather://location/{id}"
1028
- - The prompts are imported with prefixed names using the
1029
- prompt_separator Example: If server has a prompt named
1030
- "weather_prompt", it will be available as "weather_weather_prompt"
1031
- - The mounted server's lifespan will be executed when the parent
1032
- server's lifespan runs, ensuring that any setup needed by the mounted
1033
- server is performed
1069
+ available as "prefix_get_weather"
1070
+ - The resources are imported with prefixed URIs using the new format
1071
+ Example: If server has a resource with URI "weather://forecast", it will
1072
+ be available as "weather://prefix/forecast"
1073
+ - The templates are imported with prefixed URI templates using the new format
1074
+ Example: If server has a template with URI "weather://location/{id}", it will
1075
+ be available as "weather://prefix/location/{id}"
1076
+ - The prompts are imported with prefixed names
1077
+ Example: If server has a prompt named "weather_prompt", it will be available as
1078
+ "prefix_weather_prompt"
1034
1079
 
1035
1080
  Args:
1036
- prefix: The prefix to use for the mounted server server: The FastMCP
1037
- server to mount tool_separator: Separator for tool names (defaults
1038
- to "_") resource_separator: Separator for resource URIs (defaults to
1039
- "+") prompt_separator: Separator for prompt names (defaults to "_")
1081
+ prefix: The prefix to use for the imported server
1082
+ server: The FastMCP server to import
1083
+ tool_separator: Deprecated. Separator for tool names.
1084
+ resource_separator: Deprecated and ignored. Prefix is now
1085
+ applied using the protocol://prefix/path format
1086
+ prompt_separator: Deprecated. Separator for prompt names.
1040
1087
  """
1041
- if tool_separator is None:
1042
- tool_separator = "_"
1043
- if resource_separator is None:
1044
- resource_separator = "+"
1045
- if prompt_separator is None:
1046
- prompt_separator = "_"
1088
+ if tool_separator is not None:
1089
+ # Deprecated since 2.3.6
1090
+ warnings.warn(
1091
+ "The tool_separator parameter is deprecated and will be removed in a future version. "
1092
+ "Tools are now prefixed using 'prefix_toolname' format.",
1093
+ DeprecationWarning,
1094
+ stacklevel=2,
1095
+ )
1096
+
1097
+ if resource_separator is not None:
1098
+ # Deprecated since 2.3.6
1099
+ warnings.warn(
1100
+ "The resource_separator parameter is deprecated and ignored. "
1101
+ "Resource prefixes are now added using the protocol://prefix/path format.",
1102
+ DeprecationWarning,
1103
+ stacklevel=2,
1104
+ )
1105
+
1106
+ if prompt_separator is not None:
1107
+ # Deprecated since 2.3.6
1108
+ warnings.warn(
1109
+ "The prompt_separator parameter is deprecated and will be removed in a future version. "
1110
+ "Prompts are now prefixed using 'prefix_promptname' format.",
1111
+ DeprecationWarning,
1112
+ stacklevel=2,
1113
+ )
1047
1114
 
1048
1115
  # Import tools from the mounted server
1049
- tool_prefix = f"{prefix}{tool_separator}"
1116
+ tool_prefix = f"{prefix}_"
1050
1117
  for key, tool in (await server.get_tools()).items():
1051
1118
  self._tool_manager.add_tool(tool, key=f"{tool_prefix}{key}")
1052
1119
 
1053
1120
  # Import resources and templates from the mounted server
1054
- resource_prefix = f"{prefix}{resource_separator}"
1055
- _validate_resource_prefix(resource_prefix)
1056
1121
  for key, resource in (await server.get_resources()).items():
1057
- self._resource_manager.add_resource(resource, key=f"{resource_prefix}{key}")
1122
+ prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1123
+ self._resource_manager.add_resource(resource, key=prefixed_key)
1124
+
1058
1125
  for key, template in (await server.get_resource_templates()).items():
1059
- self._resource_manager.add_template(template, key=f"{resource_prefix}{key}")
1126
+ prefixed_key = add_resource_prefix(key, prefix, self.resource_prefix_format)
1127
+ self._resource_manager.add_template(template, key=prefixed_key)
1060
1128
 
1061
1129
  # Import prompts from the mounted server
1062
- prompt_prefix = f"{prefix}{prompt_separator}"
1130
+ prompt_prefix = f"{prefix}_"
1063
1131
  for key, prompt in (await server.get_prompts()).items():
1064
1132
  self._prompt_manager.add_prompt(prompt, key=f"{prompt_prefix}{key}")
1065
1133
 
1066
1134
  logger.info(f"Imported server {server.name} with prefix '{prefix}'")
1067
1135
  logger.debug(f"Imported tools with prefix '{tool_prefix}'")
1068
- logger.debug(f"Imported resources with prefix '{resource_prefix}'")
1069
- logger.debug(f"Imported templates with prefix '{resource_prefix}'")
1136
+ logger.debug(f"Imported resources and templates with prefix '{prefix}/'")
1070
1137
  logger.debug(f"Imported prompts with prefix '{prompt_prefix}'")
1071
1138
 
1072
1139
  self._cache.clear()
1073
1140
 
1074
1141
  @classmethod
1075
1142
  def from_openapi(
1076
- cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, **settings: Any
1143
+ cls,
1144
+ openapi_spec: dict[str, Any],
1145
+ client: httpx.AsyncClient,
1146
+ route_maps: list[RouteMap] | None = None,
1147
+ all_routes_as_tools: bool = False,
1148
+ **settings: Any,
1077
1149
  ) -> FastMCPOpenAPI:
1078
1150
  """
1079
1151
  Create a FastMCP server from an OpenAPI specification.
1080
1152
  """
1081
- from .openapi import FastMCPOpenAPI
1153
+ from .openapi import FastMCPOpenAPI, RouteMap, RouteType
1082
1154
 
1083
- return FastMCPOpenAPI(openapi_spec=openapi_spec, client=client, **settings)
1155
+ if all_routes_as_tools and route_maps:
1156
+ raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1157
+
1158
+ elif all_routes_as_tools:
1159
+ route_maps = [
1160
+ RouteMap(
1161
+ methods="*",
1162
+ pattern=r".*",
1163
+ route_type=RouteType.TOOL,
1164
+ )
1165
+ ]
1166
+
1167
+ return FastMCPOpenAPI(
1168
+ openapi_spec=openapi_spec,
1169
+ client=client,
1170
+ route_maps=route_maps,
1171
+ **settings,
1172
+ )
1084
1173
 
1085
1174
  @classmethod
1086
1175
  def from_fastapi(
1087
- cls, app: Any, name: str | None = None, **settings: Any
1176
+ cls,
1177
+ app: Any,
1178
+ name: str | None = None,
1179
+ route_maps: list[RouteMap] | None = None,
1180
+ all_routes_as_tools: bool = False,
1181
+ **settings: Any,
1088
1182
  ) -> FastMCPOpenAPI:
1089
1183
  """
1090
1184
  Create a FastMCP server from a FastAPI application.
1091
1185
  """
1092
1186
 
1093
- from .openapi import FastMCPOpenAPI
1187
+ from .openapi import FastMCPOpenAPI, RouteMap, RouteType
1188
+
1189
+ if all_routes_as_tools and route_maps:
1190
+ raise ValueError("Cannot specify both all_routes_as_tools and route_maps")
1191
+
1192
+ elif all_routes_as_tools:
1193
+ route_maps = [
1194
+ RouteMap(methods="*", pattern=r".*", route_type=RouteType.TOOL)
1195
+ ]
1094
1196
 
1095
1197
  client = httpx.AsyncClient(
1096
1198
  transport=httpx.ASGITransport(app=app), base_url="http://fastapi"
@@ -1099,97 +1201,271 @@ class FastMCP(Generic[LifespanResultT]):
1099
1201
  name = name or app.title
1100
1202
 
1101
1203
  return FastMCPOpenAPI(
1102
- openapi_spec=app.openapi(), client=client, name=name, **settings
1204
+ openapi_spec=app.openapi(),
1205
+ client=client,
1206
+ name=name,
1207
+ route_maps=route_maps,
1208
+ **settings,
1103
1209
  )
1104
1210
 
1105
1211
  @classmethod
1106
- def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
1107
- """
1108
- Create a FastMCP proxy server from a FastMCP client.
1212
+ def as_proxy(
1213
+ cls,
1214
+ backend: Client
1215
+ | ClientTransport
1216
+ | FastMCP[Any]
1217
+ | AnyUrl
1218
+ | Path
1219
+ | MCPConfig
1220
+ | dict[str, Any]
1221
+ | str,
1222
+ **settings: Any,
1223
+ ) -> FastMCPProxy:
1224
+ """Create a FastMCP proxy server for the given backend.
1225
+
1226
+ The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client`
1227
+ instance or any value accepted as the ``transport`` argument of
1228
+ :class:`~fastmcp.client.Client`. This mirrors the convenience of the
1229
+ ``Client`` constructor.
1109
1230
  """
1231
+ from fastmcp.client.client import Client
1110
1232
  from fastmcp.server.proxy import FastMCPProxy
1111
1233
 
1112
- return FastMCPProxy(client=client, **settings)
1234
+ if isinstance(backend, Client):
1235
+ client = backend
1236
+ else:
1237
+ client = Client(backend)
1113
1238
 
1239
+ return FastMCPProxy(client=client, **settings)
1114
1240
 
1115
- def _validate_resource_prefix(prefix: str) -> None:
1116
- valid_resource = "resource://path/to/resource"
1117
- test_case = f"{prefix}{valid_resource}"
1118
- try:
1119
- AnyUrl(test_case)
1120
- except pydantic.ValidationError as e:
1121
- raise ValueError(
1122
- "Resource prefix or separator would result in an "
1123
- f"invalid resource URI (test case was {test_case!r}): {e}"
1241
+ @classmethod
1242
+ def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
1243
+ """
1244
+ Create a FastMCP proxy server from a FastMCP client.
1245
+ """
1246
+ # Deprecated since 2.3.5
1247
+ warnings.warn(
1248
+ "FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
1249
+ DeprecationWarning,
1250
+ stacklevel=2,
1124
1251
  )
1125
1252
 
1253
+ return cls.as_proxy(client, **settings)
1254
+
1126
1255
 
1127
1256
  class MountedServer:
1128
1257
  def __init__(
1129
1258
  self,
1130
1259
  prefix: str,
1131
- server: FastMCP,
1132
- tool_separator: str | None = None,
1133
- resource_separator: str | None = None,
1134
- prompt_separator: str | None = None,
1260
+ server: FastMCP[LifespanResultT],
1135
1261
  ):
1136
- if tool_separator is None:
1137
- tool_separator = "_"
1138
- if resource_separator is None:
1139
- resource_separator = "+"
1140
- if prompt_separator is None:
1141
- prompt_separator = "_"
1142
-
1143
- _validate_resource_prefix(f"{prefix}{resource_separator}")
1144
-
1145
1262
  self.server = server
1146
1263
  self.prefix = prefix
1147
- self.tool_separator = tool_separator
1148
- self.resource_separator = resource_separator
1149
- self.prompt_separator = prompt_separator
1150
1264
 
1151
1265
  async def get_tools(self) -> dict[str, Tool]:
1152
1266
  tools = await self.server.get_tools()
1153
- return {
1154
- f"{self.prefix}{self.tool_separator}{key}": tool
1155
- for key, tool in tools.items()
1156
- }
1267
+ return {f"{self.prefix}_{key}": tool for key, tool in tools.items()}
1157
1268
 
1158
1269
  async def get_resources(self) -> dict[str, Resource]:
1159
1270
  resources = await self.server.get_resources()
1160
1271
  return {
1161
- f"{self.prefix}{self.resource_separator}{key}": resource
1272
+ add_resource_prefix(
1273
+ key, self.prefix, self.server.resource_prefix_format
1274
+ ): resource
1162
1275
  for key, resource in resources.items()
1163
1276
  }
1164
1277
 
1165
1278
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
1166
1279
  templates = await self.server.get_resource_templates()
1167
1280
  return {
1168
- f"{self.prefix}{self.resource_separator}{key}": template
1281
+ add_resource_prefix(
1282
+ key, self.prefix, self.server.resource_prefix_format
1283
+ ): template
1169
1284
  for key, template in templates.items()
1170
1285
  }
1171
1286
 
1172
1287
  async def get_prompts(self) -> dict[str, Prompt]:
1173
1288
  prompts = await self.server.get_prompts()
1174
- return {
1175
- f"{self.prefix}{self.prompt_separator}{key}": prompt
1176
- for key, prompt in prompts.items()
1177
- }
1289
+ return {f"{self.prefix}_{key}": prompt for key, prompt in prompts.items()}
1178
1290
 
1179
1291
  def match_tool(self, key: str) -> bool:
1180
- return key.startswith(f"{self.prefix}{self.tool_separator}")
1292
+ return key.startswith(f"{self.prefix}_")
1181
1293
 
1182
1294
  def strip_tool_prefix(self, key: str) -> str:
1183
- return key.removeprefix(f"{self.prefix}{self.tool_separator}")
1295
+ return key.removeprefix(f"{self.prefix}_")
1184
1296
 
1185
1297
  def match_resource(self, key: str) -> bool:
1186
- return key.startswith(f"{self.prefix}{self.resource_separator}")
1298
+ return has_resource_prefix(key, self.prefix, self.server.resource_prefix_format)
1187
1299
 
1188
1300
  def strip_resource_prefix(self, key: str) -> str:
1189
- return key.removeprefix(f"{self.prefix}{self.resource_separator}")
1301
+ return remove_resource_prefix(
1302
+ key, self.prefix, self.server.resource_prefix_format
1303
+ )
1190
1304
 
1191
1305
  def match_prompt(self, key: str) -> bool:
1192
- return key.startswith(f"{self.prefix}{self.prompt_separator}")
1306
+ return key.startswith(f"{self.prefix}_")
1193
1307
 
1194
1308
  def strip_prompt_prefix(self, key: str) -> str:
1195
- return key.removeprefix(f"{self.prefix}{self.prompt_separator}")
1309
+ return key.removeprefix(f"{self.prefix}_")
1310
+
1311
+
1312
+ def add_resource_prefix(
1313
+ uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
1314
+ ) -> str:
1315
+ """Add a prefix to a resource URI.
1316
+
1317
+ Args:
1318
+ uri: The original resource URI
1319
+ prefix: The prefix to add
1320
+
1321
+ Returns:
1322
+ The resource URI with the prefix added
1323
+
1324
+ Examples:
1325
+ >>> add_resource_prefix("resource://path/to/resource", "prefix")
1326
+ "resource://prefix/path/to/resource" # with new style
1327
+ >>> add_resource_prefix("resource://path/to/resource", "prefix")
1328
+ "prefix+resource://path/to/resource" # with legacy style
1329
+ >>> add_resource_prefix("resource:///absolute/path", "prefix")
1330
+ "resource://prefix//absolute/path" # with new style
1331
+
1332
+ Raises:
1333
+ ValueError: If the URI doesn't match the expected protocol://path format
1334
+ """
1335
+ if not prefix:
1336
+ return uri
1337
+
1338
+ # Get the server settings to check for legacy format preference
1339
+
1340
+ if prefix_format is None:
1341
+ prefix_format = fastmcp.settings.settings.resource_prefix_format
1342
+
1343
+ if prefix_format == "protocol":
1344
+ # Legacy style: prefix+protocol://path
1345
+ return f"{prefix}+{uri}"
1346
+ elif prefix_format == "path":
1347
+ # New style: protocol://prefix/path
1348
+ # Split the URI into protocol and path
1349
+ match = URI_PATTERN.match(uri)
1350
+ if not match:
1351
+ raise ValueError(
1352
+ f"Invalid URI format: {uri}. Expected protocol://path format."
1353
+ )
1354
+
1355
+ protocol, path = match.groups()
1356
+
1357
+ # Add the prefix to the path
1358
+ return f"{protocol}{prefix}/{path}"
1359
+ else:
1360
+ raise ValueError(f"Invalid prefix format: {prefix_format}")
1361
+
1362
+
1363
+ def remove_resource_prefix(
1364
+ uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
1365
+ ) -> str:
1366
+ """Remove a prefix from a resource URI.
1367
+
1368
+ Args:
1369
+ uri: The resource URI with a prefix
1370
+ prefix: The prefix to remove
1371
+ prefix_format: The format of the prefix to remove
1372
+ Returns:
1373
+ The resource URI with the prefix removed
1374
+
1375
+ Examples:
1376
+ >>> remove_resource_prefix("resource://prefix/path/to/resource", "prefix")
1377
+ "resource://path/to/resource" # with new style
1378
+ >>> remove_resource_prefix("prefix+resource://path/to/resource", "prefix")
1379
+ "resource://path/to/resource" # with legacy style
1380
+ >>> remove_resource_prefix("resource://prefix//absolute/path", "prefix")
1381
+ "resource:///absolute/path" # with new style
1382
+
1383
+ Raises:
1384
+ ValueError: If the URI doesn't match the expected protocol://path format
1385
+ """
1386
+ if not prefix:
1387
+ return uri
1388
+
1389
+ if prefix_format is None:
1390
+ prefix_format = fastmcp.settings.settings.resource_prefix_format
1391
+
1392
+ if prefix_format == "protocol":
1393
+ # Legacy style: prefix+protocol://path
1394
+ legacy_prefix = f"{prefix}+"
1395
+ if uri.startswith(legacy_prefix):
1396
+ return uri[len(legacy_prefix) :]
1397
+ return uri
1398
+ elif prefix_format == "path":
1399
+ # New style: protocol://prefix/path
1400
+ # Split the URI into protocol and path
1401
+ match = URI_PATTERN.match(uri)
1402
+ if not match:
1403
+ raise ValueError(
1404
+ f"Invalid URI format: {uri}. Expected protocol://path format."
1405
+ )
1406
+
1407
+ protocol, path = match.groups()
1408
+
1409
+ # Check if the path starts with the prefix followed by a /
1410
+ prefix_pattern = f"^{re.escape(prefix)}/(.*?)$"
1411
+ path_match = re.match(prefix_pattern, path)
1412
+ if not path_match:
1413
+ return uri
1414
+
1415
+ # Return the URI without the prefix
1416
+ return f"{protocol}{path_match.group(1)}"
1417
+ else:
1418
+ raise ValueError(f"Invalid prefix format: {prefix_format}")
1419
+
1420
+
1421
+ def has_resource_prefix(
1422
+ uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None
1423
+ ) -> bool:
1424
+ """Check if a resource URI has a specific prefix.
1425
+
1426
+ Args:
1427
+ uri: The resource URI to check
1428
+ prefix: The prefix to look for
1429
+
1430
+ Returns:
1431
+ True if the URI has the specified prefix, False otherwise
1432
+
1433
+ Examples:
1434
+ >>> has_resource_prefix("resource://prefix/path/to/resource", "prefix")
1435
+ True # with new style
1436
+ >>> has_resource_prefix("prefix+resource://path/to/resource", "prefix")
1437
+ True # with legacy style
1438
+ >>> has_resource_prefix("resource://other/path/to/resource", "prefix")
1439
+ False
1440
+
1441
+ Raises:
1442
+ ValueError: If the URI doesn't match the expected protocol://path format
1443
+ """
1444
+ if not prefix:
1445
+ return False
1446
+
1447
+ # Get the server settings to check for legacy format preference
1448
+
1449
+ if prefix_format is None:
1450
+ prefix_format = fastmcp.settings.settings.resource_prefix_format
1451
+
1452
+ if prefix_format == "protocol":
1453
+ # Legacy style: prefix+protocol://path
1454
+ legacy_prefix = f"{prefix}+"
1455
+ return uri.startswith(legacy_prefix)
1456
+ elif prefix_format == "path":
1457
+ # New style: protocol://prefix/path
1458
+ # Split the URI into protocol and path
1459
+ match = URI_PATTERN.match(uri)
1460
+ if not match:
1461
+ raise ValueError(
1462
+ f"Invalid URI format: {uri}. Expected protocol://path format."
1463
+ )
1464
+
1465
+ _, path = match.groups()
1466
+
1467
+ # Check if the path starts with the prefix followed by a /
1468
+ prefix_pattern = f"^{re.escape(prefix)}/"
1469
+ return bool(re.match(prefix_pattern, path))
1470
+ else:
1471
+ raise ValueError(f"Invalid prefix format: {prefix_format}")