Prefect
  • Blog
  • Customers
Get a Demo
Sign InSign Up

Product

  • Prefect Cloud
  • Prefect Open Source
  • Prefect Cloud vs OSS
  • Pricing
  • How Prefect Works
  • Prefect vs Airflow
  • Prefect vs Dagster
  • FastMCP
  • Prefect Horizon
    NEW

Resources

  • Docs
  • Case Studies
  • Blog
  • Resources
  • Community
  • Learn
  • Support
  • Cloud Status

Company

  • About
  • Contact
  • Careers
  • Legal
  • Security
  • Brand Assets
  • Open Source Pledge

Social

  • Twitter
  • GitHub
  • LinkedIn
  • YouTube

© Copyright 2026 Prefect Technologies, Inc. All rights reserved.

mcp, guides
April 14, 2026

MCP vs Function Calling: When to Use Which

Prefect Team
Prefect Team

If you've used function calling with OpenAI, Claude, or Gemini, your first encounter with MCP probably raised an obvious question: isn't this just function calling with extra steps?

The short answer is no. The longer answer requires understanding what each approach actually gives you, where they overlap, and where they solve fundamentally different problems. This post walks through both with real code so you can see the differences for yourself.

What Function Calling Does

Function calling is a feature built into LLM APIs. You define a set of tools as JSON schemas, send them alongside your prompt, and the model decides whether to call one of them. When it does, you get back structured arguments that your application code executes.

Here's a tool defined for the Anthropic API using the Python SDK:

import anthropic
 
client = anthropic.Anthropic()
 
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "input_schema": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"},
                    "units": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature units",
                    },
                },
                "required": ["city"],
            },
        }
    ],
    messages=[{"role": "user", "content": "What's the weather in Portland?"}],
)

And the same tool for the OpenAI API:

from openai import OpenAI
 
client = OpenAI()
 
response = client.chat.completions.create(
    model="gpt-4o",
    tools=[
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get current weather for a city",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string", "description": "City name"},
                        "units": {
                            "type": "string",
                            "enum": ["celsius", "fahrenheit"],
                            "description": "Temperature units",
                        },
                    },
                    "required": ["city"],
                },
            },
        }
    ],
    messages=[{"role": "user", "content": "What's the weather in Portland?"}],
)

Notice that the tool definitions are almost identical, but the wrapping structures differ. The input_schema key in Anthropic's API becomes parameters nested inside a function object in OpenAI's. If you want your tool to work with both providers, you need to maintain two definitions or write a translation layer.

That's the core limitation of function calling: your tools are bound to a specific API provider's format. The tool logic (actually fetching the weather) lives in your application code, and you're responsible for the dispatch loop that matches the model's tool call to your function, executes it, and sends the result back.

What MCP Does

MCP takes a different approach. Instead of embedding tool definitions in your API calls, you run a separate server that exposes tools through a standardized protocol. Any MCP client can connect to that server and discover what tools are available.

Here's the same weather tool as an MCP server using FastMCP:

from fastmcp import FastMCP
 
mcp = FastMCP("Weather")
 
@mcp.tool()
def get_weather(city: str, units: str = "celsius") -> str:
    """Get current weather for a city."""
    # Fetch from weather API
    data = fetch_weather(city, units)
    return f"{data['temp']}°{'C' if units == 'celsius' else 'F'}, {data['conditions']}"

That's the entire server. The tool definition, the execution logic, and the API surface all live in one place. Any MCP client (Claude Desktop, Cursor, Windsurf, a custom agent, a CLI tool) can connect to this server and call get_weather without knowing anything about how the tool was built.

The key difference: with function calling, your application sends tool schemas to the LLM and handles execution. With MCP, a server handles both the schema and the execution, and clients discover tools at runtime.

The Same Tool, Two Architectures

Let's make this concrete with a more realistic example. Say you have an internal tool that queries your company's database of customer support tickets.

Function Calling Approach

import anthropic
import json
 
client = anthropic.Anthropic()
 
# You define the tool schema
tools = [
    {
        "name": "search_tickets",
        "description": "Search support tickets by status and priority",
        "input_schema": {
            "type": "object",
            "properties": {
                "status": {
                    "type": "string",
                    "enum": ["open", "closed", "pending"],
                },
                "priority": {
                    "type": "string",
                    "enum": ["low", "medium", "high", "critical"],
                },
                "limit": {"type": "integer", "default": 10},
            },
            "required": ["status"],
        },
    }
]
 
# You send the schema with every API call
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "Show me open critical tickets"}],
)
 
# You handle the dispatch
for block in response.content:
    if block.type == "tool_use":
        if block.name == "search_tickets":
            result = query_ticket_database(**block.input)
            # You send the result back to the model
            # ... another API call with the tool result

Every application that wants to search tickets needs to: (1) duplicate the tool schema, (2) implement the dispatch logic, and (3) maintain a connection to the ticket database. Three applications means three copies of all of this.

MCP Approach

from fastmcp import FastMCP
 
mcp = FastMCP("Support Tickets")
 
@mcp.tool()
def search_tickets(
    status: str,
    priority: str | None = None,
    limit: int = 10,
) -> list[dict]:
    """Search support tickets by status and priority."""
    return query_ticket_database(status=status, priority=priority, limit=limit)

