Who this is for and what you will learn
This article is for readers who can already run a FastMCP server and want to turn their own functions into tools. By the end you should be able to write tools with clear parameter types and useful descriptions, know how return values reach the client, and write error messages that help the model without exposing internal details.
What a tool is
In FastMCP, a tool is a Python function exposed to LLMs through the MCP protocol. Tools let an LLM reach external systems, run code and access data that is not in its training data[1]. When an LLM uses a tool, it sends parameters based on the tool's schema. FastMCP validates them against the function signature, runs the function with the validated inputs, and returns the result to the model[1].
Define a tool with @mcp.tool
Decorate a function with @mcp.tool to register it as a tool[1]:
from fastmcp import FastMCP
mcp = FastMCP(name="CalculatorServer")
@mcp.tool
def add(a: int, b: int) -> int:
"""Adds two integer numbers together."""
return a + bWhen the tool is registered, FastMCP uses the function name as the tool name and reads the tool description from the docstring[1]. The input schema is generated from the parameters and their type annotations. FastMCP also handles parameter validation and error reporting[1].
Write parameters
Type annotations
FastMCP uses standard Python type annotations to determine argument types[1]. The analyze_text example below shows the syntax[1]:
@mcp.tool
def analyze_text(
text: str,
max_tokens: int = 100,
language: str | None = None
) -> dict:
"""Analyze the provided text."""
# Implementation...Common types work directly, including int, float, str, bool, list[str], dict[str, int], Literal, Enum, Path, UUID and Pydantic models[1]. A few cases need extra care:
bytesparameters accept raw strings without automatic base64 decoding. For base64 data, declare the parameter asstrand decode it withbase64.b64decode()[1].- Enum parameters: clients send values, such as
"red", not names such as"RED". The function receives the Enum member[1]. - String inputs are converted automatically to
PathandUUIDobjects[1]. - Pydantic model parameters must be JSON objects (dicts), not stringified JSON[1].
Required and optional parameters
Parameters without default values are required. Parameters with default values are optional[1].
Validation modes
By default, FastMCP uses Pydantic's flexible validation. It coerces compatible inputs to the declared types, so a string such as "10" becomes an integer. This helps with LLM clients that send string values[1]. For stricter checks, enable strict input validation with FastMCP("StrictServer", strict_input_validation=True). With it on, coercions such as "10" for an int are rejected[1].
Descriptions from the docstring
FastMCP parses Google, NumPy and Sphinx docstring styles to extract the tool description and each parameter's description[1]. If a parameter already has a description given with Annotated or Field(description=...), that description takes precedence. The docstring fills in the rest[1].
Return values
A tool can return traditional content blocks and structured output. When the function has a return type annotation, FastMCP also generates an output schema[1][2].
- A
strreturn value is sent asTextContent[1]. bytesare decoded as UTF-8 when possible and base64-encoded otherwise, and sent asTextContent. To send binary data as an embedded resource with a MIME type, return aFileinstead[1].
Structured content was introduced by the 2025-06-18 MCP specification update. It is a JSON object sent alongside the traditional content[2]. FastMCP applies these rules[1]:
- Object-like results (
dict, Pydantic models, dataclasses) always become structured content. - Non-object results (
int,str,list) become structured content only when an output schema exists. - All results become traditional content blocks, for backward compatibility.
For primitive return types such as int, str and bool, FastMCP wraps the result under a "result" key in the structured output[1].
Output schemas were also introduced by the 2025-06-18 specification update. When a tool has one, it must return structured output that matches the schema. FastMCP generates the schema from the return type annotation[1][2].
Full control with ToolResult
When you need control over content, structured data and metadata together, return a ToolResult[1]:
from fastmcp.tools import ToolResult
from mcp.types import TextContent
@mcp.tool
def advanced_tool() -> ToolResult:
"""Tool with full control over output."""
return ToolResult(
content=[TextContent(type="text", text="Human-readable summary")],
structured_content={"data": "value", "count": 42},
meta={"execution_time_ms": 145}
)A ToolResult must include at least one of content or structured_content[1].
Error handling
ToolError gives the model a message it can act on. It is also meant for expected business errors, such as an invalid date range or an entity ID that does not exist, and not only for server bugs[1].
FastMCP defines four exception classes. ToolError means the tool cannot complete its operation. ValidationError means parameter or return-value validation failed. AuthorizationError means access was denied. NotFoundError means a component lookup failed, such as a missing tool, resource or prompt[1].
With default error handling, ToolError, ValidationError and AuthorizationError that reach the tools/call handler become a tool result with isError: true. This is a JSON-RPC result that describes a failed call. It is different from a JSON-RPC error with a numeric code[1].
To keep internal exception details from reaching the caller, set mask_error_details=True. Ordinary exceptions raised during tool execution are then masked, while ToolError and ValidationError keep their messages. Those messages must therefore be safe to send to the caller[1]. The example below exposes only a date-range error as an actionable message[1]:
import logging
from datetime import date
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
mcp = FastMCP("DateTools", mask_error_details=True)
@mcp.tool
def count_days(start_date: date, end_date: date) -> int:
"""Count the days in a date range."""
if start_date >= end_date:
raise ToolError(
"start_date must be before end_date. Please correct the date range.",
log_level=logging.WARNING,
)
return (end_date - start_date).daysThe log_level argument of ToolError controls the level of its entry in the tool-execution log. The default is logging.ERROR[1]. FastMCP logs ValidationError at WARNING during tool execution. That log entry omits argument values, validation locations and validator messages, because they may contain private client data[1].
Limits
- Functions that take
*argsor**kwargscannot be tools, because FastMCP must generate a complete parameter schema for the MCP protocol[1]. - Pydantic model parameters do not accept stringified JSON, and
bytesparameters are not decoded from base64 automatically[1]. - Check that error messages are safe to send. With
mask_error_details=True, only ordinary exceptions are masked; explicitToolErrorandValidationErrormessages are returned as written[1].
Next steps
For more detail on parameter metadata, timeouts and component visibility, read the full FastMCP tools documentation[1].
Adapted from the Tools page of the FastMCP official documentation[1], with reference to the MCP specification's notes on structured content and output schemas[2], released under the Apache-2.0 License.
Sources
- 1FastMCP ToolsFastMCP docs (Apache-2.0)
- 2MCP specification: Tools (2025-06-18)Model Context Protocol
Cite this page: Defining tools in FastMCP. TellWorld, https://www.tellworld.net/practice/practice-fastmcp-tools. Updated 2026-11-14