MCP vs function calling: what changes, and when a data team needs MCP

By Arshad Ansari

"MCP vs function calling" sounds like a choice between two ways for a model to call tools. It isn't one. An MCP client takes the tools it finds on a server and gives them to the model through function calling. The model never sees MCP.

What MCP changes is everything around that call: who writes the tool definitions, where the code runs, and who can reuse them. This post shows that with a working server, then with my own production system, which serves one set of 80 tools both ways. Then it answers the related question data teams ask: MCP vs API, and when a team needs MCP at all.

MCP vs function calling: the short answer

Function callingMCP
What it isA model API featureA protocol between a client and a tool server
Who defines the toolsYour application, in each requestThe server, discovered at runtime with tools/list
Who runs the toolYour applicationThe MCP server, called by the client
Who can reuse the toolsOnly your applicationAny MCP client: Claude Code, Cursor, Claude Desktop, your own
AuthWhatever your app doesEnvironment for stdio; OAuth 2.1 for HTTP
The model seesTool definitions and resultsThe same: tool definitions and results

The last row is the point. Both sides of the table end in the same place.

What the model actually sees

In function calling, you send each tool as a name, a description and a JSON Schema for its arguments. The model replies with a structured call, your code runs it, and you send the result back in a second request. Anthropic's tool use docs describe the same loop: the model stops with stop_reason: "tool_use", your code runs the tool and returns a tool_result.

An MCP server publishes the same three things. Here is the query tool from my read-only DuckDB server (built in the FastMCP post), read with tools/list and turned into each vendor's format. This is roughly what an MCP client does before every model request:

async with Client(Path("server.py")) as c:
    q = next(t for t in await c.list_tools() if t.name == "query")
    anthropic_tool = {"name": q.name, "description": q.description, "input_schema": q.inputSchema}
    openai_tool = {"type": "function", "name": q.name, "description": q.description,
                   "parameters": q.inputSchema}

The output, as it printed:

{"name": "query", "description": "Run one read-only SQL query. Returns at most MAX_ROWS rows.", "input_schema": {"type": "object", "additionalProperties": false, "properties": {"sql": {"type": "string"}}, "required": ["sql"]}}
{"type": "function", "name": "query", "description": "Run one read-only SQL query. Returns at most MAX_ROWS rows.", "parameters": {"type": "object", "additionalProperties": false, "properties": {"sql": {"type": "string"}}, "required": ["sql"]}}

The schema is byte-for-byte the same in both. Only the key that holds it changes: inputSchema in MCP, input_schema for Anthropic, parameters for OpenAI. When the model calls the tool, the client sends the arguments back to the server with tools/call. That call returned {"columns":["n"],"rows":[["50000"]],"truncated":false}.

So anything you know about writing good function definitions applies unchanged to MCP tools. Short, exact descriptions. Narrow arguments. Results the model can read. The protocol doesn't fix a bad tool.

Where the call runs

The real difference is location. There are three places a tool call can run, and data teams tend to need all three at some point.

  1. Inside your application (function calling). Your code holds the database connection and runs the query. Nothing leaves your process except the model request.
  2. In an MCP server next to the client. Claude Code or Cursor launches the server as a subprocess over stdio, or connects to it over HTTP. The client relays the model's call to the server and the result back to the model.
  3. In an MCP server called by the model provider. Anthropic's MCP connector and OpenAI's remote MCP tool take a server URL in the API request, and the provider's platform calls your server. The connector supports tool calls only, and needs a server exposed over public HTTP; a local stdio server can't be connected directly.

The third option deserves a pause before you point it at a database. Your server must be reachable from the provider's network, and every result goes back through the provider. OpenAI's page says it bluntly: a malicious server "can exfiltrate sensitive data from anything that enters the model's context". The same holds the other way round: your server's data enters the context of whatever else is connected.

One registry, served both ways: what I run

My own system, AEGIS, does both. It has a registry of 80 tools: tasks, notes, research, finance, infrastructure. Its own chat loop runs local and hosted models through LiteLLM and passes those tools with plain function calling. The same tools are also served over MCP to headless Claude Code and Kimi CLI runs, and to Claude Desktop.

