MCP vs Function Calling: Which One Should You Choose in 2026?
By 2026, AI agents have moved from novelty to infrastructure. Almost every production LLM application now needs to reach outside the model — to query databases, call APIs, read files, or trigger workflows. Two dominant approaches have emerged for giving models that capability: Function Calling, the older but battle-tested technique baked into most model APIs, and the Model Context Protocol (MCP), the open standard introduced by Anthropic in late 2024 that has since gained broad adoption across providers, IDEs, and agent frameworks.
This tutorial breaks down what each approach is, why the distinction matters in 2026, how to implement both, and — most importantly — how to decide which one fits your project. We will include working code examples, architectural tradeoffs, and best practices drawn from real-world deployments.
What Is Function Calling?
Function Calling is the native mechanism most LLM providers (OpenAI, Anthropic, Google, Mistral, Cohere) expose to let a model emit a structured request to execute a function. The model does not run the function itself — it produces a JSON payload describing which function it wants called and with what arguments. Your application code then executes the function and returns the result back to the model in a subsequent turn.
The key characteristics of Function Calling in 2026:
- Tightly coupled to a specific provider's API. Each vendor has its own schema format, parameter naming, and tool-result conventions.
- Defined inline per request. Tools are declared in the request payload, so changing tools means changing the request.
- Stateless by default. The application is responsible for managing tool state, retries, and orchestration.
- Fast to prototype. A single API call is enough to wire up a custom tool for a one-off task.
A Minimal Function Calling Example
Here is a complete example using OpenAI's 2026-era Responses API. The model is given two tools — get_weather and send_email — and decides which to invoke based on the user's prompt.
import openai
import json
client = openai.Client(api_key="sk-...")
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"]
}
},
{
"type": "function",
"name": "send_email",
"description": "Send an email to a recipient",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string"},
"subject": {"type": "string"},
"body": {"type": "string"}
},
"required": ["to", "subject", "body"]
}
}
]
def get_weather(city: str, unit: str = "celsius") -> str:
# In production, call a real weather API here
return f"It is 22 degrees {unit} in {city}."
def send_email(to: str, subject: str, body: str) -> str:
# In production, integrate with an SMTP or transactional API
return f"Email sent to {to} with subject '{subject}'."
FUNCTIONS = {
"get_weather": get_weather,
"send_email": send_email,
}
response = client.responses.create(
model="gpt-5-mini",
input="What's the weather in Tokyo, then email alice@example.com with the result.",
tools=tools,
)
# Loop through any tool calls the model emits
while response.output:
tool_outputs = []
for item in response.output:
if item.type == "function_call":
args = json.loads(item.arguments)
result = FUNCTIONS[item.name](**args)
tool_outputs.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": str(result),
})
if not tool_outputs:
break
response = client.responses.create(
model="gpt-5-mini",
input=response.output + tool_outputs,
tools=tools,
)
print(response.output_text)
Notice that everything — tool definitions, dispatch logic, result formatting — lives inside your application. There is no shared protocol; if you switch providers, you rewrite the tool layer.
What Is the Model Context Protocol (MCP)?
MCP is an open, JSON-RPC 2.0-based protocol that standardizes how LLM applications discover and invoke external capabilities. Instead of declaring tools inline in every API request, you run an MCP server that exposes tools, resources, and prompts. Any MCP-compatible client — Claude Desktop, Cursor, VS Code, a custom agent, or increasingly OpenAI and Google clients — can connect to that server and use what it offers.
The protocol defines three primitive types:
- Tools — executable functions the model can call (analogous to function calls).
- Resources — read-only data the model can read, like files or database rows.
- Prompts — reusable prompt templates the server can publish.
The crucial shift is architectural: MCP decouples tool implementation from the LLM client. A single MCP server can serve many clients, and a single client can connect to many servers. This is the same kind of separation that LSP (Language Server Protocol) brought to editors — and indeed MCP was explicitly modeled on LSP's success.
A Minimal MCP Server Example
Here is a complete MCP server using the official Python SDK in 2026. It exposes the same get_weather and send_email tools, but now any MCP-compatible client can consume them.
# weather_server.py
from mcp.server.fastmcp import FastMCP
import httpx
mcp = FastMCP("weather-and-email")
@mcp.tool()
def get_weather(city: str, unit: str = "celsius") -> str:
"""Get current weather for a city."""
# Real implementation would call a weather API
return f"It is 22 degrees {unit} in {city}."
@mcp.tool()
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to a recipient."""
# Real implementation would integrate with an email provider
return f"Email sent to {to} with subject '{subject}'."
@mcp.resource("notes://daily")
def daily_notes() -> str:
"""Expose a static resource the client can read."""
return "Today's ops notes: deploy at 14:00 UTC, monitor queue depth."
if __name__ == "__main__":
mcp.run(transport="stdio")
Run it with python weather_server.py. A client connects over stdio (or, in production, over HTTP/SSE) and automatically discovers the available tools and resources. No per-request tool definitions, no vendor-specific schema format.
Consuming an MCP Server From an Agent
Here is how a client agent connects to that server and uses its tools. This example uses the mcp client library together with Anthropic's SDK, but the same server would work identically with any other MCP client.
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from anthropic import Anthropic
server_params = StdioServerParameters(
command="python",
args=["weather_server.py"],
)
async def main():
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Discover tools dynamically
tools_result = await session.list_tools()
tools = [
{
"name": t.name,
"description": t.description,
"input_schema": t.inputSchema,
}
for t in tools_result.tools
]
client = Anthropic()
messages = [
{"role": "user", "content": "What's the weather in Tokyo?"}
]
response = client.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
tools=tools,
messages=messages,
)
# If the model wants to call a tool, dispatch through MCP
for block in response.content:
if block.type == "tool_use":
result = await session.call_tool(
block.name, block.input
)
print(f"Tool {block.name} returned: {result.content[0].text}")
asyncio.run(main())
The important thing to notice: the agent code knows nothing about weather or email. It discovers tools at runtime from the server. Swap in a different MCP server — say, one that exposes a Postgres database or a Jira instance — and the agent code does not change at all.
Key Differences at a Glance
Both approaches ultimately let a model call code. The differences are about where the integration logic lives and how portable it is.
- Coupling: Function Calling is bound to one provider's API. MCP is provider-agnostic; one server serves all clients.
- Discovery: Function Calling requires you to ship tool definitions in every request. MCP servers advertise their capabilities dynamically.
- Composability: Function Calling tools live inside your app. MCP servers are independent processes that can be shared, versioned, and reused across projects.
- State and resources: Function Calling is purely call/response. MCP also supports long-lived resources, subscriptions, and prompt templates.
- Latency: Function Calling has one less hop — the model talks directly to your code. MCP adds a protocol layer (often a separate process or network call).
- Ecosystem: By 2026, thousands of prebuilt MCP servers exist for popular SaaS tools, databases, and developer platforms. Function Calling libraries exist too, but they are typically vendor-specific.
When to Choose Function Calling
Function Calling remains the right choice in several concrete scenarios:
- Tight latency budgets. If you are building a real-time voice agent or a high-throughput classification pipeline, the extra hop of an MCP server may be unacceptable.
- Single-provider applications. If your product is locked to one model vendor and you have no plans to switch, the portability benefit of MCP is wasted overhead.
- Simple, app-internal tools. A handful of tools that only make sense inside your application — like
lookup_user_in_my_db— do not benefit from being externalized into a server. - Custom orchestration logic. When you need fine-grained control over retries, parallelism, caching, or fallbacks, inline function dispatch gives you maximum flexibility.
- Constrained environments. Embedded systems, edge deployments, or locked-down enterprise runtimes may not allow spawning a separate MCP server process.
When to Choose MCP
MCP shines when any of the following are true:
- You want to reuse tools across multiple agents or clients. A single "company knowledge" MCP server can serve your support bot, your internal coding assistant, and your analytics agent simultaneously.
- You are building for multiple model providers. MCP abstracts away vendor-specific tool schemas, so you can swap Claude for GPT for Gemini without rewriting your tool layer.
- You want to consume third-party capabilities. In 2026 there are mature MCP servers for GitHub, Slack, Postgres, Linear, Notion, Kubernetes, and hundreds more. Installing one is dramatically cheaper than reimplementing the integration.
- You need resources, not just actions. MCP's resource primitive is ideal for exposing files, database tables, or live documents the model can browse without you writing custom retrieval code.
- You are building a platform. If you are exposing your product's API to AI agents generally, publishing an MCP server is the 2026 equivalent of publishing a REST API in 2015.
Hybrid Patterns
The two approaches are not mutually exclusive. A common 2026 production pattern is to use MCP servers for broad, reusable capabilities (database access, third-party SaaS integrations) and Function Calling for narrow, app-specific logic (calling internal microservices, applying business rules). Many agent frameworks — LangGraph, CrewAI, Mastra — now support both side by side, automatically merging MCP-discovered tools with inline function tools into a single tool palette the model can choose from.
Here is a sketch of that hybrid pattern:
async def build_tool_palette(session, internal_tools):
# Pull tools from any connected MCP servers
mcp_tools = await session.list_tools()
palette = [
{"name": t.name, "description": t.description, "input_schema": t.inputSchema}
for t in mcp_tools.tools
]
# Add app-internal function tools
palette.extend(internal_tools)
return palette
internal_tools = [
{
"name": "apply_discount",
"description": "Apply a loyalty discount to an order",
"input_schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"percent": {"type": "number"}
},
"required": ["order_id", "percent"]
}
}
]
The model sees one unified list. Your dispatch code routes calls either to the MCP session or to your local function implementations based on the tool name.
Best Practices
For Function Calling
- Write precise descriptions. The description is the model's only signal for when to use a tool. Ambiguous descriptions cause over-calling or missed calls.
- Validate arguments server-side. Never trust the model's JSON blindly. Validate types, ranges, and permissions before executing.
- Return structured, concise results. Models handle short, well-structured outputs better than verbose prose. Return JSON when the data is structured.
- Handle errors gracefully. Return a clear error string the model can reason about, rather than raising an exception that breaks the conversation loop.
- Limit tool count per request. Beyond roughly 20–30 tools, model selection accuracy degrades. Group related tools or use retrieval to pick a subset.
For MCP
- Run servers as separate processes. This isolates failures, lets you version servers independently, and matches the protocol's design intent.
- Use HTTP/SSE transport for production. Stdio is great for local development but does not scale to remote or multi-tenant deployments.
- Authenticate and authorize at the server. Treat an MCP server like any other networked API. Use OAuth, API keys, or mTLS, and scope tools per client.
- Version your tools. When you change a tool's schema, bump a version. Many clients cache tool definitions, and silent breakage is painful to debug.
- Prefer resources for read-only data. Do not expose a
read_filetool when a resource will do. Resources are cheaper, cacheable, and let the client decide when to fetch them. - Audit third-party servers before installing. An MCP server can execute arbitrary code on behalf of your agent. Review the source, check the publisher, and run untrusted servers in a sandbox.
For Both
- Log every tool invocation. Tool calls are the highest-risk surface in an agent system. Structured logs make debugging and auditing tractable.
- Set per-tool timeouts. A hung tool call can stall an entire conversation. Enforce hard limits and return a timeout error to the model.
- Test with adversarial prompts. Prompt injection via tool results is a real attack vector. Ensure tool outputs cannot coerce the model into unauthorized actions.
- Monitor cost and latency per tool. Some tools (large database scans, heavy API calls) can dominate your bill. Instrument them individually.
Decision Framework
If you are starting a new agent project in 2026, use this rough decision tree:
- Is the tool reusable across projects or clients? Yes → MCP.
- Do you need to support multiple model vendors? Yes → MCP.
- Is there an existing MCP server for what you need? Yes → use it.
- Is the tool deeply specific to this one application? Yes → Function Calling.
- Are you on a hard latency budget with no room for a protocol hop? Yes → Function Calling.
- Are you exposing your product to third-party AI agents? Yes → publish an MCP server.
In practice, most serious agent platforms end up with both: a constellation of MCP servers for shared capabilities, plus a thin layer of inline function tools for app-specific logic. The question is rarely "MCP or Function Calling" — it is "which tools belong in which layer."
Conclusion
Function Calling and MCP solve overlapping but distinct problems. Function Calling is the low-level primitive: a model emits a structured request, your code executes it. It is fast, simple, and ideal for tightly coupled, app-specific tools. MCP is the protocol layer on top: it standardizes discovery, invocation, and resource access so that tools become shareable, vendor-agnostic, and composable across the entire agent ecosystem. In 2026, the mature answer is not to pick one — it is to use Function Calling for the narrow logic that lives inside your application and MCP for everything you want to reuse, share, or expose to the wider world. Choose Function Calling when you need speed and tight coupling; choose MCP when you need portability, composability, and access to the growing ecosystem of prebuilt servers. Get that split right, and your agents will be both fast to build and durable as the landscape keeps evolving.