fastmcp 2.12.5__py3-none-any.whl → 2.13.0rc2__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.
Files changed (68) hide show
  1. fastmcp/cli/cli.py +6 -6
  2. fastmcp/cli/install/claude_code.py +3 -3
  3. fastmcp/cli/install/claude_desktop.py +3 -3
  4. fastmcp/cli/install/cursor.py +7 -7
  5. fastmcp/cli/install/gemini_cli.py +3 -3
  6. fastmcp/cli/install/mcp_json.py +3 -3
  7. fastmcp/cli/run.py +13 -8
  8. fastmcp/client/auth/oauth.py +100 -208
  9. fastmcp/client/client.py +11 -11
  10. fastmcp/client/logging.py +18 -14
  11. fastmcp/client/oauth_callback.py +81 -171
  12. fastmcp/client/transports.py +76 -22
  13. fastmcp/contrib/component_manager/component_service.py +6 -6
  14. fastmcp/contrib/mcp_mixin/README.md +32 -1
  15. fastmcp/contrib/mcp_mixin/mcp_mixin.py +14 -2
  16. fastmcp/experimental/utilities/openapi/json_schema_converter.py +4 -0
  17. fastmcp/experimental/utilities/openapi/parser.py +23 -3
  18. fastmcp/prompts/prompt.py +13 -6
  19. fastmcp/prompts/prompt_manager.py +16 -101
  20. fastmcp/resources/resource.py +13 -6
  21. fastmcp/resources/resource_manager.py +5 -164
  22. fastmcp/resources/template.py +107 -17
  23. fastmcp/server/auth/auth.py +40 -32
  24. fastmcp/server/auth/jwt_issuer.py +289 -0
  25. fastmcp/server/auth/oauth_proxy.py +1228 -233
  26. fastmcp/server/auth/oidc_proxy.py +8 -6
  27. fastmcp/server/auth/providers/auth0.py +13 -7
  28. fastmcp/server/auth/providers/aws.py +14 -3
  29. fastmcp/server/auth/providers/azure.py +137 -124
  30. fastmcp/server/auth/providers/descope.py +4 -6
  31. fastmcp/server/auth/providers/github.py +14 -8
  32. fastmcp/server/auth/providers/google.py +15 -9
  33. fastmcp/server/auth/providers/introspection.py +281 -0
  34. fastmcp/server/auth/providers/jwt.py +8 -2
  35. fastmcp/server/auth/providers/scalekit.py +179 -0
  36. fastmcp/server/auth/providers/supabase.py +172 -0
  37. fastmcp/server/auth/providers/workos.py +17 -14
  38. fastmcp/server/context.py +89 -34
  39. fastmcp/server/http.py +57 -17
  40. fastmcp/server/low_level.py +121 -2
  41. fastmcp/server/middleware/caching.py +469 -0
  42. fastmcp/server/middleware/error_handling.py +6 -2
  43. fastmcp/server/middleware/logging.py +48 -37
  44. fastmcp/server/middleware/middleware.py +28 -15
  45. fastmcp/server/middleware/rate_limiting.py +3 -3
  46. fastmcp/server/proxy.py +6 -6
  47. fastmcp/server/server.py +638 -183
  48. fastmcp/settings.py +22 -9
  49. fastmcp/tools/tool.py +7 -3
  50. fastmcp/tools/tool_manager.py +22 -108
  51. fastmcp/tools/tool_transform.py +3 -3
  52. fastmcp/utilities/cli.py +32 -22
  53. fastmcp/utilities/components.py +5 -0
  54. fastmcp/utilities/inspect.py +77 -21
  55. fastmcp/utilities/logging.py +118 -8
  56. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +6 -6
  57. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +3 -3
  58. fastmcp/utilities/mcp_server_config/v1/schema.json +3 -0
  59. fastmcp/utilities/tests.py +87 -4
  60. fastmcp/utilities/types.py +1 -1
  61. fastmcp/utilities/ui.py +497 -0
  62. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/METADATA +8 -4
  63. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/RECORD +66 -62
  64. fastmcp/cli/claude.py +0 -135
  65. fastmcp/utilities/storage.py +0 -204
  66. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/WHEEL +0 -0
  67. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/entry_points.txt +0 -0
  68. {fastmcp-2.12.5.dist-info → fastmcp-2.13.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -11,12 +11,15 @@ Tools:
11
11
  * [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools)
12
12
  * [annotations](https://gofastmcp.com/servers/tools#annotations-2)
13
13
  * [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments)
14
+ * [meta](https://gofastmcp.com/servers/tools#param-meta)
14
15
 
15
16
  Prompts:
16
17
  * [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts)
18
+ * [meta](https://gofastmcp.com/servers/prompts#param-meta)
17
19
 
18
20
  Resources:
19
21
  * [enable/disable](https://gofastmcp.com/servers/resources#disabling-resources)
22
+ * [meta](https://gofastmcp.com/servers/resources#param-meta)
20
23
 
21
24
  ## Usage
22
25
 
@@ -78,7 +81,16 @@ class MyComponent(MCPMixin):
78
81
  if delete_all:
79
82
  return "99 records deleted. I bet you're not a tool :)"
80
83
  return "Tool executed, but you might be a tool!"
81
-
84
+
85
+ # example tool w/ meta
86
+ @mcp_tool(
87
+ name="data_tool",
88
+ description="Fetches user data from database",
89
+ meta={"version": "2.0", "category": "database", "author": "dev-team"}
90
+ )
91
+ def data_tool_method(self, user_id: int):
92
+ return f"Fetching data for user {user_id}"
93
+
82
94
  @mcp_resource(uri="component://data")
83
95
  def resource_method(self):
84
96
  return {"data": "some data"}
@@ -88,6 +100,15 @@ class MyComponent(MCPMixin):
88
100
  def resource_method(self):
89
101
  return {"data": "some data"}
90
102
 
103
+ # example resource w/meta and title
104
+ @mcp_resource(
105
+ uri="component://config",
106
+ title="Data resource Title,
107
+ meta={"internal": True, "cache_ttl": 3600, "priority": "high"}
108
+ )
109
+ def config_resource_method(self):
110
+ return {"config": "data"}
111
+
91
112
  # prompt
92
113
  @mcp_prompt(name="A prompt")
93
114
  def prompt_method(self, name):
@@ -98,6 +119,16 @@ class MyComponent(MCPMixin):
98
119
  def prompt_method(self, name):
99
120
  return f"What's up {name}?"
100
121
 
122
+ # example prompt w/title and meta
123
+ @mcp_prompt(
124
+ name="analysis_prompt",
125
+ title="Data Analysis Prompt",
126
+ description="Analyzes data patterns",
127
+ meta={"complexity": "high", "domain": "analytics", "requires_context": True}
128
+ )
129
+ def analysis_prompt_method(self, dataset: str):
130
+ return f"Analyze the patterns in {dataset}"
131
+
101
132
  mcp_server = FastMCP()
102
133
  component = MyComponent()
103
134
 
@@ -3,7 +3,7 @@
3
3
  from collections.abc import Callable
4
4
  from typing import TYPE_CHECKING, Any
5
5
 
6
- from mcp.types import ToolAnnotations
6
+ from mcp.types import Annotations, ToolAnnotations
7
7
 
8
8
  from fastmcp.prompts.prompt import Prompt
9
9
  from fastmcp.resources.resource import Resource
@@ -29,6 +29,7 @@ def mcp_tool(
29
29
  annotations: ToolAnnotations | dict[str, Any] | None = None,
30
30
  exclude_args: list[str] | None = None,
31
31
  serializer: Callable[[Any], str] | None = None,
32
+ meta: dict[str, Any] | None = None,
32
33
  enabled: bool | None = None,
33
34
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
34
35
  """Decorator to mark a method as an MCP tool for later registration."""
@@ -41,6 +42,7 @@ def mcp_tool(
41
42
  "annotations": annotations,
42
43
  "exclude_args": exclude_args,
43
44
  "serializer": serializer,
45
+ "meta": meta,
44
46
  "enabled": enabled,
45
47
  }
46
48
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -54,9 +56,12 @@ def mcp_resource(
54
56
  uri: str,
55
57
  *,
56
58
  name: str | None = None,
59
+ title: str | None = None,
57
60
  description: str | None = None,
58
61
  mime_type: str | None = None,
59
62
  tags: set[str] | None = None,
63
+ annotations: Annotations | None = None,
64
+ meta: dict[str, Any] | None = None,
60
65
  enabled: bool | None = None,
61
66
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
62
67
  """Decorator to mark a method as an MCP resource for later registration."""
@@ -65,9 +70,12 @@ def mcp_resource(
65
70
  call_args = {
66
71
  "uri": uri,
67
72
  "name": name or get_fn_name(func),
73
+ "title": title,
68
74
  "description": description,
69
75
  "mime_type": mime_type,
70
76
  "tags": tags,
77
+ "annotations": annotations,
78
+ "meta": meta,
71
79
  "enabled": enabled,
72
80
  }
73
81
  call_args = {k: v for k, v in call_args.items() if v is not None}
@@ -81,8 +89,10 @@ def mcp_resource(
81
89
 
82
90
  def mcp_prompt(
83
91
  name: str | None = None,
92
+ title: str | None = None,
84
93
  description: str | None = None,
85
94
  tags: set[str] | None = None,
95
+ meta: dict[str, Any] | None = None,
86
96
  enabled: bool | None = None,
87
97
  ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
88
98
  """Decorator to mark a method as an MCP prompt for later registration."""
@@ -90,8 +100,10 @@ def mcp_prompt(
90
100
  def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
91
101
  call_args = {
92
102
  "name": name or get_fn_name(func),
103
+ "title": title,
93
104
  "description": description,
94
105
  "tags": tags,
106
+ "meta": meta,
95
107
  "enabled": enabled,
96
108
  }
97
109
 
@@ -151,7 +163,6 @@ class MCPMixin:
151
163
  tool = Tool.from_function(
152
164
  fn=method,
153
165
  name=registration_info.get("name"),
154
- title=registration_info.get("title"),
155
166
  description=registration_info.get("description"),
156
167
  tags=registration_info.get("tags"),
157
168
  annotations=registration_info.get("annotations"),
@@ -195,6 +206,7 @@ class MCPMixin:
195
206
  fn=method,
196
207
  uri=registration_info["uri"],
197
208
  name=registration_info.get("name"),
209
+ title=registration_info.get("title"),
198
210
  description=registration_info.get("description"),
199
211
  mime_type=registration_info.get("mime_type"),
200
212
  tags=registration_info.get("tags"),
@@ -176,6 +176,10 @@ def _convert_nullable_field(schema: dict[str, Any]) -> dict[str, Any]:
176
176
  # Wrap allOf in anyOf with null option
177
177
  result["anyOf"] = [{"allOf": result.pop("allOf")}, {"type": "null"}]
178
178
 
179
+ # Handle enum fields - add null to enum values if present
180
+ if "enum" in result and None not in result["enum"]:
181
+ result["enum"] = result["enum"] + [None]
182
+
179
183
  return result
180
184
 
181
185
 
@@ -474,9 +474,22 @@ class OpenAPIParser(
474
474
  and media_type_obj.media_type_schema
475
475
  ):
476
476
  try:
477
- schema_dict = self._extract_schema_as_dict(
478
- media_type_obj.media_type_schema
479
- )
477
+ # Track if this is a top-level $ref before resolution
478
+ top_level_schema_name = None
479
+ media_schema = media_type_obj.media_type_schema
480
+ if isinstance(media_schema, self.reference_cls):
481
+ ref_str = media_schema.ref
482
+ if isinstance(ref_str, str) and ref_str.startswith(
483
+ "#/components/schemas/"
484
+ ):
485
+ top_level_schema_name = ref_str.split("/")[-1]
486
+
487
+ schema_dict = self._extract_schema_as_dict(media_schema)
488
+ # Add marker for top-level schema if it was a ref
489
+ if top_level_schema_name:
490
+ schema_dict["x-fastmcp-top-level-schema"] = (
491
+ top_level_schema_name
492
+ )
480
493
  resp_info.content_schema[media_type_str] = schema_dict
481
494
  except ValueError as e:
482
495
  # Re-raise ValueError for external reference errors
@@ -625,6 +638,13 @@ class OpenAPIParser(
625
638
  for response in responses.values():
626
639
  if response.content_schema:
627
640
  for content_schema in response.content_schema.values():
641
+ # Check if this schema was originally a top-level $ref
642
+ if "x-fastmcp-top-level-schema" in content_schema:
643
+ schema_name = content_schema["x-fastmcp-top-level-schema"]
644
+ if schema_name in all_schemas:
645
+ needed_schemas.add(schema_name)
646
+
647
+ # Extract all dependencies (transitive refs within the schema)
628
648
  deps = self._extract_schema_dependencies(
629
649
  content_schema, all_schemas
630
650
  )
fastmcp/prompts/prompt.py CHANGED
@@ -4,12 +4,11 @@ from __future__ import annotations as _annotations
4
4
 
5
5
  import inspect
6
6
  import json
7
- from abc import ABC, abstractmethod
8
7
  from collections.abc import Awaitable, Callable, Sequence
9
8
  from typing import Any
10
9
 
11
10
  import pydantic_core
12
- from mcp.types import ContentBlock, PromptMessage, Role, TextContent
11
+ from mcp.types import ContentBlock, Icon, PromptMessage, Role, TextContent
13
12
  from mcp.types import Prompt as MCPPrompt
14
13
  from mcp.types import PromptArgument as MCPPromptArgument
15
14
  from pydantic import Field, TypeAdapter
@@ -62,7 +61,7 @@ class PromptArgument(FastMCPBaseModel):
62
61
  )
63
62
 
64
63
 
65
- class Prompt(FastMCPComponent, ABC):
64
+ class Prompt(FastMCPComponent):
66
65
  """A prompt template that can be rendered with parameters."""
67
66
 
68
67
  arguments: list[PromptArgument] | None = Field(
@@ -106,6 +105,7 @@ class Prompt(FastMCPComponent, ABC):
106
105
  description=overrides.get("description", self.description),
107
106
  arguments=arguments,
108
107
  title=overrides.get("title", self.title),
108
+ icons=overrides.get("icons", self.icons),
109
109
  _meta=overrides.get(
110
110
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
111
111
  ),
@@ -117,6 +117,7 @@ class Prompt(FastMCPComponent, ABC):
117
117
  name: str | None = None,
118
118
  title: str | None = None,
119
119
  description: str | None = None,
120
+ icons: list[Icon] | None = None,
120
121
  tags: set[str] | None = None,
121
122
  enabled: bool | None = None,
122
123
  meta: dict[str, Any] | None = None,
@@ -134,18 +135,22 @@ class Prompt(FastMCPComponent, ABC):
134
135
  name=name,
135
136
  title=title,
136
137
  description=description,
138
+ icons=icons,
137
139
  tags=tags,
138
140
  enabled=enabled,
139
141
  meta=meta,
140
142
  )
141
143
 
142
- @abstractmethod
143
144
  async def render(
144
145
  self,
145
146
  arguments: dict[str, Any] | None = None,
146
147
  ) -> list[PromptMessage]:
147
- """Render the prompt with arguments."""
148
- raise NotImplementedError("Prompt.render() must be implemented by subclasses")
148
+ """Render the prompt with arguments.
149
+
150
+ This method is not implemented in the base Prompt class and must be
151
+ implemented by subclasses.
152
+ """
153
+ raise NotImplementedError("Subclasses must implement render()")
149
154
 
150
155
 
151
156
  class FunctionPrompt(Prompt):
@@ -160,6 +165,7 @@ class FunctionPrompt(Prompt):
160
165
  name: str | None = None,
161
166
  title: str | None = None,
162
167
  description: str | None = None,
168
+ icons: list[Icon] | None = None,
163
169
  tags: set[str] | None = None,
164
170
  enabled: bool | None = None,
165
171
  meta: dict[str, Any] | None = None,
@@ -253,6 +259,7 @@ class FunctionPrompt(Prompt):
253
259
  name=func_name,
254
260
  title=title,
255
261
  description=description,
262
+ icons=icons,
256
263
  arguments=arguments,
257
264
  tags=tags or set(),
258
265
  enabled=enabled if enabled is not None else True,
@@ -2,7 +2,7 @@ from __future__ import annotations as _annotations
2
2
 
3
3
  import warnings
4
4
  from collections.abc import Awaitable, Callable
5
- from typing import TYPE_CHECKING, Any
5
+ from typing import Any
6
6
 
7
7
  from mcp import GetPromptResult
8
8
 
@@ -12,9 +12,6 @@ from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
12
12
  from fastmcp.settings import DuplicateBehavior
13
13
  from fastmcp.utilities.logging import get_logger
14
14
 
15
- if TYPE_CHECKING:
16
- from fastmcp.server.server import MountedServer
17
-
18
15
  logger = get_logger(__name__)
19
16
 
20
17
 
@@ -27,7 +24,6 @@ class PromptManager:
27
24
  mask_error_details: bool | None = None,
28
25
  ):
29
26
  self._prompts: dict[str, Prompt] = {}
30
- self._mounted_servers: list[MountedServer] = []
31
27
  self.mask_error_details = mask_error_details or settings.mask_error_details
32
28
 
33
29
  # Default to "warn" if None is provided
@@ -42,52 +38,6 @@ class PromptManager:
42
38
 
43
39
  self.duplicate_behavior = duplicate_behavior
44
40
 
45
- def mount(self, server: MountedServer) -> None:
46
- """Adds a mounted server as a source for prompts."""
47
- self._mounted_servers.append(server)
48
-
49
- async def _load_prompts(self, *, via_server: bool = False) -> dict[str, Prompt]:
50
- """
51
- The single, consolidated recursive method for fetching prompts. The 'via_server'
52
- parameter determines the communication path.
53
-
54
- - via_server=False: Manager-to-manager path for complete, unfiltered inventory
55
- - via_server=True: Server-to-server path for filtered MCP requests
56
- """
57
- all_prompts: dict[str, Prompt] = {}
58
-
59
- for mounted in self._mounted_servers:
60
- try:
61
- if via_server:
62
- # Use the server-to-server filtered path
63
- child_results = await mounted.server._list_prompts()
64
- else:
65
- # Use the manager-to-manager unfiltered path
66
- child_results = await mounted.server._prompt_manager.list_prompts()
67
-
68
- # The combination logic is the same for both paths
69
- child_dict = {p.key: p for p in child_results}
70
- if mounted.prefix:
71
- for prompt in child_dict.values():
72
- prefixed_prompt = prompt.model_copy(
73
- key=f"{mounted.prefix}_{prompt.key}"
74
- )
75
- all_prompts[prefixed_prompt.key] = prefixed_prompt
76
- else:
77
- all_prompts.update(child_dict)
78
- except Exception as e:
79
- # Skip failed mounts silently, matches existing behavior
80
- logger.warning(
81
- f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}"
82
- )
83
- if settings.mounted_components_raise_on_load_error:
84
- raise
85
- continue
86
-
87
- # Finally, add local prompts, which always take precedence
88
- all_prompts.update(self._prompts)
89
- return all_prompts
90
-
91
41
  async def has_prompt(self, key: str) -> bool:
92
42
  """Check if a prompt exists."""
93
43
  prompts = await self.get_prompts()
@@ -102,16 +52,9 @@ class PromptManager:
102
52
 
103
53
  async def get_prompts(self) -> dict[str, Prompt]:
104
54
  """
105
- Gets the complete, unfiltered inventory of all prompts.
106
- """
107
- return await self._load_prompts(via_server=False)
108
-
109
- async def list_prompts(self) -> list[Prompt]:
110
- """
111
- Lists all prompts, applying protocol filtering.
55
+ Gets the complete, unfiltered inventory of local prompts.
112
56
  """
113
- prompts_dict = await self._load_prompts(via_server=True)
114
- return list(prompts_dict.values())
57
+ return dict(self._prompts)
115
58
 
116
59
  def add_prompt_from_fn(
117
60
  self,
@@ -160,44 +103,16 @@ class PromptManager:
160
103
  Internal API for servers: Finds and renders a prompt, respecting the
161
104
  filtered protocol path.
162
105
  """
163
- # 1. Check local prompts first. The server will have already applied its filter.
164
- if name in self._prompts:
165
- prompt = await self.get_prompt(name)
166
- if not prompt:
167
- raise NotFoundError(f"Unknown prompt: {name}")
168
-
169
- try:
170
- messages = await prompt.render(arguments)
171
- return GetPromptResult(
172
- description=prompt.description, messages=messages
173
- )
174
-
175
- # Pass through PromptErrors as-is
176
- except PromptError as e:
177
- logger.exception(f"Error rendering prompt {name!r}")
178
- raise e
179
-
180
- # Handle other exceptions
181
- except Exception as e:
182
- logger.exception(f"Error rendering prompt {name!r}")
183
- if self.mask_error_details:
184
- # Mask internal details
185
- raise PromptError(f"Error rendering prompt {name!r}") from e
186
- else:
187
- # Include original error details
188
- raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
189
-
190
- # 2. Check mounted servers using the filtered protocol path.
191
- for mounted in reversed(self._mounted_servers):
192
- prompt_key = name
193
- if mounted.prefix:
194
- if name.startswith(f"{mounted.prefix}_"):
195
- prompt_key = name.removeprefix(f"{mounted.prefix}_")
196
- else:
197
- continue
198
- try:
199
- return await mounted.server._get_prompt(prompt_key, arguments)
200
- except NotFoundError:
201
- continue
202
-
203
- raise NotFoundError(f"Unknown prompt: {name}")
106
+ prompt = await self.get_prompt(name)
107
+ try:
108
+ messages = await prompt.render(arguments)
109
+ return GetPromptResult(description=prompt.description, messages=messages)
110
+ except PromptError as e:
111
+ logger.exception(f"Error rendering prompt {name!r}")
112
+ raise e
113
+ except Exception as e:
114
+ logger.exception(f"Error rendering prompt {name!r}")
115
+ if self.mask_error_details:
116
+ raise PromptError(f"Error rendering prompt {name!r}") from e
117
+ else:
118
+ raise PromptError(f"Error rendering prompt {name!r}: {e}") from e
@@ -2,13 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import abc
6
5
  import inspect
7
6
  from collections.abc import Callable
8
7
  from typing import TYPE_CHECKING, Annotated, Any
9
8
 
10
9
  import pydantic_core
11
- from mcp.types import Annotations
10
+ from mcp.types import Annotations, Icon
12
11
  from mcp.types import Resource as MCPResource
13
12
  from pydantic import (
14
13
  AnyUrl,
@@ -31,7 +30,7 @@ if TYPE_CHECKING:
31
30
  pass
32
31
 
33
32
 
34
- class Resource(FastMCPComponent, abc.ABC):
33
+ class Resource(FastMCPComponent):
35
34
  """Base class for all resources."""
36
35
 
37
36
  model_config = ConfigDict(validate_default=True)
@@ -73,6 +72,7 @@ class Resource(FastMCPComponent, abc.ABC):
73
72
  name: str | None = None,
74
73
  title: str | None = None,
75
74
  description: str | None = None,
75
+ icons: list[Icon] | None = None,
76
76
  mime_type: str | None = None,
77
77
  tags: set[str] | None = None,
78
78
  enabled: bool | None = None,
@@ -85,6 +85,7 @@ class Resource(FastMCPComponent, abc.ABC):
85
85
  name=name,
86
86
  title=title,
87
87
  description=description,
88
+ icons=icons,
88
89
  mime_type=mime_type,
89
90
  tags=tags,
90
91
  enabled=enabled,
@@ -111,10 +112,13 @@ class Resource(FastMCPComponent, abc.ABC):
111
112
  raise ValueError("Either name or uri must be provided")
112
113
  return self
113
114
 
114
- @abc.abstractmethod
115
115
  async def read(self) -> str | bytes:
116
- """Read the resource content."""
117
- pass
116
+ """Read the resource content.
117
+
118
+ This method is not implemented in the base Resource class and must be
119
+ implemented by subclasses.
120
+ """
121
+ raise NotImplementedError("Subclasses must implement read()")
118
122
 
119
123
  def to_mcp_resource(
120
124
  self,
@@ -130,6 +134,7 @@ class Resource(FastMCPComponent, abc.ABC):
130
134
  description=overrides.get("description", self.description),
131
135
  mimeType=overrides.get("mimeType", self.mime_type),
132
136
  title=overrides.get("title", self.title),
137
+ icons=overrides.get("icons", self.icons),
133
138
  annotations=overrides.get("annotations", self.annotations),
134
139
  _meta=overrides.get(
135
140
  "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
@@ -173,6 +178,7 @@ class FunctionResource(Resource):
173
178
  name: str | None = None,
174
179
  title: str | None = None,
175
180
  description: str | None = None,
181
+ icons: list[Icon] | None = None,
176
182
  mime_type: str | None = None,
177
183
  tags: set[str] | None = None,
178
184
  enabled: bool | None = None,
@@ -188,6 +194,7 @@ class FunctionResource(Resource):
188
194
  name=name or get_fn_name(fn),
189
195
  title=title,
190
196
  description=description or inspect.getdoc(fn),
197
+ icons=icons,
191
198
  mime_type=mime_type or "text/plain",
192
199
  tags=tags or set(),
193
200
  enabled=enabled if enabled is not None else True,