MCP vs Function Calling: TL;DR
| MCP | Function Calling | |
|---|---|---|
| Portability | Works across any MCP-compatible model | Tied to one model's API format |
| Setup | MCP server + client (more boilerplate) | JSON schema + handler in one place |
| Best for | Reusable tool ecosystems, multi-agent setups | Single-app integrations, fast prototypes |
| Spec owner | Anthropic (open standard) | Each LLM provider (OpenAI, Anthropic, Google) |
| Streaming | ✅ Native | ✅ Via API |
| Self-hostable | ✅ | ✅ |
| Tooling ecosystem | Growing fast (200+ community servers) | Mature (3+ years) |
Choose MCP if: you're building tools that multiple agents or models will reuse.
Choose function calling if: you need one model to call one set of tools in one app.
What We're Comparing
Both MCP and function calling let an LLM trigger external actions — searching the web, querying a database, running code. The difference is architectural. Function calling is a model-API contract. MCP is a transport protocol. That gap matters more than it sounds when you're building anything beyond a quick prototype.
Function Calling Overview
Function calling (also called tool use) lets you define a set of functions in JSON schema, pass them with your prompt, and have the model return a structured call when it decides to use one. You handle execution, pass the result back, and the model continues.
Every major provider has their own flavor: OpenAI's tools array, Anthropic's tools block, Google's function_declarations. The schema is similar but not identical.
Pros:
- Minimal setup — define schema, add a handler, done
- Battle-tested since GPT-3.5 Turbo; huge community examples
- No extra infrastructure; everything lives in your app code
Cons:
- Schema format is provider-specific — migrating from OpenAI to Claude means rewriting tool definitions
- Tools live inside your app; sharing them across projects means copy-pasting
- No standard discovery mechanism — you hand-code which tools exist
MCP Overview
Model Context Protocol is an open standard released by Anthropic in late 2024. It separates the tool layer from the model layer. You run an MCP server (a small process exposing tools), and any MCP-compatible client (Claude Desktop, your custom agent, Cursor, etc.) can discover and call those tools over a standard JSON-RPC-style transport.
Think of it as USB-C for LLM tools: one connector, any device.
Pros:
- Write a tool once, use it with any MCP client — Claude, GPT-4o via adapters, LangGraph agents
- Built-in tool discovery via
list_tools— clients enumerate available tools at runtime - Clean separation of concerns: tool logic lives in the server, not tangled in agent code
Cons:
- More moving parts: you run a server process, manage the transport (stdio or SSE), handle lifecycles
- Debugging is harder — failures can happen at transport layer, not just in your handler
- Ecosystem is younger; some edge cases in the spec still have rough implementations
Head-to-Head
Setup Complexity
A minimal function calling setup is 30 lines:
import anthropic
client = anthropic.Anthropic()
tools = [
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
}
]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Tokyo?"}]
)
# Handle tool_use block in response
for block in response.content:
if block.type == "tool_use":
result = get_weather(block.input["city"]) # your function
# send result back in next turn
A minimal MCP server takes more structure:
# server.py — run as a separate process
from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types
server = Server("weather-server")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="get_weather",
description="Get current weather for a city",
inputSchema={
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "get_weather":
result = fetch_weather(arguments["city"]) # your logic
return [types.TextContent(type="text", text=result)]
async def main():
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
Then wire it into your MCP client config. More files, more process management — but now any MCP client can use that weather tool without changes.
Portability
This is where MCP wins clearly. A function calling schema written for OpenAI's tools format looks like this:
{
"type": "function",
"function": {
"name": "get_weather",
"parameters": { "type": "object", "properties": { "city": { "type": "string" } } }
}
}
Anthropic's format:
{
"name": "get_weather",
"input_schema": { "type": "object", "properties": { "city": { "type": "string" } } }
}
Small differences, but they add up across 10 tools and 3 providers. With MCP, the server definition doesn't change — only the client changes.
Debugging
Function calling failures are easy to isolate. The model either returns a tool_use block or it doesn't. Your handler either succeeds or throws. You inspect the API response in one place.
MCP debugging involves the transport layer. If your server crashes, the client gets a connection error, not a tool error. Check server logs separately, watch the stdio/SSE stream, and verify the handshake completes before assuming tool logic is the problem.
# Quick MCP server health check — run this before debugging tool logic
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}' \
| python server.py
If you see an initialize response, the transport works. If not, the issue is server startup, not your tool code.
Ecosystem
Function calling has three years of production use. Stack Overflow, GitHub, LangChain docs — examples everywhere. Most LLM frameworks (LangChain, LlamaIndex, LangGraph) have built-in function calling abstractions.
MCP hit 200+ community servers by mid-2025 and is now the default tool interface in Claude Desktop and Cursor. LangGraph added native MCP client support in 0.2.x. The gap is closing fast but function calling still wins on community resources.
Which Should You Use?
Pick function calling when:
- You're prototyping or building a single-app integration
- Your tool logic is tightly coupled to your app (accesses in-memory state, uses app internals)
- You're targeting one specific model and don't plan to switch
- You want the simplest possible setup
Pick MCP when:
- You're building tools meant to be reused — across agents, projects, or teams
- You want tool logic decoupled from agent logic (cleaner architecture at scale)
- You're building for Claude Desktop, Cursor, or other MCP-native clients
- You're running a multi-agent system where different agents need the same tools
Use both when: you're building a production agent that needs some portable shared tools (MCP) and some app-specific tools that don't justify a server (function calling). Claude's API supports both patterns in the same message.
FAQ
Q: Can I use MCP with OpenAI models?
A: Not natively, but community adapters exist that translate MCP tool calls to OpenAI's function calling format. It's functional but adds a translation layer. Worth it only if portability matters more than simplicity.
Q: Does MCP replace function calling for Anthropic's API?
A: No. The Anthropic API still uses function calling (their tools block) as the wire format. MCP is a higher-level protocol for building tool ecosystems around agents and clients, not a replacement for the API contract.
Q: Which is faster at runtime?
A: Function calling has slightly lower latency since there's no separate process to communicate with. For most use cases the difference is under 20ms and irrelevant. High-throughput production systems should benchmark both, but this shouldn't drive your architecture decision.