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.
The Basics: What Is a Tool?
In MCP, a tool is a function that an AI can call. It 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
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 Schema: The Contract
The inputSchema uses JSON Schema to define valid inputs. This is crucial because:
- Type safety — Ensures the AI passes correct types
- Validation — Server can reject malformed requests
- Documentation — Schema IS the documentation
Common 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..."
}
]
}Real-World Example: Filesystem Server
Let's look at how the official MCP filesystem server defines its tools:
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "read_file",
description: "Read the complete contents of a file",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "Path to the file" }
},
required: ["path"]
}
},
{
name: "write_file",
description: "Create or overwrite a file",
inputSchema: {
type: "object",
properties: {
path: { type: "string" },
content: { type: "string" }
},
required: ["path", "content"]
}
},
{
name: "list_directory",
description: "List contents of a directory",
inputSchema: {
type: "object",
properties: {
path: { type: "string" }
},
required: ["path"]
}
}
]
}));Notice how each tool has a clear, single responsibility. Good tool design follows the same principles as good API design.
Tool Annotations (New in 2025)
The latest MCP spec adds tool annotations — hints that help clients understand tool behavior:
{
name: "delete_file",
description: "Permanently delete a file",
inputSchema: { ... },
annotations: {
destructive: true, // Changes state irreversibly
idempotent: false, // Calling twice has different effects
readOnly: false, // Modifies external state
openWorld: false // Only affects known resources
}
}These annotations let clients:
- Warn users before destructive operations
- Optimize caching for read-only tools
- Make better retry decisions
Common Pitfalls
1. Vague Descriptions
// Bad
{ name: "process", description: "Process the data" }
// Good
{ 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 Required Fields
// Bad - what does "query" mean?
{
inputSchema: {
properties: { query: { type: "string" } }
}
}
// Good - clear expectations
{
inputSchema: {
properties: {
query: {
type: "string",
description: "SQL query to execute",
examples: ["SELECT * FROM users LIMIT 10"]
}
},
required: ["query"]
}
}3. Overly Complex Schemas
If your input schema needs 20 properties, you probably need multiple tools instead.
4. No Error Handling
Tools should return clear error messages:
{
content: [
{
type: "text",
text: "Error: File not found at /path/to/file"
}
],
isError: true
}Building Your Own Tools
Here's a template for implementing a tool in TypeScript:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({
name: "my-server",
version: "1.0.0"
});
// Define tools
server.tool(
"my_tool",
"Description of what this tool does",
{
param1: z.string().describe("First parameter"),
param2: z.number().optional().describe("Optional number")
},
async ({ param1, param2 }) => {
// Tool implementation
const result = await doSomething(param1, param2);
return {
content: [
{ type: "text", text: JSON.stringify(result) }
]
};
}
);The SDK handles:
- Schema validation (via Zod)
- JSON-RPC protocol
- Error formatting
You focus on the logic.
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're how AI decides what to use
- 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 will help you build better integrations and debug issues faster.
Want to see tools in action? Check out the official MCP servers for real-world examples.