One server. Every client connects to it and discovers the tool. When the support team adds a new filter parameter, they update the server once. Every client sees the change on their next connection.

When Function Calling is the Right Choice

Function calling wins in scenarios where the tool is tightly coupled to a specific application and there's no reason to share it.

Single-model applications. If your app only talks to one LLM provider and the tools are purpose-built for that app's workflow, function calling keeps things simple. You don't need a separate server process, you don't need to manage connections, and the tool definitions live right next to the code that uses them.

Ephemeral or dynamic tools. Some tools are generated at runtime based on the conversation context. A coding assistant might create tools on the fly that match the user's current project structure. Function calling is built for this because the tool schemas are just data you pass with each request.

Latency-sensitive paths. Function calling happens within a single API call. MCP adds a network hop to a separate server. For applications where every millisecond matters, the simpler architecture might be worth the trade-off in portability.

Prototyping. When you're exploring whether a tool is useful at all, function calling lets you iterate without setting up infrastructure. Build the tool inline, test it, and if it proves valuable, graduate it to an MCP server later.

When MCP is the Right Choice

MCP wins when tools need to be shared, discovered, or governed independently of any single application.

Multi-client access. If the same tool needs to work across Claude Desktop, Cursor, a custom Slack bot, and an automated pipeline, MCP eliminates the duplication. Build the tool once, run one server, connect many clients.

Multi-model portability. An MCP server works with any MCP client regardless of which LLM powers it. Your tools aren't locked to OpenAI's format or Anthropic's format. Switch models and your tools keep working without changes.

Team-shared tools. When the data team builds a query tool that the engineering team, the support team, and the executive team all use through different clients, MCP provides a single source of truth. Changes to the tool propagate to every client. Access control can be managed at the server level rather than per-application.

Production governance. MCP servers can sit behind a gateway that handles authentication, rate limiting, audit logging, and access control. You can answer "which agent called which tool with what arguments at what time" because the gateway sees every request. Function calling tools embedded in application code don't have a natural interception point for this kind of governance.

Tool ecosystems. The MCP server ecosystem already has hundreds of pre-built servers for common services: GitHub, Slack, databases, file systems, APIs. Using MCP means you can plug into this ecosystem instead of reimplementing integrations from scratch.

They're Complementary, Not Competing

The most practical architecture uses both. Function calling handles application-specific tools that don't need to be shared. MCP handles shared infrastructure tools that multiple clients and agents access.

A concrete example: you're building an AI agent that helps with incident response. The agent needs tools to:

  1. Search your PagerDuty alerts (shared across many agents and clients)
  2. Query your internal runbook database (shared across the team)
  3. Format a Slack summary in a specific way for your team's channel (specific to this agent)
  4. Track the agent's own reasoning steps for debugging (specific to this agent)

Tools 1 and 2 should be MCP servers. They're shared resources that multiple clients and teams benefit from. Centralizing them means one place to update, one place to manage access, one place to audit.

Tools 3 and 4 should be function calling tools. They're specific to this one agent's behavior. Nobody else needs them. Putting them on an MCP server would add infrastructure overhead for no benefit.

The Migration Path

If you have existing function calling tools and you're considering MCP, the migration is incremental. You don't need to rewrite everything at once.

Start with your most-shared tool. The one that's copy-pasted across three repos with slightly different implementations. Consolidate it into a FastMCP server:

from fastmcp import FastMCP
 
mcp = FastMCP("Company Tools")
 
@mcp.tool()
def lookup_customer(email: str) -> dict:
    """Look up a customer by email address."""
    return customer_db.find_by_email(email)
 
@mcp.tool()
def search_docs(query: str, limit: int = 5) -> list[dict]:
    """Search internal documentation."""
    return docs_index.search(query, limit=limit)

Run it locally with fastmcp run server.py. Point Claude Desktop at it. If it works well, deploy it for the team. Then move the next tool. The function calling versions keep working while you migrate; there's no cutover event.

For production deployment, Horizon Deploy handles hosting, authentication, and CI/CD so your team can push MCP servers from GitHub without managing infrastructure. But you can also deploy to any hosting platform that runs Python. The protocol is an open standard, not a vendor lock-in.

Making the Call

Here's a quick decision framework:

ConsiderationFunction CallingMCP
How many clients use this tool?OneMultiple
How many LLM providers might you use?OneMore than one
Does the tool need auth/audit?Not reallyYes
Is the tool specific to one app's workflow?YesNo
Will multiple teams maintain or use it?NoYes
Are you prototyping or shipping?PrototypingShipping

Most teams end up in a mixed world. The tools that are tightly coupled to a single app stay as function calling. The tools that represent shared capabilities, querying databases, searching docs, calling internal APIs, managing infrastructure, move to MCP servers.

The good news is that the decision isn't permanent. Function calling tools can become MCP servers when the sharing need arises. The code change is small. The architectural shift is the part that matters, and understanding when to make it is what separates a clean tool architecture from a sprawling mess of duplicated implementations.

Further Reading

  • Model Context Protocol specification for the full protocol details
  • FastMCP documentation for building MCP servers in Python
  • Anthropic function calling guide for Claude's tool use API
  • MCP server directory for pre-built MCP servers