n4nAI

MCP vs function calling: what's the difference

A practitioner's comparison of MCP vs function calling across capabilities, cost, latency, ergonomics, and ecosystem — with a clear verdict for each use case.

n4n Team7 min read1,509 words

Audio narration

Coming soon — every post will get a voice note here.

MCP vs function calling is the wrong framing if you treat them as interchangeable tool-invocation mechanisms. Model Context Protocol is a standardized transport for exposing arbitrary capabilities — tools, resources, prompts — over a persistent session. Function calling is a model-native feature where the LLM emits structured JSON to request a tool execution that your code fulfills. They solve adjacent but distinct problems, and most production systems need both.

What MCP actually is

MCP defines a client-server protocol over JSON-RPC 2.0. The server advertises capabilities: tools (functions the model can invoke), resources (read-only data blobs identified by URIs), and prompts (templated interactions). The client — typically an LLM application or an agent framework — discovers these capabilities at session start and invokes them as needed.

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}

The server responds with a schema for each tool. When the model decides to use one, the client sends a tools/call request with arguments. The server executes and returns a result. This happens over stdio, HTTP+SSE, or WebSocket — the transport is pluggable.

Key point: MCP servers are independent processes. They can be written in any language, run anywhere, and expose capabilities that have nothing to do with the model provider. A single MCP server can serve multiple clients simultaneously. The protocol handles initialization, capability negotiation, and heartbeat/ping for long-lived sessions.

What function calling actually is

Function calling is a model-level capability. You pass a JSON Schema array of function definitions in the request. The model — if trained for it — may emit a special tool_calls block in its response instead of (or alongside) natural language. Your code parses that block, executes the function, and feeds the result back in a subsequent turn.

tools = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location"]
        }
    }
}]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Weather in Tokyo?"}],
    tools=tools,
    tool_choice="auto"
)

The model outputs:

{
  "tool_calls": [{
    "id": "call_abc123",
    "type": "function",
    "function": {
      "name": "get_weather",
      "arguments": "{\"location\": \"Tokyo\", \"unit\": \"celsius\"}"
    }
  }]
}

Your code executes get_weather("Tokyo", "celsius"), then sends a tool role message with the result. The model continues. This is synchronous, request-scoped, and tied to a single model invocation.

Capabilities

MCP exposes three primitive types: tools, resources, and prompts. Tools are the closest analog to function calling — but MCP tools can be long-running, streaming, or interactive. Resources let the model read data (files, database rows, API responses) without a custom tool per data type. Prompts let servers ship reusable prompt templates that clients can invoke by name.

Function calling only does tools. If you need the model to browse a codebase, you write a read_file tool. If you need it to query a database, you write a query_db tool. Each tool is a bespoke integration point.

MCP’s resource abstraction is underrated. A single resources/list + resources/read pair replaces N custom tools for “read X, read Y, read Z.” The model discovers available URIs and reads them like files. This scales better when the data surface is large or dynamic.

Cost model

Function calling costs are baked into the model API. You pay per token for the function definitions in the request, the model’s reasoning tokens, the tool call emission, and the result tokens you feed back. There’s no separate infrastructure cost — but you’re locked into the provider’s pricing.

MCP adds infrastructure costs. You run the server. That means compute, network, and operational overhead. But the server can be a thin wrapper around existing internal APIs — no new code if you already have a service layer. The model only sees tool schemas, not your business logic. This separation lets you swap models without rewriting integrations.

For high-volume workloads, MCP servers can batch, cache, and optimize in ways the model API cannot. A single MCP tools/call might aggregate three downstream API calls. The model sees one tool; you amortize latency and cost server-side.

Latency and throughput

Function calling adds one round-trip per tool invocation: model emits call → you execute → you send result → model continues. With parallel tool calls (supported by most frontier models), you can batch independent calls in one turn. But each sequential dependency adds a full model inference cycle.

MCP adds network hop(s) between client and server. Over stdio to a local process, this is sub-millisecond. Over HTTP to a remote server, add 10–100ms depending on proximity. The protocol itself is lightweight — JSON-RPC over the wire. But you now have two failure domains: the model API and your MCP server.

Throughput differs. Function calling is limited by the model provider’s rate limits. MCP servers you control — you can horizontally scale, add caching, implement rate limiting per client, or queue long-running operations. For bursty workloads, MCP gives you knobs the model API doesn’t.

Ergonomics

Function calling is ergonomic for the model consumer. Define schemas in your request, handle callbacks in your loop. The mental model is simple: the model asks, you answer. Most SDKs (OpenAI, Anthropic, Vertex) have first-class helpers.

