fastmcp 2.6.1__py3-none-any.whl → 2.7.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
@@ -1,4 +1,4 @@
1
- """FastmMCP CLI tools."""
1
+ """FastMCP CLI tools."""
2
2
 
3
3
  import importlib.metadata
4
4
  import importlib.util
@@ -219,8 +219,9 @@ def dev(
219
219
  sys.exit(1)
220
220
 
221
221
 
222
- @app.command()
222
+ @app.command(context_settings={"allow_extra_args": True})
223
223
  def run(
224
+ ctx: typer.Context,
224
225
  server_spec: str = typer.Argument(
225
226
  ...,
226
227
  help="Python file, object specification (file:obj), or URL",
@@ -266,7 +267,12 @@ def run(
266
267
 
267
268
  Note: This command runs the server directly. You are responsible for ensuring
268
269
  all dependencies are available.
270
+
271
+ Server arguments can be passed after -- :
272
+ fastmcp run server.py -- --config config.json --debug
269
273
  """
274
+ server_args = ctx.args # extra args after --
275
+
270
276
  logger.debug(
271
277
  "Running server or client",
272
278
  extra={
@@ -275,6 +281,7 @@ def run(
275
281
  "host": host,
276
282
  "port": port,
277
283
  "log_level": log_level,
284
+ "server_args": server_args,
278
285
  },
279
286
  )
280
287
 
@@ -285,6 +292,7 @@ def run(
285
292
  host=host,
286
293
  port=port,
287
294
  log_level=log_level,
295
+ server_args=server_args,
288
296
  )
289
297
  except Exception as e:
290
298
  logger.error(
fastmcp/cli/run.py CHANGED
@@ -71,6 +71,9 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
71
71
  logger.error("Could not load module", extra={"file": str(file)})
72
72
  sys.exit(1)
73
73
 
74
+ assert spec is not None
75
+ assert spec.loader is not None
76
+
74
77
  module = importlib.util.module_from_spec(spec)
75
78
  spec.loader.exec_module(module)
76
79
 
@@ -89,6 +92,8 @@ def import_server(file: Path, server_object: str | None = None) -> Any:
89
92
  )
90
93
  sys.exit(1)
91
94
 
95
+ assert server_object is not None
96
+
92
97
  # Handle module:object syntax
93
98
  if ":" in server_object:
94
99
  module_name, object_name = server_object.split(":", 1)
@@ -135,12 +140,37 @@ def create_client_server(url: str) -> Any:
135
140
  sys.exit(1)
136
141
 
137
142
 
143
+ def import_server_with_args(
144
+ file: Path, server_object: str | None = None, server_args: list[str] | None = None
145
+ ) -> Any:
146
+ """Import a server with optional command line arguments.
147
+
148
+ Args:
149
+ file: Path to the server file
150
+ server_object: Optional server object name
151
+ server_args: Optional command line arguments to inject
152
+
153
+ Returns:
154
+ The imported server object
155
+ """
156
+ if server_args:
157
+ original_argv = sys.argv[:]
158
+ try:
159
+ sys.argv = [str(file)] + server_args
160
+ return import_server(file, server_object)
161
+ finally:
162
+ sys.argv = original_argv
163
+ else:
164
+ return import_server(file, server_object)
165
+
166
+
138
167
  def run_command(
139
168
  server_spec: str,
140
169
  transport: str | None = None,
141
170
  host: str | None = None,
142
171
  port: int | None = None,
143
172
  log_level: str | None = None,
173
+ server_args: list[str] | None = None,
144
174
  ) -> None:
145
175
  """Run a MCP server or connect to a remote one.
146
176
 
@@ -150,6 +180,7 @@ def run_command(
150
180
  host: Host to bind to when using http transport
151
181
  port: Port to bind to when using http transport
152
182
  log_level: Log level
183
+ server_args: Additional arguments to pass to the server
153
184
  """
154
185
  if is_url(server_spec):
155
186
  # Handle URL case
@@ -158,7 +189,7 @@ def run_command(
158
189
  else:
159
190
  # Handle file case
160
191
  file, server_object = parse_file_path(server_spec)
161
- server = import_server(file, server_object)
192
+ server = import_server_with_args(file, server_object, server_args)
162
193
  logger.debug(f'Found server "{server.name}" in {file}')
163
194
 
164
195
  # Run the server
@@ -27,15 +27,12 @@ from mcp.client.session import (
27
27
  MessageHandlerFnT,
28
28
  SamplingFnT,
29
29
  )
30
- from mcp.client.sse import sse_client
31
- from mcp.client.stdio import stdio_client
32
- from mcp.client.streamable_http import streamablehttp_client
33
- from mcp.client.websocket import websocket_client
34
30
  from mcp.server.fastmcp import FastMCP as FastMCP1Server
35
31
  from mcp.shared.memory import create_connected_server_and_client_session
36
32
  from pydantic import AnyUrl
37
33
  from typing_extensions import Unpack
38
34
 
35
+ from fastmcp.client.auth.bearer import BearerAuth
39
36
  from fastmcp.client.auth.oauth import OAuth
40
37
  from fastmcp.server.dependencies import get_http_headers
41
38
  from fastmcp.server.server import FastMCP
@@ -141,6 +138,13 @@ class WSTransport(ClientTransport):
141
138
  async def connect_session(
142
139
  self, **session_kwargs: Unpack[SessionKwargs]
143
140
  ) -> AsyncIterator[ClientSession]:
141
+ try:
142
+ from mcp.client.websocket import websocket_client
143
+ except ImportError:
144
+ raise ImportError(
145
+ "The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
146
+ )
147
+
144
148
  async with websocket_client(self.url) as transport:
145
149
  read_stream, write_stream = transport
146
150
  async with ClientSession(
@@ -149,7 +153,7 @@ class WSTransport(ClientTransport):
149
153
  yield session
150
154
 
151
155
  def __repr__(self) -> str:
152
- return f"<WebSocket(url='{self.url}')>"
156
+ return f"<WebSocketTransport(url='{self.url}')>"
153
157
 
154
158
 
155
159
  class SSETransport(ClientTransport):
@@ -180,14 +184,15 @@ class SSETransport(ClientTransport):
180
184
  if auth == "oauth":
181
185
  auth = OAuth(self.url)
182
186
  elif isinstance(auth, str):
183
- self.headers["Authorization"] = auth
184
- auth = None
187
+ auth = BearerAuth(auth)
185
188
  self.auth = auth
186
189
 
187
190
  @contextlib.asynccontextmanager
188
191
  async def connect_session(
189
192
  self, **session_kwargs: Unpack[SessionKwargs]
190
193
  ) -> AsyncIterator[ClientSession]:
194
+ from mcp.client.sse import sse_client
195
+
191
196
  client_kwargs: dict[str, Any] = {}
192
197
 
193
198
  # load headers from an active HTTP request, if available. This will only be true
@@ -216,7 +221,7 @@ class SSETransport(ClientTransport):
216
221
  yield session
217
222
 
218
223
  def __repr__(self) -> str:
219
- return f"<SSE(url='{self.url}')>"
224
+ return f"<SSETransport(url='{self.url}')>"
220
225
 
221
226
 
222
227
  class StreamableHttpTransport(ClientTransport):
@@ -247,14 +252,15 @@ class StreamableHttpTransport(ClientTransport):
247
252
  if auth == "oauth":
248
253
  auth = OAuth(self.url)
249
254
  elif isinstance(auth, str):
250
- self.headers["Authorization"] = auth
251
- auth = None
255
+ auth = BearerAuth(auth)
252
256
  self.auth = auth
253
257
 
254
258
  @contextlib.asynccontextmanager
255
259
  async def connect_session(
256
260
  self, **session_kwargs: Unpack[SessionKwargs]
257
261
  ) -> AsyncIterator[ClientSession]:
262
+ from mcp.client.streamable_http import streamablehttp_client
263
+
258
264
  client_kwargs: dict[str, Any] = {}
259
265
 
260
266
  # load headers from an active HTTP request, if available. This will only be true
@@ -284,7 +290,7 @@ class StreamableHttpTransport(ClientTransport):
284
290
  yield session
285
291
 
286
292
  def __repr__(self) -> str:
287
- return f"<StreamableHttp(url='{self.url}')>"
293
+ return f"<StreamableHttpTransport(url='{self.url}')>"
288
294
 
289
295
 
290
296
  class StdioTransport(ClientTransport):
@@ -350,6 +356,8 @@ class StdioTransport(ClientTransport):
350
356
  return
351
357
 
352
358
  async def _connect_task():
359
+ from mcp.client.stdio import stdio_client
360
+
353
361
  async with contextlib.AsyncExitStack() as stack:
354
362
  try:
355
363
  server_params = StdioServerParameters(
@@ -674,7 +682,7 @@ class FastMCPTransport(ClientTransport):
674
682
  yield session
675
683
 
676
684
  def __repr__(self) -> str:
677
- return f"<FastMCP(server='{self.server.name}')>"
685
+ return f"<FastMCPTransport(server='{self.server.name}')>"
678
686
 
679
687
 
680
688
  class MCPConfigTransport(ClientTransport):
@@ -760,7 +768,7 @@ class MCPConfigTransport(ClientTransport):
760
768
  yield session
761
769
 
762
770
  def __repr__(self) -> str:
763
- return f"<MCPConfig(config='{self.config}')>"
771
+ return f"<MCPConfigTransport(config='{self.config}')>"
764
772
 
765
773
 
766
774
  @overload
@@ -6,7 +6,7 @@ from fastmcp.contrib.bulk_tool_caller import BulkToolCaller
6
6
  mcp = FastMCP()
7
7
 
8
8
 
9
- @mcp.tool()
9
+ @mcp.tool
10
10
  def echo_tool(text: str) -> str:
11
11
  """Echo the input text"""
12
12
  return text
@@ -3,6 +3,10 @@
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
+ from fastmcp.prompts.prompt import Prompt
7
+ from fastmcp.resources.resource import Resource
8
+ from fastmcp.tools.tool import Tool
9
+
6
10
  if TYPE_CHECKING:
7
11
  from fastmcp.server import FastMCP
8
12
 
@@ -128,7 +132,8 @@ class MCPMixin:
128
132
  registration_info["name"] = (
129
133
  f"{prefix}{separator}{registration_info['name']}"
130
134
  )
131
- mcp_server.add_tool(fn=method, **registration_info)
135
+ tool = Tool.from_function(fn=method, **registration_info)
136
+ mcp_server.add_tool(tool)
132
137
 
133
138
  def register_resources(
134
139
  self,
@@ -156,7 +161,8 @@ class MCPMixin:
156
161
  registration_info["uri"] = (
157
162
  f"{prefix}{separator}{registration_info['uri']}"
158
163
  )
159
- mcp_server.add_resource_fn(fn=method, **registration_info)
164
+ resource = Resource.from_function(fn=method, **registration_info)
165
+ mcp_server.add_resource(resource)
160
166
 
161
167
  def register_prompts(
162
168
  self,
@@ -180,7 +186,8 @@ class MCPMixin:
180
186
  registration_info["name"] = (
181
187
  f"{prefix}{separator}{registration_info['name']}"
182
188
  )
183
- mcp_server.add_prompt(fn=method, **registration_info)
189
+ prompt = Prompt.from_function(fn=method, **registration_info)
190
+ mcp_server.add_prompt(prompt)
184
191
 
185
192
  def register_all(
186
193
  self,
fastmcp/prompts/prompt.py CHANGED
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations as _annotations
4
4
 
5
5
  import inspect
6
+ from abc import ABC, abstractmethod
6
7
  from collections.abc import Awaitable, Callable, Sequence
7
8
  from typing import TYPE_CHECKING, Annotated, Any
8
9
 
@@ -10,13 +11,14 @@ import pydantic_core
10
11
  from mcp.types import EmbeddedResource, ImageContent, PromptMessage, Role, TextContent
11
12
  from mcp.types import Prompt as MCPPrompt
12
13
  from mcp.types import PromptArgument as MCPPromptArgument
13
- from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
+ from pydantic import BeforeValidator, Field, TypeAdapter, validate_call
14
15
 
15
16
  from fastmcp.exceptions import PromptError
16
17
  from fastmcp.server.dependencies import get_context
17
18
  from fastmcp.utilities.json_schema import compress_schema
18
19
  from fastmcp.utilities.logging import get_logger
19
20
  from fastmcp.utilities.types import (
21
+ FastMCPBaseModel,
20
22
  _convert_set_defaults,
21
23
  find_kwarg_by_type,
22
24
  get_cached_typeadapter,
@@ -52,31 +54,86 @@ SyncPromptResult = (
52
54
  PromptResult = SyncPromptResult | Awaitable[SyncPromptResult]
53
55
 
54
56
 
55
- class PromptArgument(BaseModel):
57
+ class PromptArgument(FastMCPBaseModel):
56
58
  """An argument that can be passed to a prompt."""
57
59
 
58
60
  name: str = Field(description="Name of the argument")
59
61
  description: str | None = Field(
60
- None, description="Description of what the argument does"
62
+ default=None, description="Description of what the argument does"
61
63
  )
62
64
  required: bool = Field(
63
65
  default=False, description="Whether the argument is required"
64
66
  )
65
67
 
66
68
 
67
- class Prompt(BaseModel):
69
+ class Prompt(FastMCPBaseModel, ABC):
68
70
  """A prompt template that can be rendered with parameters."""
69
71
 
70
72
  name: str = Field(description="Name of the prompt")
71
73
  description: str | None = Field(
72
- None, description="Description of what the prompt does"
74
+ default=None, description="Description of what the prompt does"
73
75
  )
74
76
  tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
75
77
  default_factory=set, description="Tags for the prompt"
76
78
  )
77
79
  arguments: list[PromptArgument] | None = Field(
78
- None, description="Arguments that can be passed to the prompt"
80
+ default=None, description="Arguments that can be passed to the prompt"
79
81
  )
82
+
83
+ def __eq__(self, other: object) -> bool:
84
+ if type(self) is not type(other):
85
+ return False
86
+ assert isinstance(other, type(self))
87
+ return self.model_dump() == other.model_dump()
88
+
89
+ def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
90
+ """Convert the prompt to an MCP prompt."""
91
+ arguments = [
92
+ MCPPromptArgument(
93
+ name=arg.name,
94
+ description=arg.description,
95
+ required=arg.required,
96
+ )
97
+ for arg in self.arguments or []
98
+ ]
99
+ kwargs = {
100
+ "name": self.name,
101
+ "description": self.description,
102
+ "arguments": arguments,
103
+ }
104
+ return MCPPrompt(**kwargs | overrides)
105
+
106
+ @staticmethod
107
+ def from_function(
108
+ fn: Callable[..., PromptResult | Awaitable[PromptResult]],
109
+ name: str | None = None,
110
+ description: str | None = None,
111
+ tags: set[str] | None = None,
112
+ ) -> FunctionPrompt:
113
+ """Create a Prompt from a function.
114
+
115
+ The function can return:
116
+ - A string (converted to a message)
117
+ - A Message object
118
+ - A dict (converted to a message)
119
+ - A sequence of any of the above
120
+ """
121
+ return FunctionPrompt.from_function(
122
+ fn=fn, name=name, description=description, tags=tags
123
+ )
124
+
125
+ @abstractmethod
126
+ async def render(
127
+ self,
128
+ arguments: dict[str, Any] | None = None,
129
+ ) -> list[PromptMessage]:
130
+ """Render the prompt with arguments."""
131
+ raise NotImplementedError("Prompt.render() must be implemented by subclasses")
132
+
133
+
134
+ class FunctionPrompt(Prompt):
135
+ """A prompt that is a function."""
136
+
80
137
  fn: Callable[..., PromptResult | Awaitable[PromptResult]]
81
138
 
82
139
  @classmethod
@@ -86,7 +143,7 @@ class Prompt(BaseModel):
86
143
  name: str | None = None,
87
144
  description: str | None = None,
88
145
  tags: set[str] | None = None,
89
- ) -> Prompt:
146
+ ) -> FunctionPrompt:
90
147
  """Create a Prompt from a function.
91
148
 
92
149
  The function can return:
@@ -114,6 +171,9 @@ class Prompt(BaseModel):
114
171
  # if the fn is a callable class, we need to get the __call__ method from here out
115
172
  if not inspect.isroutine(fn):
116
173
  fn = fn.__call__
174
+ # if the fn is a staticmethod, we need to work with the underlying function
175
+ if isinstance(fn, staticmethod):
176
+ fn = fn.__func__
117
177
 
118
178
  type_adapter = get_cached_typeadapter(fn)
119
179
  parameters = type_adapter.json_schema()
@@ -147,8 +207,8 @@ class Prompt(BaseModel):
147
207
  name=func_name,
148
208
  description=description,
149
209
  arguments=arguments,
150
- fn=fn,
151
210
  tags=tags or set(),
211
+ fn=fn,
152
212
  )
153
213
 
154
214
  async def render(
@@ -212,25 +272,3 @@ class Prompt(BaseModel):
212
272
  except Exception as e:
213
273
  logger.exception(f"Error rendering prompt {self.name}: {e}")
214
274
  raise PromptError(f"Error rendering prompt {self.name}.")
215
-
216
- def __eq__(self, other: object) -> bool:
217
- if not isinstance(other, Prompt):
218
- return False
219
- return self.model_dump() == other.model_dump()
220
-
221
- def to_mcp_prompt(self, **overrides: Any) -> MCPPrompt:
222
- """Convert the prompt to an MCP prompt."""
223
- arguments = [
224
- MCPPromptArgument(
225
- name=arg.name,
226
- description=arg.description,
227
- required=arg.required,
228
- )
229
- for arg in self.arguments or []
230
- ]
231
- kwargs = {
232
- "name": self.name,
233
- "description": self.description,
234
- "arguments": arguments,
235
- }
236
- return MCPPrompt(**kwargs | overrides)
@@ -1,14 +1,13 @@
1
- """Prompt management functionality."""
2
-
3
1
  from __future__ import annotations as _annotations
4
2
 
3
+ import warnings
5
4
  from collections.abc import Awaitable, Callable
6
5
  from typing import TYPE_CHECKING, Any
7
6
 
8
7
  from mcp import GetPromptResult
9
8
 
10
9
  from fastmcp.exceptions import NotFoundError, PromptError
11
- from fastmcp.prompts.prompt import Prompt, PromptResult
10
+ from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
12
11
  from fastmcp.settings import DuplicateBehavior
13
12
  from fastmcp.utilities.logging import get_logger
14
13
 
@@ -55,10 +54,18 @@ class PromptManager:
55
54
  name: str | None = None,
56
55
  description: str | None = None,
57
56
  tags: set[str] | None = None,
58
- ) -> Prompt:
57
+ ) -> FunctionPrompt:
59
58
  """Create a prompt from a function."""
60
- prompt = Prompt.from_function(fn, name=name, description=description, tags=tags)
61
- return self.add_prompt(prompt)
59
+ # deprecated in 2.7.0
60
+ warnings.warn(
61
+ "PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.",
62
+ DeprecationWarning,
63
+ stacklevel=2,
64
+ )
65
+ prompt = FunctionPrompt.from_function(
66
+ fn, name=name, description=description, tags=tags
67
+ )
68
+ return self.add_prompt(prompt) # type: ignore
62
69
 
63
70
  def add_prompt(self, prompt: Prompt, key: str | None = None) -> Prompt:
64
71
  """Add a prompt to the manager."""
@@ -1,10 +1,9 @@
1
- from .resource import Resource
1
+ from .resource import FunctionResource, Resource
2
2
  from .template import ResourceTemplate
3
3
  from .types import (
4
4
  BinaryResource,
5
5
  DirectoryResource,
6
6
  FileResource,
7
- FunctionResource,
8
7
  HttpResource,
9
8
  TextResource,
10
9
  )
@@ -3,12 +3,14 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import abc
6
+ import inspect
7
+ from collections.abc import Callable
6
8
  from typing import TYPE_CHECKING, Annotated, Any
7
9
 
10
+ import pydantic_core
8
11
  from mcp.types import Resource as MCPResource
9
12
  from pydantic import (
10
13
  AnyUrl,
11
- BaseModel,
12
14
  BeforeValidator,
13
15
  ConfigDict,
14
16
  Field,
@@ -17,13 +19,18 @@ from pydantic import (
17
19
  field_validator,
18
20
  )
19
21
 
20
- from fastmcp.utilities.types import _convert_set_defaults
22
+ from fastmcp.server.dependencies import get_context
23
+ from fastmcp.utilities.types import (
24
+ FastMCPBaseModel,
25
+ _convert_set_defaults,
26
+ find_kwarg_by_type,
27
+ )
21
28
 
22
29
  if TYPE_CHECKING:
23
30
  pass
24
31
 
25
32
 
26
- class Resource(BaseModel, abc.ABC):
33
+ class Resource(FastMCPBaseModel, abc.ABC):
27
34
  """Base class for all resources."""
28
35
 
29
36
  model_config = ConfigDict(validate_default=True)
@@ -31,9 +38,9 @@ class Resource(BaseModel, abc.ABC):
31
38
  uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(
32
39
  default=..., description="URI of the resource"
33
40
  )
34
- name: str | None = Field(description="Name of the resource", default=None)
41
+ name: str | None = Field(default=None, description="Name of the resource")
35
42
  description: str | None = Field(
36
- description="Description of the resource", default=None
43
+ default=None, description="Description of the resource"
37
44
  )
38
45
  tags: Annotated[set[str], BeforeValidator(_convert_set_defaults)] = Field(
39
46
  default_factory=set, description="Tags for the resource"
@@ -44,6 +51,24 @@ class Resource(BaseModel, abc.ABC):
44
51
  pattern=r"^[a-zA-Z0-9]+/[a-zA-Z0-9\-+.]+$",
45
52
  )
46
53
 
54
+ @staticmethod
55
+ def from_function(
56
+ fn: Callable[[], Any],
57
+ uri: str | AnyUrl,
58
+ name: str | None = None,
59
+ description: str | None = None,
60
+ mime_type: str | None = None,
61
+ tags: set[str] | None = None,
62
+ ) -> FunctionResource:
63
+ return FunctionResource.from_function(
64
+ fn=fn,
65
+ uri=uri,
66
+ name=name,
67
+ description=description,
68
+ mime_type=mime_type,
69
+ tags=tags,
70
+ )
71
+
47
72
  @field_validator("mime_type", mode="before")
48
73
  @classmethod
49
74
  def set_default_mime_type(cls, mime_type: str | None) -> str:
@@ -68,8 +93,9 @@ class Resource(BaseModel, abc.ABC):
68
93
  pass
69
94
 
70
95
  def __eq__(self, other: object) -> bool:
71
- if not isinstance(other, Resource):
96
+ if type(self) is not type(other):
72
97
  return False
98
+ assert isinstance(other, type(self))
73
99
  return self.model_dump() == other.model_dump()
74
100
 
75
101
  def to_mcp_resource(self, **overrides: Any) -> MCPResource:
@@ -81,3 +107,63 @@ class Resource(BaseModel, abc.ABC):
81
107
  "mimeType": self.mime_type,
82
108
  }
83
109
  return MCPResource(**kwargs | overrides)
110
+
111
+
112
+ class FunctionResource(Resource):
113
+ """A resource that defers data loading by wrapping a function.
114
+
115
+ The function is only called when the resource is read, allowing for lazy loading
116
+ of potentially expensive data. This is particularly useful when listing resources,
117
+ as the function won't be called until the resource is actually accessed.
118
+
119
+ The function can return:
120
+ - str for text content (default)
121
+ - bytes for binary content
122
+ - other types will be converted to JSON
123
+ """
124
+
125
+ fn: Callable[[], Any]
126
+
127
+ @classmethod
128
+ def from_function(
129
+ cls,
130
+ fn: Callable[[], Any],
131
+ uri: str | AnyUrl,
132
+ name: str | None = None,
133
+ description: str | None = None,
134
+ mime_type: str | None = None,
135
+ tags: set[str] | None = None,
136
+ ) -> FunctionResource:
137
+ """Create a FunctionResource from a function."""
138
+ if isinstance(uri, str):
139
+ uri = AnyUrl(uri)
140
+ return cls(
141
+ fn=fn,
142
+ uri=uri,
143
+ name=name or fn.__name__,
144
+ description=description or fn.__doc__,
145
+ mime_type=mime_type or "text/plain",
146
+ tags=tags or set(),
147
+ )
148
+
149
+ async def read(self) -> str | bytes:
150
+ """Read the resource by calling the wrapped function."""
151
+ from fastmcp.server.context import Context
152
+
153
+ kwargs = {}
154
+ context_kwarg = find_kwarg_by_type(self.fn, kwarg_type=Context)
155
+ if context_kwarg is not None:
156
+ kwargs[context_kwarg] = get_context()
157
+
158
+ result = self.fn(**kwargs)
159
+ if inspect.iscoroutinefunction(self.fn):
160
+ result = await result
161
+
162
+ if isinstance(result, Resource):
163
+ return await result.read()
164
+ elif isinstance(result, bytes):
165
+ return result
166
+ elif isinstance(result, str):
167
+ return result
168
+ else:
169
+ return pydantic_core.to_json(result, fallback=str, indent=2).decode()