fastmcp 1.0__py3-none-any.whl → 2.1.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/__init__.py +15 -4
- fastmcp/cli/__init__.py +0 -1
- fastmcp/cli/claude.py +13 -11
- fastmcp/cli/cli.py +59 -39
- fastmcp/client/__init__.py +25 -0
- fastmcp/client/base.py +1 -0
- fastmcp/client/client.py +226 -0
- fastmcp/client/roots.py +75 -0
- fastmcp/client/sampling.py +50 -0
- fastmcp/client/transports.py +411 -0
- fastmcp/prompts/__init__.py +2 -2
- fastmcp/prompts/{base.py → prompt.py} +47 -26
- fastmcp/prompts/prompt_manager.py +69 -15
- fastmcp/resources/__init__.py +6 -6
- fastmcp/resources/{base.py → resource.py} +21 -2
- fastmcp/resources/resource_manager.py +116 -17
- fastmcp/resources/{templates.py → template.py} +36 -11
- fastmcp/resources/types.py +18 -13
- fastmcp/server/__init__.py +5 -0
- fastmcp/server/context.py +222 -0
- fastmcp/server/openapi.py +637 -0
- fastmcp/server/proxy.py +223 -0
- fastmcp/{server.py → server/server.py} +323 -267
- fastmcp/settings.py +81 -0
- fastmcp/tools/__init__.py +1 -1
- fastmcp/tools/{base.py → tool.py} +47 -18
- fastmcp/tools/tool_manager.py +57 -16
- fastmcp/utilities/func_metadata.py +33 -19
- fastmcp/utilities/openapi.py +797 -0
- fastmcp/utilities/types.py +15 -4
- fastmcp-2.1.0.dist-info/METADATA +770 -0
- fastmcp-2.1.0.dist-info/RECORD +39 -0
- fastmcp-2.1.0.dist-info/licenses/LICENSE +201 -0
- fastmcp/prompts/manager.py +0 -50
- fastmcp-1.0.dist-info/METADATA +0 -604
- fastmcp-1.0.dist-info/RECORD +0 -28
- fastmcp-1.0.dist-info/licenses/LICENSE +0 -21
- {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/WHEEL +0 -0
- {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/entry_points.txt +0 -0
fastmcp/__init__.py
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
|
-
"""FastMCP -
|
|
1
|
+
"""FastMCP - An ergonomic MCP interface."""
|
|
2
2
|
|
|
3
3
|
from importlib.metadata import version
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from fastmcp.server.server import FastMCP
|
|
7
|
+
from fastmcp.server.context import Context
|
|
8
|
+
from fastmcp.client import Client
|
|
9
|
+
from fastmcp.utilities.types import Image
|
|
10
|
+
from . import client, settings
|
|
6
11
|
|
|
7
12
|
__version__ = version("fastmcp")
|
|
8
|
-
__all__ = [
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FastMCP",
|
|
15
|
+
"Context",
|
|
16
|
+
"client",
|
|
17
|
+
"settings",
|
|
18
|
+
"Image",
|
|
19
|
+
]
|
fastmcp/cli/__init__.py
CHANGED
fastmcp/cli/claude.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""Claude app integration utilities."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
|
-
from
|
|
9
|
+
from fastmcp.utilities.logging import get_logger
|
|
9
10
|
|
|
10
11
|
logger = get_logger(__name__)
|
|
11
12
|
|
|
@@ -16,6 +17,10 @@ def get_claude_config_path() -> Path | None:
|
|
|
16
17
|
path = Path(Path.home(), "AppData", "Roaming", "Claude")
|
|
17
18
|
elif sys.platform == "darwin":
|
|
18
19
|
path = Path(Path.home(), "Library", "Application Support", "Claude")
|
|
20
|
+
elif sys.platform.startswith("linux"):
|
|
21
|
+
path = Path(
|
|
22
|
+
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude"
|
|
23
|
+
)
|
|
19
24
|
else:
|
|
20
25
|
return None
|
|
21
26
|
|
|
@@ -28,9 +33,9 @@ def update_claude_config(
|
|
|
28
33
|
file_spec: str,
|
|
29
34
|
server_name: str,
|
|
30
35
|
*,
|
|
31
|
-
with_editable:
|
|
32
|
-
with_packages:
|
|
33
|
-
env_vars:
|
|
36
|
+
with_editable: Path | None = None,
|
|
37
|
+
with_packages: list[str] | None = None,
|
|
38
|
+
env_vars: dict[str, str] | None = None,
|
|
34
39
|
) -> bool:
|
|
35
40
|
"""Add or update a FastMCP server in Claude's configuration.
|
|
36
41
|
|
|
@@ -49,8 +54,8 @@ def update_claude_config(
|
|
|
49
54
|
config_dir = get_claude_config_path()
|
|
50
55
|
if not config_dir:
|
|
51
56
|
raise RuntimeError(
|
|
52
|
-
"Claude Desktop config directory not found. Please ensure Claude Desktop
|
|
53
|
-
"is installed and has been run at least once to initialize its
|
|
57
|
+
"Claude Desktop config directory not found. Please ensure Claude Desktop"
|
|
58
|
+
" is installed and has been run at least once to initialize its config."
|
|
54
59
|
)
|
|
55
60
|
|
|
56
61
|
config_file = config_dir / "claude_desktop_config.json"
|
|
@@ -110,10 +115,7 @@ def update_claude_config(
|
|
|
110
115
|
# Add fastmcp run command
|
|
111
116
|
args.extend(["fastmcp", "run", file_spec])
|
|
112
117
|
|
|
113
|
-
server_config = {
|
|
114
|
-
"command": "uv",
|
|
115
|
-
"args": args,
|
|
116
|
-
}
|
|
118
|
+
server_config: dict[str, Any] = {"command": "uv", "args": args}
|
|
117
119
|
|
|
118
120
|
# Add environment variables if specified
|
|
119
121
|
if env_vars:
|
fastmcp/cli/cli.py
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""FastmMCP CLI tools."""
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
import importlib.util
|
|
5
5
|
import os
|
|
6
|
+
import platform
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
10
|
+
from typing import Annotated
|
|
10
11
|
|
|
11
12
|
import dotenv
|
|
12
13
|
import typer
|
|
13
|
-
from
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from typer import Context, Exit
|
|
14
17
|
|
|
18
|
+
import fastmcp
|
|
15
19
|
from fastmcp.cli import claude
|
|
16
20
|
from fastmcp.utilities.logging import get_logger
|
|
17
21
|
|
|
18
22
|
logger = get_logger("cli")
|
|
23
|
+
console = Console()
|
|
19
24
|
|
|
20
25
|
app = typer.Typer(
|
|
21
26
|
name="fastmcp",
|
|
22
|
-
help="FastMCP
|
|
27
|
+
help="FastMCP CLI",
|
|
23
28
|
add_completion=False,
|
|
24
29
|
no_args_is_help=True, # Show help if no args provided
|
|
25
30
|
)
|
|
@@ -41,7 +46,7 @@ def _get_npx_command():
|
|
|
41
46
|
return "npx" # On Unix-like systems, just use npx
|
|
42
47
|
|
|
43
48
|
|
|
44
|
-
def _parse_env_var(env_var: str) ->
|
|
49
|
+
def _parse_env_var(env_var: str) -> tuple[str, str]:
|
|
45
50
|
"""Parse environment variable string in format KEY=VALUE."""
|
|
46
51
|
if "=" not in env_var:
|
|
47
52
|
logger.error(
|
|
@@ -54,10 +59,10 @@ def _parse_env_var(env_var: str) -> Tuple[str, str]:
|
|
|
54
59
|
|
|
55
60
|
def _build_uv_command(
|
|
56
61
|
file_spec: str,
|
|
57
|
-
with_editable:
|
|
58
|
-
with_packages:
|
|
62
|
+
with_editable: Path | None = None,
|
|
63
|
+
with_packages: list[str] | None = None,
|
|
59
64
|
) -> list[str]:
|
|
60
|
-
"""Build the uv run command that runs a
|
|
65
|
+
"""Build the uv run command that runs a MCP server through mcp run."""
|
|
61
66
|
cmd = ["uv"]
|
|
62
67
|
|
|
63
68
|
cmd.extend(["run", "--with", "fastmcp"])
|
|
@@ -70,12 +75,12 @@ def _build_uv_command(
|
|
|
70
75
|
if pkg:
|
|
71
76
|
cmd.extend(["--with", pkg])
|
|
72
77
|
|
|
73
|
-
# Add
|
|
78
|
+
# Add mcp run command
|
|
74
79
|
cmd.extend(["fastmcp", "run", file_spec])
|
|
75
80
|
return cmd
|
|
76
81
|
|
|
77
82
|
|
|
78
|
-
def _parse_file_path(file_spec: str) ->
|
|
83
|
+
def _parse_file_path(file_spec: str) -> tuple[Path, str | None]:
|
|
79
84
|
"""Parse a file path that may include a server object specification.
|
|
80
85
|
|
|
81
86
|
Args:
|
|
@@ -106,8 +111,8 @@ def _parse_file_path(file_spec: str) -> Tuple[Path, Optional[str]]:
|
|
|
106
111
|
return file_path, server_object
|
|
107
112
|
|
|
108
113
|
|
|
109
|
-
def _import_server(file: Path, server_object:
|
|
110
|
-
"""Import a
|
|
114
|
+
def _import_server(file: Path, server_object: str | None = None):
|
|
115
|
+
"""Import a MCP server from a file.
|
|
111
116
|
|
|
112
117
|
Args:
|
|
113
118
|
file: Path to the file
|
|
@@ -172,14 +177,26 @@ def _import_server(file: Path, server_object: Optional[str] = None):
|
|
|
172
177
|
|
|
173
178
|
|
|
174
179
|
@app.command()
|
|
175
|
-
def version()
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
def version(ctx: Context):
|
|
181
|
+
if ctx.resilient_parsing:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
info = {
|
|
185
|
+
"FastMCP version": fastmcp.__version__,
|
|
186
|
+
"MCP version": importlib.metadata.version("mcp"),
|
|
187
|
+
"Python version": platform.python_version(),
|
|
188
|
+
"Platform": platform.platform(),
|
|
189
|
+
"FastMCP root path": f"~/{Path(__file__).resolve().parents[3].relative_to(Path.home())}",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
g = Table.grid(padding=(0, 1))
|
|
193
|
+
g.add_column(style="bold", justify="left")
|
|
194
|
+
g.add_column(style="cyan", justify="right")
|
|
195
|
+
for k, v in info.items():
|
|
196
|
+
g.add_row(k + ":", str(v).replace("\n", " "))
|
|
197
|
+
console.print(g)
|
|
198
|
+
|
|
199
|
+
raise Exit()
|
|
183
200
|
|
|
184
201
|
|
|
185
202
|
@app.command()
|
|
@@ -189,7 +206,7 @@ def dev(
|
|
|
189
206
|
help="Python file to run, optionally with :object suffix",
|
|
190
207
|
),
|
|
191
208
|
with_editable: Annotated[
|
|
192
|
-
|
|
209
|
+
Path | None,
|
|
193
210
|
typer.Option(
|
|
194
211
|
"--with-editable",
|
|
195
212
|
"-e",
|
|
@@ -207,7 +224,7 @@ def dev(
|
|
|
207
224
|
),
|
|
208
225
|
] = [],
|
|
209
226
|
) -> None:
|
|
210
|
-
"""Run a
|
|
227
|
+
"""Run a MCP server with the MCP Inspector."""
|
|
211
228
|
file, server_object = _parse_file_path(file_spec)
|
|
212
229
|
|
|
213
230
|
logger.debug(
|
|
@@ -273,7 +290,7 @@ def run(
|
|
|
273
290
|
help="Python file to run, optionally with :object suffix",
|
|
274
291
|
),
|
|
275
292
|
transport: Annotated[
|
|
276
|
-
|
|
293
|
+
str | None,
|
|
277
294
|
typer.Option(
|
|
278
295
|
"--transport",
|
|
279
296
|
"-t",
|
|
@@ -281,16 +298,16 @@ def run(
|
|
|
281
298
|
),
|
|
282
299
|
] = None,
|
|
283
300
|
) -> None:
|
|
284
|
-
"""Run a
|
|
301
|
+
"""Run a MCP server.
|
|
285
302
|
|
|
286
|
-
The server can be specified in two ways
|
|
287
|
-
1. Module approach: server.py - runs the module directly, expecting a server.run() call
|
|
288
|
-
2. Import approach: server.py:app - imports and runs the specified server object
|
|
303
|
+
The server can be specified in two ways:\n
|
|
304
|
+
1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n
|
|
305
|
+
2. Import approach: server.py:app - imports and runs the specified server object.\n\n
|
|
289
306
|
|
|
290
307
|
Note: This command runs the server directly. You are responsible for ensuring
|
|
291
|
-
all dependencies are available
|
|
292
|
-
or
|
|
293
|
-
"""
|
|
308
|
+
all dependencies are available.\n
|
|
309
|
+
For dependency management, use `mcp install` or `mcp dev` instead.
|
|
310
|
+
""" # noqa: E501
|
|
294
311
|
file, server_object = _parse_file_path(file_spec)
|
|
295
312
|
|
|
296
313
|
logger.debug(
|
|
@@ -331,15 +348,16 @@ def install(
|
|
|
331
348
|
help="Python file to run, optionally with :object suffix",
|
|
332
349
|
),
|
|
333
350
|
server_name: Annotated[
|
|
334
|
-
|
|
351
|
+
str | None,
|
|
335
352
|
typer.Option(
|
|
336
353
|
"--name",
|
|
337
354
|
"-n",
|
|
338
|
-
help="Custom name for the server (defaults to server's name attribute or
|
|
355
|
+
help="Custom name for the server (defaults to server's name attribute or"
|
|
356
|
+
" file name)",
|
|
339
357
|
),
|
|
340
358
|
] = None,
|
|
341
359
|
with_editable: Annotated[
|
|
342
|
-
|
|
360
|
+
Path | None,
|
|
343
361
|
typer.Option(
|
|
344
362
|
"--with-editable",
|
|
345
363
|
"-e",
|
|
@@ -360,12 +378,12 @@ def install(
|
|
|
360
378
|
list[str],
|
|
361
379
|
typer.Option(
|
|
362
380
|
"--env-var",
|
|
363
|
-
"-
|
|
381
|
+
"-v",
|
|
364
382
|
help="Environment variables in KEY=VALUE format",
|
|
365
383
|
),
|
|
366
384
|
] = [],
|
|
367
385
|
env_file: Annotated[
|
|
368
|
-
|
|
386
|
+
Path | None,
|
|
369
387
|
typer.Option(
|
|
370
388
|
"--env-file",
|
|
371
389
|
"-f",
|
|
@@ -377,7 +395,7 @@ def install(
|
|
|
377
395
|
),
|
|
378
396
|
] = None,
|
|
379
397
|
) -> None:
|
|
380
|
-
"""Install a
|
|
398
|
+
"""Install a MCP server in the Claude desktop app.
|
|
381
399
|
|
|
382
400
|
Environment variables are preserved once added and only updated if new values
|
|
383
401
|
are explicitly provided.
|
|
@@ -399,7 +417,8 @@ def install(
|
|
|
399
417
|
logger.error("Claude app not found")
|
|
400
418
|
sys.exit(1)
|
|
401
419
|
|
|
402
|
-
# Try to import server to get its name, but fall back to file name if dependencies
|
|
420
|
+
# Try to import server to get its name, but fall back to file name if dependencies
|
|
421
|
+
# missing
|
|
403
422
|
name = server_name
|
|
404
423
|
server = None
|
|
405
424
|
if not name:
|
|
@@ -408,7 +427,8 @@ def install(
|
|
|
408
427
|
name = server.name
|
|
409
428
|
except (ImportError, ModuleNotFoundError) as e:
|
|
410
429
|
logger.debug(
|
|
411
|
-
"Could not import server (likely missing dependencies), using file
|
|
430
|
+
"Could not import server (likely missing dependencies), using file"
|
|
431
|
+
" name",
|
|
412
432
|
extra={"error": str(e)},
|
|
413
433
|
)
|
|
414
434
|
name = file.stem
|
|
@@ -419,7 +439,7 @@ def install(
|
|
|
419
439
|
with_packages = list(set(with_packages + server_dependencies))
|
|
420
440
|
|
|
421
441
|
# Process environment variables if provided
|
|
422
|
-
env_dict:
|
|
442
|
+
env_dict: dict[str, str] | None = None
|
|
423
443
|
if env_file or env_vars:
|
|
424
444
|
env_dict = {}
|
|
425
445
|
# Load from .env file if specified
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .client import Client
|
|
2
|
+
from .transports import (
|
|
3
|
+
ClientTransport,
|
|
4
|
+
WSTransport,
|
|
5
|
+
SSETransport,
|
|
6
|
+
StdioTransport,
|
|
7
|
+
PythonStdioTransport,
|
|
8
|
+
NodeStdioTransport,
|
|
9
|
+
UvxStdioTransport,
|
|
10
|
+
NpxStdioTransport,
|
|
11
|
+
FastMCPTransport,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Client",
|
|
16
|
+
"ClientTransport",
|
|
17
|
+
"WSTransport",
|
|
18
|
+
"SSETransport",
|
|
19
|
+
"StdioTransport",
|
|
20
|
+
"PythonStdioTransport",
|
|
21
|
+
"NodeStdioTransport",
|
|
22
|
+
"UvxStdioTransport",
|
|
23
|
+
"NpxStdioTransport",
|
|
24
|
+
"FastMCPTransport",
|
|
25
|
+
]
|
fastmcp/client/base.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
fastmcp/client/client.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from contextlib import AbstractAsyncContextManager
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Literal, cast, overload
|
|
5
|
+
|
|
6
|
+
import mcp.types
|
|
7
|
+
from mcp import ClientSession
|
|
8
|
+
from mcp.client.session import (
|
|
9
|
+
LoggingFnT,
|
|
10
|
+
MessageHandlerFnT,
|
|
11
|
+
)
|
|
12
|
+
from pydantic import AnyUrl
|
|
13
|
+
|
|
14
|
+
from fastmcp.client.roots import (
|
|
15
|
+
RootsHandler,
|
|
16
|
+
RootsList,
|
|
17
|
+
create_roots_callback,
|
|
18
|
+
)
|
|
19
|
+
from fastmcp.client.sampling import SamplingHandler, create_sampling_callback
|
|
20
|
+
from fastmcp.server import FastMCP
|
|
21
|
+
|
|
22
|
+
from .transports import ClientTransport, SessionKwargs, infer_transport
|
|
23
|
+
|
|
24
|
+
__all__ = ["Client", "RootsHandler", "RootsList"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClientError(ValueError):
|
|
28
|
+
"""Base class for errors raised by the client."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Client:
|
|
32
|
+
"""
|
|
33
|
+
MCP client that delegates connection management to a Transport instance.
|
|
34
|
+
|
|
35
|
+
The Client class is primarily concerned with MCP protocol logic,
|
|
36
|
+
while the Transport handles connection establishment and management.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
transport: ClientTransport | FastMCP | AnyUrl | Path | str,
|
|
42
|
+
# Common args
|
|
43
|
+
roots: RootsList | RootsHandler | None = None,
|
|
44
|
+
sampling_handler: SamplingHandler | None = None,
|
|
45
|
+
log_handler: LoggingFnT | None = None,
|
|
46
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
47
|
+
read_timeout_seconds: datetime.timedelta | None = None,
|
|
48
|
+
):
|
|
49
|
+
self.transport = infer_transport(transport)
|
|
50
|
+
self._session: ClientSession | None = None
|
|
51
|
+
self._session_cm: AbstractAsyncContextManager[ClientSession] | None = None
|
|
52
|
+
|
|
53
|
+
self._session_kwargs: SessionKwargs = {
|
|
54
|
+
"sampling_callback": None,
|
|
55
|
+
"list_roots_callback": None,
|
|
56
|
+
"logging_callback": log_handler,
|
|
57
|
+
"message_handler": message_handler,
|
|
58
|
+
"read_timeout_seconds": read_timeout_seconds,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if roots is not None:
|
|
62
|
+
self.set_roots(roots)
|
|
63
|
+
|
|
64
|
+
if sampling_handler is not None:
|
|
65
|
+
self.set_sampling_callback(sampling_handler)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def session(self) -> ClientSession:
|
|
69
|
+
"""Get the current active session. Raises RuntimeError if not connected."""
|
|
70
|
+
if self._session is None:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"Client is not connected. Use 'async with client:' context manager first."
|
|
73
|
+
)
|
|
74
|
+
return self._session
|
|
75
|
+
|
|
76
|
+
def set_roots(self, roots: RootsList | RootsHandler) -> None:
|
|
77
|
+
"""Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
|
|
78
|
+
self._session_kwargs["list_roots_callback"] = create_roots_callback(roots)
|
|
79
|
+
|
|
80
|
+
def set_sampling_callback(self, sampling_callback: SamplingHandler) -> None:
|
|
81
|
+
"""Set the sampling callback for the client."""
|
|
82
|
+
self._session_kwargs["sampling_callback"] = create_sampling_callback(
|
|
83
|
+
sampling_callback
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def is_connected(self) -> bool:
|
|
87
|
+
"""Check if the client is currently connected."""
|
|
88
|
+
return self._session is not None
|
|
89
|
+
|
|
90
|
+
async def __aenter__(self):
|
|
91
|
+
if self.is_connected():
|
|
92
|
+
raise RuntimeError("Client is already connected in an async context.")
|
|
93
|
+
try:
|
|
94
|
+
self._session_cm = self.transport.connect_session(**self._session_kwargs)
|
|
95
|
+
self._session = await self._session_cm.__aenter__()
|
|
96
|
+
return self
|
|
97
|
+
except Exception as e:
|
|
98
|
+
# Ensure cleanup if __aenter__ fails partially
|
|
99
|
+
self._session = None
|
|
100
|
+
self._session_cm = None
|
|
101
|
+
raise ConnectionError(
|
|
102
|
+
f"Failed to connect using {self.transport}: {e}"
|
|
103
|
+
) from e
|
|
104
|
+
|
|
105
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
106
|
+
if self._session_cm:
|
|
107
|
+
await self._session_cm.__aexit__(exc_type, exc_val, exc_tb)
|
|
108
|
+
self._session = None
|
|
109
|
+
self._session_cm = None
|
|
110
|
+
|
|
111
|
+
# --- MCP Client Methods ---
|
|
112
|
+
async def ping(self) -> None:
|
|
113
|
+
"""Send a ping request."""
|
|
114
|
+
await self.session.send_ping()
|
|
115
|
+
|
|
116
|
+
async def progress(
|
|
117
|
+
self,
|
|
118
|
+
progress_token: str | int,
|
|
119
|
+
progress: float,
|
|
120
|
+
total: float | None = None,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Send a progress notification."""
|
|
123
|
+
await self.session.send_progress_notification(progress_token, progress, total)
|
|
124
|
+
|
|
125
|
+
async def set_logging_level(self, level: mcp.types.LoggingLevel) -> None:
|
|
126
|
+
"""Send a logging/setLevel request."""
|
|
127
|
+
await self.session.set_logging_level(level)
|
|
128
|
+
|
|
129
|
+
async def send_roots_list_changed(self) -> None:
|
|
130
|
+
"""Send a roots/list_changed notification."""
|
|
131
|
+
await self.session.send_roots_list_changed()
|
|
132
|
+
|
|
133
|
+
async def list_resources(self) -> list[mcp.types.Resource]:
|
|
134
|
+
"""Send a resources/list request."""
|
|
135
|
+
result = await self.session.list_resources()
|
|
136
|
+
return result.resources
|
|
137
|
+
|
|
138
|
+
async def list_resource_templates(self) -> list[mcp.types.ResourceTemplate]:
|
|
139
|
+
"""Send a resources/listResourceTemplates request."""
|
|
140
|
+
result = await self.session.list_resource_templates()
|
|
141
|
+
return result.resourceTemplates
|
|
142
|
+
|
|
143
|
+
async def read_resource(
|
|
144
|
+
self, uri: AnyUrl | str
|
|
145
|
+
) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]:
|
|
146
|
+
"""Send a resources/read request."""
|
|
147
|
+
if isinstance(uri, str):
|
|
148
|
+
uri = AnyUrl(uri) # Ensure AnyUrl
|
|
149
|
+
result = await self.session.read_resource(uri)
|
|
150
|
+
return result.contents
|
|
151
|
+
|
|
152
|
+
# async def subscribe_resource(self, uri: AnyUrl | str) -> None:
|
|
153
|
+
# """Send a resources/subscribe request."""
|
|
154
|
+
# if isinstance(uri, str):
|
|
155
|
+
# uri = AnyUrl(uri)
|
|
156
|
+
# await self.session.subscribe_resource(uri)
|
|
157
|
+
|
|
158
|
+
# async def unsubscribe_resource(self, uri: AnyUrl | str) -> None:
|
|
159
|
+
# """Send a resources/unsubscribe request."""
|
|
160
|
+
# if isinstance(uri, str):
|
|
161
|
+
# uri = AnyUrl(uri)
|
|
162
|
+
# await self.session.unsubscribe_resource(uri)
|
|
163
|
+
|
|
164
|
+
async def list_prompts(self) -> list[mcp.types.Prompt]:
|
|
165
|
+
"""Send a prompts/list request."""
|
|
166
|
+
result = await self.session.list_prompts()
|
|
167
|
+
return result.prompts
|
|
168
|
+
|
|
169
|
+
async def get_prompt(
|
|
170
|
+
self, name: str, arguments: dict[str, str] | None = None
|
|
171
|
+
) -> mcp.types.GetPromptResult:
|
|
172
|
+
"""Send a prompts/get request."""
|
|
173
|
+
result = await self.session.get_prompt(name, arguments)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
async def complete(
|
|
177
|
+
self,
|
|
178
|
+
ref: mcp.types.ResourceReference | mcp.types.PromptReference,
|
|
179
|
+
argument: dict[str, str],
|
|
180
|
+
) -> mcp.types.Completion:
|
|
181
|
+
"""Send a completion request."""
|
|
182
|
+
result = await self.session.complete(ref, argument)
|
|
183
|
+
return result.completion
|
|
184
|
+
|
|
185
|
+
async def list_tools(self) -> list[mcp.types.Tool]:
|
|
186
|
+
"""Send a tools/list request."""
|
|
187
|
+
result = await self.session.list_tools()
|
|
188
|
+
return result.tools
|
|
189
|
+
|
|
190
|
+
@overload
|
|
191
|
+
async def call_tool(
|
|
192
|
+
self,
|
|
193
|
+
name: str,
|
|
194
|
+
arguments: dict[str, Any] | None = None,
|
|
195
|
+
_return_raw_result: Literal[False] = False,
|
|
196
|
+
) -> list[
|
|
197
|
+
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
|
|
198
|
+
]: ...
|
|
199
|
+
|
|
200
|
+
@overload
|
|
201
|
+
async def call_tool(
|
|
202
|
+
self,
|
|
203
|
+
name: str,
|
|
204
|
+
arguments: dict[str, Any] | None = None,
|
|
205
|
+
_return_raw_result: Literal[True] = True,
|
|
206
|
+
) -> mcp.types.CallToolResult: ...
|
|
207
|
+
|
|
208
|
+
async def call_tool(
|
|
209
|
+
self,
|
|
210
|
+
name: str,
|
|
211
|
+
arguments: dict[str, Any] | None = None,
|
|
212
|
+
_return_raw_result: bool = False,
|
|
213
|
+
) -> (
|
|
214
|
+
list[
|
|
215
|
+
mcp.types.TextContent | mcp.types.ImageContent | mcp.types.EmbeddedResource
|
|
216
|
+
]
|
|
217
|
+
| mcp.types.CallToolResult
|
|
218
|
+
):
|
|
219
|
+
"""Send a tools/call request."""
|
|
220
|
+
result = await self.session.call_tool(name, arguments)
|
|
221
|
+
if _return_raw_result:
|
|
222
|
+
return result
|
|
223
|
+
elif result.isError:
|
|
224
|
+
msg = cast(mcp.types.TextContent, result.content[0]).text
|
|
225
|
+
raise ClientError(msg)
|
|
226
|
+
return result.content
|
fastmcp/client/roots.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Awaitable, Callable
|
|
3
|
+
from typing import TypeAlias
|
|
4
|
+
|
|
5
|
+
import mcp.types
|
|
6
|
+
import pydantic
|
|
7
|
+
from mcp import ClientSession
|
|
8
|
+
from mcp.client.session import ListRootsFnT
|
|
9
|
+
from mcp.shared.context import LifespanContextT, RequestContext
|
|
10
|
+
|
|
11
|
+
RootsList: TypeAlias = list[str] | list[mcp.types.Root] | list[str | mcp.types.Root]
|
|
12
|
+
|
|
13
|
+
RootsHandler: TypeAlias = (
|
|
14
|
+
Callable[[RequestContext[ClientSession, LifespanContextT]], RootsList]
|
|
15
|
+
| Callable[[RequestContext[ClientSession, LifespanContextT]], Awaitable[RootsList]]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def convert_roots_list(roots: RootsList) -> list[mcp.types.Root]:
|
|
20
|
+
roots_list = []
|
|
21
|
+
for r in roots:
|
|
22
|
+
if isinstance(r, mcp.types.Root):
|
|
23
|
+
roots_list.append(r)
|
|
24
|
+
elif isinstance(r, pydantic.FileUrl):
|
|
25
|
+
roots_list.append(mcp.types.Root(uri=r))
|
|
26
|
+
elif isinstance(r, str):
|
|
27
|
+
roots_list.append(mcp.types.Root(uri=pydantic.FileUrl(r)))
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError(f"Invalid root: {r}")
|
|
30
|
+
return roots_list
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create_roots_callback(
|
|
34
|
+
handler: RootsList | RootsHandler,
|
|
35
|
+
) -> ListRootsFnT:
|
|
36
|
+
if isinstance(handler, list):
|
|
37
|
+
return _create_roots_callback_from_roots(handler)
|
|
38
|
+
elif inspect.isfunction(handler):
|
|
39
|
+
return _create_roots_callback_from_fn(handler)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"Invalid roots handler: {handler}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _create_roots_callback_from_roots(
|
|
45
|
+
roots: RootsList,
|
|
46
|
+
) -> ListRootsFnT:
|
|
47
|
+
roots = convert_roots_list(roots)
|
|
48
|
+
|
|
49
|
+
async def _roots_callback(
|
|
50
|
+
context: RequestContext[ClientSession, LifespanContextT],
|
|
51
|
+
) -> mcp.types.ListRootsResult:
|
|
52
|
+
return mcp.types.ListRootsResult(roots=roots)
|
|
53
|
+
|
|
54
|
+
return _roots_callback
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _create_roots_callback_from_fn(
|
|
58
|
+
fn: Callable[[RequestContext[ClientSession, LifespanContextT]], RootsList]
|
|
59
|
+
| Callable[[RequestContext[ClientSession, LifespanContextT]], Awaitable[RootsList]],
|
|
60
|
+
) -> ListRootsFnT:
|
|
61
|
+
async def _roots_callback(
|
|
62
|
+
context: RequestContext[ClientSession, LifespanContextT],
|
|
63
|
+
) -> mcp.types.ListRootsResult | mcp.types.ErrorData:
|
|
64
|
+
try:
|
|
65
|
+
roots = fn(context)
|
|
66
|
+
if inspect.isawaitable(roots):
|
|
67
|
+
roots = await roots
|
|
68
|
+
return mcp.types.ListRootsResult(roots=convert_roots_list(roots))
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return mcp.types.ErrorData(
|
|
71
|
+
code=mcp.types.INTERNAL_ERROR,
|
|
72
|
+
message=str(e),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return _roots_callback
|