02 Compare

MCP vs function calling

These get compared as alternatives constantly. They are not alternatives — an MCP system still uses function calling underneath. The real question is where your tools should live.

Function calling is the model deciding to invoke a tool and returning structured arguments. MCP is a protocol for where those tools live and how a client discovers them at runtime. They sit at different layers and an MCP-based system still uses function calling underneath. The decision is not which one, it is whether your tools should live inside your application or outside it.

The distinction that gets lost

Most comparisons frame these as competing approaches. They are not, and holding the wrong mental model here leads to genuinely bad architecture decisions.

Function calling answers: how does the model tell me it wants to do something? The model receives tool schemas, decides one applies, and returns a structured request naming the tool and its arguments. Your code executes it and returns the result.

MCP answers: where do those tools live and who owns them? It defines a client-server protocol so that tools can sit in a separate process, be discovered at runtime, and be consumed by any compliant client.

If you build an MCP server, the model still uses function calling to invoke its tools. MCP standardised the plumbing, not the mechanism.

Side by side

Function calling in-processMCP
Where tools liveInside your applicationA separate server
Adding a toolShip the appShip the server; clients pick it up
Reuse across appsCopy the codePoint another client at it
DiscoveryCompile timeRuntime
Latency per callIn-processAdds a hop and a process boundary
DebuggingOne stack traceTwo processes, two logs
AuthYour app's sessionIts own problem to solve
Best whenOne app owns its toolsMany clients, or tools evolve separately

What MCP actually buys you

I run MCP servers in production, including one exposing 59 tools across an outreach system and others for a CRM and an analytics product. Three benefits are real and one is oversold.

Real: the tools outlive the client. The same server answers my terminal, a scheduled job, and anything else that speaks the protocol. Without it, each of those would carry its own copy of the tool code and they would drift.

Real: you can ship tools without shipping the app. Adding a capability means deploying the server. Clients discover it on next connect. When tools change more often than the application does, this is the whole argument.

Real: the boundary forces a clean interface. A tool that has to be described well enough for a model to pick it correctly, over a wire, ends up better specified than an internal function. That discipline is worth something on its own.

Oversold: portability across models. It is true that a compliant client can use your server. In practice, the tool descriptions that make a model choose correctly are tuned to how a particular model behaves, and that tuning does not transfer perfectly. The plumbing is portable. The prompt engineering inside your tool descriptions is less so than the pitch suggests.

What it costs

Being straight about this, because most write-ups skip it.

You now have two things to run. A server that can be down, needs deploying, needs its own error handling. If the agent stops working you are debugging across a process boundary, and the client frequently reports only that a tool failed.

Auth becomes a design problem. In-process tools inherit the caller’s session. Over MCP you have to decide what the server trusts, how credentials get there, and what happens when a tool needs to act on behalf of a specific user. This is the part people underestimate.

Long-running work does not fit naturally. A tool call that awaits a long job will time out. Anything slow needs to become start-plus-poll: one tool kicks the work off and returns an identifier, another reports status. I learned this the direct way, and it is a design constraint rather than a bug.

Tool sprawl is real. Past a few dozen tools on one server, model selection accuracy degrades and debugging gets meaningfully harder. When something misbehaves, the first thing to check is not the model — it is whether the tool returned what you think it returned.

The decision rule

One question: does anything other than this application need these tools?

If no, use function calling in-process. It is simpler, faster, has one stack trace, and inherits your auth. Reaching for MCP because it is the modern option is how you acquire a distributed system you did not need.

If yes — a second client, a scheduled job, a terminal, a colleague’s tooling — MCP pays for itself immediately, because the alternative is duplicated tool code that will drift.

There is a second trigger worth watching for: when your tools change faster than your application. If you are shipping the whole app to add a capability, the boundary is in the wrong place regardless of how many clients you have.

How I actually split it

Both, and the split is about ownership rather than technology.

Tools that are meaningless outside one product stay in-process. A function that manipulates that app’s own view state has no business being a protocol endpoint.

Tools that represent a capability — search these records, send this message, run this report — go on an MCP server, because those are the ones something else will eventually want.

The test I use: would I want this from a different client in six months? If yes, it goes on the server now, while it is cheap to move.

Practical notes

  • Write tool descriptions for a reader who cannot see your code. The model picks from the description alone. Most tool-selection failures are description failures.
  • Return errors as data, not exceptions. A model can recover from “no results, try a broader query.” It cannot recover from a stack trace.
  • Never make a tool await a long worker. Start and poll.
  • Beware empty results. An empty response reads to a model as a definitive answer. If empty can mean “something went wrong,” say so in the payload.
  • Version your server. Clients cache tool lists; a silently changed schema fails in confusing ways.

FAQ

What is the difference between MCP and function calling?

Function calling is the model requesting a tool with structured arguments. MCP is a protocol for where tools live and how clients discover them. Different layers — MCP still uses function calling underneath.

Do I need MCP for a simple agent?

No. If one app owns its tools, in-process is simpler and faster. MCP earns its cost with multiple clients or independently evolving tools.

Does MCP make an agent slower?

It adds a hop and a process boundary per call. Usually negligible against model latency, but measure it if latency matters to you.

Can I use both?

Yes, and you probably should. App-specific tools in-process, shared capabilities over MCP. The split is about ownership.

Related: what MCP is, in plain terms, how to build an MCP server, and building an agent with Claude Code and MCP.