fastmcp 2.12.4__py3-none-any.whl → 2.13.0rc1__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 +6 -6
- fastmcp/cli/install/claude_code.py +3 -3
- fastmcp/cli/install/claude_desktop.py +3 -3
- fastmcp/cli/install/cursor.py +7 -7
- fastmcp/cli/install/gemini_cli.py +3 -3
- fastmcp/cli/install/mcp_json.py +3 -3
- fastmcp/cli/run.py +13 -8
- fastmcp/client/auth/oauth.py +100 -208
- fastmcp/client/client.py +11 -11
- fastmcp/client/logging.py +18 -14
- fastmcp/client/oauth_callback.py +81 -171
- fastmcp/client/transports.py +76 -22
- fastmcp/contrib/component_manager/component_service.py +6 -6
- fastmcp/contrib/mcp_mixin/README.md +32 -1
- fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
- fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
- fastmcp/experimental/utilities/openapi/parser.py +23 -3
- fastmcp/prompts/prompt.py +13 -6
- fastmcp/prompts/prompt_manager.py +16 -101
- fastmcp/resources/resource.py +13 -6
- fastmcp/resources/resource_manager.py +5 -164
- fastmcp/resources/template.py +107 -17
- fastmcp/server/auth/auth.py +40 -32
- fastmcp/server/auth/jwt_issuer.py +289 -0
- fastmcp/server/auth/oauth_proxy.py +1238 -234
- fastmcp/server/auth/oidc_proxy.py +8 -6
- fastmcp/server/auth/providers/auth0.py +12 -6
- fastmcp/server/auth/providers/aws.py +13 -2
- fastmcp/server/auth/providers/azure.py +137 -124
- fastmcp/server/auth/providers/descope.py +4 -6
- fastmcp/server/auth/providers/github.py +13 -7
- fastmcp/server/auth/providers/google.py +13 -7
- fastmcp/server/auth/providers/introspection.py +281 -0
- fastmcp/server/auth/providers/jwt.py +8 -2
- fastmcp/server/auth/providers/scalekit.py +179 -0
- fastmcp/server/auth/providers/supabase.py +172 -0
- fastmcp/server/auth/providers/workos.py +16 -13
- fastmcp/server/context.py +89 -34
- fastmcp/server/http.py +53 -16
- fastmcp/server/low_level.py +121 -2
- fastmcp/server/middleware/caching.py +469 -0
- fastmcp/server/middleware/error_handling.py +6 -2
- fastmcp/server/middleware/logging.py +48 -37
- fastmcp/server/middleware/middleware.py +28 -15
- fastmcp/server/middleware/rate_limiting.py +3 -3
- fastmcp/server/proxy.py +6 -6
- fastmcp/server/server.py +638 -183
- fastmcp/settings.py +22 -9
- fastmcp/tools/tool.py +7 -3
- fastmcp/tools/tool_manager.py +22 -108
- fastmcp/tools/tool_transform.py +3 -3
- fastmcp/utilities/cli.py +2 -2
- fastmcp/utilities/components.py +5 -0
- fastmcp/utilities/inspect.py +77 -21
- fastmcp/utilities/logging.py +118 -8
- fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
- fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
- fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
- fastmcp/utilities/tests.py +87 -4
- fastmcp/utilities/types.py +1 -1
- fastmcp/utilities/ui.py +497 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/METADATA +8 -4
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/RECORD +66 -62
- fastmcp/cli/claude.py +0 -135
- fastmcp/utilities/storage.py +0 -204
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/WHEEL +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.12.4.dist-info → fastmcp-2.13.0rc1.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/logging.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Awaitable, Callable
|
|
2
|
+
from logging import Logger
|
|
2
3
|
from typing import TypeAlias
|
|
3
4
|
|
|
4
5
|
from mcp.client.session import LoggingFnT
|
|
@@ -6,7 +7,8 @@ from mcp.types import LoggingMessageNotificationParams
|
|
|
6
7
|
|
|
7
8
|
from fastmcp.utilities.logging import get_logger
|
|
8
9
|
|
|
9
|
-
logger = get_logger(__name__)
|
|
10
|
+
logger: Logger = get_logger(name=__name__)
|
|
11
|
+
from_server_logger: Logger = get_logger(name="fastmcp.client.from_server")
|
|
10
12
|
|
|
11
13
|
LogMessage: TypeAlias = LoggingMessageNotificationParams
|
|
12
14
|
LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
|
|
@@ -14,30 +16,32 @@ LogHandler: TypeAlias = Callable[[LogMessage], Awaitable[None]]
|
|
|
14
16
|
|
|
15
17
|
async def default_log_handler(message: LogMessage) -> None:
|
|
16
18
|
"""Default handler that properly routes server log messages to appropriate log levels."""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
# data can be any JSON-serializable type, not just a dict
|
|
20
|
+
data = message.data
|
|
19
21
|
|
|
20
22
|
# Map MCP log levels to Python logging levels
|
|
21
23
|
level_map = {
|
|
22
|
-
"debug":
|
|
23
|
-
"info":
|
|
24
|
-
"notice":
|
|
25
|
-
"warning":
|
|
26
|
-
"error":
|
|
27
|
-
"critical":
|
|
28
|
-
"alert":
|
|
29
|
-
"emergency":
|
|
24
|
+
"debug": from_server_logger.debug,
|
|
25
|
+
"info": from_server_logger.info,
|
|
26
|
+
"notice": from_server_logger.info, # Python doesn't have 'notice', map to info
|
|
27
|
+
"warning": from_server_logger.warning,
|
|
28
|
+
"error": from_server_logger.error,
|
|
29
|
+
"critical": from_server_logger.critical,
|
|
30
|
+
"alert": from_server_logger.critical, # Map alert to critical
|
|
31
|
+
"emergency": from_server_logger.critical, # Map emergency to critical
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
# Get the appropriate logging function based on the message level
|
|
33
35
|
log_fn = level_map.get(message.level.lower(), logger.info)
|
|
34
36
|
|
|
35
37
|
# Include logger name if available
|
|
38
|
+
msg_prefix: str = f"Received {message.level.upper()} from server"
|
|
39
|
+
|
|
36
40
|
if message.logger:
|
|
37
|
-
|
|
41
|
+
msg_prefix += f" ({message.logger})"
|
|
38
42
|
|
|
39
|
-
# Log with appropriate level and
|
|
40
|
-
log_fn(f"
|
|
43
|
+
# Log with appropriate level and data
|
|
44
|
+
log_fn(msg=f"{msg_prefix}: {data}")
|
|
41
45
|
|
|
42
46
|
|
|
43
47
|
def create_log_callback(handler: LogHandler | None = None) -> LoggingFnT:
|
fastmcp/client/oauth_callback.py
CHANGED
|
@@ -7,17 +7,26 @@ and display styled responses to users.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import asyncio
|
|
11
10
|
from dataclasses import dataclass
|
|
12
11
|
|
|
12
|
+
import anyio
|
|
13
13
|
from starlette.applications import Starlette
|
|
14
14
|
from starlette.requests import Request
|
|
15
|
-
from starlette.responses import HTMLResponse
|
|
16
15
|
from starlette.routing import Route
|
|
17
16
|
from uvicorn import Config, Server
|
|
18
17
|
|
|
19
18
|
from fastmcp.utilities.http import find_available_port
|
|
20
19
|
from fastmcp.utilities.logging import get_logger
|
|
20
|
+
from fastmcp.utilities.ui import (
|
|
21
|
+
HELPER_TEXT_STYLES,
|
|
22
|
+
INFO_BOX_STYLES,
|
|
23
|
+
STATUS_MESSAGE_STYLES,
|
|
24
|
+
create_info_box,
|
|
25
|
+
create_logo,
|
|
26
|
+
create_page,
|
|
27
|
+
create_secure_html_response,
|
|
28
|
+
create_status_message,
|
|
29
|
+
)
|
|
21
30
|
|
|
22
31
|
logger = get_logger(__name__)
|
|
23
32
|
|
|
@@ -29,155 +38,39 @@ def create_callback_html(
|
|
|
29
38
|
server_url: str | None = None,
|
|
30
39
|
) -> str:
|
|
31
40
|
"""Create a styled HTML response for OAuth callbacks."""
|
|
32
|
-
logo_url = "https://gofastmcp.com/assets/brand/blue-logo.png"
|
|
33
|
-
|
|
34
41
|
# Build the main status message
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
icon_bg = "#10b98120"
|
|
39
|
-
else:
|
|
40
|
-
status_title = "Authentication failed"
|
|
41
|
-
status_icon = "✕"
|
|
42
|
-
icon_bg = "#ef444420"
|
|
42
|
+
status_title = (
|
|
43
|
+
"Authentication successful" if is_success else "Authentication failed"
|
|
44
|
+
)
|
|
43
45
|
|
|
44
46
|
# Add detail info box for both success and error cases
|
|
45
47
|
detail_info = ""
|
|
46
48
|
if is_success and server_url:
|
|
47
|
-
detail_info = f""
|
|
48
|
-
<div class="info-box">
|
|
49
|
-
Connected to: <strong>{server_url}</strong>
|
|
50
|
-
</div>
|
|
51
|
-
"""
|
|
49
|
+
detail_info = create_info_box(f"Connected to: {server_url}", centered=True)
|
|
52
50
|
elif not is_success:
|
|
53
|
-
detail_info =
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
return f"""
|
|
60
|
-
<!DOCTYPE html>
|
|
61
|
-
<html lang="en">
|
|
62
|
-
<head>
|
|
63
|
-
<meta charset="UTF-8">
|
|
64
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
65
|
-
<title>{title}</title>
|
|
66
|
-
<style>
|
|
67
|
-
* {{
|
|
68
|
-
margin: 0;
|
|
69
|
-
padding: 0;
|
|
70
|
-
box-sizing: border-box;
|
|
71
|
-
}}
|
|
72
|
-
|
|
73
|
-
body {{
|
|
74
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
75
|
-
margin: 0;
|
|
76
|
-
padding: 0;
|
|
77
|
-
min-height: 100vh;
|
|
78
|
-
display: flex;
|
|
79
|
-
align-items: center;
|
|
80
|
-
justify-content: center;
|
|
81
|
-
background: #ffffff;
|
|
82
|
-
color: #0a0a0a;
|
|
83
|
-
}}
|
|
84
|
-
|
|
85
|
-
.container {{
|
|
86
|
-
background: #ffffff;
|
|
87
|
-
border: 1px solid #e5e5e5;
|
|
88
|
-
padding: 3rem 2rem;
|
|
89
|
-
border-radius: 0.75rem;
|
|
90
|
-
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
|
91
|
-
text-align: center;
|
|
92
|
-
max-width: 28rem;
|
|
93
|
-
margin: 1rem;
|
|
94
|
-
position: relative;
|
|
95
|
-
}}
|
|
96
|
-
|
|
97
|
-
.logo {{
|
|
98
|
-
width: 60px;
|
|
99
|
-
height: auto;
|
|
100
|
-
margin-bottom: 2rem;
|
|
101
|
-
display: block;
|
|
102
|
-
margin-left: auto;
|
|
103
|
-
margin-right: auto;
|
|
104
|
-
}}
|
|
105
|
-
|
|
106
|
-
.status-message {{
|
|
107
|
-
display: flex;
|
|
108
|
-
align-items: center;
|
|
109
|
-
justify-content: center;
|
|
110
|
-
gap: 0.75rem;
|
|
111
|
-
margin-bottom: 1.5rem;
|
|
112
|
-
}}
|
|
113
|
-
|
|
114
|
-
.status-icon {{
|
|
115
|
-
font-size: 1.5rem;
|
|
116
|
-
line-height: 1;
|
|
117
|
-
display: inline-flex;
|
|
118
|
-
align-items: center;
|
|
119
|
-
justify-content: center;
|
|
120
|
-
width: 2rem;
|
|
121
|
-
height: 2rem;
|
|
122
|
-
background: {icon_bg};
|
|
123
|
-
border-radius: 0.5rem;
|
|
124
|
-
flex-shrink: 0;
|
|
125
|
-
}}
|
|
126
|
-
|
|
127
|
-
.message {{
|
|
128
|
-
font-size: 1.125rem;
|
|
129
|
-
line-height: 1.75;
|
|
130
|
-
color: #0a0a0a;
|
|
131
|
-
font-weight: 600;
|
|
132
|
-
text-align: left;
|
|
133
|
-
}}
|
|
134
|
-
|
|
135
|
-
.info-box {{
|
|
136
|
-
background: #f5f5f5;
|
|
137
|
-
border: 1px solid #e5e5e5;
|
|
138
|
-
border-radius: 0.5rem;
|
|
139
|
-
padding: 0.875rem;
|
|
140
|
-
margin: 1.25rem 0;
|
|
141
|
-
font-size: 0.875rem;
|
|
142
|
-
color: #525252;
|
|
143
|
-
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Courier New', monospace;
|
|
144
|
-
text-align: left;
|
|
145
|
-
}}
|
|
146
|
-
|
|
147
|
-
.info-box.error {{
|
|
148
|
-
background: #fef2f2;
|
|
149
|
-
border-color: #fecaca;
|
|
150
|
-
color: #991b1b;
|
|
151
|
-
}}
|
|
152
|
-
|
|
153
|
-
.info-box strong {{
|
|
154
|
-
color: #0a0a0a;
|
|
155
|
-
font-weight: 600;
|
|
156
|
-
}}
|
|
157
|
-
|
|
158
|
-
.close-instruction {{
|
|
159
|
-
font-size: 0.875rem;
|
|
160
|
-
color: #737373;
|
|
161
|
-
margin-top: 1.5rem;
|
|
162
|
-
}}
|
|
163
|
-
</style>
|
|
164
|
-
</head>
|
|
165
|
-
<body>
|
|
51
|
+
detail_info = create_info_box(message, is_error=True, centered=True)
|
|
52
|
+
|
|
53
|
+
# Build the page content
|
|
54
|
+
content = f"""
|
|
166
55
|
<div class="container">
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<span class="status-icon">{status_icon}</span>
|
|
170
|
-
<div class="message">{status_title}</div>
|
|
171
|
-
</div>
|
|
56
|
+
{create_logo()}
|
|
57
|
+
{create_status_message(status_title, is_success=is_success)}
|
|
172
58
|
{detail_info}
|
|
173
59
|
<div class="close-instruction">
|
|
174
60
|
You can safely close this tab now.
|
|
175
61
|
</div>
|
|
176
62
|
</div>
|
|
177
|
-
</body>
|
|
178
|
-
</html>
|
|
179
63
|
"""
|
|
180
64
|
|
|
65
|
+
# Additional styles needed for this page
|
|
66
|
+
additional_styles = STATUS_MESSAGE_STYLES + INFO_BOX_STYLES + HELPER_TEXT_STYLES
|
|
67
|
+
|
|
68
|
+
return create_page(
|
|
69
|
+
content=content,
|
|
70
|
+
title=title,
|
|
71
|
+
additional_styles=additional_styles,
|
|
72
|
+
)
|
|
73
|
+
|
|
181
74
|
|
|
182
75
|
@dataclass
|
|
183
76
|
class CallbackResponse:
|
|
@@ -194,11 +87,21 @@ class CallbackResponse:
|
|
|
194
87
|
return {k: v for k, v in self.__dict__.items() if v is not None}
|
|
195
88
|
|
|
196
89
|
|
|
90
|
+
@dataclass
|
|
91
|
+
class OAuthCallbackResult:
|
|
92
|
+
"""Container for OAuth callback results, used with anyio.Event for async coordination."""
|
|
93
|
+
|
|
94
|
+
code: str | None = None
|
|
95
|
+
state: str | None = None
|
|
96
|
+
error: Exception | None = None
|
|
97
|
+
|
|
98
|
+
|
|
197
99
|
def create_oauth_callback_server(
|
|
198
100
|
port: int,
|
|
199
101
|
callback_path: str = "/callback",
|
|
200
102
|
server_url: str | None = None,
|
|
201
|
-
|
|
103
|
+
result_container: OAuthCallbackResult | None = None,
|
|
104
|
+
result_ready: anyio.Event | None = None,
|
|
202
105
|
) -> Server:
|
|
203
106
|
"""
|
|
204
107
|
Create an OAuth callback server.
|
|
@@ -207,7 +110,8 @@ def create_oauth_callback_server(
|
|
|
207
110
|
port: The port to run the server on
|
|
208
111
|
callback_path: The path to listen for OAuth redirects on
|
|
209
112
|
server_url: Optional server URL to display in success messages
|
|
210
|
-
|
|
113
|
+
result_container: Optional container to store callback results
|
|
114
|
+
result_ready: Optional event to signal when callback is received
|
|
211
115
|
|
|
212
116
|
Returns:
|
|
213
117
|
Configured uvicorn Server instance (not yet running)
|
|
@@ -221,32 +125,36 @@ def create_oauth_callback_server(
|
|
|
221
125
|
if callback_response.error:
|
|
222
126
|
error_desc = callback_response.error_description or "Unknown error"
|
|
223
127
|
|
|
224
|
-
#
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
128
|
+
# Create user-friendly error messages
|
|
129
|
+
if callback_response.error == "access_denied":
|
|
130
|
+
user_message = "Access was denied by the authorization server."
|
|
131
|
+
else:
|
|
132
|
+
user_message = f"Authorization failed: {error_desc}"
|
|
133
|
+
|
|
134
|
+
# Store error and signal completion if result tracking provided
|
|
135
|
+
if result_container is not None and result_ready is not None:
|
|
136
|
+
result_container.error = RuntimeError(user_message)
|
|
137
|
+
result_ready.set()
|
|
231
138
|
|
|
232
|
-
return
|
|
139
|
+
return create_secure_html_response(
|
|
233
140
|
create_callback_html(
|
|
234
|
-
|
|
141
|
+
user_message,
|
|
235
142
|
is_success=False,
|
|
236
143
|
),
|
|
237
144
|
status_code=400,
|
|
238
145
|
)
|
|
239
146
|
|
|
240
147
|
if not callback_response.code:
|
|
241
|
-
|
|
242
|
-
if response_future and not response_future.done():
|
|
243
|
-
response_future.set_exception(
|
|
244
|
-
RuntimeError("OAuth callback missing authorization code")
|
|
245
|
-
)
|
|
148
|
+
user_message = "No authorization code was received from the server."
|
|
246
149
|
|
|
247
|
-
|
|
150
|
+
# Store error and signal completion if result tracking provided
|
|
151
|
+
if result_container is not None and result_ready is not None:
|
|
152
|
+
result_container.error = RuntimeError(user_message)
|
|
153
|
+
result_ready.set()
|
|
154
|
+
|
|
155
|
+
return create_secure_html_response(
|
|
248
156
|
create_callback_html(
|
|
249
|
-
|
|
157
|
+
user_message,
|
|
250
158
|
is_success=False,
|
|
251
159
|
),
|
|
252
160
|
status_code=400,
|
|
@@ -254,29 +162,30 @@ def create_oauth_callback_server(
|
|
|
254
162
|
|
|
255
163
|
# Check for missing state parameter (indicates OAuth flow issue)
|
|
256
164
|
if callback_response.state is None:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
165
|
+
user_message = (
|
|
166
|
+
"The OAuth server did not return the expected state parameter."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Store error and signal completion if result tracking provided
|
|
170
|
+
if result_container is not None and result_ready is not None:
|
|
171
|
+
result_container.error = RuntimeError(user_message)
|
|
172
|
+
result_ready.set()
|
|
173
|
+
|
|
174
|
+
return create_secure_html_response(
|
|
266
175
|
create_callback_html(
|
|
267
|
-
|
|
176
|
+
user_message,
|
|
268
177
|
is_success=False,
|
|
269
178
|
),
|
|
270
179
|
status_code=400,
|
|
271
180
|
)
|
|
272
181
|
|
|
273
|
-
# Success case
|
|
274
|
-
if
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
182
|
+
# Success case - store result and signal completion if result tracking provided
|
|
183
|
+
if result_container is not None and result_ready is not None:
|
|
184
|
+
result_container.code = callback_response.code
|
|
185
|
+
result_container.state = callback_response.state
|
|
186
|
+
result_ready.set()
|
|
278
187
|
|
|
279
|
-
return
|
|
188
|
+
return create_secure_html_response(
|
|
280
189
|
create_callback_html("", is_success=True, server_url=server_url)
|
|
281
190
|
)
|
|
282
191
|
|
|
@@ -289,6 +198,7 @@ def create_oauth_callback_server(
|
|
|
289
198
|
port=port,
|
|
290
199
|
lifespan="off",
|
|
291
200
|
log_level="warning",
|
|
201
|
+
ws="websockets-sansio",
|
|
292
202
|
)
|
|
293
203
|
)
|
|
294
204
|
|
fastmcp/client/transports.py
CHANGED
|
@@ -8,7 +8,7 @@ import sys
|
|
|
8
8
|
import warnings
|
|
9
9
|
from collections.abc import AsyncIterator
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Literal, TypeVar, cast, overload
|
|
11
|
+
from typing import Any, Literal, TextIO, TypeVar, cast, overload
|
|
12
12
|
|
|
13
13
|
import anyio
|
|
14
14
|
import httpx
|
|
@@ -313,6 +313,7 @@ class StdioTransport(ClientTransport):
|
|
|
313
313
|
env: dict[str, str] | None = None,
|
|
314
314
|
cwd: str | None = None,
|
|
315
315
|
keep_alive: bool | None = None,
|
|
316
|
+
log_file: Path | TextIO | None = None,
|
|
316
317
|
):
|
|
317
318
|
"""
|
|
318
319
|
Initialize a Stdio transport.
|
|
@@ -326,6 +327,11 @@ class StdioTransport(ClientTransport):
|
|
|
326
327
|
Defaults to True. When True, the subprocess remains active
|
|
327
328
|
after the connection context exits, allowing reuse in
|
|
328
329
|
subsequent connections.
|
|
330
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
331
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
332
|
+
if not provided. When a Path is provided, the file will be created
|
|
333
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
334
|
+
errors will be written to this file instead of appearing in the console.
|
|
329
335
|
"""
|
|
330
336
|
self.command = command
|
|
331
337
|
self.args = args
|
|
@@ -334,6 +340,7 @@ class StdioTransport(ClientTransport):
|
|
|
334
340
|
if keep_alive is None:
|
|
335
341
|
keep_alive = True
|
|
336
342
|
self.keep_alive = keep_alive
|
|
343
|
+
self.log_file = log_file
|
|
337
344
|
|
|
338
345
|
self._session: ClientSession | None = None
|
|
339
346
|
self._connect_task: asyncio.Task | None = None
|
|
@@ -368,6 +375,7 @@ class StdioTransport(ClientTransport):
|
|
|
368
375
|
args=self.args,
|
|
369
376
|
env=self.env,
|
|
370
377
|
cwd=self.cwd,
|
|
378
|
+
log_file=self.log_file,
|
|
371
379
|
session_kwargs=session_kwargs,
|
|
372
380
|
ready_event=self._ready_event,
|
|
373
381
|
stop_event=self._stop_event,
|
|
@@ -421,6 +429,7 @@ async def _stdio_transport_connect_task(
|
|
|
421
429
|
args: list[str],
|
|
422
430
|
env: dict[str, str] | None,
|
|
423
431
|
cwd: str | None,
|
|
432
|
+
log_file: Path | TextIO | None,
|
|
424
433
|
session_kwargs: SessionKwargs,
|
|
425
434
|
ready_event: anyio.Event,
|
|
426
435
|
stop_event: anyio.Event,
|
|
@@ -438,7 +447,19 @@ async def _stdio_transport_connect_task(
|
|
|
438
447
|
env=env,
|
|
439
448
|
cwd=cwd,
|
|
440
449
|
)
|
|
441
|
-
|
|
450
|
+
# Handle log_file: Path needs to be opened, TextIO used as-is
|
|
451
|
+
if log_file is None:
|
|
452
|
+
log_file_handle = sys.stderr
|
|
453
|
+
elif isinstance(log_file, Path):
|
|
454
|
+
log_file_handle = open(log_file, "a")
|
|
455
|
+
stack.callback(log_file_handle.close)
|
|
456
|
+
else:
|
|
457
|
+
# Must be TextIO - use it directly
|
|
458
|
+
log_file_handle = log_file
|
|
459
|
+
|
|
460
|
+
transport = await stack.enter_async_context(
|
|
461
|
+
stdio_client(server_params, errlog=log_file_handle)
|
|
462
|
+
)
|
|
442
463
|
read_stream, write_stream = transport
|
|
443
464
|
session_future.set_result(
|
|
444
465
|
await stack.enter_async_context(
|
|
@@ -471,6 +492,7 @@ class PythonStdioTransport(StdioTransport):
|
|
|
471
492
|
cwd: str | None = None,
|
|
472
493
|
python_cmd: str = sys.executable,
|
|
473
494
|
keep_alive: bool | None = None,
|
|
495
|
+
log_file: Path | TextIO | None = None,
|
|
474
496
|
):
|
|
475
497
|
"""
|
|
476
498
|
Initialize a Python transport.
|
|
@@ -485,6 +507,11 @@ class PythonStdioTransport(StdioTransport):
|
|
|
485
507
|
Defaults to True. When True, the subprocess remains active
|
|
486
508
|
after the connection context exits, allowing reuse in
|
|
487
509
|
subsequent connections.
|
|
510
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
511
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
512
|
+
if not provided. When a Path is provided, the file will be created
|
|
513
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
514
|
+
errors will be written to this file instead of appearing in the console.
|
|
488
515
|
"""
|
|
489
516
|
script_path = Path(script_path).resolve()
|
|
490
517
|
if not script_path.is_file():
|
|
@@ -502,6 +529,7 @@ class PythonStdioTransport(StdioTransport):
|
|
|
502
529
|
env=env,
|
|
503
530
|
cwd=cwd,
|
|
504
531
|
keep_alive=keep_alive,
|
|
532
|
+
log_file=log_file,
|
|
505
533
|
)
|
|
506
534
|
self.script_path = script_path
|
|
507
535
|
|
|
@@ -516,6 +544,7 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
516
544
|
env: dict[str, str] | None = None,
|
|
517
545
|
cwd: str | None = None,
|
|
518
546
|
keep_alive: bool | None = None,
|
|
547
|
+
log_file: Path | TextIO | None = None,
|
|
519
548
|
):
|
|
520
549
|
script_path = Path(script_path).resolve()
|
|
521
550
|
if not script_path.is_file():
|
|
@@ -529,6 +558,7 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
529
558
|
env=env,
|
|
530
559
|
cwd=cwd,
|
|
531
560
|
keep_alive=keep_alive,
|
|
561
|
+
log_file=log_file,
|
|
532
562
|
)
|
|
533
563
|
self.script_path = script_path
|
|
534
564
|
|
|
@@ -544,6 +574,7 @@ class NodeStdioTransport(StdioTransport):
|
|
|
544
574
|
cwd: str | None = None,
|
|
545
575
|
node_cmd: str = "node",
|
|
546
576
|
keep_alive: bool | None = None,
|
|
577
|
+
log_file: Path | TextIO | None = None,
|
|
547
578
|
):
|
|
548
579
|
"""
|
|
549
580
|
Initialize a Node transport.
|
|
@@ -558,6 +589,11 @@ class NodeStdioTransport(StdioTransport):
|
|
|
558
589
|
Defaults to True. When True, the subprocess remains active
|
|
559
590
|
after the connection context exits, allowing reuse in
|
|
560
591
|
subsequent connections.
|
|
592
|
+
log_file: Optional path or file-like object where subprocess stderr will
|
|
593
|
+
be written. Can be a Path or TextIO object. Defaults to sys.stderr
|
|
594
|
+
if not provided. When a Path is provided, the file will be created
|
|
595
|
+
if it doesn't exist, or appended to if it does. When set, server
|
|
596
|
+
errors will be written to this file instead of appearing in the console.
|
|
561
597
|
"""
|
|
562
598
|
script_path = Path(script_path).resolve()
|
|
563
599
|
if not script_path.is_file():
|
|
@@ -570,7 +606,12 @@ class NodeStdioTransport(StdioTransport):
|
|
|
570
606
|
full_args.extend(args)
|
|
571
607
|
|
|
572
608
|
super().__init__(
|
|
573
|
-
command=node_cmd,
|
|
609
|
+
command=node_cmd,
|
|
610
|
+
args=full_args,
|
|
611
|
+
env=env,
|
|
612
|
+
cwd=cwd,
|
|
613
|
+
keep_alive=keep_alive,
|
|
614
|
+
log_file=log_file,
|
|
574
615
|
)
|
|
575
616
|
self.script_path = script_path
|
|
576
617
|
|
|
@@ -583,15 +624,15 @@ class UvStdioTransport(StdioTransport):
|
|
|
583
624
|
command: str,
|
|
584
625
|
args: list[str] | None = None,
|
|
585
626
|
module: bool = False,
|
|
586
|
-
project_directory:
|
|
627
|
+
project_directory: Path | None = None,
|
|
587
628
|
python_version: str | None = None,
|
|
588
629
|
with_packages: list[str] | None = None,
|
|
589
|
-
with_requirements:
|
|
630
|
+
with_requirements: Path | None = None,
|
|
590
631
|
env_vars: dict[str, str] | None = None,
|
|
591
632
|
keep_alive: bool | None = None,
|
|
592
633
|
):
|
|
593
634
|
# Basic validation
|
|
594
|
-
if project_directory and not
|
|
635
|
+
if project_directory and not project_directory.exists():
|
|
595
636
|
raise NotADirectoryError(
|
|
596
637
|
f"Project directory not found: {project_directory}"
|
|
597
638
|
)
|
|
@@ -811,29 +852,42 @@ class FastMCPTransport(ClientTransport):
|
|
|
811
852
|
|
|
812
853
|
# Create a cancel scope for the server task
|
|
813
854
|
async with anyio.create_task_group() as tg:
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
855
|
+
async with _enter_server_lifespan(server=self.server):
|
|
856
|
+
tg.start_soon(
|
|
857
|
+
lambda: self.server._mcp_server.run(
|
|
858
|
+
server_read,
|
|
859
|
+
server_write,
|
|
860
|
+
self.server._mcp_server.create_initialization_options(),
|
|
861
|
+
raise_exceptions=self.raise_exceptions,
|
|
862
|
+
)
|
|
820
863
|
)
|
|
821
|
-
)
|
|
822
864
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
865
|
+
try:
|
|
866
|
+
async with ClientSession(
|
|
867
|
+
read_stream=client_read,
|
|
868
|
+
write_stream=client_write,
|
|
869
|
+
**session_kwargs,
|
|
870
|
+
) as client_session:
|
|
871
|
+
yield client_session
|
|
872
|
+
finally:
|
|
873
|
+
tg.cancel_scope.cancel()
|
|
832
874
|
|
|
833
875
|
def __repr__(self) -> str:
|
|
834
876
|
return f"<FastMCPTransport(server='{self.server.name}')>"
|
|
835
877
|
|
|
836
878
|
|
|
879
|
+
@contextlib.asynccontextmanager
|
|
880
|
+
async def _enter_server_lifespan(
|
|
881
|
+
server: FastMCP | FastMCP1Server,
|
|
882
|
+
) -> AsyncIterator[None]:
|
|
883
|
+
"""Enters the server's lifespan context for FastMCP servers and does nothing for FastMCP 1 servers."""
|
|
884
|
+
if isinstance(server, FastMCP):
|
|
885
|
+
async with server._lifespan_manager():
|
|
886
|
+
yield
|
|
887
|
+
else:
|
|
888
|
+
yield
|
|
889
|
+
|
|
890
|
+
|
|
837
891
|
class MCPConfigTransport(ClientTransport):
|
|
838
892
|
"""Transport for connecting to one or more MCP servers defined in an MCPConfig.
|
|
839
893
|
|
|
@@ -41,7 +41,7 @@ class ComponentService:
|
|
|
41
41
|
return tool
|
|
42
42
|
|
|
43
43
|
# 2. Check mounted servers using the filtered protocol path.
|
|
44
|
-
for mounted in reversed(self.
|
|
44
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
45
45
|
if mounted.prefix:
|
|
46
46
|
if key.startswith(f"{mounted.prefix}_"):
|
|
47
47
|
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
@@ -70,7 +70,7 @@ class ComponentService:
|
|
|
70
70
|
return tool
|
|
71
71
|
|
|
72
72
|
# 2. Check mounted servers using the filtered protocol path.
|
|
73
|
-
for mounted in reversed(self.
|
|
73
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
74
74
|
if mounted.prefix:
|
|
75
75
|
if key.startswith(f"{mounted.prefix}_"):
|
|
76
76
|
tool_key = key.removeprefix(f"{mounted.prefix}_")
|
|
@@ -103,7 +103,7 @@ class ComponentService:
|
|
|
103
103
|
return template
|
|
104
104
|
|
|
105
105
|
# 2. Check mounted servers using the filtered protocol path.
|
|
106
|
-
for mounted in reversed(self.
|
|
106
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
107
107
|
if mounted.prefix:
|
|
108
108
|
if has_resource_prefix(
|
|
109
109
|
key,
|
|
@@ -146,7 +146,7 @@ class ComponentService:
|
|
|
146
146
|
return template
|
|
147
147
|
|
|
148
148
|
# 2. Check mounted servers using the filtered protocol path.
|
|
149
|
-
for mounted in reversed(self.
|
|
149
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
150
150
|
if mounted.prefix:
|
|
151
151
|
if has_resource_prefix(
|
|
152
152
|
key,
|
|
@@ -185,7 +185,7 @@ class ComponentService:
|
|
|
185
185
|
return prompt
|
|
186
186
|
|
|
187
187
|
# 2. Check mounted servers using the filtered protocol path.
|
|
188
|
-
for mounted in reversed(self.
|
|
188
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
189
189
|
if mounted.prefix:
|
|
190
190
|
if key.startswith(f"{mounted.prefix}_"):
|
|
191
191
|
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|
|
@@ -213,7 +213,7 @@ class ComponentService:
|
|
|
213
213
|
return prompt
|
|
214
214
|
|
|
215
215
|
# 2. Check mounted servers using the filtered protocol path.
|
|
216
|
-
for mounted in reversed(self.
|
|
216
|
+
for mounted in reversed(self._server._mounted_servers):
|
|
217
217
|
if mounted.prefix:
|
|
218
218
|
if key.startswith(f"{mounted.prefix}_"):
|
|
219
219
|
prompt_key = key.removeprefix(f"{mounted.prefix}_")
|