Build a server with 4 math operations—your first multi-tool server with input validation.
Before writing any code, let's plan our calculator server. The idea is to create four basic math tools: add, subtract, multiply e divide. Each will receive two floating-point numbers (float) as parameters and return the result of the operation. This will be your first server with multiple tools—a significant step toward building more complex MCP servers.
Our server will have 4 tools, all following the same signature pattern:
add(a: float, b: float) → float
subtract(a: float, b: float) → float
multiply(a: float, b: float) → float
divide(a: float, b: float) → float # with validation that b != 0
All receive two floats and return a float. The exception is division, which needs to check whether the divisor is zero before performing the operation. This consistent pattern makes implementation and use by the AI model easier.
Planning the tools before coding is a essential best practice. Define the name, parameters (with types), and return value for each tool. This ensures consistency, makes the automatic documentation generated by FastMCP easier, and helps the AI model understand when and how to use each tool. Think of tools as an API—the more predictable, the better.
Let's start with the simplest operations: addition and subtraction. These are straightforward tools that don't need special validation. They serve as a foundation for understanding the pattern that will be repeated in the other operations. With FastMCP, each tool is just a decorated Python function—it's that simple.
# server_calculadora.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Calculator")
@mcp.tool()
def add(a: float, b: float) -> float:
"""Adds two numbers."""
return a + b
@mcp.tool()
def subtract(a: float, b: float) -> float:
"""Subtracts b from a."""
return a - b
Notice how the type hints (a: float, b: float) are essential. FastMCP uses these annotations to automatically generate the JSON schema that the MCP client sends to the model. Without type hints, the model doesn't know what kind of data to send.
Always write docstrings in your tools. The docstring becomes the tool description in the MCP schema—it’s what the model reads to decide which tool to use. A clear docstring like "Adds two numbers" is much better than having no docstring at all.
FastMCP automatically converts Python type hints into JSON Schema. A parameter float becomes {"type": "number"} in the schema. A str becomes {"type": "string"}. This automatic conversion eliminates the need to write schemas manually—just annotate the Python functions correctly and FastMCP takes care of the rest.
The multiplication tool follows exactly the same pattern as the previous tools. This is intentional—consistency across tools makes life easier for both the developer and the model. This is also a good opportunity to understand how FastMCP automatically generates the schema from your Python function.
@mcp.tool()
def multiply(a: float, b: float) -> float:
"""Multiplies two numbers."""
return a * b
When FastMCP registers this tool, it automatically generates a schema like: {"name": "multiplicar", "description": "Multiplica dois números.", "inputSchema": {"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number"}}, "required": ["a", "b"]}}. All of this comes from a simple Python function with type hints and a docstring.
Division differs from the other operations because it has a dangerous special case: the division by zero. If the user asks to divide any number by zero and the tool simply executes a / b, Python will raise a ZeroDivisionError and the server may crash. We need to validate the input before running the operation.
@mcp.tool()
def divide(a: float, b: float) -> str:
"""Divides a by b. Returns an error if b is zero."""
if b == 0:
return "Error: Division by zero is not allowed."
return f"{a / b}"
Instead of letting the exception bubble up, we return a friendly error message as a string. The model receives this message and can inform the user naturally: "You can't divide by zero." This is much better than an incomprehensible Python traceback.
Never let a tool crash! An unhandled exception can bring down the entire MCP server, disconnecting the client. Always validate inputs, use try/except when needed, and return clear error messages. The model knows how to handle error messages—it doesn't know how to handle a server that has stopped responding.
With all 4 tools implemented, it's time to configure the server in Claude Desktop and test each operation. The goal is to verify that Claude correctly identifies which tool to use for each question and that all operations return correct results—including handling division by zero.
Add the server to claude_desktop_config.json and restart Claude Desktop. Then, don’t forget to add the startup block to the end of the file:
if __name__ == "__main__":
mcp.run()
Now test the tools with natural-language questions. Claude will automatically choose the right tool for each operation.
"What's 15 + 27?" — Claude should use the tool somar and return 42.
"What's 100 minus 37?" — Claude should use subtrair and return 63.
"Multiply 8 by 7." — Claude should use multiplicar and return 56.
"Divide 100 by 4." — Claude should use dividir and return 25.
"Divide 100 by 0." — Claude should receive the error message and explain that division by zero is not possible.
Also test with decimal numbers ("How much is 3.14 times 2?") and with negative numbers ("Subtract 50 from 30"). Check whether Claude can chain operations: "Add 10 and 20, then multiply by 3" — it should call the tools in the correct order.
Now that the server works, we can think about improvements. Rounding results, explicitly supporting integers and floats, and adding new operations such as exponentiation. These improvements turn a demo server into something more robust and professional.
Some practical improvements for the calculator server:
round(resultado, 10) to avoid floating-point issues like 0.1 + 0.2 = 0.30000000000000004.
a: int | float to explicitly accept both integers and floats.
potenciar(base: float, expoente: float) as an exercise — it follows the same pattern as the others.
As an exercise, implement the tool potenciar(base: float, expoente: float). It should return base ** expoente. Think: does it need special validation? What happens with 0 ** 0? And with negative exponents? Practice thinking in "what could go wrong?" — this mindset will serve you well with every MCP server you build.
Next Module: 3.5 — Weather Lookup Server