fastmcp 0.4.1__py3-none-any.whl → 2.0.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.
@@ -0,0 +1,411 @@
1
+ import abc
2
+ import contextlib
3
+ import datetime
4
+ import os
5
+ from collections.abc import AsyncIterator
6
+ from pathlib import Path
7
+ from typing import (
8
+ TypedDict,
9
+ )
10
+
11
+ from mcp import ClientSession, StdioServerParameters
12
+ from mcp.client.session import (
13
+ ListRootsFnT,
14
+ LoggingFnT,
15
+ MessageHandlerFnT,
16
+ SamplingFnT,
17
+ )
18
+ from mcp.client.sse import sse_client
19
+ from mcp.client.stdio import stdio_client
20
+ from mcp.client.websocket import websocket_client
21
+ from mcp.shared.memory import create_connected_server_and_client_session
22
+ from pydantic import AnyUrl
23
+ from typing_extensions import Unpack
24
+
25
+ from fastmcp.server import FastMCP as FastMCPServer
26
+
27
+
28
+ class SessionKwargs(TypedDict, total=False):
29
+ """Keyword arguments for the MCP ClientSession constructor."""
30
+
31
+ sampling_callback: SamplingFnT | None
32
+ list_roots_callback: ListRootsFnT | None
33
+ logging_callback: LoggingFnT | None
34
+ message_handler: MessageHandlerFnT | None
35
+ read_timeout_seconds: datetime.timedelta | None
36
+
37
+
38
+ class ClientTransport(abc.ABC):
39
+ """
40
+ Abstract base class for different MCP client transport mechanisms.
41
+
42
+ A Transport is responsible for establishing and managing connections
43
+ to an MCP server, and providing a ClientSession within an async context.
44
+ """
45
+
46
+ @abc.abstractmethod
47
+ @contextlib.asynccontextmanager
48
+ async def connect_session(
49
+ self, **session_kwargs: Unpack[SessionKwargs]
50
+ ) -> AsyncIterator[ClientSession]:
51
+ """
52
+ Establishes a connection and yields an active, initialized ClientSession.
53
+
54
+ The session is guaranteed to be valid only within the scope of the
55
+ async context manager. Connection setup and teardown are handled
56
+ within this context.
57
+
58
+ Args:
59
+ **session_kwargs: Keyword arguments to pass to the ClientSession
60
+ constructor (e.g., callbacks, timeouts).
61
+
62
+ Yields:
63
+ An initialized mcp.ClientSession instance.
64
+ """
65
+ raise NotImplementedError
66
+ yield None # type: ignore
67
+
68
+ def __repr__(self) -> str:
69
+ # Basic representation for subclasses
70
+ return f"<{self.__class__.__name__}>"
71
+
72
+
73
+ class WSTransport(ClientTransport):
74
+ """Transport implementation that connects to an MCP server via WebSockets."""
75
+
76
+ def __init__(self, url: str | AnyUrl):
77
+ if isinstance(url, AnyUrl):
78
+ url = str(url)
79
+ if not isinstance(url, str) or not url.startswith("ws"):
80
+ raise ValueError("Invalid WebSocket URL provided.")
81
+ self.url = url
82
+
83
+ @contextlib.asynccontextmanager
84
+ async def connect_session(
85
+ self, **session_kwargs: Unpack[SessionKwargs]
86
+ ) -> AsyncIterator[ClientSession]:
87
+ async with websocket_client(self.url) as transport:
88
+ read_stream, write_stream = transport
89
+ async with ClientSession(
90
+ read_stream, write_stream, **session_kwargs
91
+ ) as session:
92
+ await session.initialize() # Initialize after session creation
93
+ yield session
94
+
95
+ def __repr__(self) -> str:
96
+ return f"<WebSocket(url='{self.url}')>"
97
+
98
+
99
+ class SSETransport(ClientTransport):
100
+ """Transport implementation that connects to an MCP server via Server-Sent Events."""
101
+
102
+ def __init__(self, url: str | AnyUrl, headers: dict[str, str] | None = None):
103
+ if isinstance(url, AnyUrl):
104
+ url = str(url)
105
+ if not isinstance(url, str) or not url.startswith("http"):
106
+ raise ValueError("Invalid HTTP/S URL provided for SSE.")
107
+ self.url = url
108
+ self.headers = headers or {}
109
+
110
+ @contextlib.asynccontextmanager
111
+ async def connect_session(
112
+ self, **session_kwargs: Unpack[SessionKwargs]
113
+ ) -> AsyncIterator[ClientSession]:
114
+ async with sse_client(self.url, headers=self.headers) as transport:
115
+ read_stream, write_stream = transport
116
+ async with ClientSession(
117
+ read_stream, write_stream, **session_kwargs
118
+ ) as session:
119
+ await session.initialize()
120
+ yield session
121
+
122
+ def __repr__(self) -> str:
123
+ return f"<SSE(url='{self.url}')>"
124
+
125
+
126
+ class StdioTransport(ClientTransport):
127
+ """
128
+ Base transport for connecting to an MCP server via subprocess with stdio.
129
+
130
+ This is a base class that can be subclassed for specific command-based
131
+ transports like Python, Node, Uvx, etc.
132
+ """
133
+
134
+ def __init__(
135
+ self,
136
+ command: str,
137
+ args: list[str],
138
+ env: dict[str, str] | None = None,
139
+ cwd: str | None = None,
140
+ ):
141
+ """
142
+ Initialize a Stdio transport.
143
+
144
+ Args:
145
+ command: The command to run (e.g., "python", "node", "uvx")
146
+ args: The arguments to pass to the command
147
+ env: Environment variables to set for the subprocess
148
+ cwd: Current working directory for the subprocess
149
+ """
150
+ self.command = command
151
+ self.args = args
152
+ self.env = env
153
+ self.cwd = cwd
154
+
155
+ @contextlib.asynccontextmanager
156
+ async def connect_session(
157
+ self, **session_kwargs: Unpack[SessionKwargs]
158
+ ) -> AsyncIterator[ClientSession]:
159
+ server_params = StdioServerParameters(
160
+ command=self.command, args=self.args, env=self.env, cwd=self.cwd
161
+ )
162
+ async with stdio_client(server_params) as transport:
163
+ read_stream, write_stream = transport
164
+ async with ClientSession(
165
+ read_stream, write_stream, **session_kwargs
166
+ ) as session:
167
+ await session.initialize()
168
+ yield session
169
+
170
+ def __repr__(self) -> str:
171
+ return (
172
+ f"<{self.__class__.__name__}(command='{self.command}', args={self.args})>"
173
+ )
174
+
175
+
176
+ class PythonStdioTransport(StdioTransport):
177
+ """Transport for running Python scripts."""
178
+
179
+ def __init__(
180
+ self,
181
+ script_path: str | Path,
182
+ args: list[str] | None = None,
183
+ env: dict[str, str] | None = None,
184
+ cwd: str | None = None,
185
+ python_cmd: str = "python",
186
+ ):
187
+ """
188
+ Initialize a Python transport.
189
+
190
+ Args:
191
+ script_path: Path to the Python script to run
192
+ args: Additional arguments to pass to the script
193
+ env: Environment variables to set for the subprocess
194
+ cwd: Current working directory for the subprocess
195
+ python_cmd: Python command to use (default: "python")
196
+ """
197
+ script_path = Path(script_path).resolve()
198
+ if not script_path.is_file():
199
+ raise FileNotFoundError(f"Script not found: {script_path}")
200
+ if not str(script_path).endswith(".py"):
201
+ raise ValueError(f"Not a Python script: {script_path}")
202
+
203
+ full_args = [str(script_path)]
204
+ if args:
205
+ full_args.extend(args)
206
+
207
+ super().__init__(command=python_cmd, args=full_args, env=env, cwd=cwd)
208
+ self.script_path = script_path
209
+
210
+
211
+ class NodeStdioTransport(StdioTransport):
212
+ """Transport for running Node.js scripts."""
213
+
214
+ def __init__(
215
+ self,
216
+ script_path: str | Path,
217
+ args: list[str] | None = None,
218
+ env: dict[str, str] | None = None,
219
+ cwd: str | None = None,
220
+ node_cmd: str = "node",
221
+ ):
222
+ """
223
+ Initialize a Node transport.
224
+
225
+ Args:
226
+ script_path: Path to the Node.js script to run
227
+ args: Additional arguments to pass to the script
228
+ env: Environment variables to set for the subprocess
229
+ cwd: Current working directory for the subprocess
230
+ node_cmd: Node.js command to use (default: "node")
231
+ """
232
+ script_path = Path(script_path).resolve()
233
+ if not script_path.is_file():
234
+ raise FileNotFoundError(f"Script not found: {script_path}")
235
+ if not str(script_path).endswith(".js"):
236
+ raise ValueError(f"Not a JavaScript script: {script_path}")
237
+
238
+ full_args = [str(script_path)]
239
+ if args:
240
+ full_args.extend(args)
241
+
242
+ super().__init__(command=node_cmd, args=full_args, env=env, cwd=cwd)
243
+ self.script_path = script_path
244
+
245
+
246
+ class UvxStdioTransport(StdioTransport):
247
+ """Transport for running commands via the uvx tool."""
248
+
249
+ def __init__(
250
+ self,
251
+ tool_name: str,
252
+ tool_args: list[str] | None = None,
253
+ project_directory: str | None = None,
254
+ python_version: str | None = None,
255
+ with_packages: list[str] | None = None,
256
+ from_package: str | None = None,
257
+ env_vars: dict[str, str] | None = None,
258
+ ):
259
+ """
260
+ Initialize a Uvx transport.
261
+
262
+ Args:
263
+ tool_name: Name of the tool to run via uvx
264
+ tool_args: Arguments to pass to the tool
265
+ project_directory: Project directory (for package resolution)
266
+ python_version: Python version to use
267
+ with_packages: Additional packages to include
268
+ from_package: Package to install the tool from
269
+ env_vars: Additional environment variables
270
+ """
271
+ # Basic validation
272
+ if project_directory and not Path(project_directory).exists():
273
+ raise NotADirectoryError(
274
+ f"Project directory not found: {project_directory}"
275
+ )
276
+
277
+ # Build uvx arguments
278
+ uvx_args = []
279
+ if python_version:
280
+ uvx_args.extend(["--python", python_version])
281
+ if from_package:
282
+ uvx_args.extend(["--from", from_package])
283
+ for pkg in with_packages or []:
284
+ uvx_args.extend(["--with", pkg])
285
+
286
+ # Add the tool name and tool args
287
+ uvx_args.append(tool_name)
288
+ if tool_args:
289
+ uvx_args.extend(tool_args)
290
+
291
+ # Get environment with any additional variables
292
+ env = None
293
+ if env_vars:
294
+ env = os.environ.copy()
295
+ env.update(env_vars)
296
+
297
+ super().__init__(command="uvx", args=uvx_args, env=env, cwd=project_directory)
298
+ self.tool_name = tool_name
299
+
300
+
301
+ class NpxStdioTransport(StdioTransport):
302
+ """Transport for running commands via the npx tool."""
303
+
304
+ def __init__(
305
+ self,
306
+ package: str,
307
+ args: list[str] | None = None,
308
+ project_directory: str | None = None,
309
+ env_vars: dict[str, str] | None = None,
310
+ use_package_lock: bool = True,
311
+ ):
312
+ """
313
+ Initialize an Npx transport.
314
+
315
+ Args:
316
+ package: Name of the npm package to run
317
+ args: Arguments to pass to the package command
318
+ project_directory: Project directory with package.json
319
+ env_vars: Additional environment variables
320
+ use_package_lock: Whether to use package-lock.json (--prefer-offline)
321
+ """
322
+ # Basic validation
323
+ if project_directory and not Path(project_directory).exists():
324
+ raise NotADirectoryError(
325
+ f"Project directory not found: {project_directory}"
326
+ )
327
+
328
+ # Build npx arguments
329
+ npx_args = []
330
+ if use_package_lock:
331
+ npx_args.append("--prefer-offline")
332
+
333
+ # Add the package name and args
334
+ npx_args.append(package)
335
+ if args:
336
+ npx_args.extend(args)
337
+
338
+ # Get environment with any additional variables
339
+ env = None
340
+ if env_vars:
341
+ env = os.environ.copy()
342
+ env.update(env_vars)
343
+
344
+ super().__init__(command="npx", args=npx_args, env=env, cwd=project_directory)
345
+ self.package = package
346
+
347
+
348
+ class FastMCPTransport(ClientTransport):
349
+ """
350
+ Special transport for in-memory connections to an MCP server.
351
+
352
+ This is particularly useful for testing or when client and server
353
+ are in the same process.
354
+ """
355
+
356
+ def __init__(self, mcp: FastMCPServer):
357
+ self._fastmcp = mcp # Can be FastMCP or MCPServer
358
+
359
+ @contextlib.asynccontextmanager
360
+ async def connect_session(
361
+ self, **session_kwargs: Unpack[SessionKwargs]
362
+ ) -> AsyncIterator[ClientSession]:
363
+ # create_connected_server_and_client_session manages the session lifecycle itself
364
+ async with create_connected_server_and_client_session(
365
+ server=self._fastmcp._mcp_server,
366
+ **session_kwargs,
367
+ ) as session:
368
+ yield session
369
+
370
+ def __repr__(self) -> str:
371
+ return f"<FastMCP(server='{self._fastmcp.name}')>"
372
+
373
+
374
+ def infer_transport(
375
+ transport: ClientTransport | FastMCPServer | AnyUrl | Path | str,
376
+ ) -> ClientTransport:
377
+ """
378
+ Infer the appropriate transport type from the given transport argument.
379
+
380
+ This function attempts to infer the correct transport type from the provided
381
+ argument, handling various input types and converting them to the appropriate
382
+ ClientTransport subclass.
383
+ """
384
+ # the transport is already a ClientTransport
385
+ if isinstance(transport, ClientTransport):
386
+ return transport
387
+
388
+ # the transport is a FastMCP server
389
+ elif isinstance(transport, FastMCPServer):
390
+ return FastMCPTransport(mcp=transport)
391
+
392
+ # the transport is a path to a script
393
+ elif isinstance(transport, Path | str) and Path(transport).exists():
394
+ if str(transport).endswith(".py"):
395
+ return PythonStdioTransport(script_path=transport)
396
+ elif str(transport).endswith(".js"):
397
+ return NodeStdioTransport(script_path=transport)
398
+ else:
399
+ raise ValueError(f"Unsupported script type: {transport}")
400
+
401
+ # the transport is an http(s) URL
402
+ elif isinstance(transport, AnyUrl | str) and str(transport).startswith("http"):
403
+ return SSETransport(url=transport)
404
+
405
+ # the transport is a websocket URL
406
+ elif isinstance(transport, AnyUrl | str) and str(transport).startswith("ws"):
407
+ return WSTransport(url=transport)
408
+
409
+ # the transport is an unknown type
410
+ else:
411
+ raise ValueError(f"Could not infer a valid transport from: {transport}")
@@ -1,4 +1,4 @@
1
1
  from .base import Prompt
