fastmcp 2.0.0__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fastmcp/cli/cli.py CHANGED
@@ -65,7 +65,7 @@ def _build_uv_command(
65
65
  """Build the uv run command that runs a MCP server through mcp run."""
66
66
  cmd = ["uv"]
67
67
 
68
- cmd.extend(["run", "--with", "mcp"])
68
+ cmd.extend(["run", "--with", "fastmcp"])
69
69
 
70
70
  if with_editable:
71
71
  cmd.extend(["--with-editable", str(with_editable)])
@@ -76,7 +76,7 @@ def _build_uv_command(
76
76
  cmd.extend(["--with", pkg])
77
77
 
78
78
  # Add mcp run command
79
- cmd.extend(["mcp", "run", file_spec])
79
+ cmd.extend(["fastmcp", "run", file_spec])
80
80
  return cmd
81
81
 
82
82
 
@@ -323,6 +323,8 @@ def run(
323
323
  # Import and get server object
324
324
  server = _import_server(file, server_object)
325
325
 
326
+ logger.info(f'Found server "{server.name}" in {file}')
327
+
326
328
  # Run the server
327
329
  kwargs = {}
328
330
  if transport:
fastmcp/client/client.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import datetime
2
2
  from contextlib import AbstractAsyncContextManager
3
3
  from pathlib import Path
4
- from typing import Any
4
+ from typing import Any, Literal, cast, overload
5
5
 
6
6
  import mcp.types
7
7
  from mcp import ClientSession
@@ -24,6 +24,10 @@ from .transports import ClientTransport, SessionKwargs, infer_transport
24
24
  __all__ = ["Client", "RootsHandler", "RootsList"]
25
25
 
26
26
 
27
+ class ClientError(ValueError):
28
+ """Base class for errors raised by the client."""
29
+
30
+
27
31
  class Client:
28
32
  """
29
33
  MCP client that delegates connection management to a Transport instance.
@@ -122,60 +126,101 @@ class Client:
122
126
  """Send a logging/setLevel request."""
123
127
  await self.session.set_logging_level(level)
124
128
 
125
- async def list_resources(self) -> mcp.types.ListResourcesResult:
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]:
126
134
  """Send a resources/list request."""
127
- return await self.session.list_resources()
135
+ result = await self.session.list_resources()
136
+ return result.resources
128
137
 
129
- async def list_resource_templates(self) -> mcp.types.ListResourceTemplatesResult:
138
+ async def list_resource_templates(self) -> list[mcp.types.ResourceTemplate]:
130
139
  """Send a resources/listResourceTemplates request."""
131
- return await self.session.list_resource_templates()
140
+ result = await self.session.list_resource_templates()
141
+ return result.resourceTemplates
132
142
 
133
- async def read_resource(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult:
143
+ async def read_resource(
144
+ self, uri: AnyUrl | str
145
+ ) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]:
134
146
  """Send a resources/read request."""
135
147
  if isinstance(uri, str):
136
148
  uri = AnyUrl(uri) # Ensure AnyUrl
137
- return await self.session.read_resource(uri)
138
-
139
- async def subscribe_resource(self, uri: AnyUrl | str) -> None:
140
- """Send a resources/subscribe request."""
141
- if isinstance(uri, str):
142
- uri = AnyUrl(uri)
143
- await self.session.subscribe_resource(uri)
144
-
145
- async def unsubscribe_resource(self, uri: AnyUrl | str) -> None:
146
- """Send a resources/unsubscribe request."""
147
- if isinstance(uri, str):
148
- uri = AnyUrl(uri)
149
- await self.session.unsubscribe_resource(uri)
150
-
151
- async def list_prompts(self) -> mcp.types.ListPromptsResult:
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]:
152
165
  """Send a prompts/list request."""
153
- return await self.session.list_prompts()
166
+ result = await self.session.list_prompts()
167
+ return result.prompts
154
168
 
155
169
  async def get_prompt(
156
170
  self, name: str, arguments: dict[str, str] | None = None
157
171
  ) -> mcp.types.GetPromptResult:
158
172
  """Send a prompts/get request."""
159
- return await self.session.get_prompt(name, arguments)
173
+ result = await self.session.get_prompt(name, arguments)
174
+ return result
160
175
 
161
176
  async def complete(
162
177
  self,
163
178
  ref: mcp.types.ResourceReference | mcp.types.PromptReference,
164
179
  argument: dict[str, str],
165
- ) -> mcp.types.CompleteResult:
166
- """Send a completion/complete request."""
167
- return await self.session.complete(ref, argument)
180
+ ) -> mcp.types.Completion:
181
+ """Send a completion request."""
182
+ result = await self.session.complete(ref, argument)
183
+ return result.completion
168
184
 
169
- async def list_tools(self) -> mcp.types.ListToolsResult:
185
+ async def list_tools(self) -> list[mcp.types.Tool]:
170
186
  """Send a tools/list request."""
171
- return await self.session.list_tools()
187
+ result = await self.session.list_tools()
188
+ return result.tools
172
189
 
190
+ @overload
173
191
  async def call_tool(
174
- self, name: str, arguments: dict[str, Any] | None = None
175
- ) -> mcp.types.CallToolResult:
176
- """Send a tools/call request."""
177
- return await self.session.call_tool(name, arguments)
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: ...
178
207
 
179
- async def send_roots_list_changed(self) -> None:
180
- """Send a roots/list_changed notification."""
181
- await self.session.send_roots_list_changed()
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
@@ -208,6 +208,28 @@ class PythonStdioTransport(StdioTransport):
208
208
  self.script_path = script_path
209
209
 
210
210
 
211
+ class FastMCPStdioTransport(StdioTransport):
212
+ """Transport for running FastMCP servers using the FastMCP CLI."""
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
+ ):
221
+ script_path = Path(script_path).resolve()
222
+ if not script_path.is_file():
223
+ raise FileNotFoundError(f"Script not found: {script_path}")
224
+ if not str(script_path).endswith(".py"):
225
+ raise ValueError(f"Not a Python script: {script_path}")
226
+
227
+ super().__init__(
228
+ command="fastmcp", args=["run", str(script_path)], env=env, cwd=cwd
229
+ )
230
+ self.script_path = script_path
231
+
232
+
211
233
  class NodeStdioTransport(StdioTransport):
212
234
  """Transport for running Node.js scripts."""
213
235
 
fastmcp/exceptions.py CHANGED
@@ -17,5 +17,9 @@ class ToolError(FastMCPError):
17
17
  """Error in tool operations."""
18
18
 
19
19
 
20
+ class PromptError(FastMCPError):
21
+ """Error in prompt operations."""
22
+
23
+
20
24
  class InvalidSignature(Exception):
21
25
  """Invalid signature for use with FastMCP."""
@@ -1,4 +1,4 @@
1
- from .base import Prompt
1
+ from .prompt import Prompt, Message, UserMessage, AssistantMessage
2
2
  from .prompt_manager import PromptManager
3
3
 
4
- __all__ = ["Prompt", "PromptManager"]
4
+ __all__ = ["Prompt", "PromptManager", "Message", "UserMessage", "AssistantMessage"]
@@ -3,11 +3,14 @@
3
3
  import inspect
4
4
  import json
5
5
  from collections.abc import Awaitable, Callable, Sequence
6
- from typing import Any, Literal
6
+ from typing import Annotated, Any, Literal
7
7
 
8
8
  import pydantic_core
9
9
  from mcp.types import EmbeddedResource, ImageContent, TextContent
10
- from pydantic import BaseModel, Field, TypeAdapter, validate_call
10
+ from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
11
+ from typing_extensions import Self
12
+
13
+ from fastmcp.utilities.types import _convert_set_defaults
11
14
 
12
15
  CONTENT_TYPES = TextContent | ImageContent | EmbeddedResource
13
16
 
@@ -24,27 +27,17 @@ class Message(BaseModel):
24
27
  super().__init__(content=content, **kwargs)
25
28
 
26
29
 
27
- class UserMessage(Message):
30
+ def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
28
31
  """A message from the user."""
32
+ return Message(content=content, role="user", **kwargs)
29
33
 
30
- role: Literal["user", "assistant"] = "user"
31
-
32
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
33
- super().__init__(content=content, **kwargs)
34
34
 
35
-
36
- class AssistantMessage(Message):
35
+ def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
37
36
  """A message from the assistant."""
37
+ return Message(content=content, role="assistant", **kwargs)
38
38
 
39
- role: Literal["user", "assistant"] = "assistant"
40
39
 
41
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
42
- super().__init__(content=content, **kwargs)
43
-
44
-
45
- message_validator = TypeAdapter[UserMessage | AssistantMessage](
46
- UserMessage | AssistantMessage
47
- )
40
+ message_validator = TypeAdapter[Message](Message)
48
41
 
49
42
  SyncPromptResult = (
50
43
  str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
@@ -71,10 +64,13 @@ class Prompt(BaseModel):
71
64
  description: str | None = Field(
72
65
  None, description="Description of what the prompt does"
73
66
  )
67
+ tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
68
+ default_factory=set, description="Tags for the prompt"
69
+ )
74
70
  arguments: list[PromptArgument] | None = Field(
75
71
  None, description="Arguments that can be passed to the prompt"
76
72
  )
77
- fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
73
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]]
78
74
 
79
75
  @classmethod
80
76
  def from_function(
@@ -82,6 +78,7 @@ class Prompt(BaseModel):
82
78
  fn: Callable[..., PromptResult | Awaitable[PromptResult]],
83
79
  name: str | None = None,
84
80
  description: str | None = None,
81
+ tags: set[str] | None = None,
85
82
  ) -> "Prompt":
86
83
  """Create a Prompt from a function.
87
84
 
@@ -120,6 +117,7 @@ class Prompt(BaseModel):
120
117
  description=description or fn.__doc__ or "",
121
118
  arguments=arguments,
122
119
  fn=fn,
120
+ tags=tags or set(),
123
121
  )
124
122
 
125
123
  async def render(self, arguments: dict[str, Any] | None = None) -> list[Message]:
@@ -152,7 +150,7 @@ class Prompt(BaseModel):
152
150
  messages.append(message_validator.validate_python(msg))
153
151
  elif isinstance(msg, str):
154
152
  content = TextContent(type="text", text=msg)
155
- messages.append(UserMessage(content=content))
153
+ messages.append(Message(role="user", content=content))
156
154
  else:
157
155
  content = json.dumps(pydantic_core.to_jsonable_python(msg))
158
156
  messages.append(Message(role="user", content=content))
@@ -164,3 +162,15 @@ class Prompt(BaseModel):
164
162
  return messages
165
163
  except Exception as e:
166
164
  raise ValueError(f"Error rendering prompt {self.name}: {e}")
165
+
166
+ def copy(self, updates: dict[str, Any] | None = None) -> Self:
167
+ """Copy the prompt with optional updates."""
168
+ data = self.model_dump()
169
+ if updates:
170
+ data.update(updates)
171
+ return type(self)(**data)
172
+
173
+ def __eq__(self, other: object) -> bool:
174
+ if not isinstance(other, Prompt):
175
+ return False
176
+ return self.model_dump() == other.model_dump()
@@ -1,8 +1,11 @@
1
1
  """Prompt management functionality."""
2
2
 
3
+ from collections.abc import Awaitable, Callable
3
4
  from typing import Any
4
5
 
5
- from fastmcp.prompts.base import Message, Prompt
6
+ from fastmcp.exceptions import PromptError
7
+ from fastmcp.prompts.prompt import Message, Prompt, PromptResult
8
+ from fastmcp.settings import DuplicateBehavior
6
9
  from fastmcp.utilities.logging import get_logger
7
10
 
8
11
  logger = get_logger(__name__)
@@ -11,9 +14,9 @@ logger = get_logger(__name__)
11
14
  class PromptManager:
12
15
  """Manages FastMCP prompts."""
13
16
 
14
- def __init__(self, warn_on_duplicate_prompts: bool = True):
17
+ def __init__(self, duplicate_behavior: DuplicateBehavior = DuplicateBehavior.WARN):
15
18
  self._prompts: dict[str, Prompt] = {}
16
- self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
19
+ self.duplicate_behavior = duplicate_behavior
17
20
 
18
21
  def get_prompt(self, name: str) -> Prompt | None:
19
22
  """Get prompt by name."""
@@ -23,18 +26,32 @@ class PromptManager:
23
26
  """List all registered prompts."""
24
27
  return list(self._prompts.values())
25
28
 
26
- def add_prompt(
29
+ def add_prompt_from_fn(
27
30
  self,
28
- prompt: Prompt,
31
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]],
32
+ name: str | None = None,
33
+ description: str | None = None,
34
+ tags: set[str] | None = None,
29
35
  ) -> Prompt:
36
+ """Create a prompt from a function."""
37
+ prompt = Prompt.from_function(fn, name=name, description=description, tags=tags)
38
+ return self.add_prompt(prompt)
39
+
40
+ def add_prompt(self, prompt: Prompt) -> Prompt:
30
41
  """Add a prompt to the manager."""
31
42
 
32
43
  # Check for duplicates
33
44
  existing = self._prompts.get(prompt.name)
34
45
  if existing:
35
- if self.warn_on_duplicate_prompts:
46
+ if self.duplicate_behavior == DuplicateBehavior.WARN:
36
47
  logger.warning(f"Prompt already exists: {prompt.name}")
37
- return existing
48
+ self._prompts[prompt.name] = prompt
49
+ elif self.duplicate_behavior == DuplicateBehavior.REPLACE:
50
+ self._prompts[prompt.name] = prompt
51
+ elif self.duplicate_behavior == DuplicateBehavior.ERROR:
52
+ raise ValueError(f"Prompt already exists: {prompt.name}")
53
+ elif self.duplicate_behavior == DuplicateBehavior.IGNORE:
54
+ pass
38
55
 
39
56
  self._prompts[prompt.name] = prompt
40
57
  return prompt
@@ -45,7 +62,7 @@ class PromptManager:
45
62
  """Render a prompt by name with arguments."""
46
63
  prompt = self.get_prompt(name)
47
64
  if not prompt:
48
- raise ValueError(f"Unknown prompt: {name}")
65
+ raise PromptError(f"Unknown prompt: {name}")
49
66
 
50
67
  return await prompt.render(arguments)
51
68
 
@@ -64,11 +81,11 @@ class PromptManager:
64
81
  the imported prompt would be available as "weather/forecast_prompt"
65
82
  """
66
83
  for name, prompt in manager._prompts.items():
67
- # Create prefixed name - we keep the original name in the Prompt object
84
+ # Create prefixed name
68
85
  prefixed_name = f"{prefix}{name}" if prefix else name
69
86
 
70
- # Log the import
71
- logger.debug(f"Importing prompt with name {name} as {prefixed_name}")
87
+ new_prompt = prompt.copy(updates=dict(name=prefixed_name))
72
88
 
73
89
  # Store the prompt with the prefixed name
74
- self._prompts[prefixed_name] = prompt
90
+ self.add_prompt(new_prompt)
91
+ logger.debug(f'Imported prompt "{name}" as "{prefixed_name}"')
@@ -1,6 +1,5 @@
1
- from .base import Resource
2
- from .resource_manager import ResourceManager
3
- from .templates import ResourceTemplate
1
+ from .resource import Resource
2
+ from .template import ResourceTemplate
4
3
  from .types import (
5
4
  BinaryResource,
6
5
  DirectoryResource,
@@ -9,6 +8,7 @@ from .types import (
9
8
  HttpResource,
10
9
  TextResource,
11
10
  )
11
+ from .resource_manager import ResourceManager
12
12
 
13
13
  __all__ = [
14
14
  "Resource",
@@ -1,17 +1,21 @@
1
1
  """Base classes and interfaces for FastMCP resources."""
2
2
 
3
3
  import abc
4
- from typing import Annotated
4
+ from typing import Annotated, Any
5
5
 
6
6
  from pydantic import (
7
7
  AnyUrl,
8
8
  BaseModel,
9
+ BeforeValidator,
9
10
  ConfigDict,
10
11
  Field,
11
12
  UrlConstraints,
12
13
  ValidationInfo,
13
14
  field_validator,
14
15
  )
16
+ from typing_extensions import Self
17
+
18
+ from fastmcp.utilities.types import _convert_set_defaults
15
19
 
16
20
 
17
21
  class Resource(BaseModel, abc.ABC):
@@ -26,6 +30,9 @@ class Resource(BaseModel, abc.ABC):
26
30
  description: str | None = Field(
27
31
  description="Description of the resource", default=None
28
32
  )
33
+ tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
34
+ default_factory=set, description="Tags for the resource"
35
+ )
29
36
  mime_type: str = Field(
30
37
  default="text/plain",
31
38
  description="MIME type of the resource content",
@@ -46,3 +53,15 @@ class Resource(BaseModel, abc.ABC):
46
53
  async def read(self) -> str | bytes:
47
54
  """Read the resource content."""
48
55
  pass
56
+
57
+ def copy(self, updates: dict[str, Any] | None = None) -> Self:
58
+ """Copy the resource with optional updates."""
59
+ data = self.model_dump()
60
+ if updates:
61
+ data.update(updates)
62
+ return type(self)(**data)
63
+
64
+ def __eq__(self, other: object) -> bool:
65
+ if not isinstance(other, Resource):
66
+ return False
67
+ return self.model_dump() == other.model_dump()