There is one registry, not two. The MCP endpoint converts each entry on the way out. The conversion, from api/routes/mcp_server.py, is four lines, and its comment says it all:

return {
    "name": fn.get("name", ""),
    "description": fn.get("description", ""),
    # MCP names it inputSchema; the OpenAI-shaped registry names it
    # parameters. Same JSONSchema either way.
    "inputSchema": schema if isinstance(schema, dict) else {"type": "object", "properties": {}},
}

I measured the registry in both shapes: 50,837 bytes of JSON as function definitions, 48,197 bytes as MCP descriptors. The MCP shape is smaller only because it drops the {"type": "function", "function": {...}} wrapper. Both calls end in the same executor.

Three lessons came out of running both:

MCP exists here because I don't own the other agent loops. I can't add function definitions to Claude Code's requests. Claude Code can mount an MCP server. For my own loop, function calling is simpler, so it stays.

Function calling can fail silently in the middle. A proxy in front of one model family silently dropped the tools array from requests. The comment in services/chat.py describes the symptom: "the model never sees the tool definitions and responds in plain text (often hallucinating that no tools are available)". There was no error. The fix was a list of those model names: a turn that carries tools and would land on one of them is switched to another model. MCP doesn't prevent this kind of bug, but it moves the tool list somewhere you can test it directly, without a model.

The served set is smaller than the registry. The MCP endpoint never serves 11 of the tools, whatever an agent's configuration says. Every tool that can start another agent run is removed, because a run that starts runs has no depth limit. On the gated endpoint, only 29 named read-only tools run without a person approving them. In my own loop, I trust the code that picks the tools. Over MCP, the caller is an agent reading untrusted web pages, so the server decides.

That last point is the one I would give a data team. Function calling trusts the application. MCP has to assume it doesn't know who is calling. The MCP spec puts the duties on the server: servers MUST validate inputs, enforce access controls, rate-limit calls and sanitise outputs.

MCP vs API: what an MCP server adds to an API you already have

Most MCP servers wrap an API. The BigQuery, Snowflake, dbt and Postgres servers all call something that existed before MCP. So "MCP vs API" is the wrong frame too. The useful question is what the wrapper adds.

  • Discovery at runtime. A client lists the tools and reads their schemas. Nobody writes integration code per API.
  • Descriptions written for a model. An API's docs are for developers. A tool's description is the only documentation the model gets.
  • A narrower surface. The good wrappers expose less than the API: named queries instead of raw SQL, a row cap, read-only by default. The Postgres MCP server post shows the difference between a SQL box and named tools.
  • One transport and one auth flow. JSON-RPC over stdio or Streamable HTTP, and OAuth 2.1 for remote servers, as covered in MCP security and authentication.

What MCP doesn't add is safety. A server that passes the model's SQL straight to a read-write connection is as dangerous as the API it wraps.

The cost is the same either way

Tool definitions travel with the request in both cases. Anthropic's docs list the tool definitions as input tokens, plus a tool-use system prompt of 286 tokens for its current Opus and Sonnet 5.5 models. My registry's 50 KB of definitions is roughly 12,000 tokens on a rough four-characters-per-token rule. MCP doesn't make that smaller; the client still has to tell the model what exists. That cost is why the AEGIS registry keeps its hand-laid order (the code calls the list "the LLM's prompt"), and why each agent gets a subset rather than all 80.

If an agent needs dozens of tools, the fix is fewer, better tools per agent. That applies to function calling and MCP alike.

When does a data team need MCP?

You need MCP when at least one of these is true:

  1. People already work in an MCP client. Analysts in Claude Desktop or engineers in Claude Code or Cursor should reach your warehouse from there.
  2. More than one agent or app needs the same tools. One server means one set of permissions, one row cap and one log, instead of a copy in each app.
  3. The tools belong to a different team than the agent. The data platform team owns the server; whoever builds the agent mounts it.