2
- from .manager import PromptManager
2
+ from .prompt_manager import PromptManager
3
3
 
4
4
  __all__ = ["Prompt", "PromptManager"]
fastmcp/prompts/base.py CHANGED
@@ -1,12 +1,13 @@
1
1
  """Base classes for FastMCP prompts."""
2
2
 
3
- import json
4
- from typing import Any, Callable, Dict, Literal, Optional, Sequence, Awaitable
5
3
  import inspect
4
+ import json
5
+ from collections.abc import Awaitable, Callable, Sequence
6
+ from typing import Any, Literal
6
7
 
7
- from pydantic import BaseModel, Field, TypeAdapter, validate_call
8
- from mcp.types import TextContent, ImageContent, EmbeddedResource
9
8
  import pydantic_core
9
+ from mcp.types import EmbeddedResource, ImageContent, TextContent
10
+ from pydantic import BaseModel, Field, TypeAdapter, validate_call
10
11
 
11
12
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
12
13
 
@@ -17,7 +18,7 @@ class Message(BaseModel):
17
18
  role: Literal["user", "assistant"]
18
19
  content: CONTENT_TYPES
19
20
 
20
- def __init__(self, content: str | CONTENT_TYPES, **kwargs):
21
+ def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
21
22
  if isinstance(content, str):
