diff --git a/src/fastmcp/contrib/mcp_mixin/README.md b/src/fastmcp/contrib/mcp_mixin/README.md index 6dce9c9e3..86c46651c 100644 --- a/src/fastmcp/contrib/mcp_mixin/README.md +++ b/src/fastmcp/contrib/mcp_mixin/README.md @@ -1,26 +1,103 @@ +from mcp.types import ToolAnnotations + # MCP Mixin This module provides the `MCPMixin` base class and associated decorators (`@mcp_tool`, `@mcp_resource`, `@mcp_prompt`). It allows developers to easily define classes whose methods can be registered as tools, resources, or prompts with a `FastMCP` server instance using the `register_all()`, `register_tools()`, `register_resources()`, or `register_prompts()` methods provided by the mixin. +Includes support for +Tools: +* [enable/disable](https://gofastmcp.com/servers/tools#disabling-tools) +* [annotations](https://gofastmcp.com/servers/tools#annotations-2) +* [excluded arguments](https://gofastmcp.com/servers/tools#excluding-arguments) + +Prompts: +* [enable/disable](https://gofastmcp.com/servers/prompts#disabling-prompts) + +Resources: +* [enable/disabe](https://gofastmcp.com/servers/resources#disabling-resources) + ## Usage Inherit from `MCPMixin` and use the decorators on the methods you want to register. ```python +from mcp.types import ToolAnnotations from fastmcp import FastMCP -from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource +from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool, mcp_resource, mcp_prompt class MyComponent(MCPMixin): @mcp_tool(name="my_tool", description="Does something cool.") def tool_method(self): return "Tool executed!" + # example of disabled tool + @mcp_tool(name="my_tool", description="Does something cool.", enabled=False) + def disabled_tool_method(self): + # This function can't be called by client because it's disabled + return "You'll never get here!" + + # example of excluded parameter tool + @mcp_tool( + name="my_tool", description="Does something cool.", + enabled=False, exclude_args=['delete_everything'], + ) + def excluded_param_tool_method(self, delete_everything=False): + # MCP tool calls can't pass the "delete_everything" argument + if delete_everything: + return "Nothing to delete, I bet you're not a tool :)" + return "You might be a tool if..." + + # example tool w/annotations + @mcp_tool( + name="my_tool", description="Does something cool.", + annotations=ToolAnnotations( + title="Attn LLM, use this tool first!", + readOnlyHint=False, + destructiveHint=False, + idempotentHint=False, + ) + ) + def tool_method(self): + return "Tool executed!" + + # example tool w/everything + @mcp_tool( + name="my_tool", description="Does something cool.", + enabled=True, + exclude_args=['delete_all'], + annotations=ToolAnnotations( + title="Attn LLM, use this tool first!", + readOnlyHint=False, + destructiveHint=False, + idempotentHint=False, + ) + ) + def tool_method(self, delete_all=False): + if delete_all: + return "99 records deleted. I bet you're not a tool :)" + return "Tool executed, but you might be a tool!" + @mcp_resource(uri="component://data") def resource_method(self): return {"data": "some data"} + # Disabled resource + @mcp_resource(uri="component://data", enabled=False) + def resource_method(self): + return {"data": "some data"} + + # prompt + @mcp_prompt(name="A prompt") + def prompt_method(self, name): + return f"Whats up {name}?" + + # disabled prompt + @mcp_prompt(name="A prompt", enabled=False) + def prompt_method(self, name): + return f"Whats up {name}?" + mcp_server = FastMCP() component = MyComponent() @@ -36,4 +113,4 @@ component.register_all(mcp_server, prefix="my_comp") # Or 'my_tool' and 'component://data' are registered (if no prefix used) ``` -The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types. \ No newline at end of file +The `prefix` argument in registration methods is optional. If omitted, methods are registered with their original decorated names/URIs. Individual separators (`tools_separator`, `resources_separator`, `prompts_separator`) can also be provided to `register_all` to change the separator for specific types. diff --git a/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py b/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py index dd3a007b5..6b8cf62f7 100644 --- a/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py +++ b/src/fastmcp/contrib/mcp_mixin/mcp_mixin.py @@ -3,6 +3,8 @@ from collections.abc import Callable from typing import TYPE_CHECKING, Any +from mcp.types import ToolAnnotations + from fastmcp.prompts.prompt import Prompt from fastmcp.resources.resource import Resource from fastmcp.tools.tool import Tool @@ -23,6 +25,10 @@ def mcp_tool( name: str | None = None, description: str | None = None, tags: set[str] | None = None, + annotations: ToolAnnotations | dict[str, Any] | None = None, + exclude_args: list[str] | None = None, + serializer: Callable[[Any], str] | None = None, + enabled: bool | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP tool for later registration.""" @@ -31,6 +37,10 @@ def mcp_tool( "name": name or func.__name__, "description": description, "tags": tags, + "annotations": annotations, + "exclude_args": exclude_args, + "serializer": serializer, + "enabled": enabled, } call_args = {k: v for k, v in call_args.items() if v is not None} setattr(func, _MCP_REGISTRATION_TOOL_ATTR, call_args) @@ -46,6 +56,7 @@ def mcp_resource( description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None, + enabled: bool | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP resource for later registration.""" @@ -56,6 +67,7 @@ def mcp_resource( "description": description, "mime_type": mime_type, "tags": tags, + "enabled": enabled, } call_args = {k: v for k, v in call_args.items() if v is not None} @@ -70,6 +82,7 @@ def mcp_prompt( name: str | None = None, description: str | None = None, tags: set[str] | None = None, + enabled: bool | None = None, ) -> Callable[[Callable[..., Any]], Callable[..., Any]]: """Decorator to mark a method as an MCP prompt for later registration.""" @@ -78,6 +91,7 @@ def mcp_prompt( "name": name or func.__name__, "description": description, "tags": tags, + "enabled": enabled, } call_args = {k: v for k, v in call_args.items() if v is not None}