Function Calling vs. MCP: The Direct Request vs. The Conversational Protocol
In our previous posts, we've built agents that can reason and even critique their own work (see our self-critique guide and ReAct agents guide). But to be truly useful, our agents need to interact with the real world—they need to call APIs, query databases, or run code.
This post is for you if you're building an agent and need to decide how to connect it to its tools. This is a critical architectural choice that will impact your app's flexibility and scalability.
Today, we'll break down the two main patterns for tool use:
- Function Calling: The classic, "one-shot" request.
- Model Context Protocol (MCP): A newer, "conversational" standard for dynamic tool interaction.
The core problem: Connecting an isolated AI to the real world
An LLM is a brilliant "brain in a jar." To do anything useful, it needs "hands and eyes"—tools to interact with the world. The question is, how do we build the "nervous system" that connects the brain to the hands?
Function Calling is a direct, one-way nerve. The brain sends a single, explicit command ("Move hand").
MCP is a rich, two-way nerve. The brain can have a conversation with the hand ("What can you feel? Can you get a better grip?").
graph TD
A[AI Model] -->|Function Calling: One-shot request| B[Tool/Function]
B -->|Response| A
graph TD
C[AI Model] <-->|MCP: Ongoing Conversation| D[MCP Server/Tool]
The difference isn't just technical—it's about control, flexibility, and scalability.
1. Function Calling: The "Direct Request" method
Function Calling (or "Tool Calling" in OpenAI's API) is the most common and straightforward method. The LLM's "brain" is in charge. It parses the user's prompt, decides it needs a tool, and generates a JSON object that perfectly matches the tool's "schema" or "contract" you defined.
- Philosophy: "The LLM knows what it needs. Let it ask for it directly."
- Developer Experience (DX): Simple and explicit. You define your functions (tools) in your code, describe them to the LLM (e.g., in a JSON schema), and write an agent loop to execute the tool call when the LLM requests it.
- Best For: Straightforward tasks where the model can get all the info it needs in a single, one-off call.
How it works in an agent
This flow is a simple, "one-shot" request-response.
sequenceDiagram
participant User
participant Your_App
participant LLM
participant Tool_API
User->>Your_App: "What's the weather in Paris?"
activate Your_App
Your_App->>LLM: "User asked: '...weather in Paris?'"
activate LLM
LLM-->>Your_App: Call: `get_weather(city="Paris")`
deactivate LLM
Your_App->>Tool_API: Run get_weather("Paris")
activate Tool_API
Tool_API-->>Your_App: "18°C, cloudy"
deactivate Tool_API
Your_App->>LLM: "Tool Result: 18°C, cloudy"
activate LLM
LLM-->>Your_App: "The weather in Paris is 18°C and cloudy."
deactivate LLM
Your_App-->>User: "The weather in Paris is 18°C and cloudy."
deactivate Your_App
The Code:
Here's a basic OpenAI-style function calling example in Python.
from openai import OpenAI
client = OpenAI() # Assumes OPENAI_API_KEY is set
# 1. Define your tool's "contract"
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather in a given city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "The city, e.g., Paris"},
},
"required": ["city"],
},
},
}
]
# 2. Let the LLM decide to call the tool
messages = [{"role": "user", "content": "What's the weather in Paris?"}]
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=tools,
)
# 3. Check if the LLM wanted to call a tool
tool_calls = response.choices[0].message.tool_calls
if tool_calls:
tool_call = tool_calls[0]
if tool_call.function.name == "get_weather":
# 4. Execute the function (we'll mock it here)
weather_data = {"temp": 18, "condition": "cloudy"}
# 5. Feed the result *back* to the LLM
messages.append(response.choices[0].message) # Add the AI's tool call
messages.append(
{
"role": "tool",
"tool_call_id": tool_call.id,
"name": "get_weather",
"content": str(weather_data)
}
)
# 6. Get the final, natural-language answer
final_response = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
print(final_response.choices[0].message.content)
Observation: This is efficient for simple agents. The logic is clear: Ask -> LLM Decides -> Execute -> LLM Answers. It's great for static tools that don't change.
Think About It: Function calling puts the burden on the LLM to get the call right the first time. What happens if the tool's schema changes? You have to update your prompt. What if the tool needs clarification? You have to build a complex, manual loop, which adds latency.
2. MCP: The "Conversational Context" protocol
MCP (Model Context Protocol) is a newer, standardized protocol designed for AI models to interact with external "servers" that provide tools, data, or context dynamically.
- Philosophy: "Tools aren't static items on a list. They are services the model can have an ongoing conversation with."
- Developer Experience (DX): More setup upfront. You need to build or use an "MCP Server" for your tools. But once you do, it's highly scalable and flexible.
- Best For: Complex agents that need dynamic tool discovery (i.e., "what tools are available right now?") or real-time, streaming data.
How it works in an agent
In this model, your agent (the MCP Client) connects to an MCP Server. The AI model can then "browse" the available tools on that server, make requests, and even receive streamed updates from the tool.
The Code:
This example shows the concept of an MCP server/client.
# --- 1. The MCP Server (The Tool Provider) ---
# This runs as a separate service
from mcp_server import MCPServer, tool
server = MCPServer(host='0.0.0.0', port=8000)
@tool(
name="get_weather",
description="Get weather for a city",
parameters={"city": {"type": "string", "required": True}}
)
def get_weather(city: str):
# This could be a real, live API call
return {"temp": 18, "condition": "cloudy", "city": city}
# server.run() # This would start the server
# --- 2. Your Agent (The MCP Client) ---
from mcp_client import MCPClient
from your_llm import LLMWrapper # e.g., OpenAI or Claude
# The client connects to the LLM *and* the tool servers
client = MCPClient(
llm=LLMWrapper(model="claude-3.5-sonnet"),
servers=["http://localhost:8000"] # Connects to our weather server
)
# The client handles the discovery, calling, and response loop
response = client.query("What's the weather in Paris?")
print(response)
Observation: This is a much more powerful and decoupled architecture. Your agent's code doesn't need to know the get_weather function. It just knows how to talk to an MCP server, which tells the agent that get_weather is available. If you add a new tool (get_stock_price) to the server, the agent discovers it automatically, without you needing to update your agent's prompt.
Head-to-head comparison
| Aspect | Function Calling (Tool Calling) | MCP (Model Context Protocol) |
|---|---|---|
| Interaction Style | One-shot request/response | Ongoing "conversation" with updates |
| Tool Discovery | Static (defined in the prompt/API call) | Dynamic (browses servers) |
| "Who is in charge?" | The LLM (it decides what to call) | The Application (it controls the protocol) |
| Best For | Simple, static, one-off tool calls | Complex, dynamic, or streaming tool use |
| Complexity | Low setup, but high maintenance | Higher setup, but low maintenance |
When to use what: Scenarios and recommendations
Scenario 1: "I need a simple chatbot to answer questions about the weather or my calendar."
- Choice: Function Calling.
- Reason: Your tools are fixed and well-defined. The interaction is a simple, one-shot "question -> tool call -> answer." MCP would be overkill.
Scenario 2: "My agent needs to monitor a live stock feed and notify me if a stock price changes."
- Choice: MCP.
- Reason: This is a streaming and real-time data problem. Function Calling is a "pull" model (the LLM asks). MCP supports a "push" model, where the tool server can send updates to the model, making it truly conversational.
Scenario 3: "I'm building a multi-agent system (like in our agent framework comparison) where many agents need to share a common set of tools."
- Choice: MCP.
- Reason: You can build one MCP tool server (e.g.,
company-tools-server) and have all your agents connect to it. This standardizes how tools are accessed across your entire organization.
Challenge for you
- Use Case: You are building an agent that books flights. The user asks, "Find me a flight to NYC."
- The Problem: The
find_flighttool needs to stream back results as they come in, because prices change in real-time. - Your Task: Which pattern, Function Calling or MCP, would be the only choice for this, and why?
Key takeaways
- Function Calling is simple and direct: Use it when your tools are static and your interactions are one-shot request/response patterns
- MCP enables dynamic tool discovery: Use it when tools change frequently, when you need streaming updates, or when multiple agents need to share the same tool infrastructure
- The choice impacts scalability: Function Calling requires updating prompts when tools change; MCP allows tools to be discovered dynamically
- Both patterns solve different problems: Function Calling for simplicity, MCP for flexibility and enterprise-scale tool management
- Consider your team structure: If you have multiple agents or teams building tools, MCP's standardized protocol can reduce coordination overhead
For more on building production AI systems, check out our AI Bootcamp for Software Engineers.