22
23
  content = TextContent(type="text", text=content)
23
24
  super().__init__(content=content, **kwargs)
@@ -26,22 +27,24 @@ class Message(BaseModel):
26
27
  class UserMessage(Message):
27
28
  """A message from the user."""
28
29
 
29
- role: Literal["user"] = "user"
30
+ role: Literal["user", "assistant"] = "user"
30
31
 
31
- def __init__(self, content: str | CONTENT_TYPES, **kwargs):
32
+ def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
32
33
  super().__init__(content=content, **kwargs)
33
34
 
34
35
 
35
36
  class AssistantMessage(Message):
36
37
  """A message from the assistant."""
37
38
 
38
- role: Literal["assistant"] = "assistant"
39
+ role: Literal["user", "assistant"] = "assistant"
39
40
 
40
- def __init__(self, content: str | CONTENT_TYPES, **kwargs):
41
+ def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
41
42
  super().__init__(content=content, **kwargs)
42
43
 
43
44
 
44
- message_validator = TypeAdapter(UserMessage | AssistantMessage)
45
+ message_validator = TypeAdapter[UserMessage | AssistantMessage](
46
+ UserMessage | AssistantMessage
47
+ )
45
48
 
46
49
  SyncPromptResult = (
47
50
  str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
@@ -71,14 +74,14 @@ class Prompt(BaseModel):
71
74
  arguments: list[PromptArgument] | None = Field(
72
75
  None, description="Arguments that can be passed to the prompt"
73
76
  )
74
- fn: Callable = Field(exclude=True)
77
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
75
78
 
76
79
  @classmethod
77
80
  def from_function(
78
81
  cls,
79
- fn: Callable[..., PromptResult],
80
- name: Optional[str] = None,
81
- description: Optional[str] = None,
82
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]],
83
+ name: str | None = None,
84
+ description: str | None = None,
82
85
  ) -> "Prompt":
