The Model Context Protocol (MCP) is becoming the standard way for AI assistants to interact with external tools. But how do tools actually work under the hood? Let's dig in.
What Is a Tool?
In MCP, a tool is a function that an AI can call. Every tool has:
- A name — unique identifier (e.g.,
read_file,search_web) - A description — tells the AI when to use it
- An input schema — JSON Schema defining what parameters it accepts
- An output — what the tool returns (text, images, or structured data)
Here's a minimal tool definition:
{
"name": "get_weather",
"description": "Get current weather for a location",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or coordinates"
}
},
"required": ["location"]
}
}How Discovery Works
When an MCP client connects to a server, it calls tools/list to discover available tools. The server responds with an array of tool definitions:
{
"tools": [
{
"name": "read_file",
"description": "Read contents of a file",
"inputSchema": { "..." }
},
{
"name": "write_file",
"description": "Write content to a file",
"inputSchema": { "..." }
}
]
}The client (usually an AI assistant) uses these definitions to:
- Know what tools exist
- Understand when each tool is appropriate
- Validate arguments before calling
Input Schemas: The Contract
The inputSchema uses JSON Schema to define valid inputs. This matters because:
- Type safety — Ensures the AI passes correct types
- Validation — Server can reject malformed requests
- Documentation — The schema IS the documentation
Common Schema Patterns
Required vs optional parameters:
{
"type": "object",
"properties": {
"path": { "type": "string" },
"encoding": { "type": "string", "default": "utf-8" }
},
"required": ["path"]
}Enums for constrained values:
{
"type": "object",
"properties": {
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE"]
}
}
}Arrays for multiple items:
{
"type": "object",
"properties": {
"files": {
"type": "array",
"items": { "type": "string" }
}
}
}Tool Execution Flow
When the AI decides to use a tool:
- Client sends
tools/callwith tool name and arguments - Server validates arguments against inputSchema
- Server executes the tool logic
- Server returns content (text, images, or structured data)
// Request
{
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/etc/hosts"
}
}
}
// Response
{
"content": [
{
"type": "text",
"text": "127.0.0.1 localhost\n..."
}
]
}Defining Tools in TypeScript
The MCP TypeScript SDK makes tool definition clean with Zod schemas:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0"
});
server.tool(
"search_files",
"Search for files matching a pattern",
{
pattern: z.string().describe("Glob pattern to match"),
directory: z.string().optional().describe("Starting directory"),
maxResults: z.number().default(10).describe("Maximum results to return")
},
async ({ pattern, directory, maxResults }) => {
const results = await searchFiles(pattern, directory, maxResults);
return {
content: [
{ type: "text", text: JSON.stringify(results, null, 2) }
]
};
}
);The SDK handles schema validation, JSON-RPC protocol, and error formatting. You focus on the logic.
Defining Tools in Python
The Python SDK is equally straightforward:
from mcp.server import Server
from mcp.types import Tool, TextContent
import mcp.server.stdio
server = Server("my-server")
@server.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="search_files",
description="Search for files matching a pattern",
inputSchema={
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Glob pattern to match"
},
"directory": {
"type": "string",
"description": "Starting directory"
},
"maxResults": {
"type": "integer",
"default": 10,
"description": "Maximum results to return"
}
},
"required": ["pattern"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
if name == "search_files":
results = await search_files(
arguments["pattern"],
arguments.get("directory"),
arguments.get("maxResults", 10)
)
return [TextContent(type="text", text=str(results))]
raise ValueError(f"Unknown tool: {name}")
async def main():
async with mcp.server.stdio.stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())Tool Annotations
The MCP spec includes annotations — hints that help clients understand tool behavior:
{
"name": "delete_file",
"description": "Permanently delete a file",
"inputSchema": { "..." },
"annotations": {
"destructive": true,
"idempotent": false,
"readOnly": false,
"openWorld": false
}
}What these mean:
| Annotation | Meaning |
|---|---|
destructive | Changes state irreversibly (deletes, overwrites) |
idempotent | Calling multiple times has same effect as once |
readOnly | Doesn't modify any external state |
openWorld | May affect resources not directly referenced |
Clients use annotations to:
- Warn users before destructive operations
- Cache results from read-only tools
- Make smarter retry decisions
Common Pitfalls
1. Vague Descriptions
# Bad - AI won't know when to use this
Tool(name="process", description="Process the data")
# Good - clear purpose
Tool(name="parse_csv", description="Parse CSV text into structured rows with headers")The AI uses descriptions to decide WHEN to use a tool. Be specific.
2. Missing Property Descriptions
// Bad - what does "query" mean?
{
"properties": { "query": { "type": "string" } }
}
// Good - clear expectations
{
"properties": {
"query": {
"type": "string",
"description": "SQL query to execute",
"examples": ["SELECT * FROM users LIMIT 10"]
}
}
}3. Overly Complex Schemas
If your input schema needs 20 properties, you probably need multiple tools instead. Keep tools focused on a single responsibility.
4. Poor Error Messages
Tools should return clear, actionable errors:
return [
TextContent(
type="text",
text="Error: File not found at /path/to/file. Check the path exists and you have read permissions."
)
]Key Takeaways
- Tools are functions with structured inputs and outputs
- JSON Schema defines the contract between client and server
- Discovery happens via
tools/listat connection time - Good descriptions are critical — they determine when AI uses your tool
- Annotations provide behavioral hints for smarter clients
- Keep tools focused — one responsibility per tool
The MCP tool system is elegantly simple but powerful. Understanding it deeply helps you build better integrations and debug issues faster.
Want to see tools in action? Check out the official MCP servers for real-world examples.