fastmcp 2.1.0__py3-none-any.whl → 2.1.2__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
@@ -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:
@@ -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 .prompt 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"]
fastmcp/prompts/prompt.py CHANGED
@@ -8,7 +8,6 @@ from typing import Annotated, Any, Literal
8
8
  import pydantic_core
9
9
  from mcp.types import EmbeddedResource, ImageContent, TextContent
10
10
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
11
- from typing_extensions import Self
12
11
 
13
12
  from fastmcp.utilities.types import _convert_set_defaults
14
13
 
@@ -27,27 +26,17 @@ class Message(BaseModel):
27
26
  super().__init__(content=content, **kwargs)
28
27
 
29
28
 
30
- class UserMessage(Message):
29
+ def UserMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
31
30
  """A message from the user."""
31
+ return Message(content=content, role="user", **kwargs)
32
32
 
33
- role: Literal["user", "assistant"] = "user"
34
33
 
35
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
36
- super().__init__(content=content, **kwargs)
37
-
38
-
39
- class AssistantMessage(Message):
34
+ def AssistantMessage(content: str | CONTENT_TYPES, **kwargs: Any) -> Message:
40
35
  """A message from the assistant."""
36
+ return Message(content=content, role="assistant", **kwargs)
41
37
 
42
- role: Literal["user", "assistant"] = "assistant"
43
38
 
44
- def __init__(self, content: str | CONTENT_TYPES, **kwargs: Any):
45
- super().__init__(content=content, **kwargs)
46
-
47
-
48
- message_validator = TypeAdapter[UserMessage | AssistantMessage](
49
- UserMessage | AssistantMessage
50
- )
39
+ message_validator = TypeAdapter[Message](Message)
51
40
 
52
41
  SyncPromptResult = (
53
42
  str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]
@@ -160,7 +149,7 @@ class Prompt(BaseModel):
160
149
  messages.append(message_validator.validate_python(msg))
161
150
  elif isinstance(msg, str):
162
151
  content = TextContent(type="text", text=msg)
163
- messages.append(UserMessage(content=content))
152
+ messages.append(Message(role="user", content=content))
164
153
  else:
165
154
  content = json.dumps(pydantic_core.to_jsonable_python(msg))
166
155
  messages.append(Message(role="user", content=content))
@@ -173,13 +162,6 @@ class Prompt(BaseModel):
173
162
  except Exception as e:
174
163
  raise ValueError(f"Error rendering prompt {self.name}: {e}")
175
164
 
176
- def copy(self, updates: dict[str, Any] | None = None) -> Self:
177
- """Copy the prompt with optional updates."""
178
- data = self.model_dump()
179
- if updates:
180
- data.update(updates)
181
- return type(self)(**data)
182
-
183
165
  def __eq__(self, other: object) -> bool:
184
166
  if not isinstance(other, Prompt):
185
167
  return False
@@ -1,8 +1,10 @@
1
1
  """Prompt management functionality."""
2
2
 
3
+ import copy
3
4
  from collections.abc import Awaitable, Callable
4
5
  from typing import Any
5
6
 
7
+ from fastmcp.exceptions import PromptError
6
8
  from fastmcp.prompts.prompt import Message, Prompt, PromptResult
7
9
  from fastmcp.settings import DuplicateBehavior
8
10
  from fastmcp.utilities.logging import get_logger
@@ -61,7 +63,7 @@ class PromptManager:
61
63
  """Render a prompt by name with arguments."""
62
64
  prompt = self.get_prompt(name)
63
65
  if not prompt:
64
- raise ValueError(f"Unknown prompt: {name}")
66
+ raise PromptError(f"Unknown prompt: {name}")
65
67
 
66
68
  return await prompt.render(arguments)
67
69
 
@@ -83,7 +85,8 @@ class PromptManager:
83
85
  # Create prefixed name
84
86
  prefixed_name = f"{prefix}{name}" if prefix else name
85
87
 
86
- new_prompt = prompt.copy(updates=dict(name=prefixed_name))
88
+ new_prompt = copy.copy(prompt)
89
+ new_prompt.name = prefixed_name
87
90
 
88
91
  # Store the prompt with the prefixed name
89
92
  self.add_prompt(new_prompt)
@@ -1,5 +1,4 @@
1
1
  from .resource import Resource
2
- from .resource_manager import ResourceManager
3
2
  from .template import ResourceTemplate