83
86
  """Create a Prompt from a function.
84
87
 
@@ -97,7 +100,7 @@ class Prompt(BaseModel):
97
100
  parameters = TypeAdapter(fn).json_schema()
98
101
 
99
102
  # Convert parameters to PromptArguments
100
- arguments = []
103
+ arguments: list[PromptArgument] = []
101
104
  if "properties" in parameters:
102
105
  for param_name, param in parameters["properties"].items():
103
106
  required = param_name in parameters.get("required", [])
@@ -119,7 +122,7 @@ class Prompt(BaseModel):
119
122
  fn=fn,
120
123
  )
121
124
 
122
- async def render(self, arguments: Optional[Dict[str, Any]] = None) -> list[Message]:
125
+ async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
123
126
  """Render the prompt with arguments."""
124
127
  # Validate required arguments
125
128
  if self.arguments:
@@ -136,25 +139,23 @@ class Prompt(BaseModel):
136
139
  result = await result
137
140
 
138
141
  # Validate messages
139
- if not isinstance(result, (list, tuple)):
142
+ if not isinstance(result, list | tuple):
140
143
  result = [result]
141
144
 
142
145
  # Convert result to messages
143
- messages = []
144
- for msg in result:
146
+ messages: list[Message] = []
147
+ for msg in result: # type: ignore[reportUnknownVariableType]
145
148
  try:
146
149
  if isinstance(msg, Message):
147
150
  messages.append(msg)
148
151
  elif isinstance(msg, dict):
149
- msg = message_validator.validate_python(msg)
150
- messages.append(msg)
152
+ messages.append(message_validator.validate_python(msg))
151
153
  elif isinstance(msg, str):
152
- messages.append(
153
- UserMessage(content=TextContent(type="text", text=msg))
154
- )
154
+ content = TextContent(type="text", text=msg)
155
+ messages.append(UserMessage(content=content))
155
156
  else:
156
- msg = json.dumps(pydantic_core.to_jsonable_python(msg))
157
- messages.append(Message(role="user", content=msg))
157
+ content = json.dumps(pydantic_core.to_jsonable_python(msg))
158
+ messages.append(Message(role="user", content=content))
158
159
  except Exception:
159
160
  raise ValueError(
160
161
  f"Could not convert prompt result to message: {msg}"
@@ -1,9 +1,8 @@
1
1
  """Prompt management functionality."""
2
2
 
3
- from typing import Dict, Optional
3
+ from typing import Any
4
4
 
5
-
6
- from fastmcp.prompts.base import Prompt
5
+ from fastmcp.prompts.base import Message, Prompt
7
6
  from fastmcp.utilities.logging import get_logger
8
7
 
9
8
  logger = get_logger(__name__)
@@ -13,24 +12,63 @@ class PromptManager:
13
12
  """Manages FastMCP prompts."""
14
13
 
15
14
  def __init__(self, warn_on_duplicate_prompts: bool = True):
16
- self._prompts: Dict[str, Prompt] = {}
15
+ self._prompts: dict[str, Prompt] = {}
17
16
  self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
18
17
 
19
- def add_prompt(self, prompt: Prompt) -> Prompt:
18
+ def get_prompt(self, name: str) -> Prompt | None:
19
+ """Get prompt by name."""
20
+ return self._prompts.get(name)
21
+
22
+ def list_prompts(self) -> list[Prompt]:
23
+ """List all registered prompts."""
24
+ return list(self._prompts.values())
25
+
26
+ def add_prompt(
27
+ self,
28
+ prompt: Prompt,
29
+ ) -> Prompt:
20
30
  """Add a prompt to the manager."""
21
- logger.debug(f"Adding prompt: {prompt.name}")
31
+
32
+ # Check for duplicates
22
33
  existing = self._prompts.get(prompt.name)
23
34
  if existing:
24
35
  if self.warn_on_duplicate_prompts:
25
36
  logger.warning(f"Prompt already exists: {prompt.name}")
26
37
  return existing
38
+
27
39
  self._prompts[prompt.name] = prompt
28
40
  return prompt
29
41
 
30
- def get_prompt(self, name: str) -> Optional[Prompt]:
31
- """Get prompt by name."""
32
- return self._prompts.get(name)
42
+ async def render_prompt(
43
+ self, name: str, arguments: dict[str, Any] | None = None
44
+ ) -> list[Message]:
45
+ """Render a prompt by name with arguments."""
46
+ prompt = self.get_prompt(name)
47
+ if not prompt:
48
+ raise ValueError(f"Unknown prompt: {name}")
33
49
 
34
- def list_prompts(self) -> list[Prompt]:
35
- """List all registered prompts."""
36
- return list(self._prompts.values())
50
+ return await prompt.render(arguments)
51
+
52
+ def import_prompts(
53
+ self, manager: "PromptManager", prefix: str | None = None
54
+ ) -> None:
55
+ """
56
+ Import all prompts from another PromptManager with prefixed names.
57
+
58
+ Args:
59
+ manager: Another PromptManager instance to import prompts from
60
+ prefix: Prefix to add to prompt names. The resulting prompt name will
61
+ be in the format "{prefix}{original_name}" if prefix is provided,
62
+ otherwise the original name is used.
63
+ For example, with prefix "weather/" and prompt "forecast_prompt",
64
+ the imported prompt would be available as "weather/forecast_prompt"
65
+ """
66
+ for name, prompt in manager._prompts.items():
67
+ # Create prefixed name - we keep the original name in the Prompt object
68
+ prefixed_name = f"{prefix}{name}" if prefix else name
69
+
70
+ # Log the import
71
+ logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
72
+
73
+ # Store the prompt with the prefixed name
74
+ self._prompts[prefixed_name] = prompt
@@ -1,14 +1,14 @@
1
1
  from .base import Resource
2
+ from .resource_manager import ResourceManager
3
+ from .templates import ResourceTemplate
2
4
  from .types import (
3
- TextResource,
4
5
  BinaryResource,
5
- FunctionResource,
6
+ DirectoryResource,
6
7
  FileResource,
8
+ FunctionResource,
7
9
  HttpResource,
8
- DirectoryResource,
10
+ TextResource,
9
11
  )
10
- from .templates import ResourceTemplate
11
- from .resource_manager import ResourceManager
12
12
 
13
13
  __all__ = [
14
14
  "Resource",
fastmcp/resources/base.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Base classes and interfaces for FastMCP resources."""
2
2
 
3
3
  import abc
4
- from typing import Union, Annotated
4
+ from typing import Annotated
5
5
 
6
6
  from pydantic import (
7
7
  AnyUrl,
@@ -43,6 +43,6 @@ class Resource(BaseModel, abc.ABC):
43
43
  raise ValueError("Either name or uri must be provided")
44
44
 
45
45
  @abc.abstractmethod
46
- async def read(self) -> Union[str, bytes]:
46
+ async def read(self) -> str | bytes:
47
47
  """Read the resource content."""
48
48
  pass