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/cli/cli.py +30 -138
- fastmcp/cli/run.py +179 -0
- fastmcp/client/client.py +80 -20
- fastmcp/client/logging.py +20 -6
- fastmcp/client/progress.py +38 -0
- fastmcp/client/transports.py +153 -67
- fastmcp/server/context.py +6 -3
- fastmcp/server/http.py +70 -15
- fastmcp/server/openapi.py +113 -10
- fastmcp/server/server.py +414 -138
- fastmcp/settings.py +16 -0
- fastmcp/utilities/mcp_config.py +76 -0
- fastmcp/utilities/openapi.py +233 -602
- fastmcp/utilities/tests.py +8 -4
- {fastmcp-2.3.4.dist-info → fastmcp-2.4.0.dist-info}/METADATA +26 -3
- {fastmcp-2.3.4.dist-info → fastmcp-2.4.0.dist-info}/RECORD +19 -17
- fastmcp/low_level/sse_server_transport.py +0 -104
- {fastmcp-2.3.4.dist-info → fastmcp-2.4.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.4.dist-info → fastmcp-2.4.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.4.dist-info → fastmcp-2.4.0.dist-info}/licenses/LICENSE +0 -0
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
|
|
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.
|
|
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[
|
|
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
|
|
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
|
|
198
|
-
await self.run_http_async(transport=
|
|
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
|
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
|
|
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
|
-
"
|
|
945
|
-
- Templates are accessible with prefixed URI templates
|
|
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 "
|
|
948
|
-
- Prompts are accessible with prefixed names
|
|
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
|
|
1017
|
-
|
|
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 "
|
|
1020
|
-
- The resources are imported with prefixed URIs using the
|
|
1021
|
-
|
|
1022
|
-
"weather://forecast"
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
"
|
|
1028
|
-
|
|
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
|
|
1037
|
-
server
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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 '{
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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(),
|
|
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
|
|
1107
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
"
|
|
1123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
1295
|
+
return key.removeprefix(f"{self.prefix}_")
|
|
1184
1296
|
|
|
1185
1297
|
def match_resource(self, key: str) -> bool:
|
|
1186
|
-
return key
|
|
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
|
|
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}
|
|
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}
|
|
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}")
|