MCP, Function Calling, Tool Use: The Complete Guide
Large Language Models (LLMs) are powerful, but they have a fundamental limitation: they can only generate text. For an AI agent to truly take action — search the web, query a database, send an email — it needs a mechanism to call external tools. That's exactly what MCP (Model Context Protocol), Function Calling (OpenAI), and Tool Use (Anthropic) provide.
In this guide, we break down each approach, compare their architectures, and give you ready-to-use code examples to integrate tools into your AI agents.
🧠 Why LLMs Need Tools
An LLM on its own cannot:
- Access real-time data (weather, stock prices, news)
- Perform precise calculations (complex arithmetic, conversions)
- Interact with external systems (APIs, databases, files)
- Execute code or system commands
Without a tool-calling mechanism, an LLM is doomed to hallucinate answers to these questions. Tool-calling protocols solve this by allowing the model to declare that it wants to use a tool, then receive the result to formulate its final response.
The general flow is always the same:
- The user asks a question
- The model identifies that a tool is needed
- The model produces a structured call (tool name + parameters)
- The host system executes the tool
- The result is sent back to the model
- The model formulates its final response
What changes between approaches is how steps 3 through 5 are implemented.
🔧 Function Calling (OpenAI)
How It Works
Function Calling is the approach introduced by OpenAI in June 2023. The idea is simple: you describe available functions in your system prompt as JSON schemas, and the model can decide to call one instead of responding directly.
Architecture
User → OpenAI API (with function definitions)
↓
Model chooses a function
↓
Returns structured JSON
↓
Your code executes the function
↓
Result sent back to the model
↓
Final response to the user
Function Definition
Each function is described with a JSON Schema:
{
"name": "get_weather",
"description": "Retrieves the current weather for a given city",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name (e.g., Paris, London)"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The temperature unit"
}
},
"required": ["city"]
}
}
Full Python Example
import openai
import json
client = openai.OpenAI()
# Define available functions
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Retrieves the weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
}
]
# Actual function (your logic)
def get_weather(city, unit="celsius"):
# In production: call a weather API
return {"city": city, "temperature": 18, "unit": unit, "condition": "cloudy"}
# Call the model
messages = [{"role": "user", "content": "What's the weather like in Paris?"}]
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto"
)
# Check if the model wants to call a function
message = response.choices[0].message
if message.tool_calls:
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
# Execute the function
result = get_weather(**func_args)
# Send the result back to the model
messages.append(message)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Get the final response
final = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)
print(final.choices[0].message.content)
Calling Modes
OpenAI offers three modes via the tool_choice parameter:
| Mode | Behavior |
|---|---|
"auto" |
The model decides on its own whether to call a function |
"none" |
The model cannot call any function |
"required" |
The model must call at least one function |
{"type": "function", "function": {"name": "X"}} |
Forces a specific function call |
Parallel Calls
Since late 2023, GPT-4 and GPT-3.5-turbo can trigger multiple function calls in parallel within a single response. For example, if the user asks "weather in Paris and London," the model will produce two simultaneous tool_calls.
# The model can return multiple tool_calls
for tool_call in message.tool_calls:
# Process each call independently
...
You can disable this behavior with parallel_tool_calls: false.
Strengths and Limitations
Advantages:
- Simple, well-documented syntax
- Native support for parallel calls
- Large ecosystem of examples
- Compatible with all recent GPT models
Limitations:
- Proprietary to OpenAI (though the format has been adopted by others)
- Function definitions consume tokens
- No official open standard
🛠️ Tool Use (Anthropic)
How It Works
Anthropic introduced Tool Use for Claude in April 2024. The concept is similar to OpenAI's Function Calling, but with some notable architectural differences, particularly in message format and result handling.
Architecture
The architecture is similar to OpenAI's, but the message format differs:
User → Anthropic API (with tool definitions)
↓
Claude chooses a tool
↓
Returns a tool_use block in the content
↓
Your code executes the tool
↓
Result sent back via a tool_result message
↓
Final response to the user
Tool Definition
Anthropic also uses JSON Schema but with a slightly different structure:
{
"name": "get_weather",
"description": "Retrieves the current weather for a given city. Returns temperature and conditions.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "The desired temperature unit"
}
},
"required": ["city"]
}
}
The key difference: input_schema instead of parameters.
Full Python Example
import anthropic
import json
client = anthropic.Anthropic()
# Define tools
tools = [
{
"name": "get_weather",
"description": "Retrieves the weather for a given city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
}
]
def get_weather(city, unit="celsius"):
return {"city": city, "temperature": 18, "unit": unit, "condition": "cloudy"}
# First call
messages = [{"role": "user", "content": "What's the weather like in Paris?"}]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
# Check the stop_reason
if response.stop_reason == "tool_use":
# Extract the tool_use block
tool_block = next(b for b in response.content if b.type == "tool_use")
# Execute the tool
result = get_weather(**tool_block.input)
# Build the complete conversation
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_block.id,
"content": json.dumps(result)
}
]
})
# Get the final response
final = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
print(final.content[0].text)
Tool Usage Control
Anthropic offers a similar tool_choice parameter:
| Mode | Behavior |
|---|---|
{"type": "auto"} |
Claude decides on its own |
{"type": "any"} |
Claude must use a tool (any tool) |
{"type": "tool", "name": "X"} |
Forces use of a specific tool |
Anthropic-Specific Features
"Thinking" before the tool call: Claude can include text (reasoning) before the tool call in the same message. This provides transparency into its thought process.
Error results: You can signal that a tool has failed:
{
"type": "tool_result",
"tool_use_id": "toolu_abc123",
"is_error": True,
"content": "Error: city not found"
}
Claude will adapt its response accordingly, potentially reformulating its request or informing the user.
Strengths and Limitations
Advantages:
- Visible reasoning before tool calls
- Native error handling with is_error
- Very detailed tool descriptions for better selection
- Streaming support with tool_use blocks
Limitations:
- Slightly more verbose format
- Smaller example ecosystem (but growing rapidly)
- Tool results must be in a user message (unique architecture)
🌐 MCP (Model Context Protocol)
How It Works
MCP is an open protocol created by Anthropic and released in late 2024. Unlike Function Calling and Tool Use, which are API mechanisms specific to each provider, MCP aims to be a universal standard for connecting LLMs to data sources and tools.
The analogy often used: MCP is to LLMs what USB is to peripherals. One protocol to connect everything.
Architecture
MCP introduces a client-server architecture:
┌─────────────────────────────────┐
│ Host Application │
│ (Claude Desktop, IDE, Agent) │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ MCP │ │ MCP │ │
│ │ Client#1 │ │ Client#2 │ │
│ └────┬─────┘ └────┬─────┘ │
└───────┼──────────────┼───────────┘
│ │
┌────▼─────┐ ┌────▼─────┐
│ MCP │ │ MCP │
│ Server │ │ Server │
│ (Files) │ │ (GitHub) │
└──────────┘ └──────────┘
Components:
- MCP Host: The application (Claude Desktop, an IDE, your agent)
- MCP Client: Maintains a 1:1 connection with a server
- MCP Server: Exposes tools, resources, and prompts
The Three MCP Primitives
MCP isn't limited to tools. It exposes three types of capabilities:
| Primitive | Description | Example |
|---|---|---|
| Tools | Functions callable by the LLM | Search the web, execute SQL |
| Resources | Queryable data (read-only) | File contents, query results |
| Prompts | Reusable prompt templates | Summary template, analysis template |
Python MCP Server Example
Using the official Python SDK:
from mcp.server.fastmcp import FastMCP
# Create an MCP server
mcp = FastMCP("weather-server")
@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> dict:
"""Retrieves the current weather for a given city.
Args:
city: The city name (e.g., Paris, London)
unit: The temperature unit (celsius or fahrenheit)
"""
# In production: call a real API
return {
"city": city,
"temperature": 18,
"unit": unit,
"condition": "cloudy"
}
@mcp.resource("weather://current/{city}")
def current_weather(city: str) -> str:
"""Resource: current weather for a city."""
data = get_weather(city)
return f"{data['city']}: {data['temperature']}°C, {data['condition']}"
@mcp.prompt()
def weather_report(city: str) -> str:
"""Generates a prompt for a detailed weather report."""
return f"Generate a comprehensive weather report for {city} including a 3-day forecast."
# Start the server
if __name__ == "__main__":
mcp.run()
TypeScript MCP Server Example
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "weather-server",
version: "1.0.0"
});
server.tool(
"get_weather",
"Retrieves the weather for a city",
{
city: z.string().describe("City name"),
unit: z.enum(["celsius", "fahrenheit"]).optional()
},
async ({ city, unit = "celsius" }) => {
return {
content: [{
type: "text",
text: JSON.stringify({
city,
temperature: 18,
unit,
condition: "cloudy"
})
}]
};
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Transports
MCP supports two transport modes:
| Transport | Use Case | Communication |
|---|---|---|
| stdio | Local servers | The client launches the server as a child process |
| SSE (Server-Sent Events) | Remote servers | HTTP communication, ideal for cloud services |
Configuration in Claude Desktop
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["/path/to/weather_server.py"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "ghp_xxx"
}
}
}
}
MCP Server Ecosystem
One of MCP's major advantages is its growing ecosystem of ready-to-use servers:
- Files: Read/write local files
- GitHub: Repos, issues, pull requests
- Databases: PostgreSQL, SQLite, MongoDB
- Web: Brave Search, Fetch
- Productivity: Google Drive, Slack, Notion
- Development: Docker, Kubernetes
Strengths and Limitations
Advantages:
- Open and interoperable standard
- Three primitives (tools, resources, prompts)
- Ecosystem of reusable servers
- Clear client/server separation
- Works with any LLM (not tied to a provider)
Limitations:
- More complex to set up than simple function calling
- Still young (evolving spec)
- Requires a runtime for servers
- Overhead for simple use cases
📊 Full Comparison Table
| Criteria | Function Calling (OpenAI) | Tool Use (Anthropic) | MCP |
|---|---|---|---|
| Type | Proprietary API | Proprietary API | Open protocol |
| Creator | OpenAI | Anthropic | Anthropic (open source) |
| Definition format | JSON Schema (parameters) |
JSON Schema (input_schema) |
Decorators / SDK |
| Parallel calls | ✅ Native | ✅ Possible | ✅ Via the client |
| Error handling | Via message content | Native is_error |
Via the protocol |
| Streaming | ✅ | ✅ | ✅ |
| Multi-provider | ❌ (format adopted by others) | ❌ | ✅ |
| Resources (data) | ❌ | ❌ | ✅ |
| Reusable prompts | ❌ | ❌ | ✅ |
| Setup complexity | ⭐ Low | ⭐ Low | ⭐⭐⭐ Medium |
| Maturity | ⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
| Documentation | Excellent | Very good | Good (growing) |
🔀 When to Use What?
Use Function Calling (OpenAI) if:
- You're already using GPT models
- Your use case is simple (a few functions)
- You want the fastest possible setup
- You don't need to share tools between projects
Use Tool Use (Anthropic) if:
- You're using Claude as your main model
- You need visible model reasoning
- Fine-grained error handling matters
- You want a model that excels at choosing among complex tools
Use MCP if:
- You're building an agent that needs to work with multiple LLMs
- You want a reusable tool ecosystem across projects
- You need more than just tools (resources, prompts)
- You're building a platform or agent framework
- Standardization and interoperability are priorities
Combining Approaches
In practice, many developers combine these approaches. For example:
- Use MCP to define and share tools
- Translate MCP tools into Function Calling or Tool Use depending on the LLM being used
- Frameworks like LangChain and OpenClaw handle this translation automatically
# Pseudo-code: an agent that uses MCP internally
# but communicates via Function Calling or Tool Use
class UniversalAgent:
def __init__(self, mcp_servers, llm_provider):
self.mcp_client = MCPClient(mcp_servers)
self.llm = llm_provider # "openai" or "anthropic"
def get_tools_for_llm(self):
mcp_tools = self.mcp_client.list_tools()
if self.llm == "openai":
return convert_to_function_calling(mcp_tools)
elif self.llm == "anthropic":
return convert_to_tool_use(mcp_tools)
async def run(self, user_message):
tools = self.get_tools_for_llm()
response = await self.llm.complete(user_message, tools)
# Handle tool calls via MCP
...
🚀 Common Best Practices
Regardless of the approach you choose, certain best practices always apply:
1. Clear and Precise Descriptions
The model chooses its tools based on descriptions. Be explicit:
# ❌ Bad
"description": "Search for stuff"
# ✅ Good
"description": "Searches articles in the database by keyword. Returns the 10 most recent results with title, date, and summary."
2. Validate Parameters
Never trust model-generated parameters without validation:
def execute_tool(name, params):
# Always validate!
if name == "delete_file":
path = params.get("path", "")
if ".." in path or path.startswith("/"):
raise ValueError("Unauthorized path")
# ...
3. Handle Errors Gracefully
Return clear error messages so the model can adapt:
try:
result = execute_tool(name, params)
return {"success": True, "data": result}
except Exception as e:
return {"success": False, "error": str(e), "suggestion": "Check the parameters"}
4. Limit the Number of Tools
The more tools you expose, the more likely the model is to make mistakes. Keep a focused and relevant set for the task.
| Number of Tools | Recommendation |
|---|---|
| 1-5 | ✅ Ideal |
| 6-15 | ⚠️ Acceptable with good descriptions |
| 15-30 | ⚠️ Group into categories |
| 30+ | ❌ Too many — use a router or tool selector |
5. Test with Edge Cases
Models can hallucinate parameters or call the wrong tool. Systematically test:
- Ambiguous requests ("search for that" → which tool?)
- Missing parameters
- Empty results or errors
- Call chains (tool A → tool B)
🔮 The Future of LLM-Tool Interactions
The ecosystem is evolving rapidly:
- MCP is gaining adoption and could become the de facto standard
- OpenAI and Google are working on their own tool protocols
- Frameworks (LangChain, CrewAI, OpenClaw) abstract away these differences
- The trend is toward interoperability: define a tool once, use it everywhere
What matters isn't choosing "the best" protocol, but understanding the underlying principles. The fundamental mechanism — the model declares it wants to use a tool, your code executes it, the result goes back to the model — remains the same everywhere.
Master this pattern, and you'll be able to build AI agents capable of interacting with any system.
📚 Related Articles
- What Is OpenClaw? — Discover how OpenClaw natively implements MCP and Tool Use
- Configuring OpenClaw: SOUL, AGENTS, and Skills — Set up your agent's tools and skills
- Claude (Anthropic) — Everything about Claude and its Tool Use approach
- OpenRouter — Access all LLMs via a single API, compatible with Function Calling
- Automate Your Life with OpenClaw — Put tools into practice with real-world automations