Plain function calling is enough when:

  1. One application owns the model loop and the tools, and nobody else needs them.
  2. The tool list is small and fixed, and changes with the application's own releases.
  3. You want no extra process to run, secure and monitor.

If you are unsure, start with function calling inside your app, and write each tool as a function with a clear schema. Moving it behind an MCP server later is the four-line conversion above.

So: MCP vs function calling?

Both. Function calling is how a model asks for a tool. MCP is how the tool gets to the model when you don't own the application around it. For a data team, the decision is about ownership and reach. Who runs the agent? Who owns the data access? How many clients need it?

Whichever you choose, the controls live in the tool, not the protocol: read-only access enforced by the database, row caps, timeouts and a log of every call. Testing an MCP server shows how to check them without a model in the loop.

Local-First Analytics doesn't cover MCP or function calling. The nearest part is Chapter 12, which turns plain-English questions into DuckDB SQL with a local model inside one Python program, the no-protocol version of this post. The rest of the book covers the data such a tool reads: DuckDB over Parquet on your own hardware. Chapter 1 is free to read; the rest is on Amazon.

If your team is deciding whether to put its warehouse behind an MCP server, and what that server should allow, that is work I do. If one app uses three fixed tools, plain function calling is fine, and you don't need me.

More in this series: how to test an MCP server, building an MCP server with FastMCP, dbt MCP server and MCP security and authentication.

Common questions

What is the difference between MCP and function calling?
Function calling is a model feature: you send tool definitions (a name, a description and a JSON Schema) with each request, the model answers with a structured call, and your application runs it and sends back the result. MCP is a protocol for publishing those tools from a separate server, so any MCP client can discover them at runtime with tools/list and run them with tools/call. An MCP client still hands the tools to the model through function calling. MCP decides where the tools come from; function calling is how the model asks for one.
MCP vs tool calling: when should I use which?
Use plain function calling when one application owns both the model loop and the tools, for example a backend feature with three fixed tools. Use MCP when the tools must work in clients you don't control, such as Claude Code, Cursor or Claude Desktop, or when several agents or teams should share one set of tools with one set of permissions and one log. Many systems do both: function calling inside their own loop, and MCP for everyone else.
What is the difference between MCP and an API?
An API is built for programs written by developers who read its docs. An MCP server is built for a model that reads tool descriptions at runtime. Most MCP servers wrap an existing API, and the useful ones narrow it: fewer operations, model-readable descriptions, row caps, read-only defaults. MCP also fixes the transport (JSON-RPC over stdio or Streamable HTTP) and the auth flow (OAuth 2.1 for HTTP), so a client written once works with every server.
Can I use MCP with OpenAI function calling?
Yes, two ways. Your own code can act as the MCP client: call tools/list, convert each tool's inputSchema into the function's parameters field, and forward the model's function calls to tools/call. Or, for a remote server on the public internet, OpenAI's Responses API accepts a tool of type "mcp" with a server_url, and OpenAI's platform calls the server itself. Anthropic's Messages API has the same feature, the MCP connector, which supports tool calls only.
Is an MCP server the same as a REST API?
No. A REST API exposes resources over HTTP verbs and URLs. An MCP server exposes named tools over JSON-RPC, with a schema and a description for each, and a list method so a client can find them without reading documentation. It can run as a local subprocess over stdio with no HTTP at all. Many MCP servers call a REST API inside each tool.

Get new posts by email

Data engineering notes like this one — what breaks and what it costs, in production.

What breaks and what it costs — pipelines, warehouse bills, and the failures that only show up in production. A few a month, never padded to hit a schedule. No sequence, no pitch deck. Reply 'stop' once and you're off — it reaches me, not a queue.

Want the whole playbook?

If this was useful, the long version is my book. Local-First Analytics — 313 pages, runnable code for every chapter — is the full build: DuckDB, Parquet and Arrow, from install to production. On Amazon, and chapter 1 is free to read here.

Get the book

Not ready to buy? Read chapter 1 free — the whole chapter, no email required.

Rather talk it through? Book a free 30-minute call. No slot that suits your time zone? Email info@hikmahtech.in.