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/cli/cli.py +30 -138
- fastmcp/cli/run.py +179 -0
- fastmcp/client/__init__.py +2 -0
- fastmcp/client/client.py +131 -24
- fastmcp/client/logging.py +8 -0
- fastmcp/client/progress.py +38 -0
- fastmcp/client/transports.py +80 -64
- fastmcp/exceptions.py +2 -0
- fastmcp/prompts/prompt.py +12 -6
- fastmcp/resources/resource_manager.py +22 -1
- fastmcp/resources/template.py +21 -17
- fastmcp/resources/types.py +25 -27
- fastmcp/server/context.py +6 -3
- fastmcp/server/http.py +47 -14
- fastmcp/server/openapi.py +14 -1
- fastmcp/server/proxy.py +4 -4
- fastmcp/server/server.py +159 -96
- fastmcp/settings.py +55 -29
- fastmcp/tools/tool.py +45 -45
- fastmcp/tools/tool_manager.py +27 -2
- fastmcp/utilities/exceptions.py +49 -0
- fastmcp/utilities/json_schema.py +78 -17
- fastmcp/utilities/logging.py +11 -6
- fastmcp/utilities/openapi.py +122 -7
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/METADATA +3 -3
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/RECORD +29 -27
- fastmcp/low_level/sse_server_transport.py +0 -104
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.3.dist-info → fastmcp-2.3.5.dist-info}/licenses/LICENSE +0 -0
fastmcp/server/server.py
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import datetime
|
|
6
|
-
import inspect
|
|
7
6
|
import warnings
|
|
8
7
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
9
8
|
from contextlib import (
|
|
@@ -12,6 +11,7 @@ from contextlib import (
|
|
|
12
11
|
asynccontextmanager,
|
|
13
12
|
)
|
|
14
13
|
from functools import partial
|
|
14
|
+
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
16
16
|
|
|
17
17
|
import anyio
|
|
@@ -20,7 +20,7 @@ import pydantic
|
|
|
20
20
|
import uvicorn
|
|
21
21
|
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
|
|
22
22
|
from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
23
|
-
from mcp.server.lowlevel.server import LifespanResultT
|
|
23
|
+
from mcp.server.lowlevel.server import LifespanResultT, NotificationOptions
|
|
24
24
|
from mcp.server.lowlevel.server import Server as MCPServer
|
|
25
25
|
from mcp.server.stdio import stdio_server
|
|
26
26
|
from mcp.types import (
|
|
@@ -36,7 +36,6 @@ from mcp.types import Resource as MCPResource
|
|
|
36
36
|
from mcp.types import ResourceTemplate as MCPResourceTemplate
|
|
37
37
|
from mcp.types import Tool as MCPTool
|
|
38
38
|
from pydantic import AnyUrl
|
|
39
|
-
from starlette.applications import Starlette
|
|
40
39
|
from starlette.middleware import Middleware
|
|
41
40
|
from starlette.requests import Request
|
|
42
41
|
from starlette.responses import Response
|
|
@@ -44,28 +43,34 @@ from starlette.routing import BaseRoute, Route
|
|
|
44
43
|
|
|
45
44
|
import fastmcp.server
|
|
46
45
|
import fastmcp.settings
|
|
47
|
-
from fastmcp.exceptions import NotFoundError
|
|
46
|
+
from fastmcp.exceptions import NotFoundError
|
|
48
47
|
from fastmcp.prompts import Prompt, PromptManager
|
|
49
48
|
from fastmcp.prompts.prompt import PromptResult
|
|
50
49
|
from fastmcp.resources import Resource, ResourceManager
|
|
51
50
|
from fastmcp.resources.template import ResourceTemplate
|
|
52
|
-
from fastmcp.server.http import
|
|
51
|
+
from fastmcp.server.http import (
|
|
52
|
+
StarletteWithLifespan,
|
|
53
|
+
create_sse_app,
|
|
54
|
+
create_streamable_http_app,
|
|
55
|
+
)
|
|
53
56
|
from fastmcp.tools import ToolManager
|
|
54
57
|
from fastmcp.tools.tool import Tool
|
|
55
58
|
from fastmcp.utilities.cache import TimedCache
|
|
56
59
|
from fastmcp.utilities.decorators import DecoratedFunction
|
|
57
|
-
from fastmcp.utilities.logging import
|
|
60
|
+
from fastmcp.utilities.logging import get_logger
|
|
58
61
|
|
|
59
62
|
if TYPE_CHECKING:
|
|
60
63
|
from fastmcp.client import Client
|
|
64
|
+
from fastmcp.client.transports import ClientTransport
|
|
61
65
|
from fastmcp.server.openapi import FastMCPOpenAPI
|
|
62
66
|
from fastmcp.server.proxy import FastMCPProxy
|
|
63
|
-
|
|
64
67
|
logger = get_logger(__name__)
|
|
65
68
|
|
|
69
|
+
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
70
|
+
|
|
66
71
|
|
|
67
72
|
@asynccontextmanager
|
|
68
|
-
async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
|
73
|
+
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
69
74
|
"""Default lifespan context manager that does nothing.
|
|
70
75
|
|
|
71
76
|
Args:
|
|
@@ -78,8 +83,10 @@ async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
|
|
78
83
|
|
|
79
84
|
|
|
80
85
|
def _lifespan_wrapper(
|
|
81
|
-
app: FastMCP,
|
|
82
|
-
lifespan: Callable[
|
|
86
|
+
app: FastMCP[LifespanResultT],
|
|
87
|
+
lifespan: Callable[
|
|
88
|
+
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
89
|
+
],
|
|
83
90
|
) -> Callable[
|
|
84
91
|
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
85
92
|
]:
|
|
@@ -107,40 +114,52 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
107
114
|
| None
|
|
108
115
|
) = None,
|
|
109
116
|
tags: set[str] | None = None,
|
|
117
|
+
dependencies: list[str] | None = None,
|
|
110
118
|
tool_serializer: Callable[[Any], str] | None = None,
|
|
119
|
+
cache_expiration_seconds: float | None = None,
|
|
120
|
+
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
121
|
+
on_duplicate_resources: DuplicateBehavior | None = None,
|
|
122
|
+
on_duplicate_prompts: DuplicateBehavior | None = None,
|
|
111
123
|
**settings: Any,
|
|
112
124
|
):
|
|
113
|
-
|
|
125
|
+
if settings:
|
|
126
|
+
# TODO: remove settings. Deprecated since 2.3.4
|
|
127
|
+
warnings.warn(
|
|
128
|
+
"Passing runtime and transport-specific settings as kwargs "
|
|
129
|
+
"to the FastMCP constructor is deprecated (as of 2.3.4), "
|
|
130
|
+
"including most transport settings. If possible, provide settings when calling "
|
|
131
|
+
"run() instead.",
|
|
132
|
+
DeprecationWarning,
|
|
133
|
+
stacklevel=2,
|
|
134
|
+
)
|
|
114
135
|
self.settings = fastmcp.settings.ServerSettings(**settings)
|
|
136
|
+
|
|
137
|
+
self.tags: set[str] = tags or set()
|
|
138
|
+
self.dependencies = dependencies
|
|
115
139
|
self._cache = TimedCache(
|
|
116
|
-
expiration=datetime.timedelta(
|
|
117
|
-
seconds=self.settings.cache_expiration_seconds
|
|
118
|
-
)
|
|
140
|
+
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
|
119
141
|
)
|
|
120
|
-
|
|
121
142
|
self._mounted_servers: dict[str, MountedServer] = {}
|
|
143
|
+
self._additional_http_routes: list[BaseRoute] = []
|
|
144
|
+
self._tool_manager = ToolManager(
|
|
145
|
+
duplicate_behavior=on_duplicate_tools,
|
|
146
|
+
serializer=tool_serializer,
|
|
147
|
+
)
|
|
148
|
+
self._resource_manager = ResourceManager(
|
|
149
|
+
duplicate_behavior=on_duplicate_resources
|
|
150
|
+
)
|
|
151
|
+
self._prompt_manager = PromptManager(duplicate_behavior=on_duplicate_prompts)
|
|
122
152
|
|
|
123
153
|
if lifespan is None:
|
|
124
154
|
self._has_lifespan = False
|
|
125
155
|
lifespan = default_lifespan
|
|
126
156
|
else:
|
|
127
157
|
self._has_lifespan = True
|
|
128
|
-
|
|
129
158
|
self._mcp_server = MCPServer[LifespanResultT](
|
|
130
159
|
name=name or "FastMCP",
|
|
131
160
|
instructions=instructions,
|
|
132
161
|
lifespan=_lifespan_wrapper(self, lifespan),
|
|
133
162
|
)
|
|
134
|
-
self._tool_manager = ToolManager(
|
|
135
|
-
duplicate_behavior=self.settings.on_duplicate_tools,
|
|
136
|
-
serializer=tool_serializer,
|
|
137
|
-
)
|
|
138
|
-
self._resource_manager = ResourceManager(
|
|
139
|
-
duplicate_behavior=self.settings.on_duplicate_resources
|
|
140
|
-
)
|
|
141
|
-
self._prompt_manager = PromptManager(
|
|
142
|
-
duplicate_behavior=self.settings.on_duplicate_prompts
|
|
143
|
-
)
|
|
144
163
|
|
|
145
164
|
if (self.settings.auth is not None) != (auth_server_provider is not None):
|
|
146
165
|
# TODO: after we support separate authorization servers (see
|
|
@@ -150,15 +169,9 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
150
169
|
)
|
|
151
170
|
self._auth_server_provider = auth_server_provider
|
|
152
171
|
|
|
153
|
-
self._additional_http_routes: list[BaseRoute] = []
|
|
154
|
-
self.dependencies = self.settings.dependencies
|
|
155
|
-
|
|
156
172
|
# Set up MCP protocol handlers
|
|
157
173
|
self._setup_handlers()
|
|
158
174
|
|
|
159
|
-
# Configure logging
|
|
160
|
-
configure_logging(self.settings.log_level)
|
|
161
|
-
|
|
162
175
|
def __repr__(self) -> str:
|
|
163
176
|
return f"{type(self).__name__}({self.name!r})"
|
|
164
177
|
|
|
@@ -182,15 +195,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
182
195
|
"""
|
|
183
196
|
if transport is None:
|
|
184
197
|
transport = "stdio"
|
|
185
|
-
if transport not in
|
|
198
|
+
if transport not in {"stdio", "streamable-http", "sse"}:
|
|
186
199
|
raise ValueError(f"Unknown transport: {transport}")
|
|
187
200
|
|
|
188
201
|
if transport == "stdio":
|
|
189
202
|
await self.run_stdio_async(**transport_kwargs)
|
|
190
|
-
elif transport
|
|
191
|
-
await self.run_http_async(transport=
|
|
192
|
-
elif transport == "sse":
|
|
193
|
-
await self.run_http_async(transport="sse", **transport_kwargs)
|
|
203
|
+
elif transport in {"streamable-http", "sse"}:
|
|
204
|
+
await self.run_http_async(transport=transport, **transport_kwargs)
|
|
194
205
|
else:
|
|
195
206
|
raise ValueError(f"Unknown transport: {transport}")
|
|
196
207
|
|
|
@@ -204,7 +215,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
204
215
|
Args:
|
|
205
216
|
transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
|
|
206
217
|
"""
|
|
207
|
-
logger.info(f'Starting server "{self.name}"...')
|
|
208
218
|
|
|
209
219
|
anyio.run(partial(self.run_async, transport, **transport_kwargs))
|
|
210
220
|
|
|
@@ -221,7 +231,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
221
231
|
async def get_tools(self) -> dict[str, Tool]:
|
|
222
232
|
"""Get all registered tools, indexed by registered key."""
|
|
223
233
|
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
|
224
|
-
tools = {}
|
|
234
|
+
tools: dict[str, Tool] = {}
|
|
225
235
|
for server in self._mounted_servers.values():
|
|
226
236
|
server_tools = await server.get_tools()
|
|
227
237
|
tools.update(server_tools)
|
|
@@ -232,7 +242,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
232
242
|
async def get_resources(self) -> dict[str, Resource]:
|
|
233
243
|
"""Get all registered resources, indexed by registered key."""
|
|
234
244
|
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
|
|
235
|
-
resources = {}
|
|
245
|
+
resources: dict[str, Resource] = {}
|
|
236
246
|
for server in self._mounted_servers.values():
|
|
237
247
|
server_resources = await server.get_resources()
|
|
238
248
|
resources.update(server_resources)
|
|
@@ -245,7 +255,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
245
255
|
if (
|
|
246
256
|
templates := self._cache.get("resource_templates")
|
|
247
257
|
) is self._cache.NOT_FOUND:
|
|
248
|
-
templates = {}
|
|
258
|
+
templates: dict[str, ResourceTemplate] = {}
|
|
249
259
|
for server in self._mounted_servers.values():
|
|
250
260
|
server_templates = await server.get_resource_templates()
|
|
251
261
|
templates.update(server_templates)
|
|
@@ -258,7 +268,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
258
268
|
List all available prompts.
|
|
259
269
|
"""
|
|
260
270
|
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
|
261
|
-
prompts = {}
|
|
271
|
+
prompts: dict[str, Prompt] = {}
|
|
262
272
|
for server in self._mounted_servers.values():
|
|
263
273
|
server_prompts = await server.get_prompts()
|
|
264
274
|
prompts.update(server_prompts)
|
|
@@ -378,16 +388,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
378
388
|
with fastmcp.server.context.Context(fastmcp=self):
|
|
379
389
|
if self._resource_manager.has_resource(uri):
|
|
380
390
|
resource = await self._resource_manager.get_resource(uri)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
except Exception as e:
|
|
389
|
-
logger.error(f"Error reading resource {uri}: {e}")
|
|
390
|
-
raise ResourceError(str(e))
|
|
391
|
+
content = await self._resource_manager.read_resource(uri)
|
|
392
|
+
return [
|
|
393
|
+
ReadResourceContents(
|
|
394
|
+
content=content,
|
|
395
|
+
mime_type=resource.mime_type,
|
|
396
|
+
)
|
|
397
|
+
]
|
|
391
398
|
else:
|
|
392
399
|
for server in self._mounted_servers.values():
|
|
393
400
|
if server.match_resource(str(uri)):
|
|
@@ -414,7 +421,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
414
421
|
for server in self._mounted_servers.values():
|
|
415
422
|
if server.match_prompt(name):
|
|
416
423
|
new_key = server.strip_prompt_prefix(name)
|
|
417
|
-
|
|
424
|
+
return await server.server._mcp_get_prompt(new_key, arguments)
|
|
418
425
|
else:
|
|
419
426
|
raise NotFoundError(f"Unknown prompt: {name}")
|
|
420
427
|
|
|
@@ -450,6 +457,18 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
450
457
|
)
|
|
451
458
|
self._cache.clear()
|
|
452
459
|
|
|
460
|
+
def remove_tool(self, name: str) -> None:
|
|
461
|
+
"""Remove a tool from the server.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
name: The name of the tool to remove
|
|
465
|
+
|
|
466
|
+
Raises:
|
|
467
|
+
NotFoundError: If the tool is not found
|
|
468
|
+
"""
|
|
469
|
+
self._tool_manager.remove_tool(name)
|
|
470
|
+
self._cache.clear()
|
|
471
|
+
|
|
453
472
|
def tool(
|
|
454
473
|
self,
|
|
455
474
|
name: str | None = None,
|
|
@@ -712,10 +731,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
712
731
|
async def run_stdio_async(self) -> None:
|
|
713
732
|
"""Run the server using stdio transport."""
|
|
714
733
|
async with stdio_server() as (read_stream, write_stream):
|
|
734
|
+
logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
|
|
715
735
|
await self._mcp_server.run(
|
|
716
736
|
read_stream,
|
|
717
737
|
write_stream,
|
|
718
|
-
self._mcp_server.create_initialization_options(
|
|
738
|
+
self._mcp_server.create_initialization_options(
|
|
739
|
+
NotificationOptions(tools_changed=True)
|
|
740
|
+
),
|
|
719
741
|
)
|
|
720
742
|
|
|
721
743
|
async def run_http_async(
|
|
@@ -725,7 +747,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
725
747
|
port: int | None = None,
|
|
726
748
|
log_level: str | None = None,
|
|
727
749
|
path: str | None = None,
|
|
728
|
-
uvicorn_config: dict | None = None,
|
|
750
|
+
uvicorn_config: dict[str, Any] | None = None,
|
|
751
|
+
middleware: list[Middleware] | None = None,
|
|
729
752
|
) -> None:
|
|
730
753
|
"""Run the server using HTTP transport.
|
|
731
754
|
|
|
@@ -737,21 +760,29 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
737
760
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
738
761
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
739
762
|
"""
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
763
|
+
host = host or self.settings.host
|
|
764
|
+
port = port or self.settings.port
|
|
765
|
+
default_log_level_to_use = log_level or self.settings.log_level.lower()
|
|
766
|
+
|
|
767
|
+
app = self.http_app(path=path, transport=transport, middleware=middleware)
|
|
768
|
+
|
|
769
|
+
_uvicorn_config_from_user = uvicorn_config or {}
|
|
770
|
+
|
|
771
|
+
config_kwargs: dict[str, Any] = {
|
|
772
|
+
"timeout_graceful_shutdown": 0,
|
|
773
|
+
"lifespan": "on",
|
|
774
|
+
}
|
|
775
|
+
config_kwargs.update(_uvicorn_config_from_user)
|
|
776
|
+
|
|
777
|
+
if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
|
|
778
|
+
config_kwargs["log_level"] = default_log_level_to_use
|
|
779
|
+
|
|
780
|
+
config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
|
|
754
781
|
server = uvicorn.Server(config)
|
|
782
|
+
path = app.state.path.lstrip("/") # type: ignore
|
|
783
|
+
logger.info(
|
|
784
|
+
f"Starting MCP server {self.name!r} with transport {transport!r} on http://{host}:{port}/{path}"
|
|
785
|
+
)
|
|
755
786
|
await server.serve()
|
|
756
787
|
|
|
757
788
|
async def run_sse_async(
|
|
@@ -761,18 +792,17 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
761
792
|
log_level: str | None = None,
|
|
762
793
|
path: str | None = None,
|
|
763
794
|
message_path: str | None = None,
|
|
764
|
-
uvicorn_config: dict | None = None,
|
|
795
|
+
uvicorn_config: dict[str, Any] | None = None,
|
|
765
796
|
) -> None:
|
|
766
797
|
"""Run the server using SSE transport."""
|
|
798
|
+
|
|
799
|
+
# Deprecated since 2.3.2
|
|
767
800
|
warnings.warn(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
modern (non-SSE) alternative, or create an SSE app with
|
|
772
|
-
`fastmcp.server.http.create_sse_app` and run it directly.
|
|
773
|
-
"""
|
|
774
|
-
),
|
|
801
|
+
"The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a "
|
|
802
|
+
"modern (non-SSE) alternative, or create an SSE app with "
|
|
803
|
+
"`fastmcp.server.http.create_sse_app` and run it directly.",
|
|
775
804
|
DeprecationWarning,
|
|
805
|
+
stacklevel=2,
|
|
776
806
|
)
|
|
777
807
|
await self.run_http_async(
|
|
778
808
|
transport="sse",
|
|
@@ -788,7 +818,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
788
818
|
path: str | None = None,
|
|
789
819
|
message_path: str | None = None,
|
|
790
820
|
middleware: list[Middleware] | None = None,
|
|
791
|
-
) ->
|
|
821
|
+
) -> StarletteWithLifespan:
|
|
792
822
|
"""
|
|
793
823
|
Create a Starlette app for the SSE server.
|
|
794
824
|
|
|
@@ -797,14 +827,12 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
797
827
|
message_path: The path to the message endpoint
|
|
798
828
|
middleware: A list of middleware to apply to the app
|
|
799
829
|
"""
|
|
830
|
+
# Deprecated since 2.3.2
|
|
800
831
|
warnings.warn(
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
The sse_app method is deprecated. Use http_app as a modern (non-SSE)
|
|
804
|
-
alternative, or call `fastmcp.server.http.create_sse_app` directly.
|
|
805
|
-
"""
|
|
806
|
-
),
|
|
832
|
+
"The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) "
|
|
833
|
+
"alternative, or call `fastmcp.server.http.create_sse_app` directly.",
|
|
807
834
|
DeprecationWarning,
|
|
835
|
+
stacklevel=2,
|
|
808
836
|
)
|
|
809
837
|
return create_sse_app(
|
|
810
838
|
server=self,
|
|
@@ -821,7 +849,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
821
849
|
self,
|
|
822
850
|
path: str | None = None,
|
|
823
851
|
middleware: list[Middleware] | None = None,
|
|
824
|
-
) ->
|
|
852
|
+
) -> StarletteWithLifespan:
|
|
825
853
|
"""
|
|
826
854
|
Create a Starlette app for the StreamableHTTP server.
|
|
827
855
|
|
|
@@ -829,9 +857,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
829
857
|
path: The path to the StreamableHTTP endpoint
|
|
830
858
|
middleware: A list of middleware to apply to the app
|
|
831
859
|
"""
|
|
860
|
+
# Deprecated since 2.3.2
|
|
832
861
|
warnings.warn(
|
|
833
|
-
"The streamable_http_app method is deprecated. Use http_app() instead.",
|
|
862
|
+
"The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.",
|
|
834
863
|
DeprecationWarning,
|
|
864
|
+
stacklevel=2,
|
|
835
865
|
)
|
|
836
866
|
return self.http_app(path=path, middleware=middleware)
|
|
837
867
|
|
|
@@ -840,7 +870,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
840
870
|
path: str | None = None,
|
|
841
871
|
middleware: list[Middleware] | None = None,
|
|
842
872
|
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
|
843
|
-
) ->
|
|
873
|
+
) -> StarletteWithLifespan:
|
|
844
874
|
"""Create a Starlette app using the specified HTTP transport.
|
|
845
875
|
|
|
846
876
|
Args:
|
|
@@ -851,7 +881,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
851
881
|
Returns:
|
|
852
882
|
A Starlette application configured with the specified transport
|
|
853
883
|
"""
|
|
854
|
-
from fastmcp.server.http import create_streamable_http_app
|
|
855
884
|
|
|
856
885
|
if transport == "streamable-http":
|
|
857
886
|
return create_streamable_http_app(
|
|
@@ -884,11 +913,14 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
884
913
|
port: int | None = None,
|
|
885
914
|
log_level: str | None = None,
|
|
886
915
|
path: str | None = None,
|
|
887
|
-
uvicorn_config: dict | None = None,
|
|
916
|
+
uvicorn_config: dict[str, Any] | None = None,
|
|
888
917
|
) -> None:
|
|
918
|
+
# Deprecated since 2.3.2
|
|
889
919
|
warnings.warn(
|
|
890
|
-
"The run_streamable_http_async method is deprecated
|
|
920
|
+
"The run_streamable_http_async method is deprecated (as of 2.3.2). "
|
|
921
|
+
"Use run_http_async instead.",
|
|
891
922
|
DeprecationWarning,
|
|
923
|
+
stacklevel=2,
|
|
892
924
|
)
|
|
893
925
|
await self.run_http_async(
|
|
894
926
|
transport="streamable-http",
|
|
@@ -1008,9 +1040,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1008
1040
|
- The prompts are imported with prefixed names using the
|
|
1009
1041
|
prompt_separator Example: If server has a prompt named
|
|
1010
1042
|
"weather_prompt", it will be available as "weather_weather_prompt"
|
|
1011
|
-
- The mounted server's lifespan will be executed when the parent
|
|
1012
|
-
server's lifespan runs, ensuring that any setup needed by the mounted
|
|
1013
|
-
server is performed
|
|
1014
1043
|
|
|
1015
1044
|
Args:
|
|
1016
1045
|
prefix: The prefix to use for the mounted server server: The FastMCP
|
|
@@ -1082,14 +1111,48 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1082
1111
|
openapi_spec=app.openapi(), client=client, name=name, **settings
|
|
1083
1112
|
)
|
|
1084
1113
|
|
|
1114
|
+
@classmethod
|
|
1115
|
+
def as_proxy(
|
|
1116
|
+
cls,
|
|
1117
|
+
backend: Client
|
|
1118
|
+
| ClientTransport
|
|
1119
|
+
| FastMCP[Any]
|
|
1120
|
+
| AnyUrl
|
|
1121
|
+
| Path
|
|
1122
|
+
| dict[str, Any]
|
|
1123
|
+
| str,
|
|
1124
|
+
**settings: Any,
|
|
1125
|
+
) -> FastMCPProxy:
|
|
1126
|
+
"""Create a FastMCP proxy server for the given backend.
|
|
1127
|
+
|
|
1128
|
+
The ``backend`` argument can be either an existing :class:`~fastmcp.client.Client`
|
|
1129
|
+
instance or any value accepted as the ``transport`` argument of
|
|
1130
|
+
:class:`~fastmcp.client.Client`. This mirrors the convenience of the
|
|
1131
|
+
``Client`` constructor.
|
|
1132
|
+
"""
|
|
1133
|
+
from fastmcp.client.client import Client
|
|
1134
|
+
from fastmcp.server.proxy import FastMCPProxy
|
|
1135
|
+
|
|
1136
|
+
if isinstance(backend, Client):
|
|
1137
|
+
client = backend
|
|
1138
|
+
else:
|
|
1139
|
+
client = Client(backend)
|
|
1140
|
+
|
|
1141
|
+
return FastMCPProxy(client=client, **settings)
|
|
1142
|
+
|
|
1085
1143
|
@classmethod
|
|
1086
1144
|
def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
|
|
1087
1145
|
"""
|
|
1088
1146
|
Create a FastMCP proxy server from a FastMCP client.
|
|
1089
1147
|
"""
|
|
1090
|
-
|
|
1148
|
+
# Deprecated since 2.3.5
|
|
1149
|
+
warnings.warn(
|
|
1150
|
+
"FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.",
|
|
1151
|
+
DeprecationWarning,
|
|
1152
|
+
stacklevel=2,
|
|
1153
|
+
)
|
|
1091
1154
|
|
|
1092
|
-
return
|
|
1155
|
+
return cls.as_proxy(client, **settings)
|
|
1093
1156
|
|
|
1094
1157
|
|
|
1095
1158
|
def _validate_resource_prefix(prefix: str) -> None:
|
|
@@ -1108,7 +1171,7 @@ class MountedServer:
|
|
|
1108
1171
|
def __init__(
|
|
1109
1172
|
self,
|
|
1110
1173
|
prefix: str,
|
|
1111
|
-
server: FastMCP,
|
|
1174
|
+
server: FastMCP[LifespanResultT],
|
|
1112
1175
|
tool_separator: str | None = None,
|
|
1113
1176
|
resource_separator: str | None = None,
|
|
1114
1177
|
prompt_separator: str | None = None,
|
fastmcp/settings.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations as _annotations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import inspect
|
|
4
|
+
from typing import TYPE_CHECKING, Annotated, Literal
|
|
4
5
|
|
|
5
6
|
from mcp.server.auth.settings import AuthSettings
|
|
6
|
-
from pydantic import Field
|
|
7
|
+
from pydantic import Field, model_validator
|
|
7
8
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
|
+
from typing_extensions import Self
|
|
8
10
|
|
|
9
11
|
if TYPE_CHECKING:
|
|
10
12
|
pass
|
|
@@ -27,16 +29,46 @@ class Settings(BaseSettings):
|
|
|
27
29
|
|
|
28
30
|
test_mode: bool = False
|
|
29
31
|
log_level: LOG_LEVEL = "INFO"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
client_raise_first_exceptiongroup_error: Annotated[
|
|
33
|
+
bool,
|
|
34
|
+
Field(
|
|
35
|
+
default=True,
|
|
36
|
+
description=inspect.cleandoc(
|
|
37
|
+
"""
|
|
38
|
+
Many MCP components operate in anyio taskgroups, and raise
|
|
39
|
+
ExceptionGroups instead of exceptions. If this setting is True, FastMCP Clients
|
|
40
|
+
will `raise` the first error in any ExceptionGroup instead of raising
|
|
41
|
+
the ExceptionGroup as a whole. This is useful for debugging, but may
|
|
42
|
+
mask other errors.
|
|
43
|
+
"""
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
] = True
|
|
47
|
+
tool_attempt_parse_json_args: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
Field(
|
|
50
|
+
default=False,
|
|
51
|
+
description=inspect.cleandoc(
|
|
52
|
+
"""
|
|
53
|
+
Note: this enables a legacy behavior. If True, will attempt to parse
|
|
54
|
+
stringified JSON lists and objects strings in tool arguments before
|
|
55
|
+
passing them to the tool. This is an old behavior that can create
|
|
56
|
+
unexpected type coercion issues, but may be helpful for less powerful
|
|
57
|
+
LLMs that stringify JSON instead of passing actual lists and objects.
|
|
58
|
+
Defaults to False.
|
|
59
|
+
"""
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
] = False
|
|
63
|
+
|
|
64
|
+
@model_validator(mode="after")
|
|
65
|
+
def setup_logging(self) -> Self:
|
|
66
|
+
"""Finalize the settings."""
|
|
67
|
+
from fastmcp.utilities.logging import configure_logging
|
|
68
|
+
|
|
69
|
+
configure_logging(self.log_level)
|
|
70
|
+
|
|
71
|
+
return self
|
|
40
72
|
|
|
41
73
|
|
|
42
74
|
class ServerSettings(BaseSettings):
|
|
@@ -54,7 +86,10 @@ class ServerSettings(BaseSettings):
|
|
|
54
86
|
nested_model_default_partial_update=True,
|
|
55
87
|
)
|
|
56
88
|
|
|
57
|
-
log_level:
|
|
89
|
+
log_level: Annotated[
|
|
90
|
+
LOG_LEVEL,
|
|
91
|
+
Field(default_factory=lambda: Settings().log_level),
|
|
92
|
+
]
|
|
58
93
|
|
|
59
94
|
# HTTP settings
|
|
60
95
|
host: str = "127.0.0.1"
|
|
@@ -73,10 +108,13 @@ class ServerSettings(BaseSettings):
|
|
|
73
108
|
# prompt settings
|
|
74
109
|
on_duplicate_prompts: DuplicateBehavior = "warn"
|
|
75
110
|
|
|
76
|
-
dependencies:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
111
|
+
dependencies: Annotated[
|
|
112
|
+
list[str],
|
|
113
|
+
Field(
|
|
114
|
+
default_factory=list,
|
|
115
|
+
description="List of dependencies to install in the server environment",
|
|
116
|
+
),
|
|
117
|
+
] = []
|
|
80
118
|
|
|
81
119
|
# cache settings (for checking mounted servers)
|
|
82
120
|
cache_expiration_seconds: float = 0
|
|
@@ -90,16 +128,4 @@ class ServerSettings(BaseSettings):
|
|
|
90
128
|
)
|
|
91
129
|
|
|
92
130
|
|
|
93
|
-
class ClientSettings(BaseSettings):
|
|
94
|
-
"""FastMCP client settings."""
|
|
95
|
-
|
|
96
|
-
model_config = SettingsConfigDict(
|
|
97
|
-
env_prefix="FASTMCP_CLIENT_",
|
|
98
|
-
env_file=".env",
|
|
99
|
-
extra="ignore",
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
log_level: LOG_LEVEL = Field(default_factory=lambda: Settings().log_level)
|
|
103
|
-
|
|
104
|
-
|
|
105
131
|
settings = Settings()
|