4
3
  from .types import (
5
4
  BinaryResource,
@@ -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,7 +1,7 @@
1
1
  """Base classes and interfaces for FastMCP resources."""
2
2
 
3
3
  import abc
4
- from typing import Annotated, Any
4
+ from typing import Annotated
5
5
 
6
6
  from pydantic import (
7
7
  AnyUrl,
@@ -13,7 +13,6 @@ from pydantic import (
13
13
  ValidationInfo,
14
14
  field_validator,
15
15
  )
16
- from typing_extensions import Self
17
16
 
18
17
  from fastmcp.utilities.types import _convert_set_defaults
19
18
 
@@ -54,13 +53,6 @@ class Resource(BaseModel, abc.ABC):
54
53
  """Read the resource content."""
55
54
  pass
56
55
 
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
56
  def __eq__(self, other: object) -> bool:
65
57
  if not isinstance(other, Resource):
66
58
  return False
@@ -1,11 +1,15 @@
1
1
  """Resource manager functionality."""
2
2
 
3
+ import copy
4
+ import inspect
5
+ import re
3
6
  from collections.abc import Callable
4
7
  from typing import Any
5
8
 
6
9
  from pydantic import AnyUrl
7
10
 
8
- from fastmcp.resources.resource import Resource
11
+ from fastmcp.exceptions import ResourceError
12
+ from fastmcp.resources import FunctionResource, Resource
9
13
  from fastmcp.resources.template import ResourceTemplate
10
14
  from fastmcp.settings import DuplicateBehavior
11
15
  from fastmcp.utilities.logging import get_logger
@@ -21,16 +25,86 @@ class ResourceManager:
21
25
  self._templates: dict[str, ResourceTemplate] = {}
22
26
  self.duplicate_behavior = duplicate_behavior
23
27
 
24
- def add_resource(self, resource: Resource) -> Resource:
25
- """Add a resource to the manager.
28
+ def add_resource_or_template_from_fn(
29
+ self,
30
+ fn: Callable[..., Any],
31
+ uri: str,
32
+ name: str | None = None,
33
+ description: str | None = None,
34
+ mime_type: str | None = None,
35
+ tags: set[str] | None = None,
36
+ ) -> Resource | ResourceTemplate:
37
+ """Add a resource or template to the manager from a function.
26
38
 
27
39
  Args:
28
- resource: A Resource instance to add
40
+ fn: The function to register as a resource or template
41
+ uri: The URI for the resource or template
42
+ name: Optional name for the resource or template
43
+ description: Optional description of the resource or template
44
+ mime_type: Optional MIME type for the resource or template
45
+ tags: Optional set of tags for categorizing the resource or template
46
+
47
+ Returns:
48
+ The added resource or template. If a resource or template with the same URI already exists,
49
+ returns the existing resource or template.
50
+ """
51
+ # Check if this should be a template
52
+ has_uri_params = "{" in uri and "}" in uri
53
+ has_func_params = bool(inspect.signature(fn).parameters)
54
+
55
+ if has_uri_params and has_func_params:
56
+ return self.add_template_from_fn(
57
+ fn, uri, name, description, mime_type, tags
58
+ )
59
+ elif not has_uri_params and not has_func_params:
60
+ return self.add_resource_from_fn(
61
+ fn, uri, name, description, mime_type, tags
62
+ )
63
+ else:
64
+ raise ValueError(
65
+ "Invalid resource or template definition due to a "
66
+ "mismatch between URI parameters and function parameters."
67
+ )
68
+
69
+ def add_resource_from_fn(
70
+ self,
71
+ fn: Callable[..., Any],
72
+ uri: str,
73
+ name: str | None = None,
74
+ description: str | None = None,
75
+ mime_type: str | None = None,
76
+ tags: set[str] | None = None,
77
+ ) -> Resource:
78
+ """Add a resource to the manager from a function.
79
+
80
+ Args:
81
+ fn: The function to register as a resource
82
+ uri: The URI for the resource
83
+ name: Optional name for the resource
84
+ description: Optional description of the resource
85
+ mime_type: Optional MIME type for the resource
86
+ tags: Optional set of tags for categorizing the resource
29
87
 
30
88
  Returns:
31
89
  The added resource. If a resource with the same URI already exists,
32
90
  returns the existing resource.
33
91
  """
92
+ resource = FunctionResource(
93
+ uri=AnyUrl(uri),
94
+ name=name,
95
+ description=description,
96
+ mime_type=mime_type or "text/plain",
97
+ fn=fn,
98
+ tags=tags or set(),
99
+ )
100
+ return self.add_resource(resource)
101
+
102
+ def add_resource(self, resource: Resource) -> Resource:
103
+ """Add a resource to the manager.
104
+
105
+ Args:
106
+ resource: A Resource instance to add
107
+ """
34
108
  logger.debug(
35
109
  "Adding resource",
36
110
  extra={
@@ -63,6 +137,17 @@ class ResourceManager:
63
137
  tags: set[str] | None = None,
64
138
  ) -> ResourceTemplate:
65
139
  """Create a template from a function."""
140
+
141
+ # Validate that URI params match function params
142
+ uri_params = set(re.findall(r"{(\w+)}", uri_template))
143
+ func_params = set(inspect.signature(fn).parameters.keys())
144
+
145
+ if uri_params != func_params:
146
+ raise ValueError(
147
+ f"Mismatch between URI parameters {uri_params} "
148
+ f"and function parameters {func_params}"
149
+ )
150
+
66
151
  template = ResourceTemplate.from_function(
67
152
  fn,
68
153
  uri_template=uri_template,
@@ -122,7 +207,7 @@ class ResourceManager:
122
207
  except Exception as e:
123
208
  raise ValueError(f"Error creating resource from template: {e}")
124
209
 
125
- raise ValueError(f"Unknown resource: {uri}")
210
+ raise ResourceError(f"Unknown resource: {uri}")
126
211
 
127
212
  def list_resources(self) -> list[Resource]:
128
213
  """List all registered resources."""
@@ -154,7 +239,8 @@ class ResourceManager:
154
239
  # Create prefixed URI and copy the resource with the new URI
155
240
  prefixed_uri = f"{prefix}{uri}" if prefix else uri
156
241
 
157
- new_resource = resource.copy(updates=dict(uri=prefixed_uri))
242
+ new_resource = copy.copy(resource)
243
+ new_resource.uri = AnyUrl(prefixed_uri)
158
244
 
159
245
  # Store directly in resources dictionary
160
246
  self.add_resource(new_resource)
@@ -182,9 +268,8 @@ class ResourceManager:
182
268
  f"{prefix}{uri_template}" if prefix else uri_template
183
269
  )
184
270
 
185
- new_template = template.copy(
186
- updates=dict(uri_template=prefixed_uri_template)
187
- )
271
+ new_template = copy.copy(template)
272
+ new_template.uri_template = prefixed_uri_template
188
273
 
189
274
  # Store directly in templates dictionary
190
275
  self.add_template(new_template)
@@ -8,7 +8,6 @@ from collections.abc import Callable
8
8
  from typing import Annotated, Any
9
9
 
10
10
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
11
- from typing_extensions import Self
12
11
 
13
12
  from fastmcp.resources.types import FunctionResource, Resource
14
13
  from fastmcp.utilities.types import _convert_set_defaults
@@ -92,13 +91,6 @@ class ResourceTemplate(BaseModel):
92
91
  except Exception as e:
93
92
  raise ValueError(f"Error creating resource from template: {e}")
94
93
 
95
- def copy(self, updates: dict[str, Any] | None = None) -> Self:
96
- """Copy the resource template with optional updates."""
97
- data = self.model_dump()
98
- if updates:
99
- data.update(updates)
100
- return type(self)(**data)
101
-
102
94
  def __eq__(self, other: object) -> bool:
103
95
  if not isinstance(other, ResourceTemplate):
104
96
  return False
fastmcp/server/context.py CHANGED
@@ -118,7 +118,7 @@ class Context(BaseModel, Generic[ServerSessionT, LifespanContextT]):
118
118
  assert self._fastmcp is not None, (
119
119
  "Context is not available outside of a request"
120
120
  )
121
- return await self._fastmcp.read_resource(uri)
121
+ return await self._fastmcp._mcp_read_resource(uri)
122
122
 
123
123
  async def log(
124
124
  self,
fastmcp/server/proxy.py CHANGED
@@ -1,11 +1,11 @@
1
1
  from typing import Any, cast
2
2
 
3
3
  import mcp.types
4
- from mcp.types import BlobResourceContents, PromptMessage, TextResourceContents
4
+ from mcp.types import BlobResourceContents, TextResourceContents
5
5
 
6
6
  import fastmcp
7
7
  from fastmcp.client import Client
8
- from fastmcp.prompts import Prompt
8
+ from fastmcp.prompts import Message, Prompt
9
9
  from fastmcp.resources import Resource, ResourceTemplate
10
10
  from fastmcp.server.context import Context
11
11
  from fastmcp.server.server import FastMCP
@@ -142,10 +142,10 @@ class ProxyPrompt(Prompt):
142
142
  fn=_proxy_passthrough,
143
143
  )
144
144
 
145
- async def render(self, arguments: dict[str, Any]) -> list[PromptMessage]:
145
+ async def render(self, arguments: dict[str, Any]) -> list[Message]:
146
146
  async with self._client:
147
147
  result = await self._client.get_prompt(self.name, arguments)
148
- return result.messages
148
+ return [Message(role=m.role, content=m.content) for m in result.messages]
149
149
 
150
150
 
151
151
  class FastMCPProxy(FastMCP):