fastmcp 2.12.1__py3-none-any.whl → 2.13.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.
Files changed (109) hide show
  1. fastmcp/__init__.py +2 -2
  2. fastmcp/cli/cli.py +56 -36
  3. fastmcp/cli/install/__init__.py +2 -0
  4. fastmcp/cli/install/claude_code.py +7 -16
  5. fastmcp/cli/install/claude_desktop.py +4 -12
  6. fastmcp/cli/install/cursor.py +20 -30
  7. fastmcp/cli/install/gemini_cli.py +241 -0
  8. fastmcp/cli/install/mcp_json.py +4 -12
  9. fastmcp/cli/run.py +15 -94
  10. fastmcp/client/__init__.py +9 -9
  11. fastmcp/client/auth/oauth.py +117 -206
  12. fastmcp/client/client.py +123 -47
  13. fastmcp/client/elicitation.py +6 -1
  14. fastmcp/client/logging.py +18 -14
  15. fastmcp/client/oauth_callback.py +85 -171
  16. fastmcp/client/sampling.py +1 -1
  17. fastmcp/client/transports.py +81 -26
  18. fastmcp/contrib/component_manager/__init__.py +1 -1
  19. fastmcp/contrib/component_manager/component_manager.py +2 -2
  20. fastmcp/contrib/component_manager/component_service.py +7 -7
  21. fastmcp/contrib/mcp_mixin/README.md +35 -4
  22. fastmcp/contrib/mcp_mixin/__init__.py +2 -2
  23. fastmcp/contrib/mcp_mixin/mcp_mixin.py +54 -7
  24. fastmcp/experimental/sampling/handlers/openai.py +2 -2
  25. fastmcp/experimental/server/openapi/__init__.py +5 -8
  26. fastmcp/experimental/server/openapi/components.py +11 -7
  27. fastmcp/experimental/server/openapi/routing.py +2 -2
  28. fastmcp/experimental/utilities/openapi/__init__.py +10 -15
  29. fastmcp/experimental/utilities/openapi/director.py +16 -10
  30. fastmcp/experimental/utilities/openapi/json_schema_converter.py +6 -2
  31. fastmcp/experimental/utilities/openapi/models.py +3 -3
  32. fastmcp/experimental/utilities/openapi/parser.py +37 -16
  33. fastmcp/experimental/utilities/openapi/schemas.py +33 -7
  34. fastmcp/mcp_config.py +3 -4
  35. fastmcp/prompts/__init__.py +1 -1
  36. fastmcp/prompts/prompt.py +32 -27
  37. fastmcp/prompts/prompt_manager.py +16 -101
  38. fastmcp/resources/__init__.py +5 -5
  39. fastmcp/resources/resource.py +28 -20
  40. fastmcp/resources/resource_manager.py +9 -168
  41. fastmcp/resources/template.py +119 -27
  42. fastmcp/resources/types.py +30 -24
  43. fastmcp/server/__init__.py +1 -1
  44. fastmcp/server/auth/__init__.py +9 -5
  45. fastmcp/server/auth/auth.py +80 -47
  46. fastmcp/server/auth/handlers/authorize.py +326 -0
  47. fastmcp/server/auth/jwt_issuer.py +236 -0
  48. fastmcp/server/auth/middleware.py +96 -0
  49. fastmcp/server/auth/oauth_proxy.py +1556 -265
  50. fastmcp/server/auth/oidc_proxy.py +412 -0
  51. fastmcp/server/auth/providers/auth0.py +193 -0
  52. fastmcp/server/auth/providers/aws.py +263 -0
  53. fastmcp/server/auth/providers/azure.py +314 -129
  54. fastmcp/server/auth/providers/bearer.py +1 -1
  55. fastmcp/server/auth/providers/debug.py +114 -0
  56. fastmcp/server/auth/providers/descope.py +229 -0
  57. fastmcp/server/auth/providers/discord.py +308 -0
  58. fastmcp/server/auth/providers/github.py +31 -6
  59. fastmcp/server/auth/providers/google.py +50 -7
  60. fastmcp/server/auth/providers/in_memory.py +27 -3
  61. fastmcp/server/auth/providers/introspection.py +281 -0
  62. fastmcp/server/auth/providers/jwt.py +48 -31
  63. fastmcp/server/auth/providers/oci.py +233 -0
  64. fastmcp/server/auth/providers/scalekit.py +238 -0
  65. fastmcp/server/auth/providers/supabase.py +188 -0
  66. fastmcp/server/auth/providers/workos.py +37 -15
  67. fastmcp/server/context.py +194 -67
  68. fastmcp/server/dependencies.py +56 -16
  69. fastmcp/server/elicitation.py +1 -1
  70. fastmcp/server/http.py +57 -18
  71. fastmcp/server/low_level.py +121 -2
  72. fastmcp/server/middleware/__init__.py +1 -1
  73. fastmcp/server/middleware/caching.py +476 -0
  74. fastmcp/server/middleware/error_handling.py +14 -10
  75. fastmcp/server/middleware/logging.py +158 -116
  76. fastmcp/server/middleware/middleware.py +30 -16
  77. fastmcp/server/middleware/rate_limiting.py +3 -3
  78. fastmcp/server/middleware/tool_injection.py +116 -0
  79. fastmcp/server/openapi.py +15 -7
  80. fastmcp/server/proxy.py +22 -11
  81. fastmcp/server/server.py +744 -254
  82. fastmcp/settings.py +65 -15
  83. fastmcp/tools/__init__.py +1 -1
  84. fastmcp/tools/tool.py +173 -108
  85. fastmcp/tools/tool_manager.py +30 -112
  86. fastmcp/tools/tool_transform.py +13 -11
  87. fastmcp/utilities/cli.py +67 -28
  88. fastmcp/utilities/components.py +7 -2
  89. fastmcp/utilities/inspect.py +79 -23
  90. fastmcp/utilities/json_schema.py +21 -4
  91. fastmcp/utilities/json_schema_type.py +4 -4
  92. fastmcp/utilities/logging.py +182 -10
  93. fastmcp/utilities/mcp_server_config/__init__.py +3 -3
  94. fastmcp/utilities/mcp_server_config/v1/environments/base.py +1 -2
  95. fastmcp/utilities/mcp_server_config/v1/environments/uv.py +10 -45
  96. fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py +8 -7
  97. fastmcp/utilities/mcp_server_config/v1/schema.json +5 -1
  98. fastmcp/utilities/mcp_server_config/v1/sources/base.py +0 -1
  99. fastmcp/utilities/openapi.py +11 -11
  100. fastmcp/utilities/tests.py +93 -10
  101. fastmcp/utilities/types.py +87 -21
  102. fastmcp/utilities/ui.py +626 -0
  103. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/METADATA +141 -60
  104. fastmcp-2.13.2.dist-info/RECORD +144 -0
  105. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/WHEEL +1 -1
  106. fastmcp/cli/claude.py +0 -144
  107. fastmcp-2.12.1.dist-info/RECORD +0 -128
  108. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/entry_points.txt +0 -0
  109. {fastmcp-2.12.1.dist-info → fastmcp-2.13.2.dist-info}/licenses/LICENSE +0 -0
