Function Calling vs MCP: When to Use Which
Every major LLM provider now supports function calling. Anthropic, OpenAI, Google. You define tools, the model decides when to call them, you execute the function, return the result. Simple.
Then MCP arrived. Another way to give agents tools. And the first question everyone asks is: why do I need a protocol when function calling already works?
Fair question. The answer is that they solve different problems. And in most production systems, you'll use both.
If you've worked in backend systems, here's a quick mental model. Function calling is like using Java reflection to invoke methods on other services directly — you know the exact class, method signature, and you wire the call yourself. It's powerful and precise, but every integration is hand-stitched into your code. MCP is more like a service mesh. You don't call services directly. Instead, there's a standardized layer that handles discovery, routing, and communication. Services register themselves, consumers look them up, and the mesh handles the plumbing. Same tradeoffs apply: reflection gives you full control with tight coupling; a service mesh gives you decoupling and discoverability at the cost of infrastructure.
Function Calling: Tools Inside Your Code
Function calling is the built-in mechanism. You define your tools inline with your API request.
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=[{
"type": "function",
"function": {
"name": "get_order_status",
"description": "Look up current status of a customer order",
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string"}
},
"required": ["order_id"]
}
}
}]
)
The model sees the tool definition, decides when to call it, and returns structured arguments. You execute the function in your own code and feed the result back.
Everything lives in your codebase. You write the tool definitions. You write the execution logic. You control the entire chain.
Strengths:
- Zero infrastructure overhead. No servers, no protocols. Just functions in your code.
- Full control. You decide exactly what the tool does, what it returns, how errors are handled.
- Easy to debug. Set a breakpoint in your tool function and step through it.
- Works with any LLM that supports tool use. No vendor lock-in beyond the model API.
Limitations:
- Every integration is bespoke. Want to add Slack? Write the API calls, define the schema, handle auth. Want GitHub? Do it again. Jira? Again. Nothing is reusable across projects.
- Tight coupling. Your agent code directly depends on every system it talks to. When the inventory team adds a field to their API, you update your tool definition. When they rename an endpoint, you update your implementation. Every change on the provider side requires a change on the consumer side.
Function calling is the right choice when the tools are yours. Custom business logic, internal APIs, domain-specific operations that nobody else needs.
MCP: Tools as a Service
MCP flips the ownership model. Instead of defining tools inside your agent, they live on a separate server. Your agent connects and discovers them at runtime.
# Your agent doesn't define these tools.
# It asks the MCP server: "What can you do?"
tools = mcp_client.list_tools()
# Returns: [
# {"name": "get_order_status", "schema": {...}},
# {"name": "update_order", "schema": {...}},
# {"name": "cancel_order", "schema": {...}}
# ]
The MCP server publishes its capabilities. Your agent discovers them, picks the right ones, and calls them through a standardized protocol.
The tool provider and tool consumer are now decoupled. This is the key difference from function calling.
With function calling, when the provider changes, the client changes. With MCP, the provider updates their server, and your agent picks up the new schemas on the next list_tools() call. Zero changes on the agent side. The inventory team adds a warehouse_location field to their API? They update their MCP server. Your agent discovers the new schema automatically. No PR to your repo. No redeployment of your agent.
In enterprise settings where different teams own different systems, this matters a lot.
Strengths:
- Provider changes don't break consumers. The MCP server is the contract boundary. As long as the server updates, every connected agent gets the new capabilities for free.
- Standardized protocol. Every MCP server describes tools the same way. One client library works with any server. No bespoke integration code per backend.
- Decoupled ownership. The team that owns the CRM publishes an MCP server. The team building the agent just connects to it. Neither touches the other's code.
- Ecosystem. Pre-built servers for dozens of common tools, all speaking the same protocol. The community is building more every week.
Limitations:
- Infrastructure. You're running additional servers. That means deployment, monitoring, and network calls where a local function would suffice.
- Debugging across boundaries. When something breaks, you're tracing through your agent, the MCP client, the network, and the MCP server. More moving parts.
- Maturity. The ecosystem is growing fast but not everything has an MCP server yet. You'll still write custom tools for many integrations.
MCP is the right choice when the tools aren't yours. Third-party systems, shared infrastructure, integrations that multiple agents or teams need.
The Shared Problem: Too Many Tools
Both approaches hit the same wall at scale. Load 100 tools into an agent, whether through function calling or MCP, and you're burning context on schemas before the user says a word.
This isn't an MCP problem or a function calling problem. It's a tool management problem. Discovery, routing, and context pressure exist regardless of how the tools get there. The solution (tool filtering, lazy loading, routing layers) is the same in both cases.
The Relationship: Not Either/Or
MCP doesn't replace function calling. It sits on top of it.
When your agent connects to an MCP server and discovers tools, those tools still get invoked through function calling. The model still sees tool definitions, still decides when to call them, still returns structured arguments. MCP just standardizes where those definitions come from and how the calls get routed.
Function Calling = the mechanism (how the LLM triggers a tool)
MCP = the protocol (how tools are published and discovered)
This is like the relationship between HTTP and REST. HTTP is the transport. REST is the convention for how to use it. You don't choose between them. You use REST over HTTP.
Similarly, you use MCP over function calling. The model calls functions. MCP standardizes the plumbing.
The Decision Framework
Use function calling directly when:
- The tool is custom to your application (proprietary business logic, internal workflows)
- You have a small number of tools that rarely change
- You need maximum control over execution and error handling
- You're prototyping and speed matters more than architecture
Use MCP when:
- You're connecting to third-party or shared systems (databases, SaaS tools, external APIs)
- Multiple agents or applications need the same tools
- Tool providers and consumers are different teams or organizations
- You want tools to be discoverable and self-describing at runtime
- You're building a platform, not a single agent
Use both when:
- Your agent has custom logic (function calling) AND connects to external systems (MCP)
- This is most production agents
At Enterprise Scale: 50 Teams, 200 Agents
The decision framework above works for a single team building a single agent. But the calculus shifts in a large enterprise where dozens of teams are building agents simultaneously.
Picture a company with separate teams for CRM, ERP, HR, supply chain, finance, and support. Each team builds agents for their own domain. Each team also owns backend systems that other teams need access to.
With function calling only, every team that needs inventory data writes their own integration. The CRM team writes check_inventory. The support team writes check_inventory. The fulfillment team writes check_inventory. Three implementations of the same tool, three sets of schemas to maintain, three things to update when the inventory API changes. Multiply this across every shared system in the org, and you get an explosion of duplicated, drifting integration code.
MCP changes the ownership model. The inventory team publishes one MCP server. Every other team's agent connects to it. One implementation, one schema, one place to update. The inventory team controls what's exposed and how. Consumer teams don't write inventory code at all.
This creates a natural architecture:
- Platform teams publish MCP servers for shared systems (databases, ERPs, internal APIs). They own the contract. They version it. They monitor it.
- Product teams build agents that combine MCP connections (for shared services) with local function calls (for their own business logic). Each team's proprietary rules stay in their codebase.
- Governance becomes possible. You can see which agents connect to which servers, enforce access policies at the MCP layer, and audit tool usage centrally. With function calling, every integration is invisible, buried in application code.
The pattern that emerges is: function calling for what's yours, MCP for what's shared. At the single-team level this is a convenience. At the enterprise level it's the difference between manageable and chaotic.
Try It Yourself
I built a progressive example that lets you feel the tradeoffs firsthand. Five stages, each building on the last:
| Stage | Approach | Tools | What you'll see |
|---|---|---|---|
| 1 | Function calling | 3 | Clean, simple, full control |
| 2 | Function calling | 18 | Boilerplate explosion, tight coupling |
| 3 | MCP | 3 | Decoupled, zero hardcoded schemas |
| 4 | MCP | 13 | Infrastructure overhead, same context pressure |
| 5 | Both | 6 | Right tool for each job |
Stage 2 is where function calling starts to hurt. 18 tools across 6 integrations, 250 lines of hand-written schemas, and a dispatcher that grows with every tool you add. Stage 4 is where MCP shows its costs. Three servers to manage, startup latency, and the same context pressure you had in Stage 2.
Stage 5 combines both. Business logic stays as local function calls. Shared services go through MCP. The routing is explicit in the output:
[MCP] get_customer({"customer_id": "CUST-001"})
[MCP] check_inventory({"product_id": "LAPTOP-001"})
[Function] validate_order({"items": [...]})
[Function] calculate_discount({"customer_tier": "gold", "order_total": 3849.93})
[MCP] create_order({"customer_id": "CUST-001", "items": [...]})
The code is on GitHub: function-calling-vs-mcp
The line between the two is simple. If the tool is yours and only your agent uses it, function calling. If the tool connects to someone else's system or multiple consumers need it, MCP. Most agents end up with both.