fastmcp 2.5.2__py3-none-any.whl → 2.6.1__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/client/__init__.py +3 -0
- fastmcp/client/auth/__init__.py +4 -0
- fastmcp/client/auth/bearer.py +17 -0
- fastmcp/client/auth/oauth.py +391 -0
- fastmcp/client/client.py +74 -26
- fastmcp/client/oauth_callback.py +310 -0
- fastmcp/client/transports.py +76 -14
- fastmcp/server/auth/__init__.py +4 -0
- fastmcp/server/auth/auth.py +45 -0
- fastmcp/server/auth/providers/bearer.py +377 -0
- fastmcp/server/auth/providers/bearer_env.py +62 -0
- fastmcp/server/auth/providers/in_memory.py +325 -0
- fastmcp/server/dependencies.py +10 -0
- fastmcp/server/http.py +38 -66
- fastmcp/server/openapi.py +2 -0
- fastmcp/server/server.py +21 -26
- fastmcp/settings.py +27 -8
- fastmcp/tools/tool.py +22 -3
- fastmcp/tools/tool_manager.py +2 -0
- fastmcp/utilities/http.py +8 -0
- fastmcp/utilities/tests.py +22 -10
- {fastmcp-2.5.2.dist-info → fastmcp-2.6.1.dist-info}/METADATA +28 -16
- {fastmcp-2.5.2.dist-info → fastmcp-2.6.1.dist-info}/RECORD +27 -19
- fastmcp/client/base.py +0 -0
- fastmcp/low_level/README.md +0 -1
- /fastmcp/{low_level → server/auth/providers}/__init__.py +0 -0
- {fastmcp-2.5.2.dist-info → fastmcp-2.6.1.dist-info}/WHEEL +0 -0
- {fastmcp-2.5.2.dist-info → fastmcp-2.6.1.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.5.2.dist-info → fastmcp-2.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OAuth callback server for handling authorization code flows.
|
|
3
|
+
|
|
4
|
+
This module provides a reusable callback server that can handle OAuth redirects
|
|
5
|
+
and display styled responses to users.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from starlette.applications import Starlette
|
|
14
|
+
from starlette.requests import Request
|
|
15
|
+
from starlette.responses import HTMLResponse
|
|
16
|
+
from starlette.routing import Route
|
|
17
|
+
from uvicorn import Config, Server
|
|
18
|
+
|
|
19
|
+
from fastmcp.utilities.http import find_available_port
|
|
20
|
+
from fastmcp.utilities.logging import get_logger
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_callback_html(
|
|
26
|
+
message: str,
|
|
27
|
+
is_success: bool = True,
|
|
28
|
+
title: str = "FastMCP OAuth",
|
|
29
|
+
server_url: str | None = None,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Create a styled HTML response for OAuth callbacks."""
|
|
32
|
+
status_emoji = "✅" if is_success else "❌"
|
|
33
|
+
status_color = "#10b981" if is_success else "#ef4444" # emerald-500 / red-500
|
|
34
|
+
|
|
35
|
+
# Add server info for success cases
|
|
36
|
+
server_info = ""
|
|
37
|
+
if is_success and server_url:
|
|
38
|
+
server_info = f"""
|
|
39
|
+
<div class="server-info">
|
|
40
|
+
Connected to: <strong>{server_url}</strong>
|
|
41
|
+
</div>
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
return f"""
|
|
45
|
+
<!DOCTYPE html>
|
|
46
|
+
<html lang="en">
|
|
47
|
+
<head>
|
|
48
|
+
<meta charset="UTF-8">
|
|
49
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
50
|
+
<title>{title}</title>
|
|
51
|
+
<style>
|
|
52
|
+
body {{
|
|
53
|
+
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Roboto Mono', monospace;
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
min-height: 100vh;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 25%, #16213e 50%, #0f0f23 100%);
|
|
61
|
+
color: #e2e8f0;
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
}}
|
|
64
|
+
|
|
65
|
+
body::before {{
|
|
66
|
+
content: '';
|
|
67
|
+
position: fixed;
|
|
68
|
+
top: 0;
|
|
69
|
+
left: 0;
|
|
70
|
+
width: 100%;
|
|
71
|
+
height: 100%;
|
|
72
|
+
background:
|
|
73
|
+
radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
|
|
74
|
+
radial-gradient(circle at 80% 20%, rgba(16, 185, 129, 0.1) 0%, transparent 50%),
|
|
75
|
+
radial-gradient(circle at 40% 40%, rgba(14, 165, 233, 0.1) 0%, transparent 50%);
|
|
76
|
+
pointer-events: none;
|
|
77
|
+
z-index: -1;
|
|
78
|
+
}}
|
|
79
|
+
|
|
80
|
+
.container {{
|
|
81
|
+
background: rgba(30, 41, 59, 0.9);
|
|
82
|
+
backdrop-filter: blur(10px);
|
|
83
|
+
border: 1px solid rgba(71, 85, 105, 0.3);
|
|
84
|
+
padding: 3rem 2rem;
|
|
85
|
+
border-radius: 1rem;
|
|
86
|
+
box-shadow:
|
|
87
|
+
0 25px 50px -12px rgba(0, 0, 0, 0.7),
|
|
88
|
+
0 0 0 1px rgba(255, 255, 255, 0.05),
|
|
89
|
+
inset 0 1px 0 0 rgba(255, 255, 255, 0.1);
|
|
90
|
+
text-align: center;
|
|
91
|
+
max-width: 500px;
|
|
92
|
+
margin: 1rem;
|
|
93
|
+
position: relative;
|
|
94
|
+
}}
|
|
95
|
+
|
|
96
|
+
.container::before {{
|
|
97
|
+
content: '';
|
|
98
|
+
position: absolute;
|
|
99
|
+
top: 0;
|
|
100
|
+
left: 0;
|
|
101
|
+
right: 0;
|
|
102
|
+
height: 1px;
|
|
103
|
+
background: linear-gradient(90deg, transparent, rgba(16, 185, 129, 0.5), transparent);
|
|
104
|
+
}}
|
|
105
|
+
|
|
106
|
+
.status-icon {{
|
|
107
|
+
font-size: 4rem;
|
|
108
|
+
margin-bottom: 1rem;
|
|
109
|
+
display: block;
|
|
110
|
+
filter: drop-shadow(0 0 20px currentColor);
|
|
111
|
+
}}
|
|
112
|
+
|
|
113
|
+
.message {{
|
|
114
|
+
font-size: 1.25rem;
|
|
115
|
+
line-height: 1.6;
|
|
116
|
+
color: {status_color};
|
|
117
|
+
margin-bottom: 1.5rem;
|
|
118
|
+
font-weight: 600;
|
|
119
|
+
text-shadow: 0 0 10px rgba({
|
|
120
|
+
"16, 185, 129" if is_success else "239, 68, 68"
|
|
121
|
+
}, 0.3);
|
|
122
|
+
}}
|
|
123
|
+
|
|
124
|
+
.server-info {{
|
|
125
|
+
background: rgba(6, 182, 212, 0.1);
|
|
126
|
+
border: 1px solid rgba(6, 182, 212, 0.3);
|
|
127
|
+
border-radius: 0.75rem;
|
|
128
|
+
padding: 1rem;
|
|
129
|
+
margin: 1rem 0;
|
|
130
|
+
font-size: 0.9rem;
|
|
131
|
+
color: #67e8f9;
|
|
132
|
+
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Roboto Mono', monospace;
|
|
133
|
+
text-shadow: 0 0 10px rgba(103, 232, 249, 0.3);
|
|
134
|
+
}}
|
|
135
|
+
|
|
136
|
+
.server-info strong {{
|
|
137
|
+
color: #22d3ee;
|
|
138
|
+
font-weight: 700;
|
|
139
|
+
}}
|
|
140
|
+
|
|
141
|
+
.subtitle {{
|
|
142
|
+
font-size: 1rem;
|
|
143
|
+
color: #94a3b8;
|
|
144
|
+
margin-top: 1rem;
|
|
145
|
+
}}
|
|
146
|
+
|
|
147
|
+
.close-instruction {{
|
|
148
|
+
background: rgba(51, 65, 85, 0.8);
|
|
149
|
+
border: 1px solid rgba(71, 85, 105, 0.4);
|
|
150
|
+
border-radius: 0.75rem;
|
|
151
|
+
padding: 1rem;
|
|
152
|
+
margin-top: 1.5rem;
|
|
153
|
+
font-size: 0.9rem;
|
|
154
|
+
color: #cbd5e1;
|
|
155
|
+
font-family: 'SF Mono', 'Monaco', 'Consolas', 'Roboto Mono', monospace;
|
|
156
|
+
}}
|
|
157
|
+
|
|
158
|
+
@keyframes glow {{
|
|
159
|
+
0%, 100% {{ opacity: 1; }}
|
|
160
|
+
50% {{ opacity: 0.7; }}
|
|
161
|
+
}}
|
|
162
|
+
|
|
163
|
+
.status-icon {{
|
|
164
|
+
animation: glow 2s ease-in-out infinite;
|
|
165
|
+
}}
|
|
166
|
+
</style>
|
|
167
|
+
</head>
|
|
168
|
+
<body>
|
|
169
|
+
<div class="container">
|
|
170
|
+
<span class="status-icon">{status_emoji}</span>
|
|
171
|
+
<div class="message">{message}</div>
|
|
172
|
+
{server_info}
|
|
173
|
+
<div class="close-instruction">
|
|
174
|
+
You can safely close this tab now.
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</body>
|
|
178
|
+
</html>
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class CallbackResponse:
|
|
184
|
+
code: str | None = None
|
|
185
|
+
state: str | None = None
|
|
186
|
+
error: str | None = None
|
|
187
|
+
error_description: str | None = None
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def from_dict(cls, data: dict[str, str]) -> CallbackResponse:
|
|
191
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
|
|
192
|
+
|
|
193
|
+
def to_dict(self) -> dict[str, str]:
|
|
194
|
+
return {k: v for k, v in self.__dict__.items() if v is not None}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def create_oauth_callback_server(
|
|
198
|
+
port: int,
|
|
199
|
+
callback_path: str = "/callback",
|
|
200
|
+
server_url: str | None = None,
|
|
201
|
+
response_future: asyncio.Future | None = None,
|
|
202
|
+
) -> Server:
|
|
203
|
+
"""
|
|
204
|
+
Create an OAuth callback server.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
port: The port to run the server on
|
|
208
|
+
callback_path: The path to listen for OAuth redirects on
|
|
209
|
+
server_url: Optional server URL to display in success messages
|
|
210
|
+
response_future: Optional future to resolve when OAuth callback is received
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Configured uvicorn Server instance (not yet running)
|
|
214
|
+
"""
|
|
215
|
+
|
|
216
|
+
async def callback_handler(request: Request):
|
|
217
|
+
"""Handle OAuth callback requests with proper HTML responses."""
|
|
218
|
+
query_params = dict(request.query_params)
|
|
219
|
+
callback_response = CallbackResponse.from_dict(query_params)
|
|
220
|
+
|
|
221
|
+
if callback_response.error:
|
|
222
|
+
error_desc = callback_response.error_description or "Unknown error"
|
|
223
|
+
|
|
224
|
+
# Resolve future with exception if provided
|
|
225
|
+
if response_future and not response_future.done():
|
|
226
|
+
response_future.set_exception(
|
|
227
|
+
RuntimeError(
|
|
228
|
+
f"OAuth error: {callback_response.error} - {error_desc}"
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return HTMLResponse(
|
|
233
|
+
create_callback_html(
|
|
234
|
+
f"FastMCP OAuth Error: {callback_response.error}<br>{error_desc}",
|
|
235
|
+
is_success=False,
|
|
236
|
+
),
|
|
237
|
+
status_code=400,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if not callback_response.code:
|
|
241
|
+
# Resolve future with exception if provided
|
|
242
|
+
if response_future and not response_future.done():
|
|
243
|
+
response_future.set_exception(
|
|
244
|
+
RuntimeError("OAuth callback missing authorization code")
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return HTMLResponse(
|
|
248
|
+
create_callback_html(
|
|
249
|
+
"FastMCP OAuth Error: No authorization code received",
|
|
250
|
+
is_success=False,
|
|
251
|
+
),
|
|
252
|
+
status_code=400,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Success case
|
|
256
|
+
if response_future and not response_future.done():
|
|
257
|
+
response_future.set_result(
|
|
258
|
+
(callback_response.code, callback_response.state)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return HTMLResponse(
|
|
262
|
+
create_callback_html("FastMCP OAuth login complete!", server_url=server_url)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
app = Starlette(routes=[Route(callback_path, callback_handler)])
|
|
266
|
+
|
|
267
|
+
return Server(
|
|
268
|
+
Config(
|
|
269
|
+
app=app,
|
|
270
|
+
host="127.0.0.1",
|
|
271
|
+
port=port,
|
|
272
|
+
lifespan="off",
|
|
273
|
+
log_level="warning",
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
"""Run a test server when executed directly."""
|
|
280
|
+
import webbrowser
|
|
281
|
+
|
|
282
|
+
import uvicorn
|
|
283
|
+
|
|
284
|
+
port = find_available_port()
|
|
285
|
+
print("🎭 OAuth Callback Test Server")
|
|
286
|
+
print("📍 Test URLs:")
|
|
287
|
+
print(f" Success: http://localhost:{port}/callback?code=test123&state=xyz")
|
|
288
|
+
print(
|
|
289
|
+
f" Error: http://localhost:{port}/callback?error=access_denied&error_description=User%20denied"
|
|
290
|
+
)
|
|
291
|
+
print(f" Missing: http://localhost:{port}/callback")
|
|
292
|
+
print("🛑 Press Ctrl+C to stop")
|
|
293
|
+
print()
|
|
294
|
+
|
|
295
|
+
# Create test server without future (just for testing HTML responses)
|
|
296
|
+
server = create_oauth_callback_server(
|
|
297
|
+
port=port, server_url="https://fastmcp-test-server.example.com"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Open browser to success example
|
|
301
|
+
webbrowser.open(f"http://localhost:{port}/callback?code=test123&state=xyz")
|
|
302
|
+
|
|
303
|
+
# Run with uvicorn directly
|
|
304
|
+
uvicorn.run(
|
|
305
|
+
server.config.app,
|
|
306
|
+
host="127.0.0.1",
|
|
307
|
+
port=port,
|
|
308
|
+
log_level="warning",
|
|
309
|
+
access_log=False,
|
|
310
|
+
)
|
fastmcp/client/transports.py
CHANGED
|
@@ -6,10 +6,20 @@ import os
|
|
|
6
6
|
import shutil
|
|
7
7
|
import sys
|
|
8
8
|
import warnings
|
|
9
|
-
from collections.abc import AsyncIterator
|
|
9
|
+
from collections.abc import AsyncIterator, Callable
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
Literal,
|
|
15
|
+
TypedDict,
|
|
16
|
+
TypeVar,
|
|
17
|
+
cast,
|
|
18
|
+
overload,
|
|
19
|
+
)
|
|
12
20
|
|
|
21
|
+
import anyio
|
|
22
|
+
import httpx
|
|
13
23
|
from mcp import ClientSession, StdioServerParameters
|
|
14
24
|
from mcp.client.session import (
|
|
15
25
|
ListRootsFnT,
|
|
@@ -26,7 +36,7 @@ from mcp.shared.memory import create_connected_server_and_client_session
|
|
|
26
36
|
from pydantic import AnyUrl
|
|
27
37
|
from typing_extensions import Unpack
|
|
28
38
|
|
|
29
|
-
from fastmcp.
|
|
39
|
+
from fastmcp.client.auth.oauth import OAuth
|
|
30
40
|
from fastmcp.server.dependencies import get_http_headers
|
|
31
41
|
from fastmcp.server.server import FastMCP
|
|
32
42
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -40,6 +50,20 @@ logger = get_logger(__name__)
|
|
|
40
50
|
# TypeVar for preserving specific ClientTransport subclass types
|
|
41
51
|
ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
|
|
42
52
|
|
|
53
|
+
__all__ = [
|
|
54
|
+
"ClientTransport",
|
|
55
|
+
"SSETransport",
|
|
56
|
+
"StreamableHttpTransport",
|
|
57
|
+
"StdioTransport",
|
|
58
|
+
"PythonStdioTransport",
|
|
59
|
+
"FastMCPStdioTransport",
|
|
60
|
+
"NodeStdioTransport",
|
|
61
|
+
"UvxStdioTransport",
|
|
62
|
+
"NpxStdioTransport",
|
|
63
|
+
"FastMCPTransport",
|
|
64
|
+
"infer_transport",
|
|
65
|
+
]
|
|
66
|
+
|
|
43
67
|
|
|
44
68
|
class SessionKwargs(TypedDict, total=False):
|
|
45
69
|
"""Keyword arguments for the MCP ClientSession constructor."""
|
|
@@ -92,6 +116,10 @@ class ClientTransport(abc.ABC):
|
|
|
92
116
|
"""Close the transport."""
|
|
93
117
|
pass
|
|
94
118
|
|
|
119
|
+
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
120
|
+
if auth is not None:
|
|
121
|
+
raise ValueError("This transport does not support auth")
|
|
122
|
+
|
|
95
123
|
|
|
96
124
|
class WSTransport(ClientTransport):
|
|
97
125
|
"""Transport implementation that connects to an MCP server via WebSockets."""
|
|
@@ -131,7 +159,9 @@ class SSETransport(ClientTransport):
|
|
|
131
159
|
self,
|
|
132
160
|
url: str | AnyUrl,
|
|
133
161
|
headers: dict[str, str] | None = None,
|
|
162
|
+
auth: httpx.Auth | Literal["oauth"] | str | None = None,
|
|
134
163
|
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
|
164
|
+
httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
|
|
135
165
|
):
|
|
136
166
|
if isinstance(url, AnyUrl):
|
|
137
167
|
url = str(url)
|
|
@@ -139,11 +169,21 @@ class SSETransport(ClientTransport):
|
|
|
139
169
|
raise ValueError("Invalid HTTP/S URL provided for SSE.")
|
|
140
170
|
self.url = url
|
|
141
171
|
self.headers = headers or {}
|
|
172
|
+
self._set_auth(auth)
|
|
173
|
+
self.httpx_client_factory = httpx_client_factory
|
|
142
174
|
|
|
143
175
|
if isinstance(sse_read_timeout, int | float):
|
|
144
176
|
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
|
145
177
|
self.sse_read_timeout = sse_read_timeout
|
|
146
178
|
|
|
179
|
+
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
180
|
+
if auth == "oauth":
|
|
181
|
+
auth = OAuth(self.url)
|
|
182
|
+
elif isinstance(auth, str):
|
|
183
|
+
self.headers["Authorization"] = auth
|
|
184
|
+
auth = None
|
|
185
|
+
self.auth = auth
|
|
186
|
+
|
|
147
187
|
@contextlib.asynccontextmanager
|
|
148
188
|
async def connect_session(
|
|
149
189
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
@@ -165,7 +205,10 @@ class SSETransport(ClientTransport):
|
|
|
165
205
|
)
|
|
166
206
|
client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
|
|
167
207
|
|
|
168
|
-
|
|
208
|
+
if self.httpx_client_factory is not None:
|
|
209
|
+
client_kwargs["httpx_client_factory"] = self.httpx_client_factory
|
|
210
|
+
|
|
211
|
+
async with sse_client(self.url, auth=self.auth, **client_kwargs) as transport:
|
|
169
212
|
read_stream, write_stream = transport
|
|
170
213
|
async with ClientSession(
|
|
171
214
|
read_stream, write_stream, **session_kwargs
|
|
@@ -183,7 +226,9 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
183
226
|
self,
|
|
184
227
|
url: str | AnyUrl,
|
|
185
228
|
headers: dict[str, str] | None = None,
|
|
229
|
+
auth: httpx.Auth | Literal["oauth"] | str | None = None,
|
|
186
230
|
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
|
231
|
+
httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
|
|
187
232
|
):
|
|
188
233
|
if isinstance(url, AnyUrl):
|
|
189
234
|
url = str(url)
|
|
@@ -191,11 +236,21 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
191
236
|
raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.")
|
|
192
237
|
self.url = url
|
|
193
238
|
self.headers = headers or {}
|
|
239
|
+
self._set_auth(auth)
|
|
240
|
+
self.httpx_client_factory = httpx_client_factory
|
|
194
241
|
|
|
195
242
|
if isinstance(sse_read_timeout, int | float):
|
|
196
243
|
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
|
197
244
|
self.sse_read_timeout = sse_read_timeout
|
|
198
245
|
|
|
246
|
+
def _set_auth(self, auth: httpx.Auth | Literal["oauth"] | str | None):
|
|
247
|
+
if auth == "oauth":
|
|
248
|
+
auth = OAuth(self.url)
|
|
249
|
+
elif isinstance(auth, str):
|
|
250
|
+
self.headers["Authorization"] = auth
|
|
251
|
+
auth = None
|
|
252
|
+
self.auth = auth
|
|
253
|
+
|
|
199
254
|
@contextlib.asynccontextmanager
|
|
200
255
|
async def connect_session(
|
|
201
256
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
@@ -214,7 +269,14 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
214
269
|
if session_kwargs.get("read_timeout_seconds", None) is not None:
|
|
215
270
|
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
|
|
216
271
|
|
|
217
|
-
|
|
272
|
+
if self.httpx_client_factory is not None:
|
|
273
|
+
client_kwargs["httpx_client_factory"] = self.httpx_client_factory
|
|
274
|
+
|
|
275
|
+
async with streamablehttp_client(
|
|
276
|
+
self.url,
|
|
277
|
+
auth=self.auth,
|
|
278
|
+
**client_kwargs,
|
|
279
|
+
) as transport:
|
|
218
280
|
read_stream, write_stream, _ = transport
|
|
219
281
|
async with ClientSession(
|
|
220
282
|
read_stream, write_stream, **session_kwargs
|
|
@@ -264,8 +326,8 @@ class StdioTransport(ClientTransport):
|
|
|
264
326
|
|
|
265
327
|
self._session: ClientSession | None = None
|
|
266
328
|
self._connect_task: asyncio.Task | None = None
|
|
267
|
-
self._ready_event =
|
|
268
|
-
self._stop_event =
|
|
329
|
+
self._ready_event = anyio.Event()
|
|
330
|
+
self._stop_event = anyio.Event()
|
|
269
331
|
|
|
270
332
|
@contextlib.asynccontextmanager
|
|
271
333
|
async def connect_session(
|
|
@@ -328,8 +390,8 @@ class StdioTransport(ClientTransport):
|
|
|
328
390
|
|
|
329
391
|
# reset variables and events for potential future reconnects
|
|
330
392
|
self._connect_task = None
|
|
331
|
-
self._stop_event =
|
|
332
|
-
self._ready_event =
|
|
393
|
+
self._stop_event = anyio.Event()
|
|
394
|
+
self._ready_event = anyio.Event()
|
|
333
395
|
|
|
334
396
|
async def close(self):
|
|
335
397
|
await self.disconnect()
|
|
@@ -592,7 +654,7 @@ class FastMCPTransport(ClientTransport):
|
|
|
592
654
|
tests or scenarios where client and server run in the same runtime.
|
|
593
655
|
"""
|
|
594
656
|
|
|
595
|
-
def __init__(self, mcp:
|
|
657
|
+
def __init__(self, mcp: FastMCP | FastMCP1Server):
|
|
596
658
|
"""Initialize a FastMCPTransport from a FastMCP server instance."""
|
|
597
659
|
|
|
598
660
|
# Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a
|
|
@@ -706,7 +768,7 @@ def infer_transport(transport: ClientTransportT) -> ClientTransportT: ...
|
|
|
706
768
|
|
|
707
769
|
|
|
708
770
|
@overload
|
|
709
|
-
def infer_transport(transport:
|
|
771
|
+
def infer_transport(transport: FastMCP) -> FastMCPTransport: ...
|
|
710
772
|
|
|
711
773
|
|
|
712
774
|
@overload
|
|
@@ -741,7 +803,7 @@ def infer_transport(transport: Path) -> PythonStdioTransport | NodeStdioTranspor
|
|
|
741
803
|
|
|
742
804
|
def infer_transport(
|
|
743
805
|
transport: ClientTransport
|
|
744
|
-
|
|
|
806
|
+
| FastMCP
|
|
745
807
|
| FastMCP1Server
|
|
746
808
|
| AnyUrl
|
|
747
809
|
| Path
|
|
@@ -758,7 +820,7 @@ def infer_transport(
|
|
|
758
820
|
|
|
759
821
|
The function supports these input types:
|
|
760
822
|
- ClientTransport: Used directly without modification
|
|
761
|
-
-
|
|
823
|
+
- FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport
|
|
762
824
|
- Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
|
|
763
825
|
- AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
|
|
764
826
|
- MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
|
|
@@ -796,7 +858,7 @@ def infer_transport(
|
|
|
796
858
|
return transport
|
|
797
859
|
|
|
798
860
|
# the transport is a FastMCP server (2.x or 1.0)
|
|
799
|
-
elif isinstance(transport,
|
|
861
|
+
elif isinstance(transport, FastMCP | FastMCP1Server):
|
|
800
862
|
inferred_transport = FastMCPTransport(mcp=transport)
|
|
801
863
|
|
|
802
864
|
# the transport is a path to a script
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from mcp.server.auth.provider import (
|
|
2
|
+
AccessToken,
|
|
3
|
+
AuthorizationCode,
|
|
4
|
+
OAuthAuthorizationServerProvider,
|
|
5
|
+
RefreshToken,
|
|
6
|
+
)
|
|
7
|
+
from mcp.server.auth.settings import (
|
|
8
|
+
ClientRegistrationOptions,
|
|
9
|
+
RevocationOptions,
|
|
10
|
+
)
|
|
11
|
+
from pydantic import AnyHttpUrl
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OAuthProvider(
|
|
15
|
+
OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]
|
|
16
|
+
):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
issuer_url: AnyHttpUrl | str,
|
|
20
|
+
service_documentation_url: AnyHttpUrl | str | None = None,
|
|
21
|
+
client_registration_options: ClientRegistrationOptions | None = None,
|
|
22
|
+
revocation_options: RevocationOptions | None = None,
|
|
23
|
+
required_scopes: list[str] | None = None,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Initialize the OAuth provider.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
issuer_url: The URL of the OAuth issuer.
|
|
30
|
+
service_documentation_url: The URL of the service documentation.
|
|
31
|
+
client_registration_options: The client registration options.
|
|
32
|
+
revocation_options: The revocation options.
|
|
33
|
+
required_scopes: Scopes that are required for all requests.
|
|
34
|
+
"""
|
|
35
|
+
super().__init__()
|
|
36
|
+
if isinstance(issuer_url, str):
|
|
37
|
+
issuer_url = AnyHttpUrl(issuer_url)
|
|
38
|
+
if isinstance(service_documentation_url, str):
|
|
39
|
+
service_documentation_url = AnyHttpUrl(service_documentation_url)
|
|
40
|
+
|
|
41
|
+
self.issuer_url = issuer_url
|
|
42
|
+
self.service_documentation_url = service_documentation_url
|
|
43
|
+
self.client_registration_options = client_registration_options
|
|
44
|
+
self.revocation_options = revocation_options
|
|
45
|
+
self.required_scopes = required_scopes
|