diff --git a/README.md b/README.md index c2552c910aa21a5a63e403a3e776350d0e023d9e..dded24a46954aa51287961022f3324174b8adb85 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ from fastmcp import FastMCP mcp = FastMCP("Demo 🚀") -@mcp.tool() +@mcp.tool def add(a: int, b: int) -> int: """Add two numbers""" return a + b @@ -144,7 +144,7 @@ Learn more in the [**FastMCP Server Documentation**](https://gofastmcp.com/serve Tools allow LLMs to perform actions by executing your Python functions (sync or async). Ideal for computations, API calls, or side effects (like `POST`/`PUT`). FastMCP handles schema generation from type hints and docstrings. Tools can return various types, including text, JSON-serializable objects, and even images using the [`fastmcp.Image`](https://gofastmcp.com/servers/tools#return-values) helper. ```python -@mcp.tool() +@mcp.tool def multiply(a: float, b: float) -> float: """Multiplies two numbers.""" return a * b @@ -201,7 +201,7 @@ from fastmcp import FastMCP, Context mcp = FastMCP("My MCP Server") -@mcp.tool() +@mcp.tool async def process_data(uri: str, ctx: Context): # Log a message to the client await ctx.info(f"Processing {uri}...") @@ -321,7 +321,7 @@ from fastmcp import FastMCP mcp = FastMCP("Demo 🚀") -@mcp.tool() +@mcp.tool def hello(name: str) -> str: return f"Hello, {name}!" diff --git a/docs/clients/transports.mdx b/docs/clients/transports.mdx index 6ed9fc489da900f1898e9c024e879a05069162e6..94c94833cdf2dc22aabae0229ab4c49db7034cc8 100644 --- a/docs/clients/transports.mdx +++ b/docs/clients/transports.mdx @@ -359,7 +359,7 @@ import asyncio # 1. Create your FastMCP server instance server = FastMCP(name="InMemoryServer") -@server.tool() +@server.tool def ping(): return "pong" diff --git a/docs/deployment/asgi.mdx b/docs/deployment/asgi.mdx index ace7ee13676e510ea13e992248795ee999b65ae1..06da46374011e7342b887fdb55f16ddd240c4eb2 100644 --- a/docs/deployment/asgi.mdx +++ b/docs/deployment/asgi.mdx @@ -32,7 +32,7 @@ from fastmcp import FastMCP mcp = FastMCP("MyServer") -@mcp.tool() +@mcp.tool def hello(name: str) -> str: return f"Hello, {name}!" diff --git a/docs/deployment/running-server.mdx b/docs/deployment/running-server.mdx index 5f5d758d232e7607d7379d7c7c038c4b5994fdea..5b6a7eadd36e8768f0328fa16014cc015bcd3b79 100644 --- a/docs/deployment/running-server.mdx +++ b/docs/deployment/running-server.mdx @@ -22,7 +22,7 @@ from fastmcp import FastMCP mcp = FastMCP(name="MyServer") -@mcp.tool() +@mcp.tool def hello(name: str) -> str: return f"Hello, {name}!" @@ -244,7 +244,7 @@ import asyncio mcp = FastMCP(name="MyServer") -@mcp.tool() +@mcp.tool def hello(name: str) -> str: return f"Hello, {name}!" diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 13a6bbae5e367d359db96fc7fbcfbafbbb103fc2..b3569d486bf3c1891b3e3c67619249bef35fa10c 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -32,7 +32,7 @@ from fastmcp import FastMCP mcp = FastMCP("My MCP Server") -@mcp.tool() +@mcp.tool def greet(name: str) -> str: return f"Hello, {name}!" ``` @@ -49,7 +49,7 @@ from fastmcp import FastMCP, Client mcp = FastMCP("My MCP Server") -@mcp.tool() +@mcp.tool def greet(name: str) -> str: return f"Hello, {name}!" @@ -76,7 +76,7 @@ from fastmcp import FastMCP mcp = FastMCP("My MCP Server") -@mcp.tool() +@mcp.tool def greet(name: str) -> str: return f"Hello, {name}!" diff --git a/docs/getting-started/welcome.mdx b/docs/getting-started/welcome.mdx index f5574217ce14d51a067d2a496bd9d4807a7892f0..d9d921a7ae8836dda556cd519a7fe3998ada2612 100644 --- a/docs/getting-started/welcome.mdx +++ b/docs/getting-started/welcome.mdx @@ -14,7 +14,7 @@ from fastmcp import FastMCP mcp = FastMCP("Demo 🚀") -@mcp.tool() +@mcp.tool def add(a: int, b: int) -> int: """Add two numbers""" return a + b diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx index 0920afce244dcbf8cfa74a75b210ab39e69862bf..bcea25769df0ef03eb991f713a60742e290dc737 100644 --- a/docs/integrations/anthropic.mdx +++ b/docs/integrations/anthropic.mdx @@ -27,7 +27,7 @@ from fastmcp import FastMCP mcp = FastMCP(name="Dice Roller") -@mcp.tool() +@mcp.tool def roll_dice(n_dice: int) -> list[int]: """Roll `n_dice` 6-sided dice and return the results.""" return [random.randint(1, 6) for _ in range(n_dice)] @@ -170,7 +170,7 @@ auth = BearerAuthProvider( mcp = FastMCP(name="Dice Roller", auth=auth) -@mcp.tool() +@mcp.tool def roll_dice(n_dice: int) -> list[int]: """Roll `n_dice` 6-sided dice and return the results.""" return [random.randint(1, 6) for _ in range(n_dice)] diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx index 7edafa68b6f8caaa35f120e77ce7297894963a9e..9b8dc540423f297d514da666c53f3b388e9ae2a0 100644 --- a/docs/integrations/claude-desktop.mdx +++ b/docs/integrations/claude-desktop.mdx @@ -31,7 +31,7 @@ from fastmcp import FastMCP mcp = FastMCP(name="Dice Roller") -@mcp.tool() +@mcp.tool def roll_dice(n_dice: int) -> list[int]: """Roll `n_dice` 6-sided dice and return the results.""" return [random.randint(1, 6) for _ in range(n_dice)] diff --git a/docs/integrations/gemini.mdx b/docs/integrations/gemini.mdx index d61d0feb0350e917c182f6763d3fb07e79d8ca2b..46ffd7a9d6a4aa040e52da16f93cfb78d2e80230 100644 --- a/docs/integrations/gemini.mdx +++ b/docs/integrations/gemini.mdx @@ -31,7 +31,7 @@ from fastmcp import FastMCP mcp = FastMCP(name="Dice Roller") -@mcp.tool() +@mcp.tool def roll_dice(n_dice: int) -> list[int]: """Roll `n_dice` 6-sided dice and return the results.""" return [random.randint(1, 6) for _ in range(n_dice)] diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx index 86f01e6261c514a6bee9854987e642290939b409..52f3bb164e2cddd322a7f9a52b0f9aa21f997766 100644 --- a/docs/integrations/openai.mdx +++ b/docs/integrations/openai.mdx @@ -32,7 +32,7 @@ from fastmcp import FastMCP mcp = FastMCP(name="Dice Roller") -@mcp.tool() +@mcp.tool def roll_dice(n_dice: int) -> list[int]: """Roll `n_dice` 6-sided dice and return the results.""" return [random.randint(1, 6) for _ in range(n_dice)] @@ -165,7 +165,7 @@ auth = BearerAuthProvider( mcp = FastMCP(name="Dice Roller", auth=auth) -@mcp.tool() +@mcp.tool def roll_dice(n_dice: int) -> list[int]: """Roll `n_dice` 6-sided dice and return the results.""" return [random.randint(1, 6) for _ in range(n_dice)] diff --git a/docs/patterns/cli.mdx b/docs/patterns/cli.mdx index b46d928d4ab78985b7d0237694687c132e858dc1..19e1ad1ab0c5b836e4966a02809384bfee56783f 100644 --- a/docs/patterns/cli.mdx +++ b/docs/patterns/cli.mdx @@ -66,7 +66,7 @@ from fastmcp import FastMCP mcp = FastMCP("MyServer") -@mcp.tool() +@mcp.tool def hello(name: str) -> str: return f"Hello, {name}!" diff --git a/docs/patterns/decorating-methods.mdx b/docs/patterns/decorating-methods.mdx index 906142c445df8389d60e870b79f0cc45e0035663..b4ac48072c5b9ee729d63d05d948c3a4f6e30d53 100644 --- a/docs/patterns/decorating-methods.mdx +++ b/docs/patterns/decorating-methods.mdx @@ -5,11 +5,11 @@ description: Properly use instance methods, class methods, and static methods wi icon: at --- -FastMCP's decorator system is designed to work with functions, but you may see unexpected behavior if you try to decorate an instance or class method. This guide explains the correct approach for using methods with all FastMCP decorators (`@tool()`, `@resource()`, and `@prompt()`). +FastMCP's decorator system is designed to work with functions, but you may see unexpected behavior if you try to decorate an instance or class method. This guide explains the correct approach for using methods with all FastMCP decorators (`@tool`, `@resource()`, and `@prompt()`). ## Why Are Methods Hard? -When you apply a FastMCP decorator like `@tool()`, `@resource()`, or `@prompt()` to a method, the decorator captures the function at decoration time. For instance methods and class methods, this poses a challenge because: +When you apply a FastMCP decorator like `@tool`, `@resource()`, or `@prompt()` to a method, the decorator captures the function at decoration time. For instance methods and class methods, this poses a challenge because: 1. For instance methods: The decorator gets the unbound method before any instance exists 2. For class methods: The decorator gets the function before it's bound to the class @@ -28,7 +28,7 @@ from fastmcp import FastMCP mcp = FastMCP() class MyClass: - @mcp.tool() # This won't work correctly + @mcp.tool # This won't work correctly def add(self, x, y): return x + y @@ -83,7 +83,7 @@ mcp = FastMCP() class MyClass: @classmethod - @mcp.tool() # This won't work correctly + @mcp.tool # This won't work correctly def from_string(cls, s): return cls(s) ``` @@ -122,7 +122,7 @@ mcp = FastMCP() class MyClass: @staticmethod - @mcp.tool() # This works! + @mcp.tool # This works! def utility(x, y): return x + y @@ -194,7 +194,7 @@ The class automatically registers its methods during initialization, ensuring th While FastMCP's decorator pattern works seamlessly with regular functions and static methods, for instance methods and class methods, you should add them after creating the instance or class. This ensures that the methods are properly bound before being registered. These patterns apply to all FastMCP decorators and registration methods: -- `@tool()` and `add_tool()` +- `@tool()` and `add_tool` - `@resource()` and `add_resource_fn()` - `@prompt()` and `add_prompt()` diff --git a/docs/patterns/http-requests.mdx b/docs/patterns/http-requests.mdx index ceb333e19800f4db97d515dc8b9e5daf7f2dea39..c9b412c5d38f5dfd07573b6d2477342995b023e9 100644 --- a/docs/patterns/http-requests.mdx +++ b/docs/patterns/http-requests.mdx @@ -25,7 +25,7 @@ from starlette.requests import Request mcp = FastMCP(name="HTTP Request Demo") -@mcp.tool() +@mcp.tool async def user_agent_info() -> dict: """Return information about the user agent.""" # Get the HTTP request @@ -58,7 +58,7 @@ from fastmcp.server.dependencies import get_http_headers mcp = FastMCP(name="Headers Demo") -@mcp.tool() +@mcp.tool async def safe_header_info() -> dict: """Safely get header information without raising errors.""" # Get headers (returns empty dict if no request context) diff --git a/docs/patterns/testing.mdx b/docs/patterns/testing.mdx index 34c05a6f85d6ed07f13a0e202a8f0b631921c78c..adc3f516aca02987c3a1231fb25a3be691f75faa 100644 --- a/docs/patterns/testing.mdx +++ b/docs/patterns/testing.mdx @@ -22,7 +22,7 @@ from fastmcp import FastMCP, Client def mcp_server(): server = FastMCP("TestServer") - @server.tool() + @server.tool def greet(name: str) -> str: return f"Hello, {name}!" diff --git a/docs/servers/auth/bearer.mdx b/docs/servers/auth/bearer.mdx index c695c10e61d050c27526a9ef29a5d49880156933..13053b5b204c0705c5904ee59707fbb7b1030dd3 100644 --- a/docs/servers/auth/bearer.mdx +++ b/docs/servers/auth/bearer.mdx @@ -159,7 +159,7 @@ Once authenticated, your tools, resources, or prompts can access token informati from fastmcp import FastMCP, Context, ToolError from fastmcp.server.dependencies import get_access_token, AccessToken -@mcp.tool() +@mcp.tool async def get_my_data(ctx: Context) -> dict: access_token: AccessToken = get_access_token() diff --git a/docs/servers/composition.mdx b/docs/servers/composition.mdx index b1519b3f3613069b9be811d6a10e564c7364a279..04dc559d0b9d67835031071286b92829d087e82c 100644 --- a/docs/servers/composition.mdx +++ b/docs/servers/composition.mdx @@ -50,7 +50,7 @@ import asyncio # Define subservers weather_mcp = FastMCP(name="WeatherService") -@weather_mcp.tool() +@weather_mcp.tool def get_forecast(city: str) -> dict: """Get weather forecast.""" return {"city": city, "forecast": "Sunny"} @@ -102,7 +102,7 @@ from fastmcp import FastMCP, Client # Define subserver dynamic_mcp = FastMCP(name="DynamicService") -@dynamic_mcp.tool() +@dynamic_mcp.tool def initial_tool(): """Initial tool demonstration.""" return "Initial Tool Exists" @@ -112,7 +112,7 @@ main_mcp = FastMCP(name="MainAppLive") main_mcp.mount("dynamic", dynamic_mcp) # Add a tool AFTER mounting - it will be accessible through main_mcp -@dynamic_mcp.tool() +@dynamic_mcp.tool def added_later(): """Tool added after mounting.""" return "Tool Added Dynamically!" diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx index 555a0c105cdda9d7074315721ef0d6f4a7ec45a7..2818de3c038064faa885675575512f35695ab75f 100644 --- a/docs/servers/context.mdx +++ b/docs/servers/context.mdx @@ -41,7 +41,7 @@ from fastmcp import FastMCP, Context mcp = FastMCP(name="ContextDemo") -@mcp.tool() +@mcp.tool async def process_file(file_uri: str, ctx: Context) -> str: """Processes a file, using context for logging and resource access.""" # Context is available as the ctx parameter @@ -99,7 +99,7 @@ async def process_data(data: list[float]) -> dict: ctx = get_context() await ctx.info(f"Processing {len(data)} data points") -@mcp.tool() +@mcp.tool async def analyze_dataset(dataset_name: str) -> dict: # Call utility function that uses context internally data = load_data(dataset_name) @@ -118,7 +118,7 @@ async def analyze_dataset(dataset_name: str) -> dict: Send log messages back to the MCP client. This is useful for debugging and providing visibility into function execution during a request. ```python -@mcp.tool() +@mcp.tool async def analyze_data(data: list[float], ctx: Context) -> dict: """Analyze numerical data with logging.""" await ctx.debug("Starting analysis of numerical data") @@ -149,7 +149,7 @@ async def analyze_data(data: list[float], ctx: Context) -> dict: For long-running operations, notify the client about the progress. This allows clients to display progress indicators and provide a better user experience. ```python -@mcp.tool() +@mcp.tool async def process_items(items: list[str], ctx: Context) -> dict: """Process a list of items with progress updates.""" total = len(items) @@ -182,7 +182,7 @@ Progress reporting requires the client to have sent a `progressToken` in the ini Read data from resources registered with your FastMCP server. This allows functions to access files, configuration, or dynamically generated content. ```python -@mcp.tool() +@mcp.tool async def summarize_document(document_uri: str, ctx: Context) -> str: """Summarize a document by its resource URI.""" # Read the document content @@ -222,7 +222,7 @@ The returned content is typically accessed via `content_list[0].content` and can Request the client's LLM to generate text based on provided messages. This is useful when your function needs to leverage the LLM's capabilities to process data or generate responses. ```python -@mcp.tool() +@mcp.tool async def analyze_sentiment(text: str, ctx: Context) -> dict: """Analyze the sentiment of a text using the client's LLM.""" # Create a sampling prompt asking for sentiment analysis @@ -258,7 +258,7 @@ async def analyze_sentiment(text: str, ctx: Context) -> dict: When providing a simple string, it's treated as a user message. For more complex scenarios, you can provide a list of messages with different roles. ```python -@mcp.tool() +@mcp.tool async def generate_example(concept: str, ctx: Context) -> str: """Generate a Python code example for a given concept.""" # Using a system prompt and a user message @@ -280,7 +280,7 @@ See [Client Sampling](/clients/client#llm-sampling) for more details on how clie Access metadata about the current request and client. ```python -@mcp.tool() +@mcp.tool async def request_info(ctx: Context) -> dict: """Return information about the current request.""" return { @@ -300,7 +300,7 @@ async def request_info(ctx: Context) -> dict: #### FastMCP Server and Sessions ```python -@mcp.tool() +@mcp.tool async def advanced_tool(ctx: Context) -> str: """Demonstrate advanced context access.""" # Access the FastMCP server instance @@ -326,7 +326,7 @@ See the [HTTP Requests pattern](/patterns/http-requests) for more details. For web applications, you can access the underlying HTTP request: ```python -@mcp.tool() +@mcp.tool async def handle_web_request(ctx: Context) -> dict: """Access HTTP request information from the Starlette request.""" request = ctx.get_http_request() diff --git a/docs/servers/fastmcp.mdx b/docs/servers/fastmcp.mdx index 598d056b6c238f7e3d6659d98331adbcc301c09b..40808fb951c9275986ce1f3c8dfd7655828a8247 100644 --- a/docs/servers/fastmcp.mdx +++ b/docs/servers/fastmcp.mdx @@ -47,7 +47,7 @@ FastMCP servers expose several types of components to the client: Tools are functions that the client can call to perform actions or access external systems. ```python -@mcp.tool() +@mcp.tool def multiply(a: float, b: float) -> float: """Multiplies two numbers together.""" return a * b @@ -106,7 +106,7 @@ from fastmcp import FastMCP mcp = FastMCP(name="MyServer") -@mcp.tool() +@mcp.tool def greet(name: str) -> str: """Greet a user by name.""" return f"Hello, {name}!" @@ -145,7 +145,7 @@ import asyncio main = FastMCP(name="Main") sub = FastMCP(name="Sub") -@sub.tool() +@sub.tool def hello(): return "hi" @@ -216,7 +216,7 @@ def yaml_serializer(data): # Create a server with the custom serializer mcp = FastMCP(name="MyServer", tool_serializer=yaml_serializer) -@mcp.tool() +@mcp.tool def get_config(): """Returns configuration in YAML format.""" return {"api_key": "abc123", "debug": True, "rate_limit": 100} diff --git a/docs/servers/proxy.mdx b/docs/servers/proxy.mdx index d78d6d694bb7c19a80d3d007a8877d234db270e7..5a9bffccd32037986cc1e4efa5af3c81367cca38 100644 --- a/docs/servers/proxy.mdx +++ b/docs/servers/proxy.mdx @@ -90,7 +90,7 @@ from fastmcp import FastMCP # Original server original_server = FastMCP(name="Original") -@original_server.tool() +@original_server.tool def tool_a() -> str: return "A" diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index 365a8611d6c8711b9d898a8e9af45b12bd80a7b4..00fa13252f0e227440004b5e1550f893a9a1daeb 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -24,14 +24,14 @@ This allows LLMs to perform tasks like querying databases, calling APIs, making ### The `@tool` Decorator -Creating a tool is as simple as decorating a Python function with `@mcp.tool()`: +Creating a tool is as simple as decorating a Python function with `@mcp.tool`: ```python from fastmcp import FastMCP mcp = FastMCP(name="CalculatorServer") -@mcp.tool() +@mcp.tool def add(a: int, b: int) -> int: """Adds two integer numbers together.""" return a + b @@ -61,7 +61,7 @@ Type annotations for parameters are essential for proper tool functionality. The Use standard Python type annotations for parameters: ```python -@mcp.tool() +@mcp.tool def analyze_text( text: str, max_tokens: int = 100, @@ -79,7 +79,7 @@ You can provide additional metadata about parameters using Pydantic's `Field` cl from typing import Annotated from pydantic import Field -@mcp.tool() +@mcp.tool def process_image( image_url: Annotated[str, Field(description="URL of the image to process")], resize: Annotated[bool, Field(description="Whether to resize the image")] = False, @@ -97,7 +97,7 @@ def process_image( You can also use the Field as a default value, though the Annotated approach is preferred: ```python -@mcp.tool() +@mcp.tool def search_database( query: str = Field(description="Search query string"), limit: int = Field(10, description="Maximum number of results", ge=1, le=100) @@ -137,7 +137,7 @@ For additional type annotations not listed here, see the [Parameter Types](#para FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional. ```python -@mcp.tool() +@mcp.tool def search_products( query: str, # Required - no default value max_results: int = 10, # Optional - has default value @@ -197,14 +197,14 @@ FastMCP seamlessly supports both standard (`def`) and asynchronous (`async def`) ```python # Synchronous tool (suitable for CPU-bound or quick tasks) -@mcp.tool() +@mcp.tool def calculate_distance(lat1: float, lon1: float, lat2: float, lon2: float) -> float: """Calculate the distance between two coordinates.""" # Implementation... return 42.5 # Asynchronous tool (ideal for I/O-bound operations) -@mcp.tool() +@mcp.tool async def fetch_weather(city: str) -> dict: """Retrieve current weather conditions for a city.""" # Use 'async def' for operations involving network calls, file I/O, etc. @@ -244,7 +244,7 @@ except ImportError: mcp = FastMCP("Image Demo") -@mcp.tool() +@mcp.tool def generate_image(width: int, height: int, color: str) -> Image: """Generates a solid color image.""" # Create image using Pillow @@ -258,7 +258,7 @@ def generate_image(width: int, height: int, color: str) -> Image: # Return using FastMCP's Image helper return Image(data=img_bytes, format="png") -@mcp.tool() +@mcp.tool def do_nothing() -> None: """This tool performs an action but returns no data.""" print("Performing a side effect...") @@ -285,7 +285,7 @@ mcp = FastMCP(name="SecureServer", mask_error_details=True) from fastmcp import FastMCP from fastmcp.exceptions import ToolError -@mcp.tool() +@mcp.tool def divide(a: float, b: float) -> float: """Divide a by b.""" @@ -315,7 +315,7 @@ Annotations serve several purposes in client applications: - Describing the safety profile of tools (destructive vs. non-destructive) - Signaling if tools interact with external systems -You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool()` decorator: +You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator: ```python @mcp.tool( @@ -351,7 +351,7 @@ from fastmcp import FastMCP, Context mcp = FastMCP(name="ContextDemo") -@mcp.tool() +@mcp.tool async def process_data(data_uri: str, ctx: Context) -> dict: """Process data from a resource with progress reporting.""" await ctx.info(f"Processing data from {data_uri}") @@ -396,7 +396,7 @@ FastMCP supports **type coercion** when possible. This means that if a client se The most common parameter types are Python's built-in scalar types: ```python -@mcp.tool() +@mcp.tool def process_values( name: str, # Text data count: int, # Integer numbers @@ -416,7 +416,7 @@ FastMCP supports various date and time types from the `datetime` module: ```python from datetime import datetime, date, timedelta -@mcp.tool() +@mcp.tool def process_date_time( event_date: date, # ISO format date string or date object event_time: datetime, # ISO format datetime string or datetime object @@ -440,7 +440,7 @@ def process_date_time( FastMCP supports all standard Python collection types: ```python -@mcp.tool() +@mcp.tool def analyze_data( values: list[float], # List of numbers properties: dict[str, str], # Dictionary with string keys and values @@ -465,7 +465,7 @@ Collection types can be nested and combined to represent complex data structures For parameters that can accept multiple types or may be omitted: ```python -@mcp.tool() +@mcp.tool def flexible_search( query: str | int, # Can be either string or integer filters: dict[str, str] | None = None, # Optional dictionary @@ -488,7 +488,7 @@ Literals constrain parameters to a specific set of values: ```python from typing import Literal -@mcp.tool() +@mcp.tool def sort_data( data: list[float], order: Literal["ascending", "descending"] = "ascending", @@ -516,7 +516,7 @@ class Color(Enum): GREEN = "green" BLUE = "blue" -@mcp.tool() +@mcp.tool def process_image( image_path: str, color_filter: Color = Color.RED @@ -539,7 +539,7 @@ There are two approaches to handling binary data in tool parameters: #### Bytes ```python -@mcp.tool() +@mcp.tool def process_binary(data: bytes): """Process binary data directly. @@ -563,7 +563,7 @@ FastMCP does not automatically decode base64-encoded strings for bytes parameter from typing import Annotated from pydantic import Field -@mcp.tool() +@mcp.tool def process_image_data( image_data: Annotated[str, Field(description="Base64-encoded image data")] ): @@ -587,7 +587,7 @@ The `Path` type from the `pathlib` module can be used for file system paths: ```python from pathlib import Path -@mcp.tool() +@mcp.tool def process_file(path: Path) -> str: """Process a file at the given path.""" assert isinstance(path, Path) # Path is properly converted @@ -603,7 +603,7 @@ The `UUID` type from the `uuid` module can be used for unique identifiers: ```python import uuid -@mcp.tool() +@mcp.tool def process_item( item_id: uuid.UUID # String UUID or UUID object ) -> str: @@ -628,7 +628,7 @@ class User(BaseModel): age: int | None = None is_active: bool = True -@mcp.tool() +@mcp.tool def create_user(user: User): """Create a new user in the system.""" # The input is automatically validated against the User model @@ -657,7 +657,7 @@ Note that fields can be used *outside* Pydantic models to provide metadata and v from typing import Annotated from pydantic import Field -@mcp.tool() +@mcp.tool def analyze_metrics( # Numbers with range constraints count: Annotated[int, Field(ge=0, le=100)], # 0 <= count <= 100 @@ -682,7 +682,7 @@ def analyze_metrics( You can also use `Field` as a default value, though the `Annotated` approach is preferred: ```python -@mcp.tool() +@mcp.tool def validate_data( # Value constraints age: int = Field(ge=0, lt=120), # 0 <= age < 120 @@ -727,12 +727,12 @@ mcp = FastMCP( on_duplicate_tools="error" ) -@mcp.tool() +@mcp.tool def my_tool(): return "Version 1" # This will now raise a ValueError because 'my_tool' already exists # and on_duplicate_tools is set to "error". -# @mcp.tool() +# @mcp.tool # def my_tool(): return "Version 2" ``` @@ -754,7 +754,7 @@ from fastmcp import FastMCP mcp = FastMCP(name="DynamicToolServer") -@mcp.tool() +@mcp.tool def calculate_sum(a: int, b: int) -> int: """Add two numbers together.""" return a + b diff --git a/examples/complex_inputs.py b/examples/complex_inputs.py index 41276858f36aafdcc2056e84342ec28d87205ddb..014482a4ad96aa757632f3d8fee7990d5a81e310 100644 --- a/examples/complex_inputs.py +++ b/examples/complex_inputs.py @@ -20,7 +20,7 @@ class ShrimpTank(BaseModel): shrimp: list[Shrimp] -@mcp.tool() +@mcp.tool def name_shrimp( tank: ShrimpTank, # You can use pydantic Field in function signatures for validation. diff --git a/examples/config_server.py b/examples/config_server.py index d7964bc14f7605eff81282cea1c0180a2e8b8f38..9d6976d8b9c60e54216049c734254f5e377fe369 100644 --- a/examples/config_server.py +++ b/examples/config_server.py @@ -24,7 +24,7 @@ if args.debug: mcp = FastMCP(server_name) -@mcp.tool() +@mcp.tool def get_status() -> dict[str, str | bool]: """Get the current server configuration and status.""" return { @@ -34,7 +34,7 @@ def get_status() -> dict[str, str | bool]: } -@mcp.tool() +@mcp.tool def echo_message(message: str) -> str: """Echo a message, with debug info if debug mode is enabled.""" if args.debug: diff --git a/examples/desktop.py b/examples/desktop.py index 8ba0d456297a5074a4e994b25381a9163e783326..b32a314846c723f7948ef7a14e6ff5406ba57608 100644 --- a/examples/desktop.py +++ b/examples/desktop.py @@ -26,7 +26,7 @@ def get_greeting(name: str) -> str: return f"Hello, {name}!" -@mcp.tool() +@mcp.tool def add(a: int, b: int) -> int: """Add two numbers""" return a + b diff --git a/examples/echo.py b/examples/echo.py index 48c0883a51d3a880df0ce7f35d1c2b6560f402e1..c98f23d2f647409be924187093eee9db873a0dcf 100644 --- a/examples/echo.py +++ b/examples/echo.py @@ -8,7 +8,7 @@ from fastmcp import FastMCP mcp = FastMCP("Echo Server") -@mcp.tool() +@mcp.tool def echo_tool(text: str) -> str: """Echo the input text""" return text diff --git a/examples/memory.py b/examples/memory.py index 7be486dd3da1f71edb8b62e61cb717c02492444a..161f0d7f5da50da80a838c4c1fbdbc1203b49de6 100644 --- a/examples/memory.py +++ b/examples/memory.py @@ -279,7 +279,7 @@ async def display_memory_tree(deps: Deps) -> str: return result -@mcp.tool() +@mcp.tool async def remember( contents: list[str] = Field( description="List of observations or memories to store" @@ -294,7 +294,7 @@ async def remember( await deps.pool.close() -@mcp.tool() +@mcp.tool async def read_profile() -> str: deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool()) profile = await display_memory_tree(deps) diff --git a/examples/mount_example.py b/examples/mount_example.py index 821794a3942a4507b57dad1f467aec97c6454786..7720f0eb2270103467c111a2e5abcb73c64daf13 100644 --- a/examples/mount_example.py +++ b/examples/mount_example.py @@ -16,7 +16,7 @@ from fastmcp import FastMCP weather_app = FastMCP("Weather App") -@weather_app.tool() +@weather_app.tool def get_weather_forecast(location: str) -> str: """Get the weather forecast for a location.""" return f"Sunny skies for {location} today!" @@ -32,7 +32,7 @@ async def weather_data(): news_app = FastMCP("News App") -@news_app.tool() +@news_app.tool def get_news_headlines() -> list[str]: """Get the latest news headlines.""" return [ @@ -58,7 +58,7 @@ app = FastMCP( ) -@app.tool() +@app.tool def check_app_status() -> dict[str, str]: """Check the status of the main application.""" return {"status": "running", "version": "1.0.0", "uptime": "3h 24m"} diff --git a/examples/sampling.py b/examples/sampling.py index 385f9d57685fad06bbf1a6b654e88c34c4f7586a..cfb9c395a30d0f9591a9199a62a4681b28bb2c54 100644 --- a/examples/sampling.py +++ b/examples/sampling.py @@ -15,7 +15,7 @@ from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingPar mcp = FastMCP("Sampling Example") -@mcp.tool() +@mcp.tool async def example_tool(prompt: str, context: Context) -> str: """Sample a completion from the LLM.""" response = await context.sample( diff --git a/examples/screenshot.py b/examples/screenshot.py index 968d55f523faae5a00c334a6893b9bab44014a03..92b2bdb010f9596eb45876bf480564945fe551db 100644 --- a/examples/screenshot.py +++ b/examples/screenshot.py @@ -12,7 +12,7 @@ from fastmcp import FastMCP, Image mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"]) -@mcp.tool() +@mcp.tool def take_screenshot() -> Image: """ Take a screenshot of the user's screen and return it as an image. Use diff --git a/examples/serializer.py b/examples/serializer.py index 72e60843d8783fbdc8bd207a287900a2c46bf1e3..4ee5ca17a64dc3dbdba7fde10bb01ec2722881ed 100644 --- a/examples/serializer.py +++ b/examples/serializer.py @@ -14,7 +14,7 @@ def custom_dict_serializer(data: Any) -> str: server = FastMCP(name="CustomSerializerExample", tool_serializer=custom_dict_serializer) -@server.tool() +@server.tool def get_example_data() -> dict: """Returns some example data.""" return {"name": "Test", "value": 123, "status": True} diff --git a/examples/simple_echo.py b/examples/simple_echo.py index f98d8456a1a4d477f5c54d238a827c6002dc0eda..b1dc1f35a39f5bc3a526443c7685d2a035da6117 100644 --- a/examples/simple_echo.py +++ b/examples/simple_echo.py @@ -8,7 +8,7 @@ from fastmcp import FastMCP mcp = FastMCP("Echo Server") -@mcp.tool() +@mcp.tool def echo(text: str) -> str: """Echo the input text""" return text diff --git a/examples/smart_home/src/smart_home/hub.py b/examples/smart_home/src/smart_home/hub.py index 55239b8beca1d05c9745c41a7883b76c657489b9..ff91576fc324ea2328376aa5cd521a65ba628038 100644 --- a/examples/smart_home/src/smart_home/hub.py +++ b/examples/smart_home/src/smart_home/hub.py @@ -16,7 +16,7 @@ hub_mcp.mount("hue", lights_mcp) # Add a status check for the hub -@hub_mcp.tool() +@hub_mcp.tool def hub_status() -> str: """Checks the status of the main hub and connections.""" try: diff --git a/examples/smart_home/src/smart_home/lights/server.py b/examples/smart_home/src/smart_home/lights/server.py index c95382cc0cfaf5c3851d6416e340f61eb8564070..87e8d4455de3e02097bc3c0c9d771b8b52f0acce 100644 --- a/examples/smart_home/src/smart_home/lights/server.py +++ b/examples/smart_home/src/smart_home/lights/server.py @@ -43,7 +43,7 @@ lights_mcp = FastMCP( ) -@lights_mcp.tool() +@lights_mcp.tool def read_all_lights() -> list[str]: """Lists the names of all available Hue lights using phue2.""" if not (bridge := _get_bridge()): @@ -59,7 +59,7 @@ def read_all_lights() -> list[str]: # --- Tools --- -@lights_mcp.tool() +@lights_mcp.tool def toggle_light(light_name: str, state: bool) -> dict[str, Any]: """Turns a specific light on (true) or off (false) using phue2.""" if not (bridge := _get_bridge()): @@ -76,7 +76,7 @@ def toggle_light(light_name: str, state: bool) -> dict[str, Any]: return handle_phue_error(light_name, "toggle_light", e) -@lights_mcp.tool() +@lights_mcp.tool def set_brightness(light_name: str, brightness: int) -> dict[str, Any]: """Sets the brightness of a specific light (0-254) using phue2.""" if not (bridge := _get_bridge()): @@ -100,7 +100,7 @@ def set_brightness(light_name: str, brightness: int) -> dict[str, Any]: return handle_phue_error(light_name, "set_brightness", e) -@lights_mcp.tool() +@lights_mcp.tool def list_groups() -> list[str]: """Lists the names of all available Hue light groups.""" if not (bridge := _get_bridge()): @@ -113,7 +113,7 @@ def list_groups() -> list[str]: return [f"Error listing groups: {e}"] -@lights_mcp.tool() +@lights_mcp.tool def list_scenes() -> dict[str, list[str]] | list[str]: """Lists Hue scenes, grouped by the light group they belong to. @@ -154,7 +154,7 @@ def list_scenes() -> dict[str, list[str]] | list[str]: return [f"Error listing scenes by group: {e}"] -@lights_mcp.tool() +@lights_mcp.tool def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]: """Activates a specific scene within a specified light group, verifying the scene belongs to the group.""" if not (bridge := _get_bridge()): @@ -215,7 +215,7 @@ def activate_scene(group_name: str, scene_name: str) -> dict[str, Any]: return handle_phue_error(f"{group_name}/{scene_name}", "activate_scene", e) -@lights_mcp.tool() +@lights_mcp.tool def set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str, Any]: """Sets multiple attributes (e.g., hue, sat, bri, ct, xy, transitiontime) for a specific light.""" if not (bridge := _get_bridge()): @@ -242,7 +242,7 @@ def set_light_attributes(light_name: str, attributes: HueAttributes) -> dict[str return handle_phue_error(light_name, "set_light_attributes", e) -@lights_mcp.tool() +@lights_mcp.tool def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str, Any]: """Sets multiple attributes for all lights within a specific group.""" if not (bridge := _get_bridge()): @@ -267,7 +267,7 @@ def set_group_attributes(group_name: str, attributes: HueAttributes) -> dict[str return handle_phue_error(group_name, "set_group_attributes", e) -@lights_mcp.tool() +@lights_mcp.tool def list_lights_by_group() -> dict[str, list[str]] | list[str]: """Lists Hue lights, grouped by the room/group they belong to. diff --git a/src/fastmcp/contrib/bulk_tool_caller/example.py b/src/fastmcp/contrib/bulk_tool_caller/example.py index 85139fedaac6d500aa0e44633f7ea24c053304fc..b86a53e2a979c238a232cb1c1f492c0ce1c70872 100644 --- a/src/fastmcp/contrib/bulk_tool_caller/example.py +++ b/src/fastmcp/contrib/bulk_tool_caller/example.py @@ -6,7 +6,7 @@ from fastmcp.contrib.bulk_tool_caller import BulkToolCaller mcp = FastMCP() -@mcp.tool() +@mcp.tool def echo_tool(text: str) -> str: """Echo the input text""" return text diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 7dc77f4d1dd58daff07d8e959f74b0cb2f3dddd9..97a05c6729ef9b91da02f01a1e1f33b6b10db9cc 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -49,7 +49,7 @@ class Context: To use context in a tool function, add a parameter with the Context type annotation: ```python - @server.tool() + @server.tool def my_tool(x: int, ctx: Context) -> str: # Log messages to the client ctx.info(f"Processing {x}") diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 564a0aa5e53927acfd04f7eb17dacfadf7e67af2..19f4deac42752cfcf5db4dae3eeb2d18dc95c204 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -513,53 +513,69 @@ class FastMCP(Generic[LifespanResultT]): def tool( self, + name_or_fn: str | AnyFunction | None = None, + *, 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, - ) -> Callable[[AnyFunction], AnyFunction]: + ) -> Callable[[AnyFunction], AnyFunction] | AnyFunction: """Decorator to register a tool. Tools can optionally request a Context object by adding a parameter with the Context type annotation. The context provides access to MCP capabilities like logging, progress reporting, and resource access. + This decorator supports multiple calling patterns: + - @server.tool (without parentheses) + - @server.tool (with empty parentheses) + - @server.tool("custom_name") (with name as first argument) + - @server.tool(name="custom_name") (with name as keyword argument) + - server.tool(function, name="custom_name") (direct function call) + Args: - name: Optional name for the tool (defaults to function name) + name_or_fn: Either a function (when used as @tool), a string name, or None description: Optional description of what the tool does tags: Optional set of tags for categorizing the tool annotations: Optional annotations about the tool's behavior + exclude_args: Optional list of argument names to exclude from the tool schema + name: Optional name for the tool (keyword-only, alternative to name_or_fn) Example: - @server.tool() + @server.tool def my_tool(x: int) -> str: return str(x) - @server.tool() - def tool_with_context(x: int, ctx: Context) -> str: - ctx.info(f"Processing {x}") + @server.tool + def my_tool(x: int) -> str: return str(x) - @server.tool() - async def async_tool(x: int, context: Context) -> str: - await context.report_progress(50, 100) + @server.tool("custom_name") + def my_tool(x: int) -> str: return str(x) - """ - # Check if user passed function directly instead of calling decorator - if callable(name): - raise TypeError( - "The @tool decorator was used incorrectly. " - "Did you forget to call it? Use @tool() instead of @tool" - ) + @server.tool(name="custom_name") + def my_tool(x: int) -> str: + return str(x) + + # Direct function call + server.tool(my_function, name="custom_name") + """ if isinstance(annotations, dict): annotations = ToolAnnotations(**annotations) - def decorator(fn: AnyFunction) -> AnyFunction: + # Determine the actual name and function based on the calling pattern + if callable(name_or_fn): + # Case 1: @tool (without parens) - function passed directly + # Case 2: direct call like tool(fn, name="something") + fn = name_or_fn + tool_name = name # Use keyword name if provided, otherwise None + + # Register the tool immediately and return the function tool = Tool.from_function( fn, - name=name, + name=tool_name, description=description, tags=tags, annotations=annotations, @@ -569,7 +585,31 @@ class FastMCP(Generic[LifespanResultT]): self.add_tool(tool) return fn - return decorator + elif isinstance(name_or_fn, str): + # Case 3: @tool("custom_name") - name passed as first argument + if name is not None: + raise TypeError( + "Cannot specify both a name as first argument and as keyword argument. " + f"Use either @tool('{name_or_fn}') or @tool(name='{name}'), not both." + ) + tool_name = name_or_fn + elif name_or_fn is None: + # Case 4: @tool or @tool(name="something") - use keyword name + tool_name = name + else: + raise TypeError( + f"First argument to @tool must be a function, string, or None, got {type(name_or_fn)}" + ) + + # Return partial for cases where we need to wait for the function + return partial( + self.tool, + name=tool_name, + description=description, + tags=tags, + annotations=annotations, + exclude_args=exclude_args, + ) def add_resource(self, resource: Resource, key: str | None = None) -> None: """Add a resource to the server. diff --git a/tests/auth/providers/test_bearer.py b/tests/auth/providers/test_bearer.py index aed70af71184f206fb3cd43f2445f3475dda430e..6f59c96fe065f5c3416aeb2e7ed47e7a01246ad9 100644 --- a/tests/auth/providers/test_bearer.py +++ b/tests/auth/providers/test_bearer.py @@ -53,7 +53,7 @@ def run_mcp_server( ) ) - @mcp.tool() + @mcp.tool def add(a: int, b: int) -> int: return a + b diff --git a/tests/auth/test_oauth_client.py b/tests/auth/test_oauth_client.py index be3328a076e8b2bb0dd324d151a5e2941de16c94..292f6c4af48ffed2b1638dc8576c20e96dda6621 100644 --- a/tests/auth/test_oauth_client.py +++ b/tests/auth/test_oauth_client.py @@ -24,7 +24,7 @@ def fastmcp_server(issuer_url: str): ), ) - @server.tool() + @server.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b diff --git a/tests/client/test_client.py b/tests/client/test_client.py index d1398640d8621fd5fe58292886fec12937801c0b..d496bb8a53b3fb57dbf1667c51848a4484170081 100644 --- a/tests/client/test_client.py +++ b/tests/client/test_client.py @@ -25,18 +25,18 @@ def fastmcp_server(): server = FastMCP("TestServer") # Add a tool - @server.tool() + @server.tool def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" # Add a second tool - @server.tool() + @server.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b - @server.tool() + @server.tool async def sleep(seconds: float) -> str: """Sleep for a given number of seconds.""" await asyncio.sleep(seconds) @@ -347,7 +347,7 @@ async def test_concurrent_client_context_managers(): # Create a simple server server = FastMCP("Test Server") - @server.tool() + @server.tool def echo(text: str) -> str: """Echo tool""" return text @@ -510,7 +510,7 @@ class TestErrorHandling: async def test_general_tool_exceptions_are_not_masked_by_default(self): mcp = FastMCP("TestServer") - @mcp.tool() + @mcp.tool def error_tool(): raise ValueError("This is a test error (abc)") @@ -525,7 +525,7 @@ class TestErrorHandling: async def test_general_tool_exceptions_are_masked_when_enabled(self): mcp = FastMCP("TestServer", mask_error_details=True) - @mcp.tool() + @mcp.tool def error_tool(): raise ValueError("This is a test error (abc)") @@ -540,7 +540,7 @@ class TestErrorHandling: async def test_specific_tool_errors_are_sent_to_client(self): mcp = FastMCP("TestServer") - @mcp.tool() + @mcp.tool def custom_error_tool(): raise ToolError("This is a test error (abc)") diff --git a/tests/client/test_logs.py b/tests/client/test_logs.py index 93f7720a1ca467e6e8aaa77762223e01558ba52f..649bfe185bb9e77d88ff63748722e0ce5e538f49 100644 --- a/tests/client/test_logs.py +++ b/tests/client/test_logs.py @@ -17,11 +17,11 @@ class LogHandler: def fastmcp_server(): mcp = FastMCP() - @mcp.tool() + @mcp.tool async def log(context: Context) -> None: await context.info(message="hello?") - @mcp.tool() + @mcp.tool async def echo_log( message: str, context: Context, diff --git a/tests/client/test_progress.py b/tests/client/test_progress.py index f67f5c54c56d2a32af8796a4248b323ea503c814..63244df7cf3935568141629a97392f31ccf821f2 100644 --- a/tests/client/test_progress.py +++ b/tests/client/test_progress.py @@ -16,7 +16,7 @@ def clear_progress_messages(): def fastmcp_server(): mcp = FastMCP() - @mcp.tool() + @mcp.tool async def progress_tool(context: Context) -> int: for i in range(3): await context.report_progress( diff --git a/tests/client/test_roots.py b/tests/client/test_roots.py index 91739aa6b6631d9a90f38eb1e271feb0f280587b..f4df827dee6c8b02135ccce0aeb09d253d5c4569 100644 --- a/tests/client/test_roots.py +++ b/tests/client/test_roots.py @@ -9,7 +9,7 @@ from fastmcp import Client, Context, FastMCP def fastmcp_server(): mcp = FastMCP() - @mcp.tool() + @mcp.tool async def list_roots(context: Context) -> list[str]: roots = await context.list_roots() return [str(r.uri) for r in roots] diff --git a/tests/client/test_sampling.py b/tests/client/test_sampling.py index 15c2a2ff15b999598f0cadfaa50b5b6db8a4f3d9..497aa851354cbc780bd0bced03de2704fe104271 100644 --- a/tests/client/test_sampling.py +++ b/tests/client/test_sampling.py @@ -11,17 +11,17 @@ from fastmcp.client.sampling import RequestContext, SamplingMessage, SamplingPar def fastmcp_server(): mcp = FastMCP() - @mcp.tool() + @mcp.tool async def simple_sample(message: str, context: Context) -> str: result = await context.sample("Hello, world!") return cast(TextContent, result).text - @mcp.tool() + @mcp.tool async def sample_with_system_prompt(message: str, context: Context) -> str: result = await context.sample("Hello, world!", system_prompt="You love FastMCP") return cast(TextContent, result).text - @mcp.tool() + @mcp.tool async def sample_with_messages(message: str, context: Context) -> str: result = await context.sample( [ diff --git a/tests/client/test_sse.py b/tests/client/test_sse.py index 39b556dda03fbabf4a52cee04ac1fa801ab1c5e9..b0960eb14c7683d1b0ae7275dff6d4dbe110f871 100644 --- a/tests/client/test_sse.py +++ b/tests/client/test_sse.py @@ -21,18 +21,18 @@ def fastmcp_server(): server = FastMCP("TestServer") # Add a tool - @server.tool() + @server.tool def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" # Add a second tool - @server.tool() + @server.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b - @server.tool() + @server.tool async def sleep(seconds: float) -> str: """Sleep for a given number of seconds.""" await asyncio.sleep(seconds) diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index c32975b48907280adfcc0bc40789797ec9aae0d5..d9f9247d813da5955abb78ded816c4e98b948a15 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -17,7 +17,7 @@ class TestKeepAlive: mcp = FastMCP() - @mcp.tool() + @mcp.tool def pid() -> int: """Gets PID of server""" return os.getpid() diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index a806f72001bf4890a2b382d92fd5f23740039315..da8502c631f11b0035aa687176b5594eb5910fd7 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -21,18 +21,18 @@ def fastmcp_server(): server = FastMCP("TestServer") # Add a tool - @server.tool() + @server.tool def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" # Add a second tool - @server.tool() + @server.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b - @server.tool() + @server.tool async def sleep(seconds: float) -> str: """Sleep for a given number of seconds.""" await asyncio.sleep(seconds) diff --git a/tests/deprecated/test_deprecated.py b/tests/deprecated/test_deprecated.py index 50caf018a3b067aa3185f1fb14bc0c858e418c3e..fcb6fe2ed38609d6a30a8eaf13cef49676454a0a 100644 --- a/tests/deprecated/test_deprecated.py +++ b/tests/deprecated/test_deprecated.py @@ -100,7 +100,7 @@ def test_mount_tool_separator_deprecation_warning(): main_app.mount("sub", sub_app, tool_separator="-") # Verify the separator is ignored and the default is used - @sub_app.tool() + @sub_app.tool def test_tool(): return "test" diff --git a/tests/deprecated/test_mount_separators.py b/tests/deprecated/test_mount_separators.py index b088b5ef7323603795b74c93da26f43d4f0455ab..514de02cf6b3b63130d14560a94070ae23d45ad9 100644 --- a/tests/deprecated/test_mount_separators.py +++ b/tests/deprecated/test_mount_separators.py @@ -20,7 +20,7 @@ def test_mount_tool_separator_deprecation_warning(): main_app.mount("sub", sub_app, tool_separator="-") # Verify the separator is ignored and the default is used - @sub_app.tool() + @sub_app.tool def test_tool(): return "test" diff --git a/tests/server/http/test_http_dependencies.py b/tests/server/http/test_http_dependencies.py index 580ceabd3a7865def0ae84abde5b1e41bdcb3b91..60ebbb1c675ad61f1152caa7a5f2bb3c8c44a096 100644 --- a/tests/server/http/test_http_dependencies.py +++ b/tests/server/http/test_http_dependencies.py @@ -14,7 +14,7 @@ def fastmcp_server(): server = FastMCP() # Add a tool - @server.tool() + @server.tool def get_headers_tool() -> dict[str, str]: """Get the HTTP headers from the request.""" request = get_http_request() diff --git a/tests/server/test_file_server.py b/tests/server/test_file_server.py index b483ac11039b75d1f48c825e7aa442a0ddea6a8e..c10b445194621ef4d07e8f790009aeec77b9ad2b 100644 --- a/tests/server/test_file_server.py +++ b/tests/server/test_file_server.py @@ -62,7 +62,7 @@ def resources(mcp: FastMCP, test_dir: Path) -> FastMCP: @pytest.fixture(autouse=True) def tools(mcp: FastMCP, test_dir: Path) -> FastMCP: - @mcp.tool() + @mcp.tool def delete_file(path: str) -> bool: # ensure path is in test_dir if Path(path).resolve().parent != test_dir: diff --git a/tests/server/test_import_server.py b/tests/server/test_import_server.py index a6f7bccef4d2430eeff165e3e98f9685811e760b..c903cfa8aa9fc45782df66d275cd0ae3b3377da5 100644 --- a/tests/server/test_import_server.py +++ b/tests/server/test_import_server.py @@ -13,7 +13,7 @@ async def test_import_basic_functionality(): sub_app = FastMCP("SubApp") # Add a tool to the sub-app - @sub_app.tool() + @sub_app.tool def sub_tool() -> str: return "This is from the sub app" @@ -40,11 +40,11 @@ async def test_import_multiple_apps(): news_app = FastMCP("NewsApp") # Add tools to each sub-app - @weather_app.tool() + @weather_app.tool def get_forecast() -> str: return "Weather forecast" - @news_app.tool() + @news_app.tool def get_headlines() -> str: return "News headlines" @@ -65,11 +65,11 @@ async def test_import_combines_tools(): second_app = FastMCP("SecondApp") # Add tools to each sub-app - @first_app.tool() + @first_app.tool def first_tool() -> str: return "First app tool" - @second_app.tool() + @second_app.tool def second_tool() -> str: return "Second app tool" @@ -294,7 +294,7 @@ async def test_import_with_proxy_tools(): main_app = FastMCP("MainApp") api_app = FastMCP("APIApp") - @api_app.tool() + @api_app.tool def get_data(query: str) -> str: return f"Data for query: {query}" diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index 17fdf3a6847a7629442b390700262741519bac4b..e94f19b181c06153cb57e72c3f95d441d08837a1 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -21,7 +21,7 @@ class TestBasicMount: sub_app = FastMCP("SubApp") # Add a tool to the sub-app - @sub_app.tool() + @sub_app.tool def sub_tool() -> str: return "This is from the sub app" @@ -41,7 +41,7 @@ class TestBasicMount: main_app = FastMCP("MainApp") sub_app = FastMCP("SubApp") - @sub_app.tool() + @sub_app.tool def greet(name: str) -> str: return f"Hello, {name}!" @@ -77,7 +77,7 @@ class TestBasicMount: main_app = FastMCP("MainApp") sub_app = FastMCP("SubApp") - @sub_app.tool() + @sub_app.tool def sub_tool() -> str: return "This is from the sub app" @@ -103,7 +103,7 @@ class TestBasicMount: main_app = FastMCP("MainApp") sub_app = FastMCP("SubApp") - @sub_app.tool() + @sub_app.tool def sub_tool() -> str: return "This is from the sub app" @@ -124,11 +124,11 @@ class TestMultipleServerMount: weather_app = FastMCP("WeatherApp") news_app = FastMCP("NewsApp") - @weather_app.tool() + @weather_app.tool def get_forecast() -> str: return "Weather forecast" - @news_app.tool() + @news_app.tool def get_headlines() -> str: return "News headlines" @@ -154,11 +154,11 @@ class TestMultipleServerMount: first_app = FastMCP("FirstApp") second_app = FastMCP("SecondApp") - @first_app.tool() + @first_app.tool def first_tool() -> str: return "First app tool" - @second_app.tool() + @second_app.tool def second_tool() -> str: return "Second app tool" @@ -186,7 +186,7 @@ class TestMultipleServerMount: main_app = FastMCP("MainApp") working_app = FastMCP("WorkingApp") - @working_app.tool() + @working_app.tool def working_tool() -> str: return "Working tool" @@ -267,7 +267,7 @@ class TestDynamicChanges: assert not any(key.startswith("sub_") for key in tools) # Add a tool to the sub-app after mounting - @sub_app.tool() + @sub_app.tool def dynamic_tool() -> str: return "Added after mounting" @@ -284,7 +284,7 @@ class TestDynamicChanges: main_app = FastMCP("MainApp") sub_app = FastMCP("SubApp") - @sub_app.tool() + @sub_app.tool def temp_tool() -> str: return "Temporary tool" @@ -431,7 +431,7 @@ class TestProxyServer: # Create original server original_server = FastMCP("OriginalServer") - @original_server.tool() + @original_server.tool def get_data(query: str) -> str: return f"Data for {query}" @@ -467,7 +467,7 @@ class TestProxyServer: main_app.mount("proxy", proxy_server) # Add a tool to the original server - @original_server.tool() + @original_server.tool def dynamic_data() -> str: return "Dynamic data" @@ -602,7 +602,7 @@ class TestAsProxyKwarg: assert len(await mcp.get_tools()) == 0 - @sub.tool() + @sub.tool def hello(): return "hi" @@ -619,7 +619,7 @@ class TestAsProxyKwarg: mcp = FastMCP("Main") sub = FastMCP("Sub", lifespan=lifespan) - @sub.tool() + @sub.tool def hello(): return "hi" diff --git a/tests/server/test_proxy.py b/tests/server/test_proxy.py index f5ef12bf34a39c96a8c396dac4056d88899a709f..8ad2450d67c08bbedc7b70c12307dc66c7418e26 100644 --- a/tests/server/test_proxy.py +++ b/tests/server/test_proxy.py @@ -26,21 +26,21 @@ def fastmcp_server(): # --- Tools --- - @server.tool() + @server.tool def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" - @server.tool() + @server.tool def tool_without_description() -> str: return "Hello?" - @server.tool() + @server.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b - @server.tool() + @server.tool def error_tool(): """This tool always raises an error.""" raise ValueError("This is a test error") diff --git a/tests/server/test_run_server.py b/tests/server/test_run_server.py index 5109a27bc684f814397bdf0313355ec8cb4d8903..8e631c3a414ebb1fbd218198a00220d00226d910 100644 --- a/tests/server/test_run_server.py +++ b/tests/server/test_run_server.py @@ -22,17 +22,17 @@ # # --- Tools --- -# @server.tool() +# @server.tool # def greet(name: str) -> str: # """Greet someone by name.""" # return f"Hello, {name}!" -# @server.tool() +# @server.tool # def add(a: int, b: int) -> int: # """Add two numbers together.""" # return a + b -# @server.tool() +# @server.tool # def error_tool(): # """This tool always raises an error.""" # raise ValueError("This is a test error") diff --git a/tests/server/test_server.py b/tests/server/test_server.py index 006ef407404cea9c5edff13356d34f2845cc2796..00d4271ec2903bd12ca64e09f77151cff1229b69 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -57,7 +57,7 @@ class TestTools: mcp = FastMCP() - @mcp.tool() + @mcp.tool def fn(x: int) -> int: return x + 1 @@ -126,21 +126,29 @@ class TestToolDecorator: async def test_tool_decorator(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def add(x: int, y: int) -> int: return x + y result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2}) assert result[0].text == "3" # type: ignore[attr-defined] - async def test_tool_decorator_incorrect_usage(self): + async def test_tool_decorator_without_parentheses(self): + """Test that @tool decorator works without parentheses.""" mcp = FastMCP() - with pytest.raises(TypeError, match="The @tool decorator was used incorrectly"): + # Test the @tool syntax without parentheses + @mcp.tool + def add(x: int, y: int) -> int: + return x + y - @mcp.tool # Missing parentheses #type: ignore - def add(x: int, y: int) -> int: - return x + y + # Verify the tool was registered correctly + tools = await mcp.get_tools() + assert "add" in tools + + # Verify it can be called + result = await mcp._mcp_call_tool("add", {"x": 1, "y": 2}) + assert result[0].text == "3" # type: ignore[attr-defined] async def test_tool_decorator_with_name(self): mcp = FastMCP() @@ -171,7 +179,7 @@ class TestToolDecorator: def __init__(self, x: int): self.x = x - @mcp.tool() + @mcp.tool def add(self, y: int) -> int: return self.x + y @@ -199,7 +207,7 @@ class TestToolDecorator: class MyClass: @staticmethod - @mcp.tool() + @mcp.tool def add(x: int, y: int) -> int: return x + y @@ -209,7 +217,7 @@ class TestToolDecorator: async def test_tool_decorator_async_function(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool async def add(x: int, y: int) -> int: return x + y @@ -280,7 +288,7 @@ class TestToolDecorator: """Test that tools with annotated arguments work correctly.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def add( x: Annotated[int, Field(description="x is an int")], y: Annotated[str, Field(description="y is not an int")], @@ -295,7 +303,7 @@ class TestToolDecorator: """Test that tools with annotated arguments work correctly.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def add( x: int = Field(description="x is an int"), y: str = Field(description="y is not an int"), @@ -306,6 +314,59 @@ class TestToolDecorator: assert tool.parameters["properties"]["x"]["description"] == "x is an int" assert tool.parameters["properties"]["y"]["description"] == "y is not an int" + async def test_tool_direct_function_call(self): + """Test that tools can be registered via direct function call.""" + mcp = FastMCP() + + def standalone_function(x: int, y: int) -> int: + """A standalone function to be registered.""" + return x + y + + # Register it directly using the new syntax + result_fn = mcp.tool(standalone_function, name="direct_call_tool") + + # The function should be returned unchanged + assert result_fn is standalone_function + + # Verify the tool was registered correctly + tools = await mcp.get_tools() + assert "direct_call_tool" in tools + + # Verify it can be called + result = await mcp._mcp_call_tool("direct_call_tool", {"x": 5, "y": 3}) + assert result[0].text == "8" # type: ignore[attr-defined] + + async def test_tool_decorator_with_string_name(self): + """Test that @tool("custom_name") syntax works correctly.""" + mcp = FastMCP() + + @mcp.tool("string_named_tool") + def my_function(x: int) -> str: + """A function with a string name.""" + return f"Result: {x}" + + # Verify the tool was registered with the custom name + tools = await mcp.get_tools() + assert "string_named_tool" in tools + assert "my_function" not in tools # Original name should not be registered + + # Verify it can be called + result = await mcp._mcp_call_tool("string_named_tool", {"x": 42}) + assert result[0].text == "Result: 42" # type: ignore[attr-defined] + + async def test_tool_decorator_conflicting_names_error(self): + """Test that providing both positional and keyword name raises an error.""" + mcp = FastMCP() + + with pytest.raises( + TypeError, + match="Cannot specify both a name as first argument and as keyword argument", + ): + + @mcp.tool("positional_name", name="keyword_name") + def my_function(x: int) -> str: + return f"Result: {x}" + class TestResourceDecorator: async def test_no_resources_before_decorator(self): diff --git a/tests/server/test_server_interactions.py b/tests/server/test_server_interactions.py index 4e4e5d27bccced981543a862afc15b01a8aa786f..19fe2c3f6f7e236762573e4ee61f4c84f4e30e01 100644 --- a/tests/server/test_server_interactions.py +++ b/tests/server/test_server_interactions.py @@ -30,30 +30,30 @@ from fastmcp.utilities.types import Image def tool_server(): mcp = FastMCP() - @mcp.tool() + @mcp.tool def add(x: int, y: int) -> int: return x + y - @mcp.tool() + @mcp.tool def list_tool() -> list[str | int]: return ["x", 2] - @mcp.tool() + @mcp.tool def error_tool() -> None: raise ValueError("Test error") - @mcp.tool() + @mcp.tool def image_tool(path: str) -> Image: return Image(path) - @mcp.tool() + @mcp.tool def mixed_content_tool() -> list[TextContent | ImageContent]: return [ TextContent(type="text", text="Hello"), ImageContent(type="image", data="abc", mimeType="image/png"), ] - @mcp.tool() + @mcp.tool def mixed_list_fn(image_path: str) -> list: return [ "text message", @@ -100,7 +100,7 @@ class TestTools: mcp = FastMCP() client = Client(transport=FastMCPTransport(mcp)) - @mcp.tool() + @mcp.tool def error_tool(): raise ValueError("Test error") @@ -119,7 +119,7 @@ class TestToolReturnTypes: async def test_string(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def string_tool() -> str: return "Hello, world!" @@ -130,7 +130,7 @@ class TestToolReturnTypes: async def test_bytes(self, tmp_path: Path): mcp = FastMCP() - @mcp.tool() + @mcp.tool def bytes_tool() -> bytes: return b"Hello, world!" @@ -143,7 +143,7 @@ class TestToolReturnTypes: test_uuid = uuid.uuid4() - @mcp.tool() + @mcp.tool def uuid_tool() -> uuid.UUID: return test_uuid @@ -156,7 +156,7 @@ class TestToolReturnTypes: test_path = Path("/tmp/test.txt") - @mcp.tool() + @mcp.tool def path_tool() -> Path: return test_path @@ -169,7 +169,7 @@ class TestToolReturnTypes: dt = datetime.datetime(2025, 4, 25, 1, 2, 3) - @mcp.tool() + @mcp.tool def datetime_tool() -> datetime.datetime: return dt @@ -180,7 +180,7 @@ class TestToolReturnTypes: async def test_image(self, tmp_path: Path): mcp = FastMCP() - @mcp.tool() + @mcp.tool def image_tool(path: str) -> Image: return Image(path) @@ -243,7 +243,7 @@ class TestToolParameters: async def test_parameter_descriptions_with_field_annotations(self): mcp = FastMCP("Test Server") - @mcp.tool() + @mcp.tool def greet( name: Annotated[str, Field(description="The name to greet")], title: Annotated[str, Field(description="Optional title", default="")], @@ -268,7 +268,7 @@ class TestToolParameters: async def test_parameter_descriptions_with_field_defaults(self): mcp = FastMCP("Test Server") - @mcp.tool() + @mcp.tool def greet( name: str = Field(description="The name to greet"), title: str = Field(description="Optional title", default=""), @@ -293,7 +293,7 @@ class TestToolParameters: async def test_tool_with_bytes_input(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def process_image(image: bytes) -> Image: return Image(data=image) @@ -308,7 +308,7 @@ class TestToolParameters: async def test_tool_with_invalid_input(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def my_tool(x: int) -> int: return x + 1 @@ -323,7 +323,7 @@ class TestToolParameters: """Test string-to-int type coercion.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def add_one(x: int) -> int: return x + 1 @@ -336,7 +336,7 @@ class TestToolParameters: """Test string-to-bool type coercion.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def toggle(flag: bool) -> bool: return not flag @@ -351,7 +351,7 @@ class TestToolParameters: async def test_annotated_field_validation(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def analyze(x: Annotated[int, Field(ge=1)]) -> None: pass @@ -362,7 +362,7 @@ class TestToolParameters: async def test_default_field_validation(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def analyze(x: int = Field(ge=1)) -> None: pass @@ -373,7 +373,7 @@ class TestToolParameters: async def test_default_field_is_still_required_if_no_default_specified(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def analyze(x: int = Field()) -> None: pass @@ -384,7 +384,7 @@ class TestToolParameters: async def test_literal_type_validation_error(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def analyze(x: Literal["a", "b"]) -> None: pass @@ -395,7 +395,7 @@ class TestToolParameters: async def test_literal_type_validation_success(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def analyze(x: Literal["a", "b"]) -> str: return x @@ -411,7 +411,7 @@ class TestToolParameters: GREEN = "green" BLUE = "blue" - @mcp.tool() + @mcp.tool def analyze(x: MyEnum) -> str: return x.value @@ -427,7 +427,7 @@ class TestToolParameters: GREEN = "green" BLUE = "blue" - @mcp.tool() + @mcp.tool def analyze(x: MyEnum) -> str: return x.value @@ -438,7 +438,7 @@ class TestToolParameters: async def test_union_type_validation(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def analyze(x: int | float) -> str: return str(x) @@ -455,7 +455,7 @@ class TestToolParameters: async def test_path_type(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_path(path: Path) -> str: assert isinstance(path, Path) return str(path) @@ -470,7 +470,7 @@ class TestToolParameters: async def test_path_type_error(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_path(path: Path) -> str: return str(path) @@ -481,7 +481,7 @@ class TestToolParameters: async def test_uuid_type(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_uuid(x: uuid.UUID) -> str: assert isinstance(x, uuid.UUID) return str(x) @@ -495,7 +495,7 @@ class TestToolParameters: async def test_uuid_type_error(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_uuid(x: uuid.UUID) -> str: return str(x) @@ -506,7 +506,7 @@ class TestToolParameters: async def test_datetime_type(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_datetime(x: datetime.datetime) -> str: return x.isoformat() @@ -519,7 +519,7 @@ class TestToolParameters: async def test_datetime_type_parse_string(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_datetime(x: datetime.datetime) -> str: return x.isoformat() @@ -532,7 +532,7 @@ class TestToolParameters: async def test_datetime_type_error(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_datetime(x: datetime.datetime) -> str: return x.isoformat() @@ -543,7 +543,7 @@ class TestToolParameters: async def test_date_type(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_date(x: datetime.date) -> str: return x.isoformat() @@ -554,7 +554,7 @@ class TestToolParameters: async def test_date_type_parse_string(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_date(x: datetime.date) -> str: return x.isoformat() @@ -565,7 +565,7 @@ class TestToolParameters: async def test_timedelta_type(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_timedelta(x: datetime.timedelta) -> str: return str(x) @@ -578,7 +578,7 @@ class TestToolParameters: async def test_timedelta_type_parse_int(self): mcp = FastMCP() - @mcp.tool() + @mcp.tool def send_timedelta(x: datetime.timedelta) -> str: return str(x) @@ -594,7 +594,7 @@ class TestToolContextInjection: """Test that context parameters are properly detected.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def tool_with_context(x: int, ctx: Context) -> str: return f"Request {ctx.request_id}: {x}" @@ -607,7 +607,7 @@ class TestToolContextInjection: """Test that context is properly injected into tool calls.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def tool_with_context(x: int, ctx: Context) -> str: assert isinstance(ctx, Context) assert ctx.request_id is not None @@ -623,7 +623,7 @@ class TestToolContextInjection: """Test that context works in async functions.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool async def async_tool(x: int, ctx: Context) -> str: assert ctx.request_id is not None return f"Async request {ctx.request_id}: {x}" @@ -638,7 +638,7 @@ class TestToolContextInjection: """Test that context is optional.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def no_context(x: int) -> int: return x * 2 @@ -656,7 +656,7 @@ class TestToolContextInjection: def test_resource() -> str: return "resource data" - @mcp.tool() + @mcp.tool async def tool_with_resource(ctx: Context) -> str: r_iter = await ctx.read_resource("test://data") r_list = list(r_iter) diff --git a/tests/test_servers/fastmcp_server.py b/tests/test_servers/fastmcp_server.py index f24bbfeef4da351b7ea30aba538516e0b5af8edf..6e13d2ddc055e5bcd389fff2e8275211d07079c9 100644 --- a/tests/test_servers/fastmcp_server.py +++ b/tests/test_servers/fastmcp_server.py @@ -14,19 +14,19 @@ server = FastMCP("TestServer") # --- Tools --- -@server.tool() +@server.tool def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" -@server.tool() +@server.tool def add(a: int, b: int) -> int: """Add two numbers together.""" return a + b -@server.tool() +@server.tool def error_tool(): """This tool always raises an error.""" raise ValueError("This is a test error") diff --git a/tests/tools/test_tool.py b/tests/tools/test_tool.py index 97f6fdf812e5eddaa99a57f5e4e39f8ac0f49f2a..ab2808fb7b6aa04ebb9c251d61c7ab109169649b 100644 --- a/tests/tools/test_tool.py +++ b/tests/tools/test_tool.py @@ -320,7 +320,7 @@ class TestLegacyToolJsonParsing: """Test JSON string to collection type coercion.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def process_list(items: list[int]) -> int: return sum(items) @@ -335,7 +335,7 @@ class TestLegacyToolJsonParsing: """Test that a list coercion error is raised if the input is not a valid list.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def process_list(items: list[int]) -> int: return sum(items) @@ -350,7 +350,7 @@ class TestLegacyToolJsonParsing: """Test JSON string to dict type coercion.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def process_dict(data: dict[str, int]) -> int: return sum(data.values()) @@ -365,7 +365,7 @@ class TestLegacyToolJsonParsing: """Test JSON string to set type coercion.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def process_set(items: set[int]) -> int: assert isinstance(items, set) return sum(items) @@ -378,7 +378,7 @@ class TestLegacyToolJsonParsing: """Test JSON string to tuple type coercion.""" mcp = FastMCP() - @mcp.tool() + @mcp.tool def process_tuple(items: tuple[int, str]) -> int: assert isinstance(items, tuple) return items[0] + len(items[1]) diff --git a/tests/tools/test_tool_manager.py b/tests/tools/test_tool_manager.py index 83f20a745208dab4b8ebfb4c91f0f8d24000ca19..dadf3f000f224e35afd997c5780cc4583a840c4f 100644 --- a/tests/tools/test_tool_manager.py +++ b/tests/tools/test_tool_manager.py @@ -519,7 +519,7 @@ class TestCallTools: mcp = FastMCP(tool_serializer=custom_serializer) manager = mcp._tool_manager - @mcp.tool() + @mcp.tool def get_data() -> dict: return {"key": "value", "number": 123} @@ -537,7 +537,7 @@ class TestCallTools: mcp = FastMCP(tool_serializer=custom_serializer) manager = mcp._tool_manager - @mcp.tool() + @mcp.tool def get_data() -> list[dict]: return [ {"key": "value", "number": 123}, @@ -561,7 +561,7 @@ class TestCallTools: mcp = FastMCP(tool_serializer=custom_serializer) manager = mcp._tool_manager - @mcp.tool() + @mcp.tool def get_data() -> uuid.UUID: return uuid_result diff --git a/tests/utilities/test_mcp_config.py b/tests/utilities/test_mcp_config.py index bd0c84bee51c9cb839b1ed0a2a90c2b748ad304b..ec334153d9a4031f182641b36d3dcf92f3434217 100644 --- a/tests/utilities/test_mcp_config.py +++ b/tests/utilities/test_mcp_config.py @@ -102,7 +102,7 @@ async def test_multi_client(tmp_path: Path): mcp = FastMCP() - @mcp.tool() + @mcp.tool def add(a: int, b: int) -> int: return a + b