Function Calling vs MCP: Wiring LLMs to Real Systems
Both let a model act on your systems. The choice is really about who owns the integration surface.
Every LLM integration eventually needs the model to do something: read a record, call an API, run a query. There are two mainstream ways to wire that up — provider-native function calling, and the Model Context Protocol (MCP). They solve overlapping problems, and the marketing makes them sound like competitors. They are not, quite. Picking wrongly costs you weeks of the wrong kind of plumbing.
Function calling, in one paragraph
You describe your tools as JSON schemas, send them with the request, and the model replies with a structured call: name plus arguments. Your code runs it and feeds the result back. It is built into the API, has no extra infrastructure, and is the right default for a single application talking to a single model.
MCP, in one paragraph
MCP standardises how a client discovers and calls tools exposed by a separate server. You write a tool server once — “the orders server,” “the analytics server” — and any MCP-aware client can use it. The value is not a smarter model; it is a reusable, decoupled integration surface that outlives any one app or model vendor.
Function calling asks “how does this app call a tool?” MCP asks “how does any app call this tool?” That framing decides most of it.
When function calling is the right call
- One application, one team, a handful of tools tightly coupled to that app’s domain
- You want the fewest moving parts and no extra process to deploy or monitor
- The tools are cheap to redefine and unlikely to be reused elsewhere
When MCP earns its keep
- The same capability — “query the warehouse,” “search the docs” — is needed by several agents or several clients
- You want to swap model vendors without rewriting every integration
- Different teams own different tool surfaces and you want clean boundaries between them
The part nobody’s marketing mentions
Whichever transport you pick, the hard problems are identical and they are not the transport. They are: designing tool schemas the model uses correctly, authorizing each call against the real user, validating arguments before execution, and handling partial failure. A beautifully standardised MCP server with no authorization is exactly as dangerous as a function call with none.
# the schema is the real interface - the model reads it literally
{
"name": "get_order_status",
"description": "Status for ONE order owned by the current user",
"parameters": {
"order_id": { "type": "string", "description": "e.g. ORD-10432" }
}
}
# authz still happens in YOUR code, keyed to the session - not here
Notice the description does the work. Vague descriptions produce wrong calls far more often than a weak model does. Spend your time there.
A pragmatic path
Start with function calling. It is the shortest distance to a working agent, and most first integrations never need more. The day you find yourself copying the same tool definition into a second application, or a second team asks for the same capability, that is the signal to lift it into an MCP server. Migrating a well-defined function into an MCP tool is mostly moving code, because the schema and the authorization — the parts that mattered — carry straight over.
We have shipped both. The systems that aged well were never the ones that picked the trendier transport; they were the ones that treated tool schemas as a real API and put authorization in boring, testable server code.