MCP vs Plain Function Calling

AIMCPLLMAgents
Share on LinkedIn

Our agent had fourteen @tool decorators in one Python file. Marketing wanted the same Salesforce lookup in Claude Desktop, the support bot, and a GitHub Action — so we copied the function schemas three times and watched them drift within a month. Model Context Protocol (MCP) didn't replace the LLM's function-calling API; it replaced the copy-paste layer between hosts and tool implementations. Understanding where each layer stops saves you from bolting MCP onto problems a simple tools array already solves.

Two layers, not two competitors

┌─────────────┐     tool schemas / tool_calls     ┌──────────────┐
│  LLM API    │ ◄──────────────────────────────► │  Agent host  │
│ (OpenAI,    │                                   │ (your app)   │
│  Anthropic) │                                   └──────┬───────┘
└─────────────┘                                          │
                                                         MCP (optional)
                                                          │
                              ┌───────────────────────────┼───────────────────────────┐
                              ▼                           ▼                           ▼
                        MCP server                  MCP server                   MCP server
                        (git tools)                 (database)                   (browser)

Function calling is the contract between model and host: JSON Schema definitions, tool_calls in the response, results fed back as messages.

MCP is the contract between host and tool providers: list tools, call tools, optional resources and prompts — over stdio, HTTP, or SSE.

The model never speaks MCP directly. Your host translates MCP tool listings into whatever schema format the provider expects.

Plain function calling: when it's enough

For a single backend service with five stable tools, inline definitions win on simplicity:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_order_status",
            "description": "Look up order by ID",
            "parameters": {
                "type": "object",
                "properties": {"order_id": {"type": "string"}},
                "required": ["order_id"]
            }
        }
    }
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
)

Advantages:

Stick with this until you feel pain from duplication or isolation requirements.

What MCP adds

MCP servers expose capabilities through a standard protocol:

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

Returns tool names, descriptions, and input schemas. The host merges these into the LLM's tools array at session start or on reconnect.

Portability. Write one postgres-mcp-server; Cursor, Claude Desktop, and your internal agent runner connect without reimplementing queries.

Process isolation. A crashing browser automation server doesn't take down your API. Sandboxing untrusted tools (shell, SQL) in separate processes is an security win.

Dynamic discovery. Add tools to the server without redeploying the host — useful for plugin ecosystems and internal platforms where teams ship their own MCP servers.

Resources and prompts. MCP also standardizes read-only context (files, docs) and reusable prompt templates — function calling alone doesn't cover these.

Transport trade-offs.

Transport Best for Watch out for
stdio Local dev, IDE integrations One client per process; no remote scaling
HTTP + SSE Remote shared servers, team tool hubs Auth, TLS, connection drops
Streamable HTTP Simpler remote deployments (newer spec) Client library support still catching up

We run database MCP servers over stdio in dev and HTTP behind OAuth in staging. Latency added ~40ms per tool call vs inline — acceptable for human-in-the-loop agents, painful for tight autocompletion loops.

Hybrid architecture.

  1. Core business tools stay inline — order lookup, user auth, billing. Tightly coupled to domain models, versioned with the API.
  2. Shared infrastructure tools are MCP — GitHub, Jira, Datadog. One server per integration, maintained by platform team.
  3. User-supplied tools (enterprise customers connecting their CRM) run as isolated MCP servers with scoped credentials.

The agent host normalizes both sources into a single tools array before each LLM call. Tool routing middleware deduplicates names and enforces allowlists per tenant.

Schema mapping pitfalls.

MCP tool schemas use JSON Schema; providers have subtle differences (strict mode, additionalProperties, nullable unions). Your adapter layer should:

We lost an afternoon to duplicate tool names when Git and Jira MCP servers both exported search.

Security comparison.

Function calling in-process inherits your app's trust boundary. MCP expands it:

Treat MCP servers like microservices: authenticate, authorize, audit, rate-limit.

Decision checklist.

Choose inline function calling when:

Choose MCP when:

Operational maturity matters as much as protocol choice. For MCP, version servers independently from hosts, publish changelogs when tool schemas change, and run contract tests that call tools/list and validate every schema against JSON Schema draft 2020-12. For inline tools, keep definitions in a single module with unit tests that snapshot schema shape — drift shows up in CI, not in production when the model starts calling renamed parameters. Hybrid setups should expose metrics: tool latency p95 by source (inline vs MCP), error rate by server, and cache hit rate if you memoize tools/list. When onboarding a new team, default them to MCP for shared integrations but require architecture review before adding inline tools that duplicate an existing server — namespace collisions are painful at scale.

Use MCP when tools span multiple hosts and teams — function calling suffices for single-service agents with stable tool surface.

Resources

Frequently asked questions

Does MCP replace OpenAI or Anthropic function calling?

No. MCP standardizes how tools are exposed and discovered across hosts. The LLM still receives tool schemas and returns tool calls through the provider's native API. MCP sits between your agent host and tool implementations — function calling sits between the model and the host.

When should you use MCP instead of inline function definitions?

Use MCP when tools are shared across multiple agent hosts (Cursor, Claude Desktop, custom runners), when tools live in separate processes or languages, or when you want discoverable tool catalogs without redeploying the agent. Inline schemas are fine for a single app with a handful of stable tools.

What are MCP's main operational costs?

Process management for stdio servers, auth for remote HTTP/SSE transports, versioning across server and client, and debugging distributed failures. Function calling in one process is simpler to trace but harder to reuse across teams.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →