Tools Deep Dive: Functions Your AI Can Call
Master MCP tools with input validation, error handling, async operations, and patterns for building reliable, production-quality tool functions.
Premium Course Content
This lesson is part of a premium course. Upgrade to Pro to unlock all premium courses and content.
- Access all premium courses
- 1000+ AI skill templates included
- New content added weekly
Tools are the primitive you’ll use most. They’re functions that AI assistants can discover and call during conversations — the core mechanism for giving AI real-world capabilities.
In the previous lesson, you built simple tools. Now you’ll learn to build production-quality ones: with input validation, proper error handling, async operations, and patterns that work reliably at scale.
🔄 Quick Recall: In the previous lesson, you built your first MCP server with
@mcp.tool()and connected it to Claude Desktop. Now you’ll learn the full toolkit for building robust, production-ready tools.
How Tool Schemas Work
When you define a tool with type hints, the SDK generates a JSON Schema automatically:
@mcp.tool()
def search_users(
query: str,
limit: int = 10,
active_only: bool = True
) -> str:
"""Search for users by name or email."""
...
The SDK generates this schema (which the AI client sees during initialization):
{
"name": "search_users",
"description": "Search for users by name or email.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"limit": { "type": "integer", "default": 10 },
"active_only": { "type": "boolean", "default": true }
},
"required": ["query"]
}
}
Notice: query is required (no default), while limit and active_only are optional (have defaults). The AI uses this schema to construct valid calls.
Schema tips:
- Use descriptive parameter names (
user_emailnote) - Set sensible defaults for optional parameters
- Write a clear docstring — it’s the tool’s description to the AI
✅ Quick Check: You define a tool parameter as
limit: int = 10. In the generated schema, islimitrequired or optional? (Answer: Optional — parameters with default values are optional in the schema. The AI will use the default of 10 unless the user specifies a different number.)
Input Validation
Never trust that the AI will send valid inputs. Always validate:
@mcp.tool()
def get_user_profile(user_id: int) -> str:
"""Get a user's profile by their ID."""
if user_id <= 0:
return "Error: user_id must be a positive integer"
# Look up user
user = database.get_user(user_id)
if user is None:
return f"No user found with ID {user_id}"
return f"Name: {user.name}\nEmail: {user.email}\nRole: {user.role}"
Validation patterns:
- Range checks for numbers (
if amount < 0) - Format checks for strings (
if not email.contains("@")) - Length limits (
if len(query) > 500) - Allowed values (
if status not in ["active", "inactive"])
Return clear error messages — the AI reads them and explains the problem to the user.
Error Handling
Unhandled exceptions crash your server. Always catch errors and return useful messages:
@mcp.tool()
def fetch_stock_price(symbol: str) -> str:
"""Get the current stock price for a ticker symbol."""
symbol = symbol.upper().strip()
if not symbol.isalpha() or len(symbol) > 5:
return f"Error: '{symbol}' is not a valid ticker symbol"
try:
price = stock_api.get_price(symbol)
return f"{symbol}: ${price:.2f}"
except ConnectionError:
return "Error: Could not connect to the stock price service. Try again later."
except Exception as e:
return f"Error fetching price for {symbol}: {str(e)}"
Error handling rules:
- Validate inputs before doing anything
- Catch specific exceptions first, then general ones
- Return human-readable error messages (the AI will relay them)
- Never expose stack traces or internal details to the user
- Log detailed errors to stderr for your own debugging
Async Tools
For tools that make network requests, query databases, or read files — use async:
import httpx
@mcp.tool()
async def get_weather(city: str) -> str:
"""Get current weather for a city."""
async with httpx.AsyncClient() as client:
response = await client.get(
f"https://api.weather.example.com/current",
params={"city": city}
)
data = response.json()
return f"{city}: {data['temp']}°C, {data['condition']}"
The async keyword tells the server this tool can yield control during I/O operations, preventing the server from freezing while waiting for a response.
When to use async:
- API calls (HTTP requests)
- Database queries
- File reads/writes
- Any operation that involves waiting
When sync is fine:
- Pure computation (math, string manipulation)
- In-memory data lookups
- Simple formatting
✅ Quick Check: Your MCP server has a sync tool that calls a slow external API (takes 5 seconds). What happens if the AI calls this tool? (Answer: The server blocks for 5 seconds — no other requests can be processed during that time. Making the tool async would allow the server to handle other requests while waiting for the API response.)
Returning Rich Content
Tools can return more than plain text. MCP supports multiple content types:
from mcp.types import TextContent, ImageContent
@mcp.tool()
def analyze_data(dataset: str) -> list:
"""Analyze a dataset and return results with a chart."""
# ... perform analysis ...
return [
TextContent(
type="text",
text="Analysis complete:\n- Mean: 42.5\n- Median: 38.0"
),
ImageContent(
type="image",
data=base64_chart_data,
mimeType="image/png"
)
]
For most tools, returning a plain string is sufficient — the SDK wraps it in a TextContent object automatically. Use explicit content types when you need to return images, structured data, or multiple pieces of content.
Tool Design Patterns
The Wrapper Pattern
Wrap existing APIs or libraries as MCP tools:
import subprocess
@mcp.tool()
def run_git_log(repo_path: str, count: int = 5) -> str:
"""Show recent git commits for a repository."""
result = subprocess.run(
["git", "log", f"--oneline", f"-{count}"],
cwd=repo_path,
capture_output=True,
text=True
)
if result.returncode != 0:
return f"Error: {result.stderr}"
return result.stdout
The Aggregator Pattern
Combine multiple data sources into one tool response:
@mcp.tool()
async def project_status(project_id: str) -> str:
"""Get a complete project status overview."""
tasks = await get_tasks(project_id)
budget = await get_budget(project_id)
team = await get_team_members(project_id)
open_tasks = sum(1 for t in tasks if t.status == "open")
return (
f"Project: {project_id}\n"
f"Tasks: {open_tasks} open / {len(tasks)} total\n"
f"Budget: ${budget.spent:,.0f} / ${budget.total:,.0f}\n"
f"Team: {len(team)} members"
)
The Guard Pattern
Add safety checks before performing actions:
@mcp.tool()
def delete_file(filepath: str) -> str:
"""Delete a file from the allowed directory."""
allowed_dir = "/data/temp/"
# Safety: resolve the path and check it's within allowed directory
resolved = os.path.realpath(filepath)
if not resolved.startswith(os.path.realpath(allowed_dir)):
return "Error: Can only delete files in /data/temp/"
if not os.path.exists(resolved):
return f"File not found: {filepath}"
os.remove(resolved)
return f"Deleted: {filepath}"
Practice Exercise
Build an MCP server with these three tools:
lookup_word— Takes a word and returns its definition (use a free dictionary API)translate_text— Takes text and a target language, returns translated text (mock the response if no API key)summarize_url— Takes a URL, fetches the page content, and returns a brief summary
Include input validation and error handling for all three. Connect to Claude Desktop and test each one.
Key Takeaways
- Type hints generate tool schemas automatically — always include them with clear names
- Validate all inputs before processing — never trust that the AI sends clean data
- Catch errors and return descriptive messages instead of crashing
- Use async for any I/O operations (API calls, database queries, file access)
- Good tool descriptions determine whether the AI uses your tools correctly
- Follow patterns: Wrapper (wrap existing code), Aggregator (combine sources), Guard (safety checks)
Up Next
In the next lesson, you’ll learn the other two MCP primitives — Resources and Prompts — and when to use each one instead of Tools.
Knowledge Check
Complete the quiz above first
Lesson completed!