fastmcp 2.5.1__py3-none-any.whl → 2.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastmcp/cli/cli.py +1 -3
- 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 +394 -0
- fastmcp/client/client.py +154 -38
- fastmcp/client/oauth_callback.py +310 -0
- fastmcp/client/transports.py +249 -23
- fastmcp/resources/template.py +1 -1
- 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 +330 -0
- fastmcp/server/dependencies.py +27 -6
- fastmcp/server/http.py +38 -66
- fastmcp/server/openapi.py +2 -0
- fastmcp/server/server.py +64 -32
- fastmcp/settings.py +34 -8
- fastmcp/tools/tool.py +26 -5
- fastmcp/tools/tool_manager.py +2 -0
- fastmcp/utilities/http.py +8 -0
- fastmcp/utilities/tests.py +22 -10
- {fastmcp-2.5.1.dist-info → fastmcp-2.6.0.dist-info}/METADATA +9 -8
- {fastmcp-2.5.1.dist-info → fastmcp-2.6.0.dist-info}/RECORD +29 -22
- fastmcp/client/base.py +0 -0
- fastmcp/low_level/README.md +0 -1
- fastmcp/py.typed +0 -0
- /fastmcp/{low_level → server/auth/providers}/__init__.py +0 -0
- {fastmcp-2.5.1.dist-info → fastmcp-2.6.0.dist-info}/WHEEL +0 -0
- {fastmcp-2.5.1.dist-info → fastmcp-2.6.0.dist-info}/entry_points.txt +0 -0
- {fastmcp-2.5.1.dist-info → fastmcp-2.6.0.dist-info}/licenses/LICENSE +0 -0
fastmcp/client/transports.py
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import abc
|
|
2
|
+
import asyncio
|
|
2
3
|
import contextlib
|
|
3
4
|
import datetime
|
|
4
5
|
import os
|
|
5
6
|
import shutil
|
|
6
7
|
import sys
|
|
7
|
-
|
|
8
|
+
import warnings
|
|
9
|
+
from collections.abc import AsyncIterator, Callable
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
11
|
+
from typing import (
|
|
12
|
+
TYPE_CHECKING,
|
|
13
|
+
Any,
|
|
14
|
+
Literal,
|
|
15
|
+
TypedDict,
|
|
16
|
+
TypeVar,
|
|
17
|
+
cast,
|
|
18
|
+
overload,
|
|
19
|
+
)
|
|
10
20
|
|
|
21
|
+
import anyio
|
|
22
|
+
import httpx
|
|
11
23
|
from mcp import ClientSession, StdioServerParameters
|
|
12
24
|
from mcp.client.session import (
|
|
13
25
|
ListRootsFnT,
|
|
@@ -24,7 +36,7 @@ from mcp.shared.memory import create_connected_server_and_client_session
|
|
|
24
36
|
from pydantic import AnyUrl
|
|
25
37
|
from typing_extensions import Unpack
|
|
26
38
|
|
|
27
|
-
from fastmcp.
|
|
39
|
+
from fastmcp.client.auth.oauth import OAuth
|
|
28
40
|
from fastmcp.server.dependencies import get_http_headers
|
|
29
41
|
from fastmcp.server.server import FastMCP
|
|
30
42
|
from fastmcp.utilities.logging import get_logger
|
|
@@ -35,6 +47,23 @@ if TYPE_CHECKING:
|
|
|
35
47
|
|
|
36
48
|
logger = get_logger(__name__)
|
|
37
49
|
|
|
50
|
+
# TypeVar for preserving specific ClientTransport subclass types
|
|
51
|
+
ClientTransportT = TypeVar("ClientTransportT", bound="ClientTransport")
|
|
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
|
+
|
|
38
67
|
|
|
39
68
|
class SessionKwargs(TypedDict, total=False):
|
|
40
69
|
"""Keyword arguments for the MCP ClientSession constructor."""
|
|
@@ -83,11 +112,25 @@ class ClientTransport(abc.ABC):
|
|
|
83
112
|
# Basic representation for subclasses
|
|
84
113
|
return f"<{self.__class__.__name__}>"
|
|
85
114
|
|
|
115
|
+
async def close(self):
|
|
116
|
+
"""Close the transport."""
|
|
117
|
+
pass
|
|
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
|
+
|
|
86
123
|
|
|
87
124
|
class WSTransport(ClientTransport):
|
|
88
125
|
"""Transport implementation that connects to an MCP server via WebSockets."""
|
|
89
126
|
|
|
90
127
|
def __init__(self, url: str | AnyUrl):
|
|
128
|
+
# we never really used this transport, so it can be removed at any time
|
|
129
|
+
warnings.warn(
|
|
130
|
+
"WSTransport is a deprecated MCP transport and will be removed in a future version. Use StreamableHttpTransport instead.",
|
|
131
|
+
DeprecationWarning,
|
|
132
|
+
stacklevel=2,
|
|
133
|
+
)
|
|
91
134
|
if isinstance(url, AnyUrl):
|
|
92
135
|
url = str(url)
|
|
93
136
|
if not isinstance(url, str) or not url.startswith("ws"):
|
|
@@ -116,7 +159,9 @@ class SSETransport(ClientTransport):
|
|
|
116
159
|
self,
|
|
117
160
|
url: str | AnyUrl,
|
|
118
161
|
headers: dict[str, str] | None = None,
|
|
162
|
+
auth: httpx.Auth | Literal["oauth"] | str | None = None,
|
|
119
163
|
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
|
164
|
+
httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
|
|
120
165
|
):
|
|
121
166
|
if isinstance(url, AnyUrl):
|
|
122
167
|
url = str(url)
|
|
@@ -124,11 +169,21 @@ class SSETransport(ClientTransport):
|
|
|
124
169
|
raise ValueError("Invalid HTTP/S URL provided for SSE.")
|
|
125
170
|
self.url = url
|
|
126
171
|
self.headers = headers or {}
|
|
172
|
+
self._set_auth(auth)
|
|
173
|
+
self.httpx_client_factory = httpx_client_factory
|
|
127
174
|
|
|
128
175
|
if isinstance(sse_read_timeout, int | float):
|
|
129
176
|
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
|
130
177
|
self.sse_read_timeout = sse_read_timeout
|
|
131
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
|
+
|
|
132
187
|
@contextlib.asynccontextmanager
|
|
133
188
|
async def connect_session(
|
|
134
189
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
@@ -150,7 +205,10 @@ class SSETransport(ClientTransport):
|
|
|
150
205
|
)
|
|
151
206
|
client_kwargs["timeout"] = read_timeout_seconds.total_seconds()
|
|
152
207
|
|
|
153
|
-
|
|
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:
|
|
154
212
|
read_stream, write_stream = transport
|
|
155
213
|
async with ClientSession(
|
|
156
214
|
read_stream, write_stream, **session_kwargs
|
|
@@ -168,7 +226,9 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
168
226
|
self,
|
|
169
227
|
url: str | AnyUrl,
|
|
170
228
|
headers: dict[str, str] | None = None,
|
|
229
|
+
auth: httpx.Auth | Literal["oauth"] | str | None = None,
|
|
171
230
|
sse_read_timeout: datetime.timedelta | float | int | None = None,
|
|
231
|
+
httpx_client_factory: Callable[[], httpx.AsyncClient] | None = None,
|
|
172
232
|
):
|
|
173
233
|
if isinstance(url, AnyUrl):
|
|
174
234
|
url = str(url)
|
|
@@ -176,11 +236,21 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
176
236
|
raise ValueError("Invalid HTTP/S URL provided for Streamable HTTP.")
|
|
177
237
|
self.url = url
|
|
178
238
|
self.headers = headers or {}
|
|
239
|
+
self._set_auth(auth)
|
|
240
|
+
self.httpx_client_factory = httpx_client_factory
|
|
179
241
|
|
|
180
242
|
if isinstance(sse_read_timeout, int | float):
|
|
181
243
|
sse_read_timeout = datetime.timedelta(seconds=sse_read_timeout)
|
|
182
244
|
self.sse_read_timeout = sse_read_timeout
|
|
183
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
|
+
|
|
184
254
|
@contextlib.asynccontextmanager
|
|
185
255
|
async def connect_session(
|
|
186
256
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
@@ -199,7 +269,14 @@ class StreamableHttpTransport(ClientTransport):
|
|
|
199
269
|
if session_kwargs.get("read_timeout_seconds", None) is not None:
|
|
200
270
|
client_kwargs["timeout"] = session_kwargs.get("read_timeout_seconds")
|
|
201
271
|
|
|
202
|
-
|
|
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:
|
|
203
280
|
read_stream, write_stream, _ = transport
|
|
204
281
|
async with ClientSession(
|
|
205
282
|
read_stream, write_stream, **session_kwargs
|
|
@@ -224,6 +301,7 @@ class StdioTransport(ClientTransport):
|
|
|
224
301
|
args: list[str],
|
|
225
302
|
env: dict[str, str] | None = None,
|
|
226
303
|
cwd: str | None = None,
|
|
304
|
+
keep_alive: bool | None = None,
|
|
227
305
|
):
|
|
228
306
|
"""
|
|
229
307
|
Initialize a Stdio transport.
|
|
@@ -233,25 +311,90 @@ class StdioTransport(ClientTransport):
|
|
|
233
311
|
args: The arguments to pass to the command
|
|
234
312
|
env: Environment variables to set for the subprocess
|
|
235
313
|
cwd: Current working directory for the subprocess
|
|
314
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
315
|
+
Defaults to True. When True, the subprocess remains active
|
|
316
|
+
after the connection context exits, allowing reuse in
|
|
317
|
+
subsequent connections.
|
|
236
318
|
"""
|
|
237
319
|
self.command = command
|
|
238
320
|
self.args = args
|
|
239
321
|
self.env = env
|
|
240
322
|
self.cwd = cwd
|
|
323
|
+
if keep_alive is None:
|
|
324
|
+
keep_alive = True
|
|
325
|
+
self.keep_alive = keep_alive
|
|
326
|
+
|
|
327
|
+
self._session: ClientSession | None = None
|
|
328
|
+
self._connect_task: asyncio.Task | None = None
|
|
329
|
+
self._ready_event = anyio.Event()
|
|
330
|
+
self._stop_event = anyio.Event()
|
|
241
331
|
|
|
242
332
|
@contextlib.asynccontextmanager
|
|
243
333
|
async def connect_session(
|
|
244
334
|
self, **session_kwargs: Unpack[SessionKwargs]
|
|
245
335
|
) -> AsyncIterator[ClientSession]:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
336
|
+
try:
|
|
337
|
+
await self.connect(**session_kwargs)
|
|
338
|
+
assert self._session is not None
|
|
339
|
+
yield self._session
|
|
340
|
+
finally:
|
|
341
|
+
if not self.keep_alive:
|
|
342
|
+
await self.disconnect()
|
|
343
|
+
else:
|
|
344
|
+
logger.debug("Stdio transport has keep_alive=True, not disconnecting")
|
|
345
|
+
|
|
346
|
+
async def connect(
|
|
347
|
+
self, **session_kwargs: Unpack[SessionKwargs]
|
|
348
|
+
) -> ClientSession | None:
|
|
349
|
+
if self._connect_task is not None:
|
|
350
|
+
return
|
|
351
|
+
|
|
352
|
+
async def _connect_task():
|
|
353
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
354
|
+
try:
|
|
355
|
+
server_params = StdioServerParameters(
|
|
356
|
+
command=self.command, args=self.args, env=self.env, cwd=self.cwd
|
|
357
|
+
)
|
|
358
|
+
transport = await stack.enter_async_context(
|
|
359
|
+
stdio_client(server_params)
|
|
360
|
+
)
|
|
361
|
+
read_stream, write_stream = transport
|
|
362
|
+
self._session = await stack.enter_async_context(
|
|
363
|
+
ClientSession(read_stream, write_stream, **session_kwargs)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
logger.debug("Stdio transport connected")
|
|
367
|
+
self._ready_event.set()
|
|
368
|
+
|
|
369
|
+
# Wait until disconnect is requested (stop_event is set)
|
|
370
|
+
await self._stop_event.wait()
|
|
371
|
+
finally:
|
|
372
|
+
# Clean up client on exit
|
|
373
|
+
self._session = None
|
|
374
|
+
logger.debug("Stdio transport disconnected")
|
|
375
|
+
|
|
376
|
+
# start the connection task
|
|
377
|
+
self._connect_task = asyncio.create_task(_connect_task())
|
|
378
|
+
# wait for the client to be ready before returning
|
|
379
|
+
await self._ready_event.wait()
|
|
380
|
+
|
|
381
|
+
async def disconnect(self):
|
|
382
|
+
if self._connect_task is None:
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
# signal the connection task to stop
|
|
386
|
+
self._stop_event.set()
|
|
387
|
+
|
|
388
|
+
# wait for the connection task to finish cleanly
|
|
389
|
+
await self._connect_task
|
|
390
|
+
|
|
391
|
+
# reset variables and events for potential future reconnects
|
|
392
|
+
self._connect_task = None
|
|
393
|
+
self._stop_event = anyio.Event()
|
|
394
|
+
self._ready_event = anyio.Event()
|
|
395
|
+
|
|
396
|
+
async def close(self):
|
|
397
|
+
await self.disconnect()
|
|
255
398
|
|
|
256
399
|
def __repr__(self) -> str:
|
|
257
400
|
return (
|
|
@@ -269,6 +412,7 @@ class PythonStdioTransport(StdioTransport):
|
|
|
269
412
|
env: dict[str, str] | None = None,
|
|
270
413
|
cwd: str | None = None,
|
|
271
414
|
python_cmd: str = sys.executable,
|
|
415
|
+
keep_alive: bool | None = None,
|
|
272
416
|
):
|
|
273
417
|
"""
|
|
274
418
|
Initialize a Python transport.
|
|
@@ -279,6 +423,10 @@ class PythonStdioTransport(StdioTransport):
|
|
|
279
423
|
env: Environment variables to set for the subprocess
|
|
280
424
|
cwd: Current working directory for the subprocess
|
|
281
425
|
python_cmd: Python command to use (default: "python")
|
|
426
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
427
|
+
Defaults to True. When True, the subprocess remains active
|
|
428
|
+
after the connection context exits, allowing reuse in
|
|
429
|
+
subsequent connections.
|
|
282
430
|
"""
|
|
283
431
|
script_path = Path(script_path).resolve()
|
|
284
432
|
if not script_path.is_file():
|
|
@@ -290,7 +438,13 @@ class PythonStdioTransport(StdioTransport):
|
|
|
290
438
|
if args:
|
|
291
439
|
full_args.extend(args)
|
|
292
440
|
|
|
293
|
-
super().__init__(
|
|
441
|
+
super().__init__(
|
|
442
|
+
command=python_cmd,
|
|
443
|
+
args=full_args,
|
|
444
|
+
env=env,
|
|
445
|
+
cwd=cwd,
|
|
446
|
+
keep_alive=keep_alive,
|
|
447
|
+
)
|
|
294
448
|
self.script_path = script_path
|
|
295
449
|
|
|
296
450
|
|
|
@@ -303,6 +457,7 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
303
457
|
args: list[str] | None = None,
|
|
304
458
|
env: dict[str, str] | None = None,
|
|
305
459
|
cwd: str | None = None,
|
|
460
|
+
keep_alive: bool | None = None,
|
|
306
461
|
):
|
|
307
462
|
script_path = Path(script_path).resolve()
|
|
308
463
|
if not script_path.is_file():
|
|
@@ -311,7 +466,11 @@ class FastMCPStdioTransport(StdioTransport):
|
|
|
311
466
|
raise ValueError(f"Not a Python script: {script_path}")
|
|
312
467
|
|
|
313
468
|
super().__init__(
|
|
314
|
-
command="fastmcp",
|
|
469
|
+
command="fastmcp",
|
|
470
|
+
args=["run", str(script_path)],
|
|
471
|
+
env=env,
|
|
472
|
+
cwd=cwd,
|
|
473
|
+
keep_alive=keep_alive,
|
|
315
474
|
)
|
|
316
475
|
self.script_path = script_path
|
|
317
476
|
|
|
@@ -326,6 +485,7 @@ class NodeStdioTransport(StdioTransport):
|
|
|
326
485
|
env: dict[str, str] | None = None,
|
|
327
486
|
cwd: str | None = None,
|
|
328
487
|
node_cmd: str = "node",
|
|
488
|
+
keep_alive: bool | None = None,
|
|
329
489
|
):
|
|
330
490
|
"""
|
|
331
491
|
Initialize a Node transport.
|
|
@@ -336,6 +496,10 @@ class NodeStdioTransport(StdioTransport):
|
|
|
336
496
|
env: Environment variables to set for the subprocess
|
|
337
497
|
cwd: Current working directory for the subprocess
|
|
338
498
|
node_cmd: Node.js command to use (default: "node")
|
|
499
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
500
|
+
Defaults to True. When True, the subprocess remains active
|
|
501
|
+
after the connection context exits, allowing reuse in
|
|
502
|
+
subsequent connections.
|
|
339
503
|
"""
|
|
340
504
|
script_path = Path(script_path).resolve()
|
|
341
505
|
if not script_path.is_file():
|
|
@@ -347,7 +511,9 @@ class NodeStdioTransport(StdioTransport):
|
|
|
347
511
|
if args:
|
|
348
512
|
full_args.extend(args)
|
|
349
513
|
|
|
350
|
-
super().__init__(
|
|
514
|
+
super().__init__(
|
|
515
|
+
command=node_cmd, args=full_args, env=env, cwd=cwd, keep_alive=keep_alive
|
|
516
|
+
)
|
|
351
517
|
self.script_path = script_path
|
|
352
518
|
|
|
353
519
|
|
|
@@ -363,6 +529,7 @@ class UvxStdioTransport(StdioTransport):
|
|
|
363
529
|
with_packages: list[str] | None = None,
|
|
364
530
|
from_package: str | None = None,
|
|
365
531
|
env_vars: dict[str, str] | None = None,
|
|
532
|
+
keep_alive: bool | None = None,
|
|
366
533
|
):
|
|
367
534
|
"""
|
|
368
535
|
Initialize a Uvx transport.
|
|
@@ -375,6 +542,10 @@ class UvxStdioTransport(StdioTransport):
|
|
|
375
542
|
with_packages: Additional packages to include
|
|
376
543
|
from_package: Package to install the tool from
|
|
377
544
|
env_vars: Additional environment variables
|
|
545
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
546
|
+
Defaults to True. When True, the subprocess remains active
|
|
547
|
+
after the connection context exits, allowing reuse in
|
|
548
|
+
subsequent connections.
|
|
378
549
|
"""
|
|
379
550
|
# Basic validation
|
|
380
551
|
if project_directory and not Path(project_directory).exists():
|
|
@@ -402,7 +573,13 @@ class UvxStdioTransport(StdioTransport):
|
|
|
402
573
|
env = os.environ.copy()
|
|
403
574
|
env.update(env_vars)
|
|
404
575
|
|
|
405
|
-
super().__init__(
|
|
576
|
+
super().__init__(
|
|
577
|
+
command="uvx",
|
|
578
|
+
args=uvx_args,
|
|
579
|
+
env=env,
|
|
580
|
+
cwd=project_directory,
|
|
581
|
+
keep_alive=keep_alive,
|
|
582
|
+
)
|
|
406
583
|
self.tool_name = tool_name
|
|
407
584
|
|
|
408
585
|
|
|
@@ -416,6 +593,7 @@ class NpxStdioTransport(StdioTransport):
|
|
|
416
593
|
project_directory: str | None = None,
|
|
417
594
|
env_vars: dict[str, str] | None = None,
|
|
418
595
|
use_package_lock: bool = True,
|
|
596
|
+
keep_alive: bool | None = None,
|
|
419
597
|
):
|
|
420
598
|
"""
|
|
421
599
|
Initialize an Npx transport.
|
|
@@ -426,6 +604,10 @@ class NpxStdioTransport(StdioTransport):
|
|
|
426
604
|
project_directory: Project directory with package.json
|
|
427
605
|
env_vars: Additional environment variables
|
|
428
606
|
use_package_lock: Whether to use package-lock.json (--prefer-offline)
|
|
607
|
+
keep_alive: Whether to keep the subprocess alive between connections.
|
|
608
|
+
Defaults to True. When True, the subprocess remains active
|
|
609
|
+
after the connection context exits, allowing reuse in
|
|
610
|
+
subsequent connections.
|
|
429
611
|
"""
|
|
430
612
|
# verify npx is installed
|
|
431
613
|
if shutil.which("npx") is None:
|
|
@@ -453,7 +635,13 @@ class NpxStdioTransport(StdioTransport):
|
|
|
453
635
|
env = os.environ.copy()
|
|
454
636
|
env.update(env_vars)
|
|
455
637
|
|
|
456
|
-
super().__init__(
|
|
638
|
+
super().__init__(
|
|
639
|
+
command="npx",
|
|
640
|
+
args=npx_args,
|
|
641
|
+
env=env,
|
|
642
|
+
cwd=project_directory,
|
|
643
|
+
keep_alive=keep_alive,
|
|
644
|
+
)
|
|
457
645
|
self.package = package
|
|
458
646
|
|
|
459
647
|
|
|
@@ -466,7 +654,7 @@ class FastMCPTransport(ClientTransport):
|
|
|
466
654
|
tests or scenarios where client and server run in the same runtime.
|
|
467
655
|
"""
|
|
468
656
|
|
|
469
|
-
def __init__(self, mcp:
|
|
657
|
+
def __init__(self, mcp: FastMCP | FastMCP1Server):
|
|
470
658
|
"""Initialize a FastMCPTransport from a FastMCP server instance."""
|
|
471
659
|
|
|
472
660
|
# Accept both FastMCP 2.x and FastMCP 1.0 servers. Both expose a
|
|
@@ -575,9 +763,47 @@ class MCPConfigTransport(ClientTransport):
|
|
|
575
763
|
return f"<MCPConfig(config='{self.config}')>"
|
|
576
764
|
|
|
577
765
|
|
|
766
|
+
@overload
|
|
767
|
+
def infer_transport(transport: ClientTransportT) -> ClientTransportT: ...
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
@overload
|
|
771
|
+
def infer_transport(transport: FastMCP) -> FastMCPTransport: ...
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
@overload
|
|
775
|
+
def infer_transport(transport: FastMCP1Server) -> FastMCPTransport: ...
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
@overload
|
|
779
|
+
def infer_transport(transport: MCPConfig) -> MCPConfigTransport: ...
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
@overload
|
|
783
|
+
def infer_transport(transport: dict[str, Any]) -> MCPConfigTransport: ...
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
@overload
|
|
787
|
+
def infer_transport(
|
|
788
|
+
transport: AnyUrl,
|
|
789
|
+
) -> SSETransport | StreamableHttpTransport: ...
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
@overload
|
|
793
|
+
def infer_transport(
|
|
794
|
+
transport: str,
|
|
795
|
+
) -> (
|
|
796
|
+
PythonStdioTransport | NodeStdioTransport | SSETransport | StreamableHttpTransport
|
|
797
|
+
): ...
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
@overload
|
|
801
|
+
def infer_transport(transport: Path) -> PythonStdioTransport | NodeStdioTransport: ...
|
|
802
|
+
|
|
803
|
+
|
|
578
804
|
def infer_transport(
|
|
579
805
|
transport: ClientTransport
|
|
580
|
-
|
|
|
806
|
+
| FastMCP
|
|
581
807
|
| FastMCP1Server
|
|
582
808
|
| AnyUrl
|
|
583
809
|
| Path
|
|
@@ -594,7 +820,7 @@ def infer_transport(
|
|
|
594
820
|
|
|
595
821
|
The function supports these input types:
|
|
596
822
|
- ClientTransport: Used directly without modification
|
|
597
|
-
-
|
|
823
|
+
- FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport
|
|
598
824
|
- Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
|
|
599
825
|
- AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
|
|
600
826
|
- MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
|
|
@@ -632,7 +858,7 @@ def infer_transport(
|
|
|
632
858
|
return transport
|
|
633
859
|
|
|
634
860
|
# the transport is a FastMCP server (2.x or 1.0)
|
|
635
|
-
elif isinstance(transport,
|
|
861
|
+
elif isinstance(transport, FastMCP | FastMCP1Server):
|
|
636
862
|
inferred_transport = FastMCPTransport(mcp=transport)
|
|
637
863
|
|
|
638
864
|
# the transport is a path to a script
|
fastmcp/resources/template.py
CHANGED
|
@@ -148,7 +148,7 @@ class ResourceTemplate(BaseModel):
|
|
|
148
148
|
f"URI parameters {uri_params} must be a subset of the function arguments: {func_params}"
|
|
149
149
|
)
|
|
150
150
|
|
|
151
|
-
description = description or fn.__doc__
|
|
151
|
+
description = description or fn.__doc__
|
|
152
152
|
|
|
153
153
|
if not inspect.isroutine(fn):
|
|
154
154
|
fn = fn.__call__
|
|
@@ -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
|