fastmcp 2.6.0__py3-none-any.whl → 2.7.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.
fastmcp/cli/cli.py CHANGED
@@ -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
@@ -68,9 +68,6 @@ class ServerOAuthMetadata(_MCPServerOAuthMetadata):
68
68
  class OAuthClientProvider(_MCPOAuthClientProvider):
69
69
  """
70
70
  OAuth client provider with more flexible OAuth metadata discovery.
71
-
72
- This subclass handles real-world OAuth servers that may not conform
73
- strictly to the MCP OAuth specification but are still valid OAuth 2.0 servers.
74
71
  """
75
72
 
76
73
  async def _discover_oauth_metadata(
@@ -27,10 +27,6 @@ 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
@@ -141,6 +137,13 @@ class WSTransport(ClientTransport):
141
137
  async def connect_session(
142
138
  self, **session_kwargs: Unpack[SessionKwargs]
143
139
  ) -> AsyncIterator[ClientSession]:
140
+ try:
141
+ from mcp.client.websocket import websocket_client
142
+ except ImportError:
143
+ raise ImportError(
144
+ "The websocket transport is not available. Please install fastmcp[websockets] or install the websockets package manually."
145
+ )
146
+
144
147
  async with websocket_client(self.url) as transport:
145
148
  read_stream, write_stream = transport
146
149
  async with ClientSession(
@@ -188,6 +191,8 @@ class SSETransport(ClientTransport):
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
@@ -255,6 +260,8 @@ class StreamableHttpTransport(ClientTransport):
255
260
  async def connect_session(
256
261
  self, **session_kwargs: Unpack[SessionKwargs]
257
262
  ) -> AsyncIterator[ClientSession]:
263
+ from mcp.client.streamable_http import streamablehttp_client
264
+
258
265
  client_kwargs: dict[str, Any] = {}
259
266
 
260
267
  # load headers from an active HTTP request, if available. This will only be true
@@ -350,6 +357,8 @@ class StdioTransport(ClientTransport):
350
357
  return
351
358
 
352
359
  async def _connect_task():
360
+ from mcp.client.stdio import stdio_client
361
+
353
362
  async with contextlib.AsyncExitStack() as stack:
354
363
  try:
355
364
  server_params = StdioServerParameters(
@@ -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,7 +54,7 @@ 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")
@@ -64,7 +66,7 @@ class PromptArgument(BaseModel):
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")
@@ -77,6 +79,61 @@ class Prompt(BaseModel):
77
79
  arguments: list[PromptArgument] | None = Field(
78
80
  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."""
fastmcp/py.typed ADDED
File without changes
@@ -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)
@@ -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()
@@ -1,13 +1,13 @@
1
1
  """Resource manager functionality."""
2
2
 
3
3
  import inspect
4
+ import warnings
4
5
  from collections.abc import Callable
5
6
  from typing import Any
6
7
 
7
8
  from pydantic import AnyUrl
8
9
 
9
10
  from fastmcp.exceptions import NotFoundError, ResourceError
10
- from fastmcp.resources import FunctionResource
11
11
  from fastmcp.resources.resource import Resource
12
12
  from fastmcp.resources.template import (
13
13
  ResourceTemplate,
@@ -121,13 +121,19 @@ class ResourceManager:
121
121
  The added resource. If a resource with the same URI already exists,
122
122
  returns the existing resource.
123
123
  """
124
- resource = FunctionResource(
124
+ # deprecated in 2.7.0
125
+ warnings.warn(
126
+ "add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
127
+ DeprecationWarning,
128
+ stacklevel=2,
129
+ )
130
+ resource = Resource.from_function(
125
131
  fn=fn,
126
- uri=AnyUrl(uri),
132
+ uri=uri,
127
133
  name=name,
128
134
  description=description,
129
- mime_type=mime_type or "text/plain",
130
- tags=tags or set(),
135
+ mime_type=mime_type,
136
+ tags=tags,
131
137
  )
132
138
  return self.add_resource(resource)
133
139
 
@@ -172,7 +178,12 @@ class ResourceManager:
172
178
  tags: set[str] | None = None,
173
179
  ) -> ResourceTemplate:
174
180
  """Create a template from a function."""
175
-
181
+ # deprecated in 2.7.0
182
+ warnings.warn(
183
+ "add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
184
+ DeprecationWarning,
185
+ stacklevel=2,
186
+ )
176
187
  template = ResourceTemplate.from_function(
177
188
  fn,
178
189
  uri_template=uri_template,