@@ -79,6 +79,7 @@ def _replace_ref_with_defs(
79
79
 
80
80
  Examples:
81
81
  - {"type": "object", "properties": {"$ref": "#/components/schemas/..."}}
82
+ - {"type": "object", "additionalProperties": {"$ref": "#/components/schemas/..."}, "properties": {...}}
82
83
  - {"$ref": "#/components/schemas/..."}
83
84
  - {"items": {"$ref": "#/components/schemas/..."}}
84
85
  - {"anyOf": [{"$ref": "#/components/schemas/..."}]}
@@ -117,6 +118,11 @@ def _replace_ref_with_defs(
117
118
  for section in ["anyOf", "allOf", "oneOf"]:
118
119
  for i, item in enumerate(schema.get(section, [])):
119
120
  schema[section][i] = _replace_ref_with_defs(item)
121
+ if additionalProperties := schema.get("additionalProperties"):
122
+ if not isinstance(additionalProperties, bool):
123
+ schema["additionalProperties"] = _replace_ref_with_defs(
124
+ additionalProperties
125
+ )
120
126
  if info.get("description", description) and not schema.get("description"):
121
127
  schema["description"] = description
122
128
  return schema
@@ -297,9 +303,11 @@ def _combine_schemas_and_map_params(
297
303
 
298
304
  # Convert refs if needed
299
305
  if convert_refs:
300
- param_schema = _replace_ref_with_defs(param.schema_)
306
+ param_schema = _replace_ref_with_defs(param.schema_, param.description)
301
307
  else:
302
- param_schema = param.schema_
308
+ param_schema = param.schema_.copy()
309
+ if param.description and not param_schema.get("description"):
310
+ param_schema["description"] = param.description
303
311
  original_desc = param_schema.get("description", "")
304
312
  location_desc = f"({param.location.capitalize()} parameter)"
305
313
  if original_desc:
@@ -324,9 +332,11 @@ def _combine_schemas_and_map_params(
324
332
 
325
333
  # Convert refs if needed
326
334
  if convert_refs:
327
- param_schema = _replace_ref_with_defs(param.schema_)
335
+ param_schema = _replace_ref_with_defs(param.schema_, param.description)
328
336
  else:
329
- param_schema = param.schema_
337
+ param_schema = param.schema_.copy()
338
+ if param.description and not param_schema.get("description"):
339
+ param_schema["description"] = param.description
330
340
 
331
341
  # Don't make optional parameters nullable - they can simply be omitted
332
342
  # The OpenAPI specification doesn't require optional parameters to accept null values
@@ -344,7 +354,7 @@ def _combine_schemas_and_map_params(
344
354
  if route.request_body.required:
345
355
  required.append("body")
346
356
  parameter_map["body"] = {"location": "body", "openapi_name": "body"}
347
- else:
357
+ elif body_props:
348
358
  # Normal case: body has properties
349
359
  for prop_name, prop_schema in body_props.items():
350
360
  properties[prop_name] = prop_schema
@@ -357,6 +367,22 @@ def _combine_schemas_and_map_params(
357
367
 
358
368
  if route.request_body.required:
359
369
  required.extend(body_schema.get("required", []))
370
+ else:
371
+ # Handle direct array/primitive schemas (like list[str] parameters from FastAPI)
372
+ # Use the schema title as parameter name, fall back to generic name
373
+ param_name = body_schema.get("title", "body").lower()
374
+
375
+ # Clean the parameter name to be valid
376
+ import re
377
+
378
+ param_name = re.sub(r"[^a-zA-Z0-9_]", "_", param_name)
379
+ if not param_name or param_name[0].isdigit():
380
+ param_name = "body_data"
381
+
382
+ properties[param_name] = body_schema
383
+ if route.request_body.required:
384
+ required.append(param_name)
385
+ parameter_map[param_name] = {"location": "body", "openapi_name": param_name}
360
386
 
361
387
  result = {
362
388
  "type": "object",
@@ -559,9 +585,9 @@ def extract_output_schema_from_responses(
559
585
 
560
586
  # Export public symbols
561
587
  __all__ = [
562
- "clean_schema_for_display",
563
588
  "_combine_schemas",
564
589
  "_combine_schemas_and_map_params",
565
- "extract_output_schema_from_responses",
566
590
  "_make_optional_parameter_nullable",
591
+ "clean_schema_for_display",
592
+ "extract_output_schema_from_responses",
567
593
  ]
fastmcp/mcp_config.py CHANGED
@@ -101,7 +101,7 @@ class _TransformingMCPServerMixin(FastMCPBaseModel):
101
101
  ClientTransport, # pyright: ignore[reportUnusedImport]
102
102
  )
103
103
 
104
- transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
104
+ transport: ClientTransport = super().to_transport() # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType] # ty: ignore[unresolved-attribute]
105
105
  transport = cast(ClientTransport, transport)
106
106
 
107
107
  client: Client[ClientTransport] = Client(transport=transport, name=client_name)
@@ -288,9 +288,8 @@ class MCPConfig(BaseModel):
288
288
  @classmethod
289
289
  def from_file(cls, file_path: Path) -> Self:
290
290
  """Load configuration from JSON file."""
291
- if file_path.exists():
292
- if content := file_path.read_text().strip():
293
- return cls.model_validate_json(content)
291
+ if file_path.exists() and (content := file_path.read_text().strip()):
292
+ return cls.model_validate_json(content)
294
293
 
295
294
  raise ValueError(f"No MCP servers defined in the config: {file_path}")
296
295
 
@@ -2,8 +2,8 @@ from .prompt import Prompt, PromptMessage, Message
2
2
  from .prompt_manager import PromptManager
3
3
 
4
4
  __all__ = [
5
+ "Message",
5
6
  "Prompt",
6
7
  "PromptManager",
7
8
  "PromptMessage",
8
- "Message",
9
9
  ]
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(
@@ -100,14 +99,17 @@ class Prompt(FastMCPComponent, ABC):
100
99
  )
101
100
  for arg in self.arguments or []
102
101
  ]
103
- kwargs = {
104
- "name": self.name,
105
- "description": self.description,
106
- "arguments": arguments,
107
- "title": self.title,
108
- "_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
109
- }
110
- return MCPPrompt(**kwargs | overrides)
102
+
103
+ return MCPPrompt(
104
+ name=overrides.get("name", self.name),
105
+ description=overrides.get("description", self.description),
106
+ arguments=arguments,
107
+ title=overrides.get("title", self.title),
108
+ icons=overrides.get("icons", self.icons),
109
+ _meta=overrides.get(
110
+ "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
111
+ ),
112
+ )
111
113
 
112
114
  @staticmethod
113
115
  def from_function(
@@ -115,6 +117,7 @@ class Prompt(FastMCPComponent, ABC):
115
117
  name: str | None = None,
116
118
  title: str | None = None,
117
119
  description: str | None = None,
120
+ icons: list[Icon] | None = None,
118
121
  tags: set[str] | None = None,
119
122
  enabled: bool | None = None,
120
123
  meta: dict[str, Any] | None = None,
@@ -132,18 +135,22 @@ class Prompt(FastMCPComponent, ABC):
132
135
  name=name,
133
136
  title=title,
134
137
  description=description,
138
+ icons=icons,
135
139
  tags=tags,
136
140
  enabled=enabled,
137
141
  meta=meta,
138
142
  )
139
143
 
140
- @abstractmethod
141
144
  async def render(
142
145
  self,
143
146
  arguments: dict[str, Any] | None = None,
144
147
  ) -> list[PromptMessage]:
145
- """Render the prompt with arguments."""
146
- 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()")
147
154
 
148
155
 
149
156
  class FunctionPrompt(Prompt):
@@ -158,6 +165,7 @@ class FunctionPrompt(Prompt):
158
165
  name: str | None = None,
159
166
  title: str | None = None,
160
167
  description: str | None = None,
168
+ icons: list[Icon] | None = None,
161
169
  tags: set[str] | None = None,
162
170
  enabled: bool | None = None,
163
171
  meta: dict[str, Any] | None = None,
@@ -199,10 +207,7 @@ class FunctionPrompt(Prompt):
199
207
  # Auto-detect context parameter if not provided
200
208
 
201
209
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
202
- if context_kwarg:
203
- prune_params = [context_kwarg]
204
- else:
205
- prune_params = None
210
+ prune_params = [context_kwarg] if context_kwarg else None
206
211
 
207
212
  parameters = compress_schema(parameters, prune_params=prune_params)
208
213
 
@@ -251,6 +256,7 @@ class FunctionPrompt(Prompt):
251
256
  name=func_name,
252
257
  title=title,
253
258
  description=description,
259
+ icons=icons,
254
260
  arguments=arguments,
255
261
  tags=tags or set(),
256
262
  enabled=enabled if enabled is not None else True,
@@ -281,10 +287,7 @@ class FunctionPrompt(Prompt):
281
287
  if (
282
288
  param.annotation == inspect.Parameter.empty
283
289
  or param.annotation is str
284
- ):
285
- converted_kwargs[param_name] = param_value
286
- # If argument is not a string, pass as-is (already properly typed)
287
- elif not isinstance(param_value, str):
290
+ ) or not isinstance(param_value, str):
288
291
  converted_kwargs[param_name] = param_value
289
292
  else:
290
293
  # Try to convert string argument using type adapter
@@ -305,7 +308,7 @@ class FunctionPrompt(Prompt):
305
308
  raise PromptError(
306
309
  f"Could not convert argument '{param_name}' with value '{param_value}' "
307
310
  f"to expected type {param.annotation}. Error: {e}"
308
- )
311
+ ) from e
309
312
  else:
310
313
  # Parameter not in function signature, pass as-is
311
314
  converted_kwargs[param_name] = param_value
@@ -367,10 +370,12 @@ class FunctionPrompt(Prompt):
367
370
  content=TextContent(type="text", text=content),
368
371
  )
369
372
  )
370
- except Exception:
371
- raise PromptError("Could not convert prompt result to message.")
373
+ except Exception as e:
374
+ raise PromptError(
375
+ "Could not convert prompt result to message."
376
+ ) from e
372
377
 
373
378
  return messages
374
- except Exception:
379
+ except Exception as e:
375
380
  logger.exception(f"Error rendering prompt {self.name}")
376
- raise PromptError(f"Error rendering prompt {self.name}.")
381
+ raise PromptError(f"Error rendering prompt {self.name}.") from e
@@ -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
@@ -10,13 +10,13 @@ from .types import (
10
10
  from .resource_manager import ResourceManager
11
11
 
12
12
  __all__ = [
13
- "Resource",
14
- "TextResource",
15
13
  "BinaryResource",
16
- "FunctionResource",
14
+ "DirectoryResource",
17
15
  "FileResource",
16
+ "FunctionResource",
18
17
  "HttpResource",
19
- "DirectoryResource",
20
- "ResourceTemplate",
18
+ "Resource",
21
19
  "ResourceManager",
20
+ "ResourceTemplate",
21
+ "TextResource",
22
22
  ]
@@ -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,
@@ -24,13 +23,14 @@ from fastmcp.server.dependencies import get_context
24
23
  from fastmcp.utilities.components import FastMCPComponent
25
24
  from fastmcp.utilities.types import (
26
25
  find_kwarg_by_type,
26
+ get_fn_name,
27
27
  )
28
28
 
29
29
  if TYPE_CHECKING:
30
30
  pass
31
31
 
32
32
 
33
- class Resource(FastMCPComponent, abc.ABC):
33
+ class Resource(FastMCPComponent):
34
34
  """Base class for all resources."""
35
35
 
36
36
  model_config = ConfigDict(validate_default=True)
@@ -72,6 +72,7 @@ class Resource(FastMCPComponent, abc.ABC):
72
72
  name: str | None = None,
73
73
  title: str | None = None,
74
74
  description: str | None = None,
75
+ icons: list[Icon] | None = None,
75
76
  mime_type: str | None = None,
76
77
  tags: set[str] | None = None,
77
78
  enabled: bool | None = None,
@@ -84,6 +85,7 @@ class Resource(FastMCPComponent, abc.ABC):
84
85
  name=name,
85
86
  title=title,
86
87
  description=description,
88
+ icons=icons,
87
89
  mime_type=mime_type,
88
90
  tags=tags,
89
91
  enabled=enabled,
@@ -110,10 +112,13 @@ class Resource(FastMCPComponent, abc.ABC):
110
112
  raise ValueError("Either name or uri must be provided")
111
113
  return self
112
114
 
113
- @abc.abstractmethod
114
115
  async def read(self) -> str | bytes:
115
- """Read the resource content."""
116
- 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()")
117
122
 
118
123
  def to_mcp_resource(
119
124
  self,
@@ -122,16 +127,19 @@ class Resource(FastMCPComponent, abc.ABC):
122
127
  **overrides: Any,
123
128
  ) -> MCPResource:
124
129
  """Convert the resource to an MCPResource."""
125
- kwargs = {
126
- "uri": self.uri,
127
- "name": self.name,
128
- "description": self.description,
129
- "mimeType": self.mime_type,
130
- "title": self.title,
131
- "annotations": self.annotations,
132
- "_meta": self.get_meta(include_fastmcp_meta=include_fastmcp_meta),
133
- }
134
- return MCPResource(**kwargs | overrides)
130
+
131
+ return MCPResource(
132
+ name=overrides.get("name", self.name),
133
+ uri=overrides.get("uri", self.uri),
134
+ description=overrides.get("description", self.description),
135
+ mimeType=overrides.get("mimeType", self.mime_type),
136
+ title=overrides.get("title", self.title),
137
+ icons=overrides.get("icons", self.icons),
138
+ annotations=overrides.get("annotations", self.annotations),
139
+ _meta=overrides.get(
140
+ "_meta", self.get_meta(include_fastmcp_meta=include_fastmcp_meta)
141
+ ),
142
+ )
135
143
 
136
144
  def __repr__(self) -> str:
137
145
  return f"{self.__class__.__name__}(uri={self.uri!r}, name={self.name!r}, description={self.description!r}, tags={self.tags})"
@@ -170,6 +178,7 @@ class FunctionResource(Resource):
170
178
  name: str | None = None,
171
179
  title: str | None = None,
172
180
  description: str | None = None,
181
+ icons: list[Icon] | None = None,
173
182
  mime_type: str | None = None,
174
183
  tags: set[str] | None = None,
175
184
  enabled: bool | None = None,
@@ -182,9 +191,10 @@ class FunctionResource(Resource):
182
191
  return cls(
183
192
  fn=fn,
184
193
  uri=uri,
185
- name=name or fn.__name__,
194
+ name=name or get_fn_name(fn),
186
195
  title=title,
187
196
  description=description or inspect.getdoc(fn),
197
+ icons=icons,
188
198
  mime_type=mime_type or "text/plain",
189
199
  tags=tags or set(),
190
200
  enabled=enabled if enabled is not None else True,
@@ -207,9 +217,7 @@ class FunctionResource(Resource):
207
217
 
208
218
  if isinstance(result, Resource):
209
219
  return await result.read()
210
- elif isinstance(result, bytes):
211
- return result
212
- elif isinstance(result, str):
220
+ elif isinstance(result, bytes | str):
213
221
  return result
214
222
  else:
215
223
  return pydantic_core.to_json(result, fallback=str).decode()