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.
Files changed (39) hide show
  1. fastmcp/__init__.py +15 -4
  2. fastmcp/cli/__init__.py +0 -1
  3. fastmcp/cli/claude.py +13 -11
  4. fastmcp/cli/cli.py +59 -39
  5. fastmcp/client/__init__.py +25 -0
  6. fastmcp/client/base.py +1 -0
  7. fastmcp/client/client.py +226 -0
  8. fastmcp/client/roots.py +75 -0
  9. fastmcp/client/sampling.py +50 -0
  10. fastmcp/client/transports.py +411 -0
  11. fastmcp/prompts/__init__.py +2 -2
  12. fastmcp/prompts/{base.py → prompt.py} +47 -26
  13. fastmcp/prompts/prompt_manager.py +69 -15
  14. fastmcp/resources/__init__.py +6 -6
  15. fastmcp/resources/{base.py → resource.py} +21 -2
  16. fastmcp/resources/resource_manager.py +116 -17
  17. fastmcp/resources/{templates.py → template.py} +36 -11
  18. fastmcp/resources/types.py +18 -13
  19. fastmcp/server/__init__.py +5 -0
  20. fastmcp/server/context.py +222 -0
  21. fastmcp/server/openapi.py +637 -0
  22. fastmcp/server/proxy.py +223 -0
  23. fastmcp/{server.py → server/server.py} +323 -267
  24. fastmcp/settings.py +81 -0
  25. fastmcp/tools/__init__.py +1 -1
  26. fastmcp/tools/{base.py → tool.py} +47 -18
  27. fastmcp/tools/tool_manager.py +57 -16
  28. fastmcp/utilities/func_metadata.py +33 -19
  29. fastmcp/utilities/openapi.py +797 -0
  30. fastmcp/utilities/types.py +15 -4
  31. fastmcp-2.1.0.dist-info/METADATA +770 -0
  32. fastmcp-2.1.0.dist-info/RECORD +39 -0
  33. fastmcp-2.1.0.dist-info/licenses/LICENSE +201 -0
  34. fastmcp/prompts/manager.py +0 -50
  35. fastmcp-1.0.dist-info/METADATA +0 -604
  36. fastmcp-1.0.dist-info/RECORD +0 -28
  37. fastmcp-1.0.dist-info/licenses/LICENSE +0 -21
  38. {fastmcp-1.0.dist-info → fastmcp-2.1.0.dist-info}/WHEEL +0 -0
  39. {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 - A more ergonomic interface for MCP servers."""
1
+ """FastMCP - An ergonomic MCP interface."""
2
2
 
3
3
  from importlib.metadata import version
4
- from .server import FastMCP, Context
5
- from .utilities.types import Image
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__ = ["FastMCP", "Context", "Image"]
13
+ __all__ = [
14
+ "FastMCP",
15
+ "Context",
16
+ "client",
17
+ "settings",
18
+ "Image",
19
+ ]
fastmcp/cli/__init__.py CHANGED
@@ -2,6 +2,5 @@
2
2
 
3
3
  from .cli import app
4
4
 
5
-
6
5
  if __name__ == "__main__":
7
6
  app()
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 Optional, Dict
7
+ from typing import Any
7
8
 
8
- from ..utilities.logging import get_logger
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: Optional[Path] = None,
32
- with_packages: Optional[list[str]] = None,
33
- env_vars: Optional[Dict[str, str]] = None,
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 configuration."
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
- """FastMCP CLI tools."""
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 Dict, Optional, Tuple
10
+ from typing import Annotated
10
11
 
11
12
  import dotenv
12
13
  import typer
13
- from typing_extensions import Annotated
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 development tools",
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) -> Tuple[str, 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: Optional[Path] = None,
58
- with_packages: Optional[list[str]] = None,
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 FastMCP server through fastmcp run."""
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 fastmcp run command
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) -> Tuple[Path, Optional[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: Optional[str] = None):
110
- """Import a FastMCP server from a file.
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() -> None:
176
- """Show the FastMCP version."""
177
- try:
178
- version = importlib.metadata.version("fastmcp")
179
- print(f"FastMCP version {version}")
180
- except importlib.metadata.PackageNotFoundError:
181
- print("FastMCP version unknown (package not installed)")
182
- sys.exit(1)
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
- Optional[Path],
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 FastMCP server with the MCP Inspector."""
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
- Optional[str],
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 FastMCP server.
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. For dependency management, use fastmcp install
292
- or fastmcp dev instead.
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
- Optional[str],
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 file name)",
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
- Optional[Path],
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
- "-e",
381
+ "-v",
364
382
  help="Environment variables in KEY=VALUE format",
365
383
  ),
366
384
  ] = [],
367
385
  env_file: Annotated[
368
- Optional[Path],
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 FastMCP server in the Claude desktop app.
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 missing
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 name",
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: Optional[Dict[str, str]] = None
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
+
@@ -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
@@ -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