# Anthropic SDK example
message = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    tools=tools,
    messages=[{"role": "user", "content": "What's the weather?"}]
)
# Handle message.content[0].type == "tool_use"

MCP ergonomics depend on the client library. The protocol is well-specified but verbose. You need a client that handles initialization, capability discovery, and request/response correlation. The Python SDK is solid; TypeScript is maturing. Writing an MCP server is straightforward — the spec includes a reference implementation — but it’s more boilerplate than a function definition.

Debugging differs. Function calling: log the request/response, inspect tool calls in the conversation history. MCP: you have a distributed system. You need structured logging on both sides, correlation IDs, and ideally a trace viewer. The protocol supports logging/setLevel and structured log notifications, but you must wire it up.

Ecosystem

Function calling works with any model that supports it — which is most frontier models and many open weights via vLLM, Ollama, TGI. The schema format is converging on JSON Schema with provider-specific extensions (e.g., strict: true in OpenAI). Portability is high: the same tool definitions work across providers with minor tweaks.

MCP ecosystem is younger but growing fast. Anthropic, Cursor, Zed, Continue, and several agent frameworks ship MCP clients. Server implementations exist for filesystem, GitHub, PostgreSQL, SQLite, Kubernetes, AWS, and dozens more. The protocol is model-agnostic — an MCP server works with Claude, GPT-4o, Llama 3.1, or a local model, provided the client bridges the gap.

The key ecosystem difference: function calling is a model feature; MCP is an integration standard. Function calling ecosystems grow when model providers add support. MCP ecosystems grow when developers publish servers. Long term, MCP’s decoupling favors broader composability.

Limits

Function calling limits are provider-enforced: max tools per request (typically 64–128), max schema size, max parallel calls, context window pressure from tool definitions and results. You cannot exceed these without switching providers or models.

MCP limits are yours to define. The protocol imposes no hard caps on tool count, schema complexity, or session duration. Practical limits come from your server implementation: memory for resource caching, file descriptor limits, database connection pools. You can also hit client-side limits — some MCP clients cap concurrent requests or message sizes.

One subtle limit: MCP requires the model to understand the protocol. The model must know to call tools/list at session start, interpret resource URIs, and handle multi-step workflows. Not all models do this reliably without prompting. Function calling is natively trained; MCP usage is an emergent capability that varies by model.

Comparison table

Dimension Function calling MCP
Primitive Tools only Tools, resources, prompts
Transport In-band (model API request/response) Out-of-band (stdio, HTTP+SSE, WebSocket)
Session model Stateless per request Stateful, long-lived sessions
Server ownership Provider-managed You run it
Model coupling Tied to function-calling models Model-agnostic (client bridges)
Discovery Static (you pass schemas) Dynamic (tools/list, resources/list)
Resource access Custom tool per data type Uniform resources/read by URI
Prompt reuse Not supported prompts/get with arguments
Scaling Provider rate limits Your infrastructure, your policies
Debugging Conversation logs Distributed tracing required
Portability High across function-calling models High across models + clients
Operational burden Near zero Run and monitor servers

Which to choose

Use function calling when:

  • You need tool invocation inside a single model turn with minimal latency
  • Your tool set is small, stable, and model-specific
  • You want zero infrastructure and are fine with provider rate limits
  • The model you’re using has strong function-calling training (GPT-4o, Claude 3.5 Sonnet, Llama 3.1 70B+)
  • You’re building a chat assistant where tools are occasional augmentations

Use MCP when:

  • You need to expose many capabilities (50+ tools, large resource surfaces) to the model
  • You want model-agnostic integrations — swap Claude for GPT-4o for local Llama without rewriting tools
  • You have existing internal APIs and want a thin, standardized wrapper
  • You need long-running sessions with shared context (e.g., an IDE agent that maintains workspace state)
  • You need resources and prompts, not just tools
  • You need to scale, cache, or optimize tool execution independently of the model provider

Use both when:

  • The model uses function calling for its built-in tools (code interpreter, web search, file search)
  • Your MCP server exposes your domain tools and data
  • The client bridges both: function calls route to the model API; MCP calls route to your servers

This is the pattern we see in production agent systems. The model provider handles generic capabilities. Your MCP layer handles proprietary logic, data, and workflows. The distinction isn’t theoretical — it’s the difference between “the model can call a function” and “your system exposes capabilities to any model.”

Tagsmcpfunction-callingcomparison

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All model context protocol (mcp) posts →