fastmcp 2.3.4__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/client.py +62 -18
- fastmcp/client/logging.py +8 -0
- fastmcp/client/progress.py +38 -0
- fastmcp/client/transports.py +27 -36
- fastmcp/server/context.py +6 -3
- fastmcp/server/http.py +47 -14
- fastmcp/server/server.py +86 -43
- {fastmcp-2.3.4.dist-info → fastmcp-2.3.5.dist-info}/METADATA +3 -3
- {fastmcp-2.3.4.dist-info → fastmcp-2.3.5.dist-info}/RECORD +14 -13
- fastmcp/low_level/sse_server_transport.py +0 -104
- {fastmcp-2.3.4.dist-info → fastmcp-2.3.5.dist-info}/WHEEL +0 -0
- {fastmcp-2.3.4.dist-info → fastmcp-2.3.5.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.3.4.dist-info → fastmcp-2.3.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import TypeAlias
|
|
2
|
+
|
|
3
|
+
from mcp.shared.session import ProgressFnT
|
|
4
|
+
|
|
5
|
+
from fastmcp.utilities.logging import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
ProgressHandler: TypeAlias = ProgressFnT
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def default_progress_handler(
|
|
13
|
+
progress: float, total: float | None, message: str | None
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Default handler for progress notifications.
|
|
16
|
+
|
|
17
|
+
Logs progress updates at debug level, properly handling missing total or message values.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
progress: Current progress value
|
|
21
|
+
total: Optional total expected value
|
|
22
|
+
message: Optional status message
|
|
23
|
+
"""
|
|
24
|
+
if total is not None:
|
|
25
|
+
# We have both progress and total
|
|
26
|
+
percent = (progress / total) * 100
|
|
27
|
+
progress_str = f"{progress}/{total} ({percent:.1f}%)"
|
|
28
|
+
else:
|
|
29
|
+
# We only have progress
|
|
30
|
+
progress_str = f"{progress}"
|
|
31
|
+
|
|
32
|
+
# Include message if available
|
|
33
|
+
if message:
|
|
34
|
+
log_msg = f"Progress: {progress_str} - {message}"
|
|
35
|
+
else:
|
|
36
|
+
log_msg = f"Progress: {progress_str}"
|
|
37
|
+
|
|
38
|
+
logger.debug(log_msg)
|
fastmcp/client/transports.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import abc
|
|
2
2
|
import contextlib
|
|
3
3
|
import datetime
|
|
4
|
-
import inspect
|
|
5
4
|
import os
|
|
6
5
|
import shutil
|
|
7
6
|
import sys
|
|
8
|
-
import warnings
|
|
9
7
|
from collections.abc import AsyncIterator
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
from typing import Any, TypedDict, cast
|
|
10
|
+
from urllib.parse import urlparse
|
|
12
11
|
|
|
13
12
|
from mcp import ClientSession, StdioServerParameters
|
|
14
13
|
from mcp.client.session import (
|
|
@@ -26,6 +25,9 @@ from pydantic import AnyUrl
|
|
|
26
25
|
from typing_extensions import Unpack
|
|
27
26
|
|
|
28
27
|
from fastmcp.server import FastMCP as FastMCPServer
|
|
28
|
+
from fastmcp.utilities.logging import get_logger
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
class SessionKwargs(TypedDict, total=False):
|
|
@@ -44,6 +46,7 @@ class ClientTransport(abc.ABC):
|
|
|
44
46
|
|
|
45
47
|
A Transport is responsible for establishing and managing connections
|
|
46
48
|
to an MCP server, and providing a ClientSession within an async context.
|
|
49
|
+
|
|
47
50
|
"""
|
|
48
51
|
|
|
49
52
|
@abc.abstractmethod
|
|
@@ -52,7 +55,9 @@ class ClientTransport(abc.ABC):
|
|
|
52
55
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
53
56
|
) -> AsyncIterator[ClientSession]:
|
|
54
57
|
"""
|
|
55
|
-
Establishes a connection and yields an active
|
|
58
|
+
Establishes a connection and yields an active ClientSession.
|
|
59
|
+
|
|
60
|
+
The ClientSession is *not* expected to be initialized in this context manager.
|
|
56
61
|
|
|
57
62
|
The session is guaranteed to be valid only within the scope of the
|
|
58
63
|
async context manager. Connection setup and teardown are handled
|
|
@@ -63,7 +68,7 @@ class ClientTransport(abc.ABC):
|
|
|
63
68
|
constructor (e.g., callbacks, timeouts).
|
|
64
69
|
|
|
65
70
|
Yields:
|
|
66
|
-
|
|
71
|
+
A mcp.ClientSession instance.
|
|
67
72
|
"""
|
|
68
73
|
raise NotImplementedError
|
|
69
74
|
yield None # type: ignore
|
|
@@ -92,7 +97,6 @@ class WSTransport(ClientTransport):
|
|
|
92
97
|
async with ClientSession(
|
|
93
98
|
read_stream, write_stream, **session_kwargs
|
|
94
99
|
) as session:
|
|
95
|
-
await session.initialize() # Initialize after session creation
|
|
96
100
|
yield session
|
|
97
101
|
|
|
98
102
|
def __repr__(self) -> str:
|
|
@@ -141,7 +145,6 @@ class SSETransport(ClientTransport):
|
|
|
141
145
|
async with ClientSession(
|
|
142
146
|
read_stream, write_stream, **session_kwargs
|
|
143
147
|
) as session:
|
|
144
|
-
await session.initialize()
|
|
145
148
|
yield session
|
|
146
149
|
|
|
147
150
|
def __repr__(self) -> str:
|
|
@@ -187,7 +190,6 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
187
190
|
async with ClientSession(
|
|
188
191
|
read_stream, write_stream, **session_kwargs
|
|
189
192
|
) as session:
|
|
190
|
-
await session.initialize()
|
|
191
193
|
yield session
|
|
192
194
|
|
|
193
195
|
def __repr__(self) -> str:
|
|
@@ -235,7 +237,6 @@ class StdioTransport(ClientTransport):
|
|
|
235
237
|
async with ClientSession(
|
|
236
238
|
read_stream, write_stream, **session_kwargs
|
|
237
239
|
) as session:
|
|
238
|
-
await session.initialize()
|
|
239
240
|
yield session
|
|
240
241
|
|
|
241
242
|
def __repr__(self) -> str:
|
|
@@ -486,36 +487,29 @@ def infer_transport(
|
|
|
486
487
|
|
|
487
488
|
# the transport is a FastMCP server
|
|
488
489
|
elif isinstance(transport, FastMCPServer):
|
|
489
|
-
|
|
490
|
+
inferred_transport = FastMCPTransport(mcp=transport)
|
|
490
491
|
|
|
491
492
|
# the transport is a path to a script
|
|
492
493
|
elif isinstance(transport, Path | str) and Path(transport).exists():
|
|
493
494
|
if str(transport).endswith(".py"):
|
|
494
|
-
|
|
495
|
+
inferred_transport = PythonStdioTransport(script_path=transport)
|
|
495
496
|
elif str(transport).endswith(".js"):
|
|
496
|
-
|
|
497
|
+
inferred_transport = NodeStdioTransport(script_path=transport)
|
|
497
498
|
else:
|
|
498
499
|
raise ValueError(f"Unsupported script type: {transport}")
|
|
499
500
|
|
|
500
501
|
# the transport is an http(s) URL
|
|
501
502
|
elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
stacklevel=2,
|
|
513
|
-
)
|
|
514
|
-
return StreamableHttpTransport(url=transport)
|
|
515
|
-
|
|
516
|
-
# the transport is a websocket URL
|
|
517
|
-
elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
|
|
518
|
-
return WSTransport(url=transport)
|
|
503
|
+
transport_str = str(transport)
|
|
504
|
+
# Parse out just the path portion to check for /sse
|
|
505
|
+
parsed_url = urlparse(transport_str)
|
|
506
|
+
path = parsed_url.path
|
|
507
|
+
|
|
508
|
+
# Check if path contains /sse/ or ends with /sse
|
|
509
|
+
if "/sse/" in path or path.rstrip("/").endswith("/sse"):
|
|
510
|
+
inferred_transport = SSETransport(url=transport)
|
|
511
|
+
else:
|
|
512
|
+
inferred_transport = StreamableHttpTransport(url=transport)
|
|
519
513
|
|
|
520
514
|
## if the transport is a config dict
|
|
521
515
|
elif isinstance(transport, dict):
|
|
@@ -530,7 +524,7 @@ def infer_transport(
|
|
|
530
524
|
server_name = list(server.keys())[0]
|
|
531
525
|
# Stdio transport
|
|
532
526
|
if "command" in server[server_name] and "args" in server[server_name]:
|
|
533
|
-
|
|
527
|
+
inferred_transport = StdioTransport(
|
|
534
528
|
command=server[server_name]["command"],
|
|
535
529
|
args=server[server_name]["args"],
|
|
536
530
|
env=server[server_name].get("env", None),
|
|
@@ -539,19 +533,16 @@ def infer_transport(
|
|
|
539
533
|
|
|
540
534
|
# HTTP transport
|
|
541
535
|
elif "url" in server:
|
|
542
|
-
|
|
536
|
+
inferred_transport = SSETransport(
|
|
543
537
|
url=server["url"],
|
|
544
538
|
headers=server.get("headers", None),
|
|
545
539
|
)
|
|
546
540
|
|
|
547
|
-
# WebSocket transport
|
|
548
|
-
elif "ws_url" in server:
|
|
549
|
-
return WSTransport(
|
|
550
|
-
url=server["ws_url"],
|
|
551
|
-
)
|
|
552
|
-
|
|
553
541
|
raise ValueError("Cannot determine transport type from dictionary")
|
|
554
542
|
|
|
555
543
|
# the transport is an unknown type
|
|
556
544
|
else:
|
|
557
545
|
raise ValueError(f"Could not infer a valid transport from: {transport}")
|
|
546
|
+
|
|
547
|
+
logger.debug(f"Inferred transport: {inferred_transport}")
|
|
548
|
+
return inferred_transport
|
fastmcp/server/context.py
CHANGED
|
@@ -56,7 +56,7 @@ class Context:
|
|
|
56
56
|
ctx.error("Error message")
|
|
57
57
|
|
|
58
58
|
# Report progress
|
|
59
|
-
ctx.report_progress(50, 100)
|
|
59
|
+
ctx.report_progress(50, 100, "Processing")
|
|
60
60
|
|
|
61
61
|
# Access resources
|
|
62
62
|
data = ctx.read_resource("resource://data")
|
|
@@ -96,7 +96,7 @@ class Context:
|
|
|
96
96
|
return self.fastmcp._mcp_server.request_context
|
|
97
97
|
|
|
98
98
|
async def report_progress(
|
|
99
|
-
self, progress: float, total: float | None = None
|
|
99
|
+
self, progress: float, total: float | None = None, message: str | None = None
|
|
100
100
|
) -> None:
|
|
101
101
|
"""Report progress for the current operation.
|
|
102
102
|
|
|
@@ -115,7 +115,10 @@ class Context:
|
|
|
115
115
|
return
|
|
116
116
|
|
|
117
117
|
await self.request_context.session.send_progress_notification(
|
|
118
|
-
progress_token=progress_token,
|
|
118
|
+
progress_token=progress_token,
|
|
119
|
+
progress=progress,
|
|
120
|
+
total=total,
|
|
121
|
+
message=message,
|
|
119
122
|
)
|
|
120
123
|
|
|
121
124
|
async def read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents]:
|
fastmcp/server/http.py
CHANGED
|
@@ -10,9 +10,16 @@ from mcp.server.auth.middleware.bearer_auth import (
|
|
|
10
10
|
BearerAuthBackend,
|
|
11
11
|
RequireAuthMiddleware,
|
|
12
12
|
)
|
|
13
|
-
from mcp.server.auth.provider import
|
|
13
|
+
from mcp.server.auth.provider import (
|
|
14
|
+
AccessTokenT,
|
|
15
|
+
AuthorizationCodeT,
|
|
16
|
+
OAuthAuthorizationServerProvider,
|
|
17
|
+
RefreshTokenT,
|
|
18
|
+
)
|
|
14
19
|
from mcp.server.auth.routes import create_auth_routes
|
|
15
20
|
from mcp.server.auth.settings import AuthSettings
|
|
21
|
+
from mcp.server.lowlevel.server import LifespanResultT
|
|
22
|
+
from mcp.server.sse import SseServerTransport
|
|
16
23
|
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
|
|
17
24
|
from starlette.applications import Starlette
|
|
18
25
|
from starlette.middleware import Middleware
|
|
@@ -20,9 +27,8 @@ from starlette.middleware.authentication import AuthenticationMiddleware
|
|
|
20
27
|
from starlette.requests import Request
|
|
21
28
|
from starlette.responses import Response
|
|
22
29
|
from starlette.routing import BaseRoute, Mount, Route
|
|
23
|
-
from starlette.types import Receive, Scope, Send
|
|
30
|
+
from starlette.types import Lifespan, Receive, Scope, Send
|
|
24
31
|
|
|
25
|
-
from fastmcp.low_level.sse_server_transport import SseServerTransport
|
|
26
32
|
from fastmcp.utilities.logging import get_logger
|
|
27
33
|
|
|
28
34
|
if TYPE_CHECKING:
|
|
@@ -30,12 +36,19 @@ if TYPE_CHECKING:
|
|
|
30
36
|
|
|
31
37
|
logger = get_logger(__name__)
|
|
32
38
|
|
|
39
|
+
|
|
33
40
|
_current_http_request: ContextVar[Request | None] = ContextVar(
|
|
34
41
|
"http_request",
|
|
35
42
|
default=None,
|
|
36
43
|
)
|
|
37
44
|
|
|
38
45
|
|
|
46
|
+
class StarletteWithLifespan(Starlette):
|
|
47
|
+
@property
|
|
48
|
+
def lifespan(self) -> Lifespan:
|
|
49
|
+
return self.router.lifespan_context
|
|
50
|
+
|
|
51
|
+
|
|
39
52
|
@contextmanager
|
|
40
53
|
def set_http_request(request: Request) -> Generator[Request, None, None]:
|
|
41
54
|
token = _current_http_request.set(request)
|
|
@@ -62,7 +75,10 @@ class RequestContextMiddleware:
|
|
|
62
75
|
|
|
63
76
|
|
|
64
77
|
def setup_auth_middleware_and_routes(
|
|
65
|
-
auth_server_provider: OAuthAuthorizationServerProvider
|
|
78
|
+
auth_server_provider: OAuthAuthorizationServerProvider[
|
|
79
|
+
AuthorizationCodeT, RefreshTokenT, AccessTokenT
|
|
80
|
+
]
|
|
81
|
+
| None,
|
|
66
82
|
auth_settings: AuthSettings | None,
|
|
67
83
|
) -> tuple[list[Middleware], list[BaseRoute], list[str]]:
|
|
68
84
|
"""Set up authentication middleware and routes if auth is enabled.
|
|
@@ -112,7 +128,7 @@ def create_base_app(
|
|
|
112
128
|
middleware: list[Middleware],
|
|
113
129
|
debug: bool = False,
|
|
114
130
|
lifespan: Callable | None = None,
|
|
115
|
-
) ->
|
|
131
|
+
) -> StarletteWithLifespan:
|
|
116
132
|
"""Create a base Starlette app with common middleware and routes.
|
|
117
133
|
|
|
118
134
|
Args:
|
|
@@ -127,7 +143,7 @@ def create_base_app(
|
|
|
127
143
|
# Always add RequestContextMiddleware as the outermost middleware
|
|
128
144
|
middleware.append(Middleware(RequestContextMiddleware))
|
|
129
145
|
|
|
130
|
-
return
|
|
146
|
+
return StarletteWithLifespan(
|
|
131
147
|
routes=routes,
|
|
132
148
|
middleware=middleware,
|
|
133
149
|
debug=debug,
|
|
@@ -136,15 +152,18 @@ def create_base_app(
|
|
|
136
152
|
|
|
137
153
|
|
|
138
154
|
def create_sse_app(
|
|
139
|
-
server: FastMCP,
|
|
155
|
+
server: FastMCP[LifespanResultT],
|
|
140
156
|
message_path: str,
|
|
141
157
|
sse_path: str,
|
|
142
|
-
auth_server_provider: OAuthAuthorizationServerProvider
|
|
158
|
+
auth_server_provider: OAuthAuthorizationServerProvider[
|
|
159
|
+
AuthorizationCodeT, RefreshTokenT, AccessTokenT
|
|
160
|
+
]
|
|
161
|
+
| None = None,
|
|
143
162
|
auth_settings: AuthSettings | None = None,
|
|
144
163
|
debug: bool = False,
|
|
145
164
|
routes: list[BaseRoute] | None = None,
|
|
146
165
|
middleware: list[Middleware] | None = None,
|
|
147
|
-
) ->
|
|
166
|
+
) -> StarletteWithLifespan:
|
|
148
167
|
"""Return an instance of the SSE server app.
|
|
149
168
|
|
|
150
169
|
Args:
|
|
@@ -228,25 +247,33 @@ def create_sse_app(
|
|
|
228
247
|
server_middleware.extend(middleware)
|
|
229
248
|
|
|
230
249
|
# Create and return the app
|
|
231
|
-
|
|
250
|
+
app = create_base_app(
|
|
232
251
|
routes=server_routes,
|
|
233
252
|
middleware=server_middleware,
|
|
234
253
|
debug=debug,
|
|
235
254
|
)
|
|
255
|
+
# Store the FastMCP server instance on the Starlette app state
|
|
256
|
+
app.state.fastmcp_server = server
|
|
257
|
+
app.state.path = sse_path
|
|
258
|
+
|
|
259
|
+
return app
|
|
236
260
|
|
|
237
261
|
|
|
238
262
|
def create_streamable_http_app(
|
|
239
|
-
server: FastMCP,
|
|
263
|
+
server: FastMCP[LifespanResultT],
|
|
240
264
|
streamable_http_path: str,
|
|
241
265
|
event_store: None = None,
|
|
242
|
-
auth_server_provider: OAuthAuthorizationServerProvider
|
|
266
|
+
auth_server_provider: OAuthAuthorizationServerProvider[
|
|
267
|
+
AuthorizationCodeT, RefreshTokenT, AccessTokenT
|
|
268
|
+
]
|
|
269
|
+
| None = None,
|
|
243
270
|
auth_settings: AuthSettings | None = None,
|
|
244
271
|
json_response: bool = False,
|
|
245
272
|
stateless_http: bool = False,
|
|
246
273
|
debug: bool = False,
|
|
247
274
|
routes: list[BaseRoute] | None = None,
|
|
248
275
|
middleware: list[Middleware] | None = None,
|
|
249
|
-
) ->
|
|
276
|
+
) -> StarletteWithLifespan:
|
|
250
277
|
"""Return an instance of the StreamableHTTP server app.
|
|
251
278
|
|
|
252
279
|
Args:
|
|
@@ -322,9 +349,15 @@ def create_streamable_http_app(
|
|
|
322
349
|
yield
|
|
323
350
|
|
|
324
351
|
# Create and return the app with lifespan
|
|
325
|
-
|
|
352
|
+
app = create_base_app(
|
|
326
353
|
routes=server_routes,
|
|
327
354
|
middleware=server_middleware,
|
|
328
355
|
debug=debug,
|
|
329
356
|
lifespan=lifespan,
|
|
330
357
|
)
|
|
358
|
+
# Store the FastMCP server instance on the Starlette app state
|
|
359
|
+
app.state.fastmcp_server = server
|
|
360
|
+
|
|
361
|
+
app.state.path = streamable_http_path
|
|
362
|
+
|
|
363
|
+
return app
|
fastmcp/server/server.py
CHANGED
|
@@ -11,6 +11,7 @@ from contextlib import (
|
|
|
11
11
|
asynccontextmanager,
|
|
12
12
|
)
|
|
13
13
|
from functools import partial
|
|
14
|
+
from pathlib import Path
|
|
14
15
|
from typing import TYPE_CHECKING, Any, Generic, Literal
|
|
15
16
|
|
|
16
17
|
import anyio
|
|
@@ -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,7 +48,11 @@ 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
|
|
@@ -57,16 +61,16 @@ from fastmcp.utilities.logging import get_logger
|
|
|
57
61
|
|
|
58
62
|
if TYPE_CHECKING:
|
|
59
63
|
from fastmcp.client import Client
|
|
64
|
+
from fastmcp.client.transports import ClientTransport
|
|
60
65
|
from fastmcp.server.openapi import FastMCPOpenAPI
|
|
61
66
|
from fastmcp.server.proxy import FastMCPProxy
|
|
62
|
-
|
|
63
67
|
logger = get_logger(__name__)
|
|
64
68
|
|
|
65
69
|
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
@asynccontextmanager
|
|
69
|
-
async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
|
73
|
+
async def default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]:
|
|
70
74
|
"""Default lifespan context manager that does nothing.
|
|
71
75
|
|
|
72
76
|
Args:
|
|
@@ -79,8 +83,10 @@ async def default_lifespan(server: FastMCP) -> AsyncIterator[Any]:
|
|
|
79
83
|
|
|
80
84
|
|
|
81
85
|
def _lifespan_wrapper(
|
|
82
|
-
app: FastMCP,
|
|
83
|
-
lifespan: Callable[
|
|
86
|
+
app: FastMCP[LifespanResultT],
|
|
87
|
+
lifespan: Callable[
|
|
88
|
+
[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
89
|
+
],
|
|
84
90
|
) -> Callable[
|
|
85
91
|
[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]
|
|
86
92
|
]:
|
|
@@ -189,15 +195,13 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
189
195
|
"""
|
|
190
196
|
if transport is None:
|
|
191
197
|
transport = "stdio"
|
|
192
|
-
if transport not in
|
|
198
|
+
if transport not in {"stdio", "streamable-http", "sse"}:
|
|
193
199
|
raise ValueError(f"Unknown transport: {transport}")
|
|
194
200
|
|
|
195
201
|
if transport == "stdio":
|
|
196
202
|
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)
|
|
203
|
+
elif transport in {"streamable-http", "sse"}:
|
|
204
|
+
await self.run_http_async(transport=transport, **transport_kwargs)
|
|
201
205
|
else:
|
|
202
206
|
raise ValueError(f"Unknown transport: {transport}")
|
|
203
207
|
|
|
@@ -211,7 +215,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
211
215
|
Args:
|
|
212
216
|
transport: Transport protocol to use ("stdio", "sse", or "streamable-http")
|
|
213
217
|
"""
|
|
214
|
-
logger.info(f'Starting server "{self.name}"...')
|
|
215
218
|
|
|
216
219
|
anyio.run(partial(self.run_async, transport, **transport_kwargs))
|
|
217
220
|
|
|
@@ -228,7 +231,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
228
231
|
async def get_tools(self) -> dict[str, Tool]:
|
|
229
232
|
"""Get all registered tools, indexed by registered key."""
|
|
230
233
|
if (tools := self._cache.get("tools")) is self._cache.NOT_FOUND:
|
|
231
|
-
tools = {}
|
|
234
|
+
tools: dict[str, Tool] = {}
|
|
232
235
|
for server in self._mounted_servers.values():
|
|
233
236
|
server_tools = await server.get_tools()
|
|
234
237
|
tools.update(server_tools)
|
|
@@ -239,7 +242,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
239
242
|
async def get_resources(self) -> dict[str, Resource]:
|
|
240
243
|
"""Get all registered resources, indexed by registered key."""
|
|
241
244
|
if (resources := self._cache.get("resources")) is self._cache.NOT_FOUND:
|
|
242
|
-
resources = {}
|
|
245
|
+
resources: dict[str, Resource] = {}
|
|
243
246
|
for server in self._mounted_servers.values():
|
|
244
247
|
server_resources = await server.get_resources()
|
|
245
248
|
resources.update(server_resources)
|
|
@@ -252,7 +255,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
252
255
|
if (
|
|
253
256
|
templates := self._cache.get("resource_templates")
|
|
254
257
|
) is self._cache.NOT_FOUND:
|
|
255
|
-
templates = {}
|
|
258
|
+
templates: dict[str, ResourceTemplate] = {}
|
|
256
259
|
for server in self._mounted_servers.values():
|
|
257
260
|
server_templates = await server.get_resource_templates()
|
|
258
261
|
templates.update(server_templates)
|
|
@@ -265,7 +268,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
265
268
|
List all available prompts.
|
|
266
269
|
"""
|
|
267
270
|
if (prompts := self._cache.get("prompts")) is self._cache.NOT_FOUND:
|
|
268
|
-
prompts = {}
|
|
271
|
+
prompts: dict[str, Prompt] = {}
|
|
269
272
|
for server in self._mounted_servers.values():
|
|
270
273
|
server_prompts = await server.get_prompts()
|
|
271
274
|
prompts.update(server_prompts)
|
|
@@ -728,6 +731,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
728
731
|
async def run_stdio_async(self) -> None:
|
|
729
732
|
"""Run the server using stdio transport."""
|
|
730
733
|
async with stdio_server() as (read_stream, write_stream):
|
|
734
|
+
logger.info(f"Starting MCP server {self.name!r} with transport 'stdio'")
|
|
731
735
|
await self._mcp_server.run(
|
|
732
736
|
read_stream,
|
|
733
737
|
write_stream,
|
|
@@ -743,7 +747,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
743
747
|
port: int | None = None,
|
|
744
748
|
log_level: str | None = None,
|
|
745
749
|
path: str | None = None,
|
|
746
|
-
uvicorn_config: dict | None = None,
|
|
750
|
+
uvicorn_config: dict[str, Any] | None = None,
|
|
751
|
+
middleware: list[Middleware] | None = None,
|
|
747
752
|
) -> None:
|
|
748
753
|
"""Run the server using HTTP transport.
|
|
749
754
|
|
|
@@ -755,21 +760,29 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
755
760
|
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
|
756
761
|
uvicorn_config: Additional configuration for the Uvicorn server
|
|
757
762
|
"""
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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)
|
|
772
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
|
+
)
|
|
773
786
|
await server.serve()
|
|
774
787
|
|
|
775
788
|
async def run_sse_async(
|
|
@@ -779,7 +792,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
779
792
|
log_level: str | None = None,
|
|
780
793
|
path: str | None = None,
|
|
781
794
|
message_path: str | None = None,
|
|
782
|
-
uvicorn_config: dict | None = None,
|
|
795
|
+
uvicorn_config: dict[str, Any] | None = None,
|
|
783
796
|
) -> None:
|
|
784
797
|
"""Run the server using SSE transport."""
|
|
785
798
|
|
|
@@ -805,7 +818,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
805
818
|
path: str | None = None,
|
|
806
819
|
message_path: str | None = None,
|
|
807
820
|
middleware: list[Middleware] | None = None,
|
|
808
|
-
) ->
|
|
821
|
+
) -> StarletteWithLifespan:
|
|
809
822
|
"""
|
|
810
823
|
Create a Starlette app for the SSE server.
|
|
811
824
|
|
|
@@ -836,7 +849,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
836
849
|
self,
|
|
837
850
|
path: str | None = None,
|
|
838
851
|
middleware: list[Middleware] | None = None,
|
|
839
|
-
) ->
|
|
852
|
+
) -> StarletteWithLifespan:
|
|
840
853
|
"""
|
|
841
854
|
Create a Starlette app for the StreamableHTTP server.
|
|
842
855
|
|
|
@@ -857,7 +870,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
857
870
|
path: str | None = None,
|
|
858
871
|
middleware: list[Middleware] | None = None,
|
|
859
872
|
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
|
860
|
-
) ->
|
|
873
|
+
) -> StarletteWithLifespan:
|
|
861
874
|
"""Create a Starlette app using the specified HTTP transport.
|
|
862
875
|
|
|
863
876
|
Args:
|
|
@@ -868,7 +881,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
868
881
|
Returns:
|
|
869
882
|
A Starlette application configured with the specified transport
|
|
870
883
|
"""
|
|
871
|
-
from fastmcp.server.http import create_streamable_http_app
|
|
872
884
|
|
|
873
885
|
if transport == "streamable-http":
|
|
874
886
|
return create_streamable_http_app(
|
|
@@ -901,7 +913,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
901
913
|
port: int | None = None,
|
|
902
914
|
log_level: str | None = None,
|
|
903
915
|
path: str | None = None,
|
|
904
|
-
uvicorn_config: dict | None = None,
|
|
916
|
+
uvicorn_config: dict[str, Any] | None = None,
|
|
905
917
|
) -> None:
|
|
906
918
|
# Deprecated since 2.3.2
|
|
907
919
|
warnings.warn(
|
|
@@ -1028,9 +1040,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1028
1040
|
- The prompts are imported with prefixed names using the
|
|
1029
1041
|
prompt_separator Example: If server has a prompt named
|
|
1030
1042
|
"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
|
|
1034
1043
|
|
|
1035
1044
|
Args:
|
|
1036
1045
|
prefix: The prefix to use for the mounted server server: The FastMCP
|
|
@@ -1102,14 +1111,48 @@ class FastMCP(Generic[LifespanResultT]):
|
|
|
1102
1111
|
openapi_spec=app.openapi(), client=client, name=name, **settings
|
|
1103
1112
|
)
|
|
1104
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
|
+
|
|
1105
1143
|
@classmethod
|
|
1106
1144
|
def from_client(cls, client: Client, **settings: Any) -> FastMCPProxy:
|
|
1107
1145
|
"""
|
|
1108
1146
|
Create a FastMCP proxy server from a FastMCP client.
|
|
1109
1147
|
"""
|
|
1110
|
-
|
|
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
|
+
)
|
|
1111
1154
|
|
|
1112
|
-
return
|
|
1155
|
+
return cls.as_proxy(client, **settings)
|
|
1113
1156
|
|
|
1114
1157
|
|
|
1115
1158
|
def _validate_resource_prefix(prefix: str) -> None:
|
|
@@ -1128,7 +1171,7 @@ class MountedServer:
|
|
|
1128
1171
|
def __init__(
|
|
1129
1172
|
self,
|
|
1130
1173
|
prefix: str,
|
|
1131
|
-
server: FastMCP,
|
|
1174
|
+
server: FastMCP[LifespanResultT],
|
|
1132
1175
|
tool_separator: str | None = None,
|
|
1133
1176
|
resource_separator: str | None = None,
|
|
1134
1177
|
prompt_separator: str | None = None,
|