Jeremiah Lowin commited on
Commit
73ba121
·
unverified ·
2 Parent(s): 143081690538d7

Merge pull request #781 from jlowin/enabled

Browse files

Support enable/disable for all FastMCP components (tools, prompts, resources, templates)

docs/patterns/tool-transformation.mdx CHANGED
@@ -53,6 +53,30 @@ product_search_tool = Tool.from_tool(
53
 
54
  mcp.add_tool(product_search_tool)
55
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  Now, clients see a tool named `find_products` with a clear, domain-specific purpose and relevant tags, even though it still uses the original generic `search` function's logic.
57
 
58
  ### Parameters
 
53
 
54
  mcp.add_tool(product_search_tool)
55
  ```
56
+
57
+ <Tip>
58
+ When you transform a tool, the original tool remains registered on the server. To avoid confusing an LLM with two similar tools, you can disable the original one:
59
+
60
+ ```python
61
+ from fastmcp import FastMCP
62
+ from fastmcp.tools import Tool
63
+
64
+ mcp = FastMCP()
65
+
66
+ # The original, generic tool
67
+ @mcp.tool
68
+ def search(query: str, category: str = "all") -> list[dict]:
69
+ ...
70
+
71
+ # Create a more domain-specific version
72
+ product_search_tool = Tool.from_tool(search, ...)
73
+ mcp.add_tool(product_search_tool)
74
+
75
+ # Disable the original tool
76
+ search.disable()
77
+ ```
78
+ </Tip>
79
+
80
  Now, clients see a tool named `find_products` with a clear, domain-specific purpose and relevant tags, even though it still uses the original generic `search` function's logic.
81
 
82
  ### Parameters
docs/servers/prompts.mdx CHANGED
@@ -147,7 +147,32 @@ def data_analysis_prompt(
147
  - **`name`**: Sets the explicit prompt name exposed via MCP.
148
  - **`description`**: Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose.
149
  - **`tags`**: A set of strings used to categorize the prompt. Clients *might* use tags to filter or group available prompts.
 
 
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  ### Asynchronous Prompts
152
 
153
  FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as prompts.
@@ -191,6 +216,8 @@ async def generate_report_request(report_type: str, ctx: Context) -> str:
191
 
192
  For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
193
 
 
 
194
  ## Server Behavior
195
 
196
  ### Duplicate Prompts
 
147
  - **`name`**: Sets the explicit prompt name exposed via MCP.
148
  - **`description`**: Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose.
149
  - **`tags`**: A set of strings used to categorize the prompt. Clients *might* use tags to filter or group available prompts.
150
+ - **`enabled`**: A boolean to enable or disable the prompt (defaults to `True`). See [Disabling Prompts](#disabling-prompts) for more information.
151
+ ### Disabling Prompts
152
 
153
+ <VersionBadge version="2.8.0" />
154
+
155
+ You can control the visibility and availability of prompts by enabling or disabling them. Disabled prompts will not appear in the list of available prompts, and attempting to call a disabled prompt will result in an "Unknown prompt" error.
156
+
157
+ By default, all prompts are enabled. You can disable a prompt upon creation using the `enabled` parameter in the decorator:
158
+
159
+ ```python
160
+ @mcp.prompt(enabled=False)
161
+ def experimental_prompt():
162
+ """This prompt is not ready for use."""
163
+ return "This is an experimental prompt."
164
+ ```
165
+
166
+ You can also toggle a prompt's state programmatically after it has been created:
167
+
168
+ ```python
169
+ @mcp.prompt
170
+ def seasonal_prompt(): return "Happy Holidays!"
171
+
172
+ # Disable and re-enable the prompt
173
+ seasonal_prompt.disable()
174
+ seasonal_prompt.enable()
175
+ ```
176
  ### Asynchronous Prompts
177
 
178
  FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) functions as prompts.
 
216
 
217
  For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
218
 
219
+
220
+
221
  ## Server Behavior
222
 
223
  ### Duplicate Prompts
docs/servers/resources.mdx CHANGED
@@ -94,6 +94,33 @@ def get_application_status() -> dict:
94
  - **`description`**: Explanation of the resource (defaults to docstring).
95
  - **`mime_type`**: Specifies the content type (FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types).
96
  - **`tags`**: A set of strings for categorization, potentially used by clients for filtering.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  ### Accessing MCP Context
99
 
 
94
  - **`description`**: Explanation of the resource (defaults to docstring).
95
  - **`mime_type`**: Specifies the content type (FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types).
96
  - **`tags`**: A set of strings for categorization, potentially used by clients for filtering.
97
+ - **`enabled`**: A boolean to enable or disable the resource (defaults to `True`). See [Disabling Resources](#disabling-resources) for more information.
98
+
99
+ ### Disabling Resources
100
+
101
+ <VersionBadge version="2.8.0" />
102
+
103
+ You can control the visibility and availability of resources and templates by enabling or disabling them. Disabled resources will not appear in the list of available resources or templates, and attempting to read a disabled resource will result in an "Unknown resource" error.
104
+
105
+ By default, all resources are enabled. You can disable a resource upon creation using the `enabled` parameter in the decorator:
106
+
107
+ ```python
108
+ @mcp.resource("data://secret", enabled=False)
109
+ def get_secret_data():
110
+ """This resource is currently disabled."""
111
+ return "Secret data"
112
+ ```
113
+
114
+ You can also toggle a resource's state programmatically after it has been created:
115
+
116
+ ```python
117
+ @mcp.resource("data://config")
118
+ def get_config(): return {"version": 1}
119
+
120
+ # Disable and re-enable the resource
121
+ get_config.disable()
122
+ get_config.enable()
123
+ ```
124
 
125
  ### Accessing MCP Context
126
 
docs/servers/tools.mdx CHANGED
@@ -169,27 +169,58 @@ def search_products_implementation(query: str, category: str | None = None) -> l
169
 
170
  - **`name`**: Sets the explicit tool name exposed via MCP.
171
  - **`description`**: Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose.
172
- - **`tags`**: A set of strings used to categorize the tool. Clients *might* use tags to filter or group available tools.
 
 
173
 
 
174
 
175
- - **`exclude_args`**:
176
- <VersionBadge version="2.6.0" />
177
- A list of argument names to exclude from the tool schema shown to the LLM. This is useful for arguments that are injected at runtime (such as `state`, `user_id`, or credentials) and should not be exposed to the LLM or client. Only arguments with default values can be excluded; attempting to exclude a required argument will raise an error.
178
-
179
 
180
- Example:
181
 
182
- ```python
183
- @mcp.tool(
184
- name="get_user_details",
185
- exclude_args=["user_id"]
186
- )
187
- def get_user_details(user_id: str = None) -> str:
188
- # user_id will be injected by the server, not provided by the LLM
189
- ...
190
- ```
191
 
192
- With this configuration, `user_id` will not appear in the tool's parameter schema, but can still be set by the server or framework at runtime.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
  ### Async Tools
195
 
 
169
 
170
  - **`name`**: Sets the explicit tool name exposed via MCP.
171
  - **`description`**: Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose.
172
+ - **`tags`**: A set of strings to categorize the tool. Clients *might* use tags to filter or group available tools.
173
+ - **`enabled`**: A boolean to enable or disable the tool (defaults to `True`). See [Disabling Tools](#disabling-tools) for more information.
174
+ - **`exclude_args`**: A list of argument names to exclude from the tool schema shown to the LLM. See [Excluding Arguments](#excluding-arguments) for more information.
175
 
176
+ ### Excluding Arguments
177
 
178
+ <VersionBadge version="2.6.0" />
 
 
 
179
 
180
+ You can exclude certain arguments from the tool schema shown to the LLM. This is useful for arguments that are injected at runtime (such as `state`, `user_id`, or credentials) and should not be exposed to the LLM or client. Only arguments with default values can be excluded; attempting to exclude a required argument will raise an error.
181
 
182
+ Example:
 
 
 
 
 
 
 
 
183
 
184
+ ```python
185
+ @mcp.tool(
186
+ name="get_user_details",
187
+ exclude_args=["user_id"]
188
+ )
189
+ def get_user_details(user_id: str = None) -> str:
190
+ # user_id will be injected by the server, not provided by the LLM
191
+ ...
192
+ ```
193
+
194
+ With this configuration, `user_id` will not appear in the tool's parameter schema, but can still be set by the server or framework at runtime.
195
+
196
+ For more complex tool transformations, see [Transforming Tools](/patterns/tool-transformation).
197
+
198
+ ### Disabling Tools
199
+
200
+ <VersionBadge version="2.8.0" />
201
+
202
+ You can control the visibility and availability of tools by enabling or disabling them. This is useful for feature flagging, maintenance, or dynamically changing the toolset available to a client. Disabled tools will not appear in the list of available tools returned by `list_tools`, and attempting to call a disabled tool will result in an "Unknown tool" error, just as if the tool did not exist.
203
+
204
+ By default, all tools are enabled. You can disable a tool upon creation using the `enabled` parameter in the decorator:
205
+
206
+ ```python
207
+ @mcp.tool(enabled=False)
208
+ def maintenance_tool():
209
+ """This tool is currently under maintenance."""
210
+ return "This tool is disabled."
211
+ ```
212
+
213
+ You can also toggle a tool's state programmatically after it has been created:
214
+
215
+ ```python
216
+ @mcp.tool
217
+ def dynamic_tool():
218
+ return "I am a dynamic tool."
219
+
220
+ # Disable and re-enable the tool
221
+ dynamic_tool.disable()
222
+ dynamic_tool.enable()
223
+ ```
224
 
225
  ### Async Tools
226
 
src/fastmcp/exceptions.py CHANGED
@@ -33,3 +33,7 @@ class ClientError(Exception):
33
 
34
  class NotFoundError(Exception):
35
  """Object not found."""
 
 
 
 
 
33
 
34
  class NotFoundError(Exception):
35
  """Object not found."""
36
+
37
+
38
+ class DisabledError(Exception):
39
+ """Object is disabled."""
src/fastmcp/prompts/prompt.py CHANGED
@@ -96,6 +96,7 @@ class Prompt(FastMCPComponent, ABC):
96
  name: str | None = None,
97
  description: str | None = None,
98
  tags: set[str] | None = None,
 
99
  ) -> FunctionPrompt:
100
  """Create a Prompt from a function.
101
 
@@ -106,7 +107,7 @@ class Prompt(FastMCPComponent, ABC):
106
  - A sequence of any of the above
107
  """
108
  return FunctionPrompt.from_function(
109
- fn=fn, name=name, description=description, tags=tags
110
  )
111
 
112
  @abstractmethod
@@ -130,6 +131,7 @@ class FunctionPrompt(Prompt):
130
  name: str | None = None,
131
  description: str | None = None,
132
  tags: set[str] | None = None,
 
133
  ) -> FunctionPrompt:
134
  """Create a Prompt from a function.
135
 
@@ -195,6 +197,7 @@ class FunctionPrompt(Prompt):
195
  description=description,
196
  arguments=arguments,
197
  tags=tags or set(),
 
198
  fn=fn,
199
  )
200
 
 
96
  name: str | None = None,
97
  description: str | None = None,
98
  tags: set[str] | None = None,
99
+ enabled: bool | None = None,
100
  ) -> FunctionPrompt:
101
  """Create a Prompt from a function.
102
 
 
107
  - A sequence of any of the above
108
  """
109
  return FunctionPrompt.from_function(
110
+ fn=fn, name=name, description=description, tags=tags, enabled=enabled
111
  )
112
 
113
  @abstractmethod
 
131
  name: str | None = None,
132
  description: str | None = None,
133
  tags: set[str] | None = None,
134
+ enabled: bool | None = None,
135
  ) -> FunctionPrompt:
136
  """Create a Prompt from a function.
137
 
 
197
  description=description,
198
  arguments=arguments,
199
  tags=tags or set(),
200
+ enabled=enabled if enabled is not None else True,
201
  fn=fn,
202
  )
203
 
src/fastmcp/prompts/prompt_manager.py CHANGED
@@ -40,9 +40,11 @@ class PromptManager:
40
 
41
  self.duplicate_behavior = duplicate_behavior
42
 
43
- def get_prompt(self, key: str) -> Prompt | None:
44
  """Get prompt by key."""
45
- return self._prompts.get(key)
 
 
46
 
47
  def get_prompts(self) -> dict[str, Prompt]:
48
  """Get all registered prompts, indexed by registered key."""
 
40
 
41
  self.duplicate_behavior = duplicate_behavior
42
 
43
+ def get_prompt(self, key: str) -> Prompt:
44
  """Get prompt by key."""
45
+ if key in self._prompts:
46
+ return self._prompts[key]
47
+ raise NotFoundError(f"Unknown prompt: {key}")
48
 
49
  def get_prompts(self) -> dict[str, Prompt]:
50
  """Get all registered prompts, indexed by registered key."""
src/fastmcp/resources/resource.py CHANGED
@@ -52,6 +52,7 @@ class Resource(FastMCPComponent, abc.ABC):
52
  description: str | None = None,
53
  mime_type: str | None = None,
54
  tags: set[str] | None = None,
 
55
  ) -> FunctionResource:
56
  return FunctionResource.from_function(
57
  fn=fn,
@@ -60,6 +61,7 @@ class Resource(FastMCPComponent, abc.ABC):
60
  description=description,
61
  mime_type=mime_type,
62
  tags=tags,
 
63
  )
64
 
65
  @field_validator("mime_type", mode="before")
@@ -124,6 +126,7 @@ class FunctionResource(Resource):
124
  description: str | None = None,
125
  mime_type: str | None = None,
126
  tags: set[str] | None = None,
 
127
  ) -> FunctionResource:
128
  """Create a FunctionResource from a function."""
129
  if isinstance(uri, str):
@@ -135,6 +138,7 @@ class FunctionResource(Resource):
135
  description=description or fn.__doc__,
136
  mime_type=mime_type or "text/plain",
137
  tags=tags or set(),
 
138
  )
139
 
140
  async def read(self) -> str | bytes:
 
52
  description: str | None = None,
53
  mime_type: str | None = None,
54
  tags: set[str] | None = None,
55
+ enabled: bool | None = None,
56
  ) -> FunctionResource:
57
  return FunctionResource.from_function(
58
  fn=fn,
 
61
  description=description,
62
  mime_type=mime_type,
63
  tags=tags,
64
+ enabled=enabled,
65
  )
66
 
67
  @field_validator("mime_type", mode="before")
 
126
  description: str | None = None,
127
  mime_type: str | None = None,
128
  tags: set[str] | None = None,
129
+ enabled: bool | None = None,
130
  ) -> FunctionResource:
131
  """Create a FunctionResource from a function."""
132
  if isinstance(uri, str):
 
138
  description=description or fn.__doc__,
139
  mime_type=mime_type or "text/plain",
140
  tags=tags or set(),
141
+ enabled=enabled if enabled is not None else True,
142
  )
143
 
144
  async def read(self) -> str | bytes:
src/fastmcp/resources/template.py CHANGED
@@ -70,6 +70,7 @@ class ResourceTemplate(FastMCPComponent):
70
  description: str | None = None,
71
  mime_type: str | None = None,
72
  tags: set[str] | None = None,
 
73
  ) -> FunctionResourceTemplate:
74
  return FunctionResourceTemplate.from_function(
75
  fn=fn,
@@ -78,6 +79,7 @@ class ResourceTemplate(FastMCPComponent):
78
  description=description,
79
  mime_type=mime_type,
80
  tags=tags,
 
81
  )
82
 
83
  @field_validator("mime_type", mode="before")
@@ -113,6 +115,7 @@ class ResourceTemplate(FastMCPComponent):
113
  description=self.description,
114
  mime_type=self.mime_type,
115
  tags=self.tags,
 
116
  )
117
 
118
  def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
@@ -155,6 +158,7 @@ class FunctionResourceTemplate(ResourceTemplate):
155
  description: str | None = None,
156
  mime_type: str | None = None,
157
  tags: set[str] | None = None,
 
158
  ) -> FunctionResourceTemplate:
159
  """Create a template from a function."""
160
  from fastmcp.server.context import Context
@@ -237,4 +241,5 @@ class FunctionResourceTemplate(ResourceTemplate):
237
  fn=fn,
238
  parameters=parameters,
239
  tags=tags or set(),
 
240
  )
 
70
  description: str | None = None,
71
  mime_type: str | None = None,
72
  tags: set[str] | None = None,
73
+ enabled: bool | None = None,
74
  ) -> FunctionResourceTemplate:
75
  return FunctionResourceTemplate.from_function(
76
  fn=fn,
 
79
  description=description,
80
  mime_type=mime_type,
81
  tags=tags,
82
+ enabled=enabled,
83
  )
84
 
85
  @field_validator("mime_type", mode="before")
 
115
  description=self.description,
116
  mime_type=self.mime_type,
117
  tags=self.tags,
118
+ enabled=self.enabled,
119
  )
120
 
121
  def to_mcp_template(self, **overrides: Any) -> MCPResourceTemplate:
 
158
  description: str | None = None,
159
  mime_type: str | None = None,
160
  tags: set[str] | None = None,
161
+ enabled: bool | None = None,
162
  ) -> FunctionResourceTemplate:
163
  """Create a template from a function."""
164
  from fastmcp.server.context import Context
 
241
  fn=fn,
242
  parameters=parameters,
243
  tags=tags or set(),
244
+ enabled=enabled if enabled is not None else True,
245
  )
src/fastmcp/server/proxy.py CHANGED
@@ -241,20 +241,20 @@ class FastMCPProxy(FastMCP):
241
  prompts[prompt_proxy.name] = prompt_proxy
242
  return prompts
243
 
244
- async def _mcp_call_tool(
245
  self, key: str, arguments: dict[str, Any]
246
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
247
  try:
248
- result = await super()._mcp_call_tool(key, arguments)
249
  return result
250
  except NotFoundError:
251
  async with self.client:
252
  result = await self.client.call_tool(key, arguments)
253
  return result
254
 
255
- async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
256
  try:
257
- result = await super()._mcp_read_resource(uri)
258
  return result
259
  except NotFoundError:
260
  async with self.client:
@@ -270,11 +270,11 @@ class FastMCPProxy(FastMCP):
270
  ReadResourceContents(content=content, mime_type=resource[0].mimeType)
271
  ]
272
 
273
- async def _mcp_get_prompt(
274
  self, name: str, arguments: dict[str, Any] | None = None
275
  ) -> GetPromptResult:
276
  try:
277
- result = await super()._mcp_get_prompt(name, arguments)
278
  return result
279
  except NotFoundError:
280
  async with self.client:
 
241
  prompts[prompt_proxy.name] = prompt_proxy
242
  return prompts
243
 
244
+ async def _call_tool(
245
  self, key: str, arguments: dict[str, Any]
246
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
247
  try:
248
+ result = await super()._call_tool(key, arguments)
249
  return result
250
  except NotFoundError:
251
  async with self.client:
252
  result = await self.client.call_tool(key, arguments)
253
  return result
254
 
255
+ async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
256
  try:
257
+ result = await super()._read_resource(uri)
258
  return result
259
  except NotFoundError:
260
  async with self.client:
 
270
  ReadResourceContents(content=content, mime_type=resource[0].mimeType)
271
  ]
272
 
273
+ async def _get_prompt(
274
  self, name: str, arguments: dict[str, Any] | None = None
275
  ) -> GetPromptResult:
276
  try:
277
+ result = await super()._get_prompt(name, arguments)
278
  return result
279
  except NotFoundError:
280
  async with self.client:
src/fastmcp/server/server.py CHANGED
@@ -44,7 +44,7 @@ from starlette.routing import BaseRoute, Route
44
  import fastmcp
45
  import fastmcp.server
46
  import fastmcp.settings
47
- from fastmcp.exceptions import NotFoundError
48
  from fastmcp.prompts import Prompt, PromptManager
49
  from fastmcp.prompts.prompt import FunctionPrompt
50
  from fastmcp.resources import Resource, ResourceManager
@@ -291,6 +291,12 @@ class FastMCP(Generic[LifespanResultT]):
291
  self._cache.set("resources", resources)
292
  return resources
293
 
 
 
 
 
 
 
294
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
295
  """Get all registered resource templates, indexed by registered key."""
296
  if (
@@ -311,6 +317,12 @@ class FastMCP(Generic[LifespanResultT]):
311
  self._cache.set("resource_templates", templates)
312
  return templates
313
 
 
 
 
 
 
 
314
  async def get_prompts(self) -> dict[str, Prompt]:
315
  """
316
  List all available prompts.
@@ -330,6 +342,12 @@ class FastMCP(Generic[LifespanResultT]):
330
  self._cache.set("prompts", prompts)
331
  return prompts
332
 
 
 
 
 
 
 
333
  def custom_route(
334
  self,
335
  path: str,
@@ -381,7 +399,9 @@ class FastMCP(Generic[LifespanResultT]):
381
 
382
  """
383
  tools = await self.get_tools()
384
- return [tool.to_mcp_tool(name=key) for key, tool in tools.items()]
 
 
385
 
386
  async def _mcp_list_resources(self) -> list[MCPResource]:
387
  """
@@ -391,7 +411,9 @@ class FastMCP(Generic[LifespanResultT]):
391
  """
392
  resources = await self.get_resources()
393
  return [
394
- resource.to_mcp_resource(uri=key) for key, resource in resources.items()
 
 
395
  ]
396
 
397
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
@@ -404,6 +426,7 @@ class FastMCP(Generic[LifespanResultT]):
404
  return [
405
  template.to_mcp_template(uriTemplate=key)
406
  for key, template in templates.items()
 
407
  ]
408
 
409
  async def _mcp_list_prompts(self) -> list[MCPPrompt]:
@@ -413,12 +436,19 @@ class FastMCP(Generic[LifespanResultT]):
413
 
414
  """
415
  prompts = await self.get_prompts()
416
- return [prompt.to_mcp_prompt(name=key) for key, prompt in prompts.items()]
 
 
 
 
417
 
418
  async def _mcp_call_tool(
419
  self, key: str, arguments: dict[str, Any]
420
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
421
- """Handle MCP 'callTool' requests.
 
 
 
422
 
423
  Args:
424
  key: The name of the tool to call
@@ -431,43 +461,109 @@ class FastMCP(Generic[LifespanResultT]):
431
 
432
  # Create and use context for the entire call
433
  with fastmcp.server.context.Context(fastmcp=self):
434
- # Get tool, checking first from our tools, then from the mounted servers
435
- if self._tool_manager.has_tool(key):
436
- return await self._tool_manager.call_tool(key, arguments)
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
- # Check mounted servers to see if they have the tool
439
- for server in self._mounted_servers.values():
440
- if server.match_tool(key):
441
- tool_key = server.strip_tool_prefix(key)
442
- return await server.server._mcp_call_tool(tool_key, arguments)
443
 
444
- raise NotFoundError(f"Unknown tool: {key}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
  """
448
  Read a resource by URI, in the format expected by the low-level MCP
449
  server.
450
  """
451
- with fastmcp.server.context.Context(fastmcp=self):
452
- if self._resource_manager.has_resource(uri):
453
- resource = await self._resource_manager.get_resource(uri)
454
- content = await self._resource_manager.read_resource(uri)
455
- return [
456
- ReadResourceContents(
457
- content=content,
458
- mime_type=resource.mime_type,
459
- )
460
- ]
 
 
 
 
 
 
461
  else:
462
- for server in self._mounted_servers.values():
463
- if server.match_resource(str(uri)):
464
- new_uri = server.strip_resource_prefix(str(uri))
465
- return await server.server._mcp_read_resource(new_uri)
466
- else:
467
- raise NotFoundError(f"Unknown resource: {uri}")
468
 
469
  async def _mcp_get_prompt(
470
  self, name: str, arguments: dict[str, Any] | None = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
  ) -> GetPromptResult:
472
  """Handle MCP 'getPrompt' requests.
473
 
@@ -480,19 +576,20 @@ class FastMCP(Generic[LifespanResultT]):
480
  """
481
  logger.debug("Get prompt: %s with %s", name, arguments)
482
 
483
- # Create and use context for the entire call
484
- with fastmcp.server.context.Context(fastmcp=self):
485
- # Get prompt, checking first from our prompts, then from the mounted servers
486
- if self._prompt_manager.has_prompt(name):
487
- return await self._prompt_manager.render_prompt(name, arguments)
 
488
 
489
- # Check mounted servers to see if they have the prompt
490
- for server in self._mounted_servers.values():
491
- if server.match_prompt(name):
492
- prompt_name = server.strip_prompt_prefix(name)
493
- return await server.server._mcp_get_prompt(prompt_name, arguments)
494
 
495
- raise NotFoundError(f"Unknown prompt: {name}")
496
 
497
  def add_tool(self, tool: Tool) -> None:
498
  """Add a tool to the server.
@@ -528,6 +625,7 @@ class FastMCP(Generic[LifespanResultT]):
528
  tags: set[str] | None = None,
529
  annotations: ToolAnnotations | dict[str, Any] | None = None,
530
  exclude_args: list[str] | None = None,
 
531
  ) -> FunctionTool: ...
532
 
533
  @overload
@@ -540,6 +638,7 @@ class FastMCP(Generic[LifespanResultT]):
540
  tags: set[str] | None = None,
541
  annotations: ToolAnnotations | dict[str, Any] | None = None,
542
  exclude_args: list[str] | None = None,
 
543
  ) -> Callable[[AnyFunction], FunctionTool]: ...
544
 
545
  def tool(
@@ -551,6 +650,7 @@ class FastMCP(Generic[LifespanResultT]):
551
  tags: set[str] | None = None,
552
  annotations: ToolAnnotations | dict[str, Any] | None = None,
553
  exclude_args: list[str] | None = None,
 
554
  ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
555
  """Decorator to register a tool.
556
 
@@ -567,11 +667,12 @@ class FastMCP(Generic[LifespanResultT]):
567
 
568
  Args:
569
  name_or_fn: Either a function (when used as @tool), a string name, or None
 
570
  description: Optional description of what the tool does
571
  tags: Optional set of tags for categorizing the tool
572
- annotations: Optional annotations about the tool's behavior
573
  exclude_args: Optional list of argument names to exclude from the tool schema
574
- name: Optional name for the tool (keyword-only, alternative to name_or_fn)
575
 
576
  Example:
577
  @server.tool
@@ -624,6 +725,7 @@ class FastMCP(Generic[LifespanResultT]):
624
  annotations=annotations,
625
  exclude_args=exclude_args,
626
  serializer=self._tool_serializer,
 
627
  )
628
  self.add_tool(tool)
629
  return tool
@@ -652,6 +754,7 @@ class FastMCP(Generic[LifespanResultT]):
652
  tags=tags,
653
  annotations=annotations,
654
  exclude_args=exclude_args,
 
655
  )
656
 
657
  def add_resource(self, resource: Resource, key: str | None = None) -> None:
@@ -718,6 +821,7 @@ class FastMCP(Generic[LifespanResultT]):
718
  description: str | None = None,
719
  mime_type: str | None = None,
720
  tags: set[str] | None = None,
 
721
  ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
722
  """Decorator to register a function as a resource.
723
 
@@ -740,6 +844,7 @@ class FastMCP(Generic[LifespanResultT]):
740
  description: Optional description of the resource
741
  mime_type: Optional MIME type for the resource
742
  tags: Optional set of tags for categorizing the resource
 
743
 
744
  Example:
745
  @server.resource("resource://my-resource")
@@ -804,6 +909,7 @@ class FastMCP(Generic[LifespanResultT]):
804
  description=description,
805
  mime_type=mime_type,
806
  tags=tags,
 
807
  )
808
  self.add_template(template)
809
  return template
@@ -815,6 +921,7 @@ class FastMCP(Generic[LifespanResultT]):
815
  description=description,
816
  mime_type=mime_type,
817
  tags=tags,
 
818
  )
819
  self.add_resource(resource)
820
  return resource
@@ -843,6 +950,7 @@ class FastMCP(Generic[LifespanResultT]):
843
  name: str | None = None,
844
  description: str | None = None,
845
  tags: set[str] | None = None,
 
846
  ) -> FunctionPrompt: ...
847
 
848
  @overload
@@ -853,6 +961,7 @@ class FastMCP(Generic[LifespanResultT]):
853
  name: str | None = None,
854
  description: str | None = None,
855
  tags: set[str] | None = None,
 
856
  ) -> Callable[[AnyFunction], FunctionPrompt]: ...
857
 
858
  def prompt(
@@ -862,6 +971,7 @@ class FastMCP(Generic[LifespanResultT]):
862
  name: str | None = None,
863
  description: str | None = None,
864
  tags: set[str] | None = None,
 
865
  ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
866
  """Decorator to register a prompt.
867
 
@@ -878,9 +988,10 @@ class FastMCP(Generic[LifespanResultT]):
878
 
879
  Args:
880
  name_or_fn: Either a function (when used as @prompt), a string name, or None
 
881
  description: Optional description of what the prompt does
882
  tags: Optional set of tags for categorizing the prompt
883
- name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
884
 
885
  Example:
886
  @server.prompt
@@ -953,6 +1064,7 @@ class FastMCP(Generic[LifespanResultT]):
953
  name=prompt_name,
954
  description=description,
955
  tags=tags,
 
956
  )
957
  self.add_prompt(prompt)
958
 
@@ -980,6 +1092,7 @@ class FastMCP(Generic[LifespanResultT]):
980
  name=prompt_name,
981
  description=description,
982
  tags=tags,
 
983
  )
984
 
985
  async def run_stdio_async(self) -> None:
 
44
  import fastmcp
45
  import fastmcp.server
46
  import fastmcp.settings
47
+ from fastmcp.exceptions import DisabledError, NotFoundError
48
  from fastmcp.prompts import Prompt, PromptManager
49
  from fastmcp.prompts.prompt import FunctionPrompt
50
  from fastmcp.resources import Resource, ResourceManager
 
291
  self._cache.set("resources", resources)
292
  return resources
293
 
294
+ async def get_resource(self, key: str) -> Resource:
295
+ resources = await self.get_resources()
296
+ if key not in resources:
297
+ raise NotFoundError(f"Unknown resource: {key}")
298
+ return resources[key]
299
+
300
  async def get_resource_templates(self) -> dict[str, ResourceTemplate]:
301
  """Get all registered resource templates, indexed by registered key."""
302
  if (
 
317
  self._cache.set("resource_templates", templates)
318
  return templates
319
 
320
+ async def get_resource_template(self, key: str) -> ResourceTemplate:
321
+ templates = await self.get_resource_templates()
322
+ if key not in templates:
323
+ raise NotFoundError(f"Unknown resource template: {key}")
324
+ return templates[key]
325
+
326
  async def get_prompts(self) -> dict[str, Prompt]:
327
  """
328
  List all available prompts.
 
342
  self._cache.set("prompts", prompts)
343
  return prompts
344
 
345
+ async def get_prompt(self, key: str) -> Prompt:
346
+ prompts = await self.get_prompts()
347
+ if key not in prompts:
348
+ raise NotFoundError(f"Unknown prompt: {key}")
349
+ return prompts[key]
350
+
351
  def custom_route(
352
  self,
353
  path: str,
 
399
 
400
  """
401
  tools = await self.get_tools()
402
+ return [
403
+ tool.to_mcp_tool(name=key) for key, tool in tools.items() if tool.enabled
404
+ ]
405
 
406
  async def _mcp_list_resources(self) -> list[MCPResource]:
407
  """
 
411
  """
412
  resources = await self.get_resources()
413
  return [
414
+ resource.to_mcp_resource(uri=key)
415
+ for key, resource in resources.items()
416
+ if resource.enabled
417
  ]
418
 
419
  async def _mcp_list_resource_templates(self) -> list[MCPResourceTemplate]:
 
426
  return [
427
  template.to_mcp_template(uriTemplate=key)
428
  for key, template in templates.items()
429
+ if template.enabled
430
  ]
431
 
432
  async def _mcp_list_prompts(self) -> list[MCPPrompt]:
 
436
 
437
  """
438
  prompts = await self.get_prompts()
439
+ return [
440
+ prompt.to_mcp_prompt(name=key)
441
+ for key, prompt in prompts.items()
442
+ if prompt.enabled
443
+ ]
444
 
445
  async def _mcp_call_tool(
446
  self, key: str, arguments: dict[str, Any]
447
  ) -> list[TextContent | ImageContent | EmbeddedResource]:
448
+ """
449
+ Handle MCP 'callTool' requests.
450
+
451
+ Delegates to _call_tool, which should be overridden by FastMCP subclasses.
452
 
453
  Args:
454
  key: The name of the tool to call
 
461
 
462
  # Create and use context for the entire call
463
  with fastmcp.server.context.Context(fastmcp=self):
464
+ try:
465
+ return await self._call_tool(key, arguments)
466
+ except DisabledError:
467
+ # convert to NotFoundError to avoid leaking tool presence
468
+ raise NotFoundError(f"Unknown tool: {key}")
469
+ except NotFoundError:
470
+ # standardize NotFound message
471
+ raise NotFoundError(f"Unknown tool: {key}")
472
+
473
+ async def _call_tool(
474
+ self, key: str, arguments: dict[str, Any]
475
+ ) -> list[TextContent | ImageContent | EmbeddedResource]:
476
+ """
477
+ Call a tool with raw MCP arguments. FastMCP subclasses should override
478
+ this method, not _mcp_call_tool.
479
 
480
+ Args:
481
+ key: The name of the tool to call arguments: Arguments to pass to
482
+ the tool
 
 
483
 
484
+ Returns:
485
+ List of MCP Content objects containing the tool results
486
+ """
487
+
488
+ # Get tool, checking first from our tools, then from the mounted servers
489
+ if self._tool_manager.has_tool(key):
490
+ tool = self._tool_manager.get_tool(key)
491
+ if not tool.enabled:
492
+ raise DisabledError(f"Tool {key!r} is disabled")
493
+ return await self._tool_manager.call_tool(key, arguments)
494
+
495
+ # Check mounted servers to see if they have the tool
496
+ for server in self._mounted_servers.values():
497
+ if server.match_tool(key):
498
+ tool_key = server.strip_tool_prefix(key)
499
+ return await server.server._call_tool(tool_key, arguments)
500
+
501
+ raise NotFoundError(f"Unknown tool: {key!r}")
502
 
503
  async def _mcp_read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
504
+ """
505
+ Handle MCP 'readResource' requests.
506
+
507
+ Delegates to _read_resource, which should be overridden by FastMCP subclasses.
508
+ """
509
+ logger.debug("Read resource: %s", uri)
510
+
511
+ with fastmcp.server.context.Context(fastmcp=self):
512
+ try:
513
+ return await self._read_resource(uri)
514
+ except DisabledError:
515
+ # convert to NotFoundError to avoid leaking resource presence
516
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}")
517
+ except NotFoundError:
518
+ # standardize NotFound message
519
+ raise NotFoundError(f"Unknown resource: {str(uri)!r}")
520
+
521
+ async def _read_resource(self, uri: AnyUrl | str) -> list[ReadResourceContents]:
522
  """
523
  Read a resource by URI, in the format expected by the low-level MCP
524
  server.
525
  """
526
+ if self._resource_manager.has_resource(uri):
527
+ resource = await self._resource_manager.get_resource(uri)
528
+ if not resource.enabled:
529
+ raise DisabledError(f"Resource {str(uri)!r} is disabled")
530
+ content = await self._resource_manager.read_resource(uri)
531
+ return [
532
+ ReadResourceContents(
533
+ content=content,
534
+ mime_type=resource.mime_type,
535
+ )
536
+ ]
537
+ else:
538
+ for server in self._mounted_servers.values():
539
+ if server.match_resource(str(uri)):
540
+ new_uri = server.strip_resource_prefix(str(uri))
541
+ return await server.server._mcp_read_resource(new_uri)
542
  else:
543
+ raise NotFoundError(f"Unknown resource: {uri}")
 
 
 
 
 
544
 
545
  async def _mcp_get_prompt(
546
  self, name: str, arguments: dict[str, Any] | None = None
547
+ ) -> GetPromptResult:
548
+ """
549
+ Handle MCP 'getPrompt' requests.
550
+
551
+ Delegates to _get_prompt, which should be overridden by FastMCP subclasses.
552
+ """
553
+ logger.debug("Get prompt: %s with %s", name, arguments)
554
+
555
+ with fastmcp.server.context.Context(fastmcp=self):
556
+ try:
557
+ return await self._get_prompt(name, arguments)
558
+ except DisabledError:
559
+ # convert to NotFoundError to avoid leaking prompt presence
560
+ raise NotFoundError(f"Unknown prompt: {name}")
561
+ except NotFoundError:
562
+ # standardize NotFound message
563
+ raise NotFoundError(f"Unknown prompt: {name}")
564
+
565
+ async def _get_prompt(
566
+ self, name: str, arguments: dict[str, Any] | None = None
567
  ) -> GetPromptResult:
568
  """Handle MCP 'getPrompt' requests.
569
 
 
576
  """
577
  logger.debug("Get prompt: %s with %s", name, arguments)
578
 
579
+ # Get prompt, checking first from our prompts, then from the mounted servers
580
+ if self._prompt_manager.has_prompt(name):
581
+ prompt = self._prompt_manager.get_prompt(name)
582
+ if not prompt.enabled:
583
+ raise DisabledError(f"Prompt {name!r} is disabled")
584
+ return await self._prompt_manager.render_prompt(name, arguments)
585
 
586
+ # Check mounted servers to see if they have the prompt
587
+ for server in self._mounted_servers.values():
588
+ if server.match_prompt(name):
589
+ prompt_name = server.strip_prompt_prefix(name)
590
+ return await server.server._mcp_get_prompt(prompt_name, arguments)
591
 
592
+ raise NotFoundError(f"Unknown prompt: {name}")
593
 
594
  def add_tool(self, tool: Tool) -> None:
595
  """Add a tool to the server.
 
625
  tags: set[str] | None = None,
626
  annotations: ToolAnnotations | dict[str, Any] | None = None,
627
  exclude_args: list[str] | None = None,
628
+ enabled: bool | None = None,
629
  ) -> FunctionTool: ...
630
 
631
  @overload
 
638
  tags: set[str] | None = None,
639
  annotations: ToolAnnotations | dict[str, Any] | None = None,
640
  exclude_args: list[str] | None = None,
641
+ enabled: bool | None = None,
642
  ) -> Callable[[AnyFunction], FunctionTool]: ...
643
 
644
  def tool(
 
650
  tags: set[str] | None = None,
651
  annotations: ToolAnnotations | dict[str, Any] | None = None,
652
  exclude_args: list[str] | None = None,
653
+ enabled: bool | None = None,
654
  ) -> Callable[[AnyFunction], FunctionTool] | FunctionTool:
655
  """Decorator to register a tool.
656
 
 
667
 
668
  Args:
669
  name_or_fn: Either a function (when used as @tool), a string name, or None
670
+ name: Optional name for the tool (keyword-only, alternative to name_or_fn)
671
  description: Optional description of what the tool does
672
  tags: Optional set of tags for categorizing the tool
673
+ annotations: Optional annotations about the tool's behavior (e.g. {"is_async": True})
674
  exclude_args: Optional list of argument names to exclude from the tool schema
675
+ enabled: Optional boolean to enable or disable the tool
676
 
677
  Example:
678
  @server.tool
 
725
  annotations=annotations,
726
  exclude_args=exclude_args,
727
  serializer=self._tool_serializer,
728
+ enabled=enabled,
729
  )
730
  self.add_tool(tool)
731
  return tool
 
754
  tags=tags,
755
  annotations=annotations,
756
  exclude_args=exclude_args,
757
+ enabled=enabled,
758
  )
759
 
760
  def add_resource(self, resource: Resource, key: str | None = None) -> None:
 
821
  description: str | None = None,
822
  mime_type: str | None = None,
823
  tags: set[str] | None = None,
824
+ enabled: bool | None = None,
825
  ) -> Callable[[AnyFunction], Resource | ResourceTemplate]:
826
  """Decorator to register a function as a resource.
827
 
 
844
  description: Optional description of the resource
845
  mime_type: Optional MIME type for the resource
846
  tags: Optional set of tags for categorizing the resource
847
+ enabled: Optional boolean to enable or disable the resource
848
 
849
  Example:
850
  @server.resource("resource://my-resource")
 
909
  description=description,
910
  mime_type=mime_type,
911
  tags=tags,
912
+ enabled=enabled,
913
  )
914
  self.add_template(template)
915
  return template
 
921
  description=description,
922
  mime_type=mime_type,
923
  tags=tags,
924
+ enabled=enabled,
925
  )
926
  self.add_resource(resource)
927
  return resource
 
950
  name: str | None = None,
951
  description: str | None = None,
952
  tags: set[str] | None = None,
953
+ enabled: bool | None = None,
954
  ) -> FunctionPrompt: ...
955
 
956
  @overload
 
961
  name: str | None = None,
962
  description: str | None = None,
963
  tags: set[str] | None = None,
964
+ enabled: bool | None = None,
965
  ) -> Callable[[AnyFunction], FunctionPrompt]: ...
966
 
967
  def prompt(
 
971
  name: str | None = None,
972
  description: str | None = None,
973
  tags: set[str] | None = None,
974
+ enabled: bool | None = None,
975
  ) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt:
976
  """Decorator to register a prompt.
977
 
 
988
 
989
  Args:
990
  name_or_fn: Either a function (when used as @prompt), a string name, or None
991
+ name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
992
  description: Optional description of what the prompt does
993
  tags: Optional set of tags for categorizing the prompt
994
+ enabled: Optional boolean to enable or disable the prompt
995
 
996
  Example:
997
  @server.prompt
 
1064
  name=prompt_name,
1065
  description=description,
1066
  tags=tags,
1067
+ enabled=enabled,
1068
  )
1069
  self.add_prompt(prompt)
1070
 
 
1092
  name=prompt_name,
1093
  description=description,
1094
  tags=tags,
1095
+ enabled=enabled,
1096
  )
1097
 
1098
  async def run_stdio_async(self) -> None:
src/fastmcp/tools/tool.py CHANGED
@@ -62,6 +62,7 @@ class Tool(FastMCPComponent, ABC):
62
  annotations: ToolAnnotations | None = None,
63
  exclude_args: list[str] | None = None,
64
  serializer: Callable[[Any], str] | None = None,
 
65
  ) -> FunctionTool:
66
  """Create a Tool from a function."""
67
  return FunctionTool.from_function(
@@ -72,6 +73,7 @@ class Tool(FastMCPComponent, ABC):
72
  annotations=annotations,
73
  exclude_args=exclude_args,
74
  serializer=serializer,
 
75
  )
76
 
77
  @abstractmethod
@@ -92,6 +94,7 @@ class Tool(FastMCPComponent, ABC):
92
  tags: set[str] | None = None,
93
  annotations: ToolAnnotations | None = None,
94
  serializer: Callable[[Any], str] | None = None,
 
95
  ) -> TransformedTool:
96
  from fastmcp.tools.tool_transform import TransformedTool
97
 
@@ -104,6 +107,7 @@ class Tool(FastMCPComponent, ABC):
104
  tags=tags,
105
  annotations=annotations,
106
  serializer=serializer,
 
107
  )
108
 
109
 
@@ -120,6 +124,7 @@ class FunctionTool(Tool):
120
  annotations: ToolAnnotations | None = None,
121
  exclude_args: list[str] | None = None,
122
  serializer: Callable[[Any], str] | None = None,
 
123
  ) -> FunctionTool:
124
  """Create a Tool from a function."""
125
 
@@ -136,6 +141,7 @@ class FunctionTool(Tool):
136
  tags=tags or set(),
137
  annotations=annotations,
138
  serializer=serializer,
 
139
  )
140
 
141
  async def run(
 
62
  annotations: ToolAnnotations | None = None,
63
  exclude_args: list[str] | None = None,
64
  serializer: Callable[[Any], str] | None = None,
65
+ enabled: bool | None = None,
66
  ) -> FunctionTool:
67
  """Create a Tool from a function."""
68
  return FunctionTool.from_function(
 
73
  annotations=annotations,
74
  exclude_args=exclude_args,
75
  serializer=serializer,
76
+ enabled=enabled,
77
  )
78
 
79
  @abstractmethod
 
94
  tags: set[str] | None = None,
95
  annotations: ToolAnnotations | None = None,
96
  serializer: Callable[[Any], str] | None = None,
97
+ enabled: bool | None = None,
98
  ) -> TransformedTool:
99
  from fastmcp.tools.tool_transform import TransformedTool
100
 
 
107
  tags=tags,
108
  annotations=annotations,
109
  serializer=serializer,
110
+ enabled=enabled,
111
  )
112
 
113
 
 
124
  annotations: ToolAnnotations | None = None,
125
  exclude_args: list[str] | None = None,
126
  serializer: Callable[[Any], str] | None = None,
127
+ enabled: bool | None = None,
128
  ) -> FunctionTool:
129
  """Create a Tool from a function."""
130
 
 
141
  tags=tags or set(),
142
  annotations=annotations,
143
  serializer=serializer,
144
+ enabled=enabled if enabled is not None else True,
145
  )
146
 
147
  async def run(
src/fastmcp/tools/tool_transform.py CHANGED
@@ -267,6 +267,7 @@ class TransformedTool(Tool):
267
  transform_args: dict[str, ArgTransform] | None = None,
268
  annotations: ToolAnnotations | None = None,
269
  serializer: Callable[[Any], str] | None = None,
 
270
  ) -> TransformedTool:
271
  """Create a transformed tool from a parent tool.
272
 
@@ -399,6 +400,7 @@ class TransformedTool(Tool):
399
  annotations=annotations or tool.annotations,
400
  serializer=serializer or tool.serializer,
401
  transform_args=transform_args,
 
402
  )
403
 
404
  return transformed_tool
 
267
  transform_args: dict[str, ArgTransform] | None = None,
268
  annotations: ToolAnnotations | None = None,
269
  serializer: Callable[[Any], str] | None = None,
270
+ enabled: bool | None = None,
271
  ) -> TransformedTool:
272
  """Create a transformed tool from a parent tool.
273
 
 
400
  annotations=annotations or tool.annotations,
401
  serializer=serializer or tool.serializer,
402
  transform_args=transform_args,
403
+ enabled=enabled if enabled is not None else True,
404
  )
405
 
406
  return transformed_tool
src/fastmcp/utilities/components.py CHANGED
@@ -32,6 +32,11 @@ class FastMCPComponent(FastMCPBaseModel):
32
  description="Tags for the component.",
33
  )
34
 
 
 
 
 
 
35
  def __eq__(self, other: object) -> bool:
36
  if type(self) is not type(other):
37
  return False
@@ -39,4 +44,12 @@ class FastMCPComponent(FastMCPBaseModel):
39
  return self.model_dump() == other.model_dump()
40
 
41
  def __repr__(self) -> str:
42
- return f"{self.__class__.__name__}(name={self.name!r}, description={self.description!r}, tags={self.tags})"
 
 
 
 
 
 
 
 
 
32
  description="Tags for the component.",
33
  )
34
 
35
+ enabled: bool = Field(
36
+ default=True,
37
+ description="Whether the component is enabled.",
38
+ )
39
+
40
  def __eq__(self, other: object) -> bool:
41
  if type(self) is not type(other):
42
  return False
 
44
  return self.model_dump() == other.model_dump()
45
 
46
  def __repr__(self) -> str:
47
+ return f"{self.__class__.__name__}(name={self.name!r}, description={self.description!r}, tags={self.tags}, enabled={self.enabled})"
48
+
49
+ def enable(self) -> None:
50
+ """Enable the component."""
51
+ self.enabled = True
52
+
53
+ def disable(self) -> None:
54
+ """Disable the component."""
55
+ self.enabled = False
tests/server/test_proxy.py CHANGED
@@ -178,7 +178,9 @@ class TestResources:
178
  assert json.loads(result[0].text) == USERS # type: ignore[attr-defined]
179
 
180
  async def test_read_resource_returns_none_if_not_found(self, proxy_server):
181
- with pytest.raises(McpError, match="Unknown resource: resource://nonexistent"):
 
 
182
  async with Client(proxy_server) as client:
183
  await client.read_resource("resource://nonexistent")
184
 
 
178
  assert json.loads(result[0].text) == USERS # type: ignore[attr-defined]
179
 
180
  async def test_read_resource_returns_none_if_not_found(self, proxy_server):
181
+ with pytest.raises(
182
+ McpError, match="Unknown resource: 'resource://nonexistent'"
183
+ ):
184
  async with Client(proxy_server) as client:
185
  await client.read_resource("resource://nonexistent")
186
 
tests/server/test_server_interactions.py CHANGED
@@ -699,6 +699,102 @@ class TestToolContextInjection:
699
  assert result[0].text == "3" # type: ignore[attr-defined]
700
 
701
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
  class TestResource:
703
  async def test_text_resource(self):
704
  mcp = FastMCP()
@@ -783,6 +879,102 @@ class TestResourceContext:
783
  assert result[0].text == "1" # type: ignore[attr-defined]
784
 
785
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
786
  class TestResourceTemplates:
787
  async def test_resource_with_params_not_in_uri(self):
788
  """Test that a resource with function parameters raises an error if the URI
@@ -1034,6 +1226,99 @@ class TestResourceTemplateContext:
1034
  assert result[0].text.startswith("Resource template: test 1") # type: ignore[attr-defined]
1035
 
1036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1037
  class TestPrompts:
1038
  """Test prompt functionality in FastMCP server."""
1039
 
@@ -1220,6 +1505,102 @@ class TestPrompts:
1220
  assert prompt.tags == {"example", "test-tag"}
1221
 
1222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1223
  class TestPromptContext:
1224
  async def test_prompt_context(self):
1225
  mcp = FastMCP()
 
699
  assert result[0].text == "3" # type: ignore[attr-defined]
700
 
701
 
702
+ class TestToolEnabled:
703
+ async def test_toggle_enabled(self):
704
+ mcp = FastMCP()
705
+
706
+ @mcp.tool
707
+ def sample_tool(x: int) -> int:
708
+ return x * 2
709
+
710
+ assert sample_tool.enabled
711
+
712
+ tool = await mcp.get_tool("sample_tool")
713
+ assert tool.enabled
714
+
715
+ tool.disable()
716
+
717
+ assert not tool.enabled
718
+ assert not sample_tool.enabled
719
+
720
+ tool.enable()
721
+ assert tool.enabled
722
+ assert sample_tool.enabled
723
+
724
+ async def test_tool_disabled_in_decorator(self):
725
+ mcp = FastMCP()
726
+
727
+ @mcp.tool(enabled=False)
728
+ def sample_tool(x: int) -> int:
729
+ return x * 2
730
+
731
+ async with Client(mcp) as client:
732
+ tools = await client.list_tools()
733
+ assert len(tools) == 0
734
+
735
+ with pytest.raises(ToolError, match="Unknown tool"):
736
+ await client.call_tool("sample_tool", {"x": 5})
737
+
738
+ async def test_tool_toggle_enabled(self):
739
+ mcp = FastMCP()
740
+
741
+ @mcp.tool(enabled=False)
742
+ def sample_tool(x: int) -> int:
743
+ return x * 2
744
+
745
+ sample_tool.enable()
746
+
747
+ async with Client(mcp) as client:
748
+ tools = await client.list_tools()
749
+ assert len(tools) == 1
750
+
751
+ async def test_tool_toggle_disabled(self):
752
+ mcp = FastMCP()
753
+
754
+ @mcp.tool
755
+ def sample_tool(x: int) -> int:
756
+ return x * 2
757
+
758
+ sample_tool.disable()
759
+
760
+ async with Client(mcp) as client:
761
+ tools = await client.list_tools()
762
+ assert len(tools) == 0
763
+
764
+ with pytest.raises(ToolError, match="Unknown tool"):
765
+ await client.call_tool("sample_tool", {"x": 5})
766
+
767
+ async def test_get_tool_and_disable(self):
768
+ mcp = FastMCP()
769
+
770
+ @mcp.tool
771
+ def sample_tool(x: int) -> int:
772
+ return x * 2
773
+
774
+ tool = await mcp.get_tool("sample_tool")
775
+ assert tool.enabled
776
+
777
+ sample_tool.disable()
778
+
779
+ async with Client(mcp) as client:
780
+ result = await client.list_tools()
781
+ assert len(result) == 0
782
+
783
+ with pytest.raises(ToolError, match="Unknown tool"):
784
+ await client.call_tool("sample_tool", {"x": 5})
785
+
786
+ async def test_cant_call_disabled_tool(self):
787
+ mcp = FastMCP()
788
+
789
+ @mcp.tool(enabled=False)
790
+ def sample_tool(x: int) -> int:
791
+ return x * 2
792
+
793
+ with pytest.raises(Exception, match="Unknown tool"):
794
+ async with Client(mcp) as client:
795
+ await client.call_tool("sample_tool", {"x": 5})
796
+
797
+
798
  class TestResource:
799
  async def test_text_resource(self):
800
  mcp = FastMCP()
 
879
  assert result[0].text == "1" # type: ignore[attr-defined]
880
 
881
 
882
+ class TestResourceEnabled:
883
+ async def test_toggle_enabled(self):
884
+ mcp = FastMCP()
885
+
886
+ @mcp.resource("resource://data")
887
+ def sample_resource() -> str:
888
+ return "Hello, world!"
889
+
890
+ assert sample_resource.enabled
891
+
892
+ resource = await mcp.get_resource("resource://data")
893
+ assert resource.enabled
894
+
895
+ resource.disable()
896
+
897
+ assert not resource.enabled
898
+ assert not sample_resource.enabled
899
+
900
+ resource.enable()
901
+ assert resource.enabled
902
+ assert sample_resource.enabled
903
+
904
+ async def test_resource_disabled_in_decorator(self):
905
+ mcp = FastMCP()
906
+
907
+ @mcp.resource("resource://data", enabled=False)
908
+ def sample_resource() -> str:
909
+ return "Hello, world!"
910
+
911
+ async with Client(mcp) as client:
912
+ resources = await client.list_resources()
913
+ assert len(resources) == 0
914
+
915
+ with pytest.raises(McpError, match="Unknown resource"):
916
+ await client.read_resource(AnyUrl("resource://data"))
917
+
918
+ async def test_resource_toggle_enabled(self):
919
+ mcp = FastMCP()
920
+
921
+ @mcp.resource("resource://data", enabled=False)
922
+ def sample_resource() -> str:
923
+ return "Hello, world!"
924
+
925
+ sample_resource.enable()
926
+
927
+ async with Client(mcp) as client:
928
+ resources = await client.list_resources()
929
+ assert len(resources) == 1
930
+
931
+ async def test_resource_toggle_disabled(self):
932
+ mcp = FastMCP()
933
+
934
+ @mcp.resource("resource://data")
935
+ def sample_resource() -> str:
936
+ return "Hello, world!"
937
+
938
+ sample_resource.disable()
939
+
940
+ async with Client(mcp) as client:
941
+ resources = await client.list_resources()
942
+ assert len(resources) == 0
943
+
944
+ with pytest.raises(McpError, match="Unknown resource"):
945
+ await client.read_resource(AnyUrl("resource://data"))
946
+
947
+ async def test_get_resource_and_disable(self):
948
+ mcp = FastMCP()
949
+
950
+ @mcp.resource("resource://data")
951
+ def sample_resource() -> str:
952
+ return "Hello, world!"
953
+
954
+ resource = await mcp.get_resource("resource://data")
955
+ assert resource.enabled
956
+
957
+ sample_resource.disable()
958
+
959
+ async with Client(mcp) as client:
960
+ result = await client.list_resources()
961
+ assert len(result) == 0
962
+
963
+ with pytest.raises(McpError, match="Unknown resource"):
964
+ await client.read_resource(AnyUrl("resource://data"))
965
+
966
+ async def test_cant_read_disabled_resource(self):
967
+ mcp = FastMCP()
968
+
969
+ @mcp.resource("resource://data", enabled=False)
970
+ def sample_resource() -> str:
971
+ return "Hello, world!"
972
+
973
+ with pytest.raises(McpError, match="Unknown resource"):
974
+ async with Client(mcp) as client:
975
+ await client.read_resource(AnyUrl("resource://data"))
976
+
977
+
978
  class TestResourceTemplates:
979
  async def test_resource_with_params_not_in_uri(self):
980
  """Test that a resource with function parameters raises an error if the URI
 
1226
  assert result[0].text.startswith("Resource template: test 1") # type: ignore[attr-defined]
1227
 
1228
 
1229
+ class TestResourceTemplateEnabled:
1230
+ async def test_toggle_enabled(self):
1231
+ mcp = FastMCP()
1232
+
1233
+ @mcp.resource("resource://{param}")
1234
+ def sample_template(param: str) -> str:
1235
+ return f"Template: {param}"
1236
+
1237
+ assert sample_template.enabled
1238
+
1239
+ template = await mcp.get_resource_template("resource://{param}")
1240
+ assert template.enabled
1241
+
1242
+ template.disable()
1243
+
1244
+ assert not template.enabled
1245
+ assert not sample_template.enabled
1246
+
1247
+ template.enable()
1248
+ assert template.enabled
1249
+ assert sample_template.enabled
1250
+
1251
+ async def test_template_disabled_in_decorator(self):
1252
+ mcp = FastMCP()
1253
+
1254
+ @mcp.resource("resource://{param}", enabled=False)
1255
+ def sample_template(param: str) -> str:
1256
+ return f"Template: {param}"
1257
+
1258
+ async with Client(mcp) as client:
1259
+ templates = await client.list_resource_templates()
1260
+ assert len(templates) == 0
1261
+
1262
+ with pytest.raises(McpError, match="Unknown resource"):
1263
+ await client.read_resource(AnyUrl("resource://test"))
1264
+
1265
+ async def test_template_toggle_enabled(self):
1266
+ mcp = FastMCP()
1267
+
1268
+ @mcp.resource("resource://{param}", enabled=False)
1269
+ def sample_template(param: str) -> str:
1270
+ return f"Template: {param}"
1271
+
1272
+ sample_template.enable()
1273
+
1274
+ async with Client(mcp) as client:
1275
+ templates = await client.list_resource_templates()
1276
+ assert len(templates) == 1
1277
+
1278
+ async def test_template_toggle_disabled(self):
1279
+ mcp = FastMCP()
1280
+
1281
+ @mcp.resource("resource://{param}")
1282
+ def sample_template(param: str) -> str:
1283
+ return f"Template: {param}"
1284
+
1285
+ sample_template.disable()
1286
+
1287
+ async with Client(mcp) as client:
1288
+ templates = await client.list_resource_templates()
1289
+ assert len(templates) == 0
1290
+
1291
+ async def test_get_template_and_disable(self):
1292
+ mcp = FastMCP()
1293
+
1294
+ @mcp.resource("resource://{param}")
1295
+ def sample_template(param: str) -> str:
1296
+ return f"Template: {param}"
1297
+
1298
+ template = await mcp.get_resource_template("resource://{param}")
1299
+ assert template.enabled
1300
+
1301
+ sample_template.disable()
1302
+
1303
+ async with Client(mcp) as client:
1304
+ result = await client.list_resource_templates()
1305
+ assert len(result) == 0
1306
+
1307
+ with pytest.raises(McpError, match="Unknown resource"):
1308
+ await client.read_resource(AnyUrl("resource://test"))
1309
+
1310
+ async def test_cant_read_disabled_template(self):
1311
+ mcp = FastMCP()
1312
+
1313
+ @mcp.resource("resource://{param}", enabled=False)
1314
+ def sample_template(param: str) -> str:
1315
+ return f"Template: {param}"
1316
+
1317
+ with pytest.raises(McpError, match="Unknown resource"):
1318
+ async with Client(mcp) as client:
1319
+ await client.read_resource(AnyUrl("resource://test"))
1320
+
1321
+
1322
  class TestPrompts:
1323
  """Test prompt functionality in FastMCP server."""
1324
 
 
1505
  assert prompt.tags == {"example", "test-tag"}
1506
 
1507
 
1508
+ class TestPromptEnabled:
1509
+ async def test_toggle_enabled(self):
1510
+ mcp = FastMCP()
1511
+
1512
+ @mcp.prompt
1513
+ def sample_prompt() -> str:
1514
+ return "Hello, world!"
1515
+
1516
+ assert sample_prompt.enabled
1517
+
1518
+ prompt = await mcp.get_prompt("sample_prompt")
1519
+ assert prompt.enabled
1520
+
1521
+ prompt.disable()
1522
+
1523
+ assert not prompt.enabled
1524
+ assert not sample_prompt.enabled
1525
+
1526
+ prompt.enable()
1527
+ assert prompt.enabled
1528
+ assert sample_prompt.enabled
1529
+
1530
+ async def test_prompt_disabled_in_decorator(self):
1531
+ mcp = FastMCP()
1532
+
1533
+ @mcp.prompt(enabled=False)
1534
+ def sample_prompt() -> str:
1535
+ return "Hello, world!"
1536
+
1537
+ async with Client(mcp) as client:
1538
+ prompts = await client.list_prompts()
1539
+ assert len(prompts) == 0
1540
+
1541
+ with pytest.raises(McpError, match="Unknown prompt"):
1542
+ await client.get_prompt("sample_prompt")
1543
+
1544
+ async def test_prompt_toggle_enabled(self):
1545
+ mcp = FastMCP()
1546
+
1547
+ @mcp.prompt(enabled=False)
1548
+ def sample_prompt() -> str:
1549
+ return "Hello, world!"
1550
+
1551
+ sample_prompt.enable()
1552
+
1553
+ async with Client(mcp) as client:
1554
+ prompts = await client.list_prompts()
1555
+ assert len(prompts) == 1
1556
+
1557
+ async def test_prompt_toggle_disabled(self):
1558
+ mcp = FastMCP()
1559
+
1560
+ @mcp.prompt
1561
+ def sample_prompt() -> str:
1562
+ return "Hello, world!"
1563
+
1564
+ sample_prompt.disable()
1565
+
1566
+ async with Client(mcp) as client:
1567
+ prompts = await client.list_prompts()
1568
+ assert len(prompts) == 0
1569
+
1570
+ with pytest.raises(McpError, match="Unknown prompt"):
1571
+ await client.get_prompt("sample_prompt")
1572
+
1573
+ async def test_get_prompt_and_disable(self):
1574
+ mcp = FastMCP()
1575
+
1576
+ @mcp.prompt
1577
+ def sample_prompt() -> str:
1578
+ return "Hello, world!"
1579
+
1580
+ prompt = await mcp.get_prompt("sample_prompt")
1581
+ assert prompt.enabled
1582
+
1583
+ sample_prompt.disable()
1584
+
1585
+ async with Client(mcp) as client:
1586
+ result = await client.list_prompts()
1587
+ assert len(result) == 0
1588
+
1589
+ with pytest.raises(McpError, match="Unknown prompt"):
1590
+ await client.get_prompt("sample_prompt")
1591
+
1592
+ async def test_cant_get_disabled_prompt(self):
1593
+ mcp = FastMCP()
1594
+
1595
+ @mcp.prompt(enabled=False)
1596
+ def sample_prompt() -> str:
1597
+ return "Hello, world!"
1598
+
1599
+ with pytest.raises(McpError, match="Unknown prompt"):
1600
+ async with Client(mcp) as client:
1601
+ await client.get_prompt("sample_prompt")
1602
+
1603
+
1604
  class TestPromptContext:
1605
  async def test_prompt_context(self):
1606
  mcp = FastMCP()
tests/tools/test_tool_transform.py CHANGED
@@ -9,6 +9,7 @@ from typing_extensions import TypedDict
9
 
10
  from fastmcp import FastMCP
11
  from fastmcp.client.client import Client
 
12
  from fastmcp.tools import Tool, forward, forward_raw
13
  from fastmcp.tools.tool import FunctionTool
14
  from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
@@ -51,7 +52,7 @@ async def test_tool_defaults_are_maintained_on_unmapped_args(add_tool):
51
  add_tool, transform_args={"old_x": ArgTransform(name="new_x")}
52
  )
53
  result = await new_tool.run(arguments={"new_x": 1})
54
- assert result[0].text == "11" # type: ignore
55
 
56
 
57
  async def test_tool_defaults_are_maintained_on_mapped_args(add_tool):
@@ -59,7 +60,7 @@ async def test_tool_defaults_are_maintained_on_mapped_args(add_tool):
59
  add_tool, transform_args={"old_y": ArgTransform(name="new_y")}
60
  )
61
  result = await new_tool.run(arguments={"old_x": 1})
62
- assert result[0].text == "11" # type: ignore
63
 
64
 
65
  def test_tool_change_arg_name(add_tool):
@@ -86,7 +87,7 @@ async def test_tool_drop_arg(add_tool):
86
  )
87
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
88
  result = await new_tool.run(arguments={"old_x": 1})
89
- assert result[0].text == "11" # type: ignore
90
 
91
 
92
  async def test_dropped_args_error_if_provided(add_tool):
@@ -108,7 +109,7 @@ async def test_hidden_arg_with_constant_default(add_tool):
108
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
109
  # Should pass old_x=5 and old_y=20 to parent
110
  result = await new_tool.run(arguments={"old_x": 5})
111
- assert result[0].text == "25" # type: ignore
112
 
113
 
114
  async def test_hidden_arg_without_default_uses_parent_default(add_tool):
@@ -120,7 +121,7 @@ async def test_hidden_arg_without_default_uses_parent_default(add_tool):
120
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
121
  # Should pass old_x=3 and let parent use its default old_y=10
122
  result = await new_tool.run(arguments={"old_x": 3})
123
- assert result[0].text == "13" # type: ignore
124
 
125
 
126
  async def test_mixed_hidden_args_with_custom_function(add_tool):
@@ -145,7 +146,7 @@ async def test_mixed_hidden_args_with_custom_function(add_tool):
145
  assert sorted(new_tool.parameters["properties"]) == ["visible_x"]
146
  # Should pass visible_x=7 as old_x=7 and old_y=25 to parent
147
  result = await new_tool.run(arguments={"visible_x": 7})
148
- assert result[0].text == "32" # type: ignore
149
 
150
 
151
  async def test_hide_required_param_without_default_raises_error():
@@ -183,7 +184,7 @@ async def test_hide_required_param_with_user_default_works():
183
  assert sorted(new_tool.parameters["properties"]) == ["optional_param"]
184
  # Should pass required_param=5 and optional_param=20 to parent
185
  result = await new_tool.run(arguments={"optional_param": 20})
186
- assert result[0].text == "25" # type: ignore
187
 
188
 
189
  async def test_forward_with_argument_mapping(add_tool):
@@ -202,7 +203,7 @@ async def test_forward_with_argument_mapping(add_tool):
202
  )
203
 
204
  result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
205
- assert result[0].text == "5" # type: ignore
206
 
207
 
208
  async def test_forward_with_incorrect_args_raises_error(add_tool):
@@ -242,7 +243,7 @@ async def test_forward_raw_without_argument_mapping(add_tool):
242
  )
243
 
244
  result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
245
- assert result[0].text == "5" # type: ignore
246
 
247
 
248
  async def test_custom_fn_with_kwargs_and_no_transform_args(add_tool):
@@ -252,7 +253,7 @@ async def test_custom_fn_with_kwargs_and_no_transform_args(add_tool):
252
 
253
  new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
254
  result = await new_tool.run(arguments={"extra": 1, "old_x": 2, "old_y": 3})
255
- assert result[0].text == "6" # type: ignore
256
  assert new_tool.parameters["required"] == IsList(
257
  "extra", "old_x", check_order=False
258
  )
@@ -269,7 +270,7 @@ async def test_fn_with_kwargs_passes_through_original_args(add_tool):
269
 
270
  new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
271
  result = await new_tool.run(arguments={"new_y": 2, "old_y": 3})
272
- assert result[0].text == "5" # type: ignore
273
 
274
 
275
  async def test_fn_with_kwargs_receives_transformed_arg_names(add_tool):
@@ -287,7 +288,7 @@ async def test_fn_with_kwargs_receives_transformed_arg_names(add_tool):
287
  transform_args={"old_x": ArgTransform(name="new_x")},
288
  )
289
  result = await new_tool.run(arguments={"new_x": 2, "old_y": 3})
290
- assert result[0].text == "5" # type: ignore
291
 
292
 
293
  async def test_fn_with_kwargs_handles_partial_explicit_args(add_tool):
@@ -307,7 +308,7 @@ async def test_fn_with_kwargs_handles_partial_explicit_args(add_tool):
307
  result = await new_tool.run(
308
  arguments={"new_x": 3, "old_y": 7, "some_other_param": "test"}
309
  )
310
- assert result[0].text == "10" # type: ignore
311
 
312
 
313
  async def test_fn_with_kwargs_mixed_mapped_and_unmapped_args(add_tool):
@@ -325,7 +326,7 @@ async def test_fn_with_kwargs_mixed_mapped_and_unmapped_args(add_tool):
325
  transform_args={"old_x": ArgTransform(name="new_x")},
326
  ) # only map 'a'
327
  result = await new_tool.run(arguments={"new_x": 1, "old_y": 5})
328
- assert result[0].text == "6" # type: ignore
329
 
330
 
331
  async def test_fn_with_kwargs_dropped_args_not_in_kwargs(add_tool):
@@ -468,7 +469,7 @@ async def test_tool_transform_chaining(add_tool):
468
  tool2 = Tool.from_tool(tool1, transform_args={"x": ArgTransform(name="final_x")})
469
 
470
  result = await tool2.run(arguments={"final_x": 5})
471
- assert result[0].text == "15" # type: ignore
472
 
473
  # Transform tool1 with custom function that handles all parameters
474
  async def custom(final_x: int, **kwargs) -> str:
@@ -479,7 +480,7 @@ async def test_tool_transform_chaining(add_tool):
479
  tool1, transform_fn=custom, transform_args={"x": ArgTransform(name="final_x")}
480
  )
481
  result = await tool3.run(arguments={"final_x": 3, "old_y": 5})
482
- assert result[0].text == "custom 8" # type: ignore
483
 
484
 
485
  class MyModel(BaseModel):
@@ -634,7 +635,7 @@ async def test_arg_transform_precedence_over_function_with_kwargs():
634
  # Test it works at runtime
635
  result = await tool.run(arguments={"y": "test"})
636
  # Should use ArgTransform default of 42
637
- assert "42: test" in result[0].text # type: ignore
638
 
639
 
640
  def test_arg_transform_combined_attributes():
@@ -691,8 +692,8 @@ async def test_arg_transform_type_precedence_runtime():
691
 
692
  # Test it works with string input
693
  result = await tool.run(arguments={"x": "5", "y": 3})
694
- assert "String input '5'" in result[0].text # type: ignore
695
- assert "result: 8" in result[0].text # type: ignore
696
 
697
 
698
  class TestProxy:
@@ -727,7 +728,7 @@ class TestProxy:
727
  async with Client(proxy_server) as client:
728
  # The tool should be registered with its transformed name
729
  result = await client.call_tool("add_transformed", {"new_x": 1, "old_y": 2})
730
- assert result[0].text == "3" # type: ignore
731
 
732
 
733
  async def test_arg_transform_default_factory():
@@ -750,7 +751,7 @@ async def test_arg_transform_default_factory():
750
 
751
  # Should work without providing timestamp (gets value from factory)
752
  result = await new_tool.run(arguments={"x": 42})
753
- assert result[0].text == "42_12345.0" # type: ignore
754
 
755
 
756
  async def test_arg_transform_default_factory_called_each_time():
@@ -778,11 +779,11 @@ async def test_arg_transform_default_factory_called_each_time():
778
 
779
  # First call
780
  result1 = await new_tool.run(arguments={"x": 1})
781
- assert result1[0].text == "1_1" # type: ignore
782
 
783
  # Second call should get a different value
784
  result2 = await new_tool.run(arguments={"x": 2})
785
- assert result2[0].text == "2_2" # type: ignore
786
 
787
 
788
  async def test_arg_transform_hidden_with_default_factory():
@@ -807,7 +808,7 @@ async def test_arg_transform_hidden_with_default_factory():
807
 
808
  # Should pass hidden request_id with factory value
809
  result = await new_tool.run(arguments={"x": 42})
810
- assert result[0].text == "42_req_123" # type: ignore
811
 
812
 
813
  async def test_arg_transform_default_and_factory_raises_error():
@@ -942,3 +943,47 @@ async def test_arg_transform_hide_and_required_raises_error():
942
  ValueError, match="Cannot specify both 'hide=True' and 'required=True'"
943
  ):
944
  ArgTransform(hide=True, required=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  from fastmcp import FastMCP
11
  from fastmcp.client.client import Client
12
+ from fastmcp.exceptions import ToolError
13
  from fastmcp.tools import Tool, forward, forward_raw
14
  from fastmcp.tools.tool import FunctionTool
15
  from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
 
52
  add_tool, transform_args={"old_x": ArgTransform(name="new_x")}
53
  )
54
  result = await new_tool.run(arguments={"new_x": 1})
55
+ assert result[0].text == "11" # type: ignore[attr-defined]
56
 
57
 
58
  async def test_tool_defaults_are_maintained_on_mapped_args(add_tool):
 
60
  add_tool, transform_args={"old_y": ArgTransform(name="new_y")}
61
  )
62
  result = await new_tool.run(arguments={"old_x": 1})
63
+ assert result[0].text == "11" # type: ignore[attr-defined]
64
 
65
 
66
  def test_tool_change_arg_name(add_tool):
 
87
  )
88
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
89
  result = await new_tool.run(arguments={"old_x": 1})
90
+ assert result[0].text == "11" # type: ignore[attr-defined]
91
 
92
 
93
  async def test_dropped_args_error_if_provided(add_tool):
 
109
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
110
  # Should pass old_x=5 and old_y=20 to parent
111
  result = await new_tool.run(arguments={"old_x": 5})
112
+ assert result[0].text == "25" # type: ignore[attr-defined]
113
 
114
 
115
  async def test_hidden_arg_without_default_uses_parent_default(add_tool):
 
121
  assert sorted(new_tool.parameters["properties"]) == ["old_x"]
122
  # Should pass old_x=3 and let parent use its default old_y=10
123
  result = await new_tool.run(arguments={"old_x": 3})
124
+ assert result[0].text == "13" # type: ignore[attr-defined]
125
 
126
 
127
  async def test_mixed_hidden_args_with_custom_function(add_tool):
 
146
  assert sorted(new_tool.parameters["properties"]) == ["visible_x"]
147
  # Should pass visible_x=7 as old_x=7 and old_y=25 to parent
148
  result = await new_tool.run(arguments={"visible_x": 7})
149
+ assert result[0].text == "32" # type: ignore[attr-defined]
150
 
151
 
152
  async def test_hide_required_param_without_default_raises_error():
 
184
  assert sorted(new_tool.parameters["properties"]) == ["optional_param"]
185
  # Should pass required_param=5 and optional_param=20 to parent
186
  result = await new_tool.run(arguments={"optional_param": 20})
187
+ assert result[0].text == "25" # type: ignore[attr-defined]
188
 
189
 
190
  async def test_forward_with_argument_mapping(add_tool):
 
203
  )
204
 
205
  result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
206
+ assert result[0].text == "5" # type: ignore[attr-defined]
207
 
208
 
209
  async def test_forward_with_incorrect_args_raises_error(add_tool):
 
243
  )
244
 
245
  result = await new_tool.run(arguments={"new_x": 2, "new_y": 3})
246
+ assert result[0].text == "5" # type: ignore[attr-defined]
247
 
248
 
249
  async def test_custom_fn_with_kwargs_and_no_transform_args(add_tool):
 
253
 
254
  new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
255
  result = await new_tool.run(arguments={"extra": 1, "old_x": 2, "old_y": 3})
256
+ assert result[0].text == "6" # type: ignore[attr-defined]
257
  assert new_tool.parameters["required"] == IsList(
258
  "extra", "old_x", check_order=False
259
  )
 
270
 
271
  new_tool = Tool.from_tool(add_tool, transform_fn=custom_fn)
272
  result = await new_tool.run(arguments={"new_y": 2, "old_y": 3})
273
+ assert result[0].text == "5" # type: ignore[attr-defined]
274
 
275
 
276
  async def test_fn_with_kwargs_receives_transformed_arg_names(add_tool):
 
288
  transform_args={"old_x": ArgTransform(name="new_x")},
289
  )
290
  result = await new_tool.run(arguments={"new_x": 2, "old_y": 3})
291
+ assert result[0].text == "5" # type: ignore[attr-defined]
292
 
293
 
294
  async def test_fn_with_kwargs_handles_partial_explicit_args(add_tool):
 
308
  result = await new_tool.run(
309
  arguments={"new_x": 3, "old_y": 7, "some_other_param": "test"}
310
  )
311
+ assert result[0].text == "10" # type: ignore[attr-defined]
312
 
313
 
314
  async def test_fn_with_kwargs_mixed_mapped_and_unmapped_args(add_tool):
 
326
  transform_args={"old_x": ArgTransform(name="new_x")},
327
  ) # only map 'a'
328
  result = await new_tool.run(arguments={"new_x": 1, "old_y": 5})
329
+ assert result[0].text == "6" # type: ignore[attr-defined]
330
 
331
 
332
  async def test_fn_with_kwargs_dropped_args_not_in_kwargs(add_tool):
 
469
  tool2 = Tool.from_tool(tool1, transform_args={"x": ArgTransform(name="final_x")})
470
 
471
  result = await tool2.run(arguments={"final_x": 5})
472
+ assert result[0].text == "15" # type: ignore[attr-defined]
473
 
474
  # Transform tool1 with custom function that handles all parameters
475
  async def custom(final_x: int, **kwargs) -> str:
 
480
  tool1, transform_fn=custom, transform_args={"x": ArgTransform(name="final_x")}
481
  )
482
  result = await tool3.run(arguments={"final_x": 3, "old_y": 5})
483
+ assert result[0].text == "custom 8" # type: ignore[attr-defined]
484
 
485
 
486
  class MyModel(BaseModel):
 
635
  # Test it works at runtime
636
  result = await tool.run(arguments={"y": "test"})
637
  # Should use ArgTransform default of 42
638
+ assert "42: test" in result[0].text # type: ignore[attr-defined]
639
 
640
 
641
  def test_arg_transform_combined_attributes():
 
692
 
693
  # Test it works with string input
694
  result = await tool.run(arguments={"x": "5", "y": 3})
695
+ assert "String input '5'" in result[0].text # type: ignore[attr-defined]
696
+ assert "result: 8" in result[0].text # type: ignore[attr-defined]
697
 
698
 
699
  class TestProxy:
 
728
  async with Client(proxy_server) as client:
729
  # The tool should be registered with its transformed name
730
  result = await client.call_tool("add_transformed", {"new_x": 1, "old_y": 2})
731
+ assert result[0].text == "3" # type: ignore[attr-defined]
732
 
733
 
734
  async def test_arg_transform_default_factory():
 
751
 
752
  # Should work without providing timestamp (gets value from factory)
753
  result = await new_tool.run(arguments={"x": 42})
754
+ assert result[0].text == "42_12345.0" # type: ignore[attr-defined]
755
 
756
 
757
  async def test_arg_transform_default_factory_called_each_time():
 
779
 
780
  # First call
781
  result1 = await new_tool.run(arguments={"x": 1})
782
+ assert result1[0].text == "1_1" # type: ignore[attr-defined]
783
 
784
  # Second call should get a different value
785
  result2 = await new_tool.run(arguments={"x": 2})
786
+ assert result2[0].text == "2_2" # type: ignore[attr-defined]
787
 
788
 
789
  async def test_arg_transform_hidden_with_default_factory():
 
808
 
809
  # Should pass hidden request_id with factory value
810
  result = await new_tool.run(arguments={"x": 42})
811
+ assert result[0].text == "42_req_123" # type: ignore[attr-defined]
812
 
813
 
814
  async def test_arg_transform_default_and_factory_raises_error():
 
943
  ValueError, match="Cannot specify both 'hide=True' and 'required=True'"
944
  ):
945
  ArgTransform(hide=True, required=True)
946
+
947
+
948
+ class TestEnableDisable:
949
+ async def test_transform_disabled_tool(self):
950
+ """
951
+ Tests that a transformed tool can run even if the parent tool is disabled
952
+ """
953
+ mcp = FastMCP()
954
+
955
+ @mcp.tool(enabled=False)
956
+ def add(x: int, y: int = 10) -> int:
957
+ return x + y
958
+
959
+ new_add = Tool.from_tool(add, name="new_add")
960
+ mcp.add_tool(new_add)
961
+
962
+ assert new_add.enabled
963
+
964
+ async with Client(mcp) as client:
965
+ tools = await client.list_tools()
966
+ assert {tool.name for tool in tools} == {"new_add"}
967
+
968
+ result = await client.call_tool("new_add", {"x": 1, "y": 2})
969
+ assert result[0].text == "3" # type: ignore[attr-defined]
970
+
971
+ with pytest.raises(ToolError):
972
+ await client.call_tool("add", {"x": 1, "y": 2})
973
+
974
+ async def test_disable_transformed_tool(self):
975
+ mcp = FastMCP()
976
+
977
+ @mcp.tool(enabled=False)
978
+ def add(x: int, y: int = 10) -> int:
979
+ return x + y
980
+
981
+ new_add = Tool.from_tool(add, name="new_add", enabled=False)
982
+ mcp.add_tool(new_add)
983
+
984
+ async with Client(mcp) as client:
985
+ tools = await client.list_tools()
986
+ assert len(tools) == 0
987
+
988
+ with pytest.raises(ToolError):
989
+ await client.call_tool("new_add", {"x": 1, "y": 2})