← Back to DevBytes

MCP vs Function Calling: Which One Should You Choose in 2026?

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:

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:

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.

When to Choose Function Calling

Function Calling remains the right choice in several concrete scenarios:

When to Choose MCP

MCP shines when any of the following are true:

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

For MCP

For Both

Decision Framework

If you are starting a new agent project in 2026, use this rough decision tree:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles