MCP's biggest impact
In my previous blog post I gave a general introduction to MCP. We continue where we left of, with MCP tools. Tools in MCP are designed to be model-controlled, meaning that the language model can discover and invoke tools automatically based on its contextual understanding and the user’s prompts.

Tools are a core component of the MCP architecture that enable LLMs to perform actions beyond text, image, sound or video generation. They serve as the bridge between language models and external systems, allowing LLMs to:
- Gather information from APIs and databases
- Create, update, or delete records in external systems
- Trigger business logic and automated processes
- Interact with files, services, and other resources
As stated, they are LLM-driven and help accomplish actions that the LLM on its own is not able to do. This is important to note when comparing them to resources, which help give more context to the LLM through the application.
Tool specification
An MCP server supports the following three message types, as defined in the official specification:
- Tool listing: Is used to discover available tools by calling
tools/listrequest. The operation supports pagination. - Tool calling: Allows the client to run the wanted tool with
tools/callrequest. - Tool list change: Notify client about changes in tool listing by sending a notification to the client. The operation is only available when the tools capability
listChangedis enabled.

Create tools with FastMCP
An MCP server is expected to support three core message types. Fortunately, when using FastMCP, you only need to implement the tool logic itself. The library handles the rest, thanks to its support for annotations, which simplify tool creation and integration.
Let's change the main.py file and move the tools to tools.py. This would result in the main.py to look like this:
from fastmcp import FastMCP
from tools import register_tools
# STDIO by default
mcp = FastMCP(name="Demo 🚀")
# Register all tools
register_tools(mcp)
if __name__ == "__main__":
mcp.run()To demonstrate implementing tools, we'll create two different tools:
- getCurrentWeather - Retrieves current weather by calling external API
- createMockObject - Creates a mock object to showcase idempotency
getCurrentWeather
A read-only tool that retrieves current weather for a specified city and country using public APIs. Use this when the model needs up-to-date, structured weather data rather than free-form text.
- Purpose: Return structured weather data (as
WeatherResponse) for downstream processing or decision-making. - Invocation: Call the tool with
cityandcountrystrings; the tool geocodes the location then queries the weather API. - Behavior: Read-only and safe to call repeatedly; network errors, missing locations, and API failures are mapped to clear, user-facing errors.
- Best practices: Validate inputs, set timeouts/retries, add caching and rate-limiting, and never leak API credentials in responses.
from uuid import uuid4
from fastmcp import FastMCP
from mcp.types import ToolAnnotations
import requests
from models import WeatherResponse
def register_tools(mcp: FastMCP):
"""Register all tools with the MCP instance"""
@mcp.tool(
name="getCurrentWeather",
title="Get Current Weather Tool",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
def getCurrentWeather(city: str, country: str) -> WeatherResponse:
"""Get current weather for a specific city and country"""
try:
# Get coordinates for the city
geocode_url = f"https://geocoding-api.open-meteo.com/v1/search?name={city}&country={country}&count=1"
response = requests.get(geocode_url, timeout=10)
response.raise_for_status()
data = response.json()
if not data.get("results"):
raise ValueError("Location not found.")
latitude = data["results"][0]["latitude"]
longitude = data["results"][0]["longitude"]
# Get weather data
weather_url = f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}¤t=temperature_2m"
weather_response = requests.get(weather_url, timeout=10)
weather_response.raise_for_status()
weather_data = weather_response.json()
return WeatherResponse(**weather_data)
except requests.exceptions.Timeout:
raise ValueError("Request timed out. Please try again later.")
except requests.exceptions.ConnectionError:
raise ValueError("Unable to connect to weather service. Please check your internet connection.")
except requests.exceptions.HTTPError as e:
raise ValueError(f"HTTP error occurred: {e.response.status_code}")
except requests.exceptions.RequestException as e:
raise ValueError(f"An error occurred while fetching weather data: {str(e)}")
except KeyError as e:
raise ValueError(f"Unexpected response format from weather service: missing {str(e)}")
except Exception as e:
raise ValueError(f"An unexpected error occurred: {str(e)}")createMockObject
A simple example tool that returns a new object on every call — useful to demonstrate non-idempotent behavior and how output schemas can vary.
- Purpose: Illustrate a tool that creates new data and therefore isn't idempotent.
- Invocation: Call
createMockObject(value)with an integer argument. The tool returns an object containing a generatedid, aname, and the providedvalue. - Behavior: Generates a new id with each invocation. Useful for highlighting differences in JSON schemas and for scenarios where idempotency is a concern.
- Best practices: Clearly document any non-idempotent behavior in the manifest. If safe retries are required, provide idempotency keys. Avoid using non-idempotent patterns for operations that must be repeatable or deterministic.
def createMockObject(value: int) -> dict:
"""Create a new mock object for showcasing idempotency"""
mock_data = {
"id": uuid4(),
"name": "Mock Object",
"value": value
}
return mock_dataNote: The method's return type is intentionally set to dict to display different JSON schemas when comparing tools.
Error Handling
Because MCPs communicate using the JSON-RPC 2.0 protocol, tools apply two error reporting mechanisms to handle failures and exceptions.
- Protocol Errors: Standard JSON-RPC errors for issues like:
- Unknown tools
- Invalid arguments
- Server errors
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Unknown tool: invalid_tool_name"
}
}- Tool Execution Errors: Reported in tool results with
isError: true:
- API failures
- Invalid input data
- Business logic errors
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "Unable to connect to weather service. Please check your internet connection."
}
],
"isError": true
}
}JSON Schema
Each tool is exposed through the tool listing. Each tool is basically a JSON response object containing metadata about the tool itself:
- name: Unique identifier for the tool
- title: Optional human-readable name of the tool for display purposes.
- description: Human-readable description of functionality
- inputSchema: JSON Schema defining expected parameters
- outputSchema: Optional JSON Schema defining expected output structure
- annotations: Optional meta data for tool
Generated Schema for tools
When you run the MCP server, the JSON schema gets automatically generated. Understanding how the schema looks and what an LLM retrieves from the MCP is beneficial. It is handy to know how to generate the schema. Our examples are implemented through FastMCP framework. We can leverage FastMCP again by running ./venv/bin/python -m fastmcp inspect main.py --format fastmcp -o manifest.json and generate JSON schema for your project. The file will be placed in the same folder as the main.py file.
The generated schema for our two tools are seen here:
getCurrentWeather tool presented as JSON (click to expand)
{
"key": "getCurrentWeather",
"name": "getCurrentWeather",
"description": "Get current weather for a specific city and country",
"input_schema": {
"properties": {
"city": {
"type": "string"
},
"country": {
"type": "string"
}
},
"required": [
"city",
"country"
],
"type": "object"
},
"output_schema": {
"$defs": {
"Current": {
"description": "Current weather data",
"properties": {
"time": {
"type": "string"
},
"interval": {
"type": "integer"
},
"temperature_2m": {
"type": "number"
}
},
"required": [
"time",
"interval",
"temperature_2m"
],
"type": "object"
},
"CurrentUnits": {
"description": "Units for current weather data",
"properties": {
"time": {
"type": "string"
},
"interval": {
"type": "string"
},
"temperature_2m": {
"type": "string"
}
},
"required": [
"time",
"interval",
"temperature_2m"
],
"type": "object"
}
},
"description": "Weather API response model",
"properties": {
"latitude": {
"type": "number"
},
"longitude": {
"type": "number"
},
"generationtime_ms": {
"type": "number"
},
"utc_offset_seconds": {
"type": "integer"
},
"timezone": {
"type": "string"
},
"timezone_abbreviation": {
"type": "string"
},
"elevation": {
"type": "integer"
},
"current_units": {
"$ref": "#/$defs/CurrentUnits"
},
"current": {
"$ref": "#/$defs/Current"
}
},
"required": [
"latitude",
"longitude",
"generationtime_ms",
"utc_offset_seconds",
"timezone",
"timezone_abbreviation",
"elevation",
"current_units",
"current"
],
"type": "object"
},
"annotations": {
"title": null,
"readOnlyHint": true,
"destructiveHint": null,
"idempotentHint": null,
"openWorldHint": true
},
"tags": null,
"enabled": true,
"title": "Get Current Weather Tool",
"icons": null,
"meta": null
}
createMockObject tool presented as JSON (click to expand)
{
"key": "createMockObject",
"name": "createMockObject",
"description": "Create a new mock object for showcasing idempotency",
"input_schema": {
"properties": {
"value": {
"type": "integer"
}
},
"required": [
"value"
],
"type": "object"
},
"output_schema": {
"additionalProperties": true,
"type": "object"
},
"annotations": {
"title": null,
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
},
"tags": null,
"enabled": true,
"title": "Create Mock Object Tool",
"icons": null,
"meta": null
}
Conclusion
Building tools is where creativity meets control: small, well-designed tools let an LLM safely perform real-world tasks. Try the examples in this post and inspect the auto-generated manifest to better understand the contracts you expose.
Coming Next
Next up, we'll cover "resources" and "resource templates" — how to expose these to clients and when to use them.
Do you already leverage tools in your day-to-day work? If so, what functionality do you usually seek?
Published by...
