HorizonLux · AI automation

Ship AI agents that reach production

Only about 5% of enterprise agents ever go live. We build the evaluation, routing and cost control that get yours there.

Book a free scoping call
Guides

MCP vs Function Calling: Who Owns the Schema

Function calling and MCP are not rivals. The real difference is who writes the tool schema and when it arrives: yours at build time, or somebody else's at runtime. That single change is the value and the risk.

Two tool description sources, one reviewed and one not, both feeding a model that reads them identically

TL;DR

Function calling is how a model asks to run a tool. MCP is how that tool gets found and executed. The difference that actually decides your architecture is who writes the tool schema and when it arrives: with function calling you write it and ship it inside your request, with MCP it is fetched at runtime from a server that may not be yours. Everything else, the extra network hop, the cross-client portability, the security surface, follows from that one change.

This post is about the mechanism, not about whether MCP beats a plain REST API. That question has its own answer.

Deciding how many tools justify a server? We design AI agents around the boundary where runtime discovery starts paying for itself, which is later than most teams expect.

Key Takeaways

  • These are not alternatives. Function calling is the model's request format, MCP is the delivery and execution layer around it. A system using MCP still uses function calling underneath.
  • The real split is schema ownership. Function calling schemas live in your code and travel in your API request. MCP schemas live on a server and are fetched with tools/list at connection time.
  • Runtime discovery is the entire value of MCP: adding a tool means restarting a server, not redeploying an application, and any MCP-speaking client gets it for free.
  • Runtime discovery is also the entire risk. A tool description you do not control is text the model will follow, which puts a prompt-injection surface inside your tool list.
  • For one application with a handful of stable, private tools, function calling is not the compromise. It is the correct answer, and MCP is overhead with nothing on the other end.

The part everyone agrees on

Function calling is the mechanism a model uses to say "call get_weather with location: Tunis". The model does not execute anything. It returns a structured request, your code runs it, and you feed the result back. That is true whether the tool was described to the model by your own code or by an MCP server.

So MCP does not replace function calling. An agent using MCP still emits function calls. MCP standardises what happens on either side of that moment: how tools are discovered before the call, and how they are executed after it.

That much is well covered elsewhere. The useful question is what changes when you move the schema out of your application.

Who writes the schema, and when it arrives

With function calling, you write the tool definition in your own codebase and include it in the request to the model provider:

tools = [{
    "type": "function",
    "function": {
        "name": "get_project_status",
        "description": "Get the current status and open blockers for a project.",
        "parameters": {
            "type": "object",
            "properties": {"project_id": {"type": "string"}},
            "required": ["project_id"],
        },
    }
}]
response = client.chat.completions.create(model="...", messages=messages, tools=tools)

The schema is a build-time artifact. It is in your repository, it goes through your review process, and it changes when you deploy.

It is also a build-time artifact per provider. The same tool for Anthropic's API is not the same object:

tools = [{
    "name": "get_project_status",
    "description": "Get the current status and open blockers for a project.",
    "input_schema": {
        "type": "object",
        "properties": {"project_id": {"type": "string"}},
        "required": ["project_id"],
    },
}]

Identical logic, different envelope. Anthropic takes input_schema at the top level; OpenAI nests parameters inside a function object. Support both and you maintain two definitions or write a translation layer, and that cost multiplies by every tool and every provider you add.

With MCP, the client asks a server what it has, using tools/list, and gets back the definition from somewhere else entirely. The application never hardcodes the tool. It hardcodes the connection. The server describes each tool once in the protocol's own shape, and the client, not you, translates that into whatever the model it is talking to expects. Swapping providers stops being a schema migration.

One tool, two provider envelopes, or one server definitionTwo panels. On the left, one tool box splits into two differently shaped definition boxes, one labelled Anthropic input schema and one labelled OpenAI function parameters, with a caption noting two definitions to maintain. On the right, a single MCP server box holds one definition, an arrow leads to a client box, and the client fans out to both provider boxes, with a caption noting one definition and the client translating.The same tool, once or twiceFunction callingYour toolAnthropicinput_schemaOpenAIfunction.parameterstwo definitions to maintainMCPServer, one definitionClient translatesAnthropicOpenAIone definition, translated for you
Provider lock-in is the cost function calling charges quietly. It stays invisible until the second provider arrives.
Where the tool schema comes from in each approachTwo panels. On the left, function calling: a schema icon sits inside the application box itself, and an arrow carries it directly to the model provider, with a label reading build time, in your repository. On the right, MCP: the application box is empty of schemas, an arrow goes out to a separate server box which holds the schema, and a return arrow brings it back before a second arrow carries it on to the model provider, labelled runtime, from someone else’s process.The schema is the thing that movedFunction callingYour applicationtool schema, in your repoModel providerbuild time, one hopMCPYour applicationno schema heretools/listMCPserverschema lives hereModel providerruntime, one extra round trip first
Both paths end with the model emitting a function call. Only one of them lets the tool list change without a deploy, and only one of them lets it change without your review.

What runtime discovery buys

Three things, and they are real.

Tools change without a deploy. Add a tool to the server, restart it, and every connected client sees it on the next tools/list. With function calling you ship application code.

One integration serves many clients. An MCP server works with any MCP client, including the coding agents that live inside editors, which receive their MCP server list from the editor itself. A function-calling integration works with the application you wrote it into. If two products need the same tools, that difference compounds fast.

Someone else can maintain it. A vendor publishes a server, you connect to it, and you do not write or maintain the schema at all.

None of this matters if you have one application, five tools, and no plans to share them. That is the honest case for skipping MCP, and it is more common than the current discourse suggests.

What runtime discovery costs

An extra round trip, structurally. MCP adds a discovery call when the client connects, and tool execution goes through a server process rather than a function in your own address space. Whether that latency matters depends entirely on your workload, and anyone quoting you a millisecond figure without benchmarking your setup is guessing.

A schema you did not review. This is the part worth slowing down on. A tool's description field is not documentation. It is text the model reads to decide what to call and how, which means whoever writes that field is writing instructions your model will follow. With function calling, that text went through your code review. With a third-party MCP server, it did not, and it can change between one connection and the next without you noticing.

That is a prompt-injection surface sitting inside your tool list, and once a poisoned description or tool result enters context, it contaminates every turn that follows. It is also why an MCP server that reaches anything sensitive needs proper authorization rather than an API key in an env var, and why "just connect the vendor's server" deserves more scrutiny than it usually gets.

Which tool descriptions passed through a review gateTwo columns feeding into one model box at the bottom. The left column starts with a box reading your team writes the description, and its arrow passes through a solid horizontal bar labelled your code review. The right column starts with a box reading a server author writes the description, and its arrow passes through a broken bar with a visible gap, labelled no gate. Both arrows converge on a single box at the bottom reading the model reads both as instructions, showing that text of differing scrutiny arrives with equal authority.A tool description is an instruction, whoever wrote itYour team writesthe descriptionyour code reviewA server author writesthe descriptionno gateThe model reads bothas instructionsEqual authority, unequal scrutiny. That gap is the whole security argument.
The model cannot tell which column a description came from, which is why a third-party MCP server is a supply-chain decision and not just an integration.

Operational surface. A server is a process. It starts, crashes, needs logging that does not corrupt the protocol stream, and needs versioning when a schema changes. We wrote up the day-two version of that because the quickstarts stop before any of it.

Where the current spec sits

Most comparisons describe MCP generically, so two things in the 2026-07-28 spec are worth knowing before you decide.

MCP is now stateless. Every request carries the protocol version and capabilities in a _meta field, and servers advertise what they support through a mandatory server/discover request. A client can send any request directly and handle a version error, rather than performing a handshake first.

There is also a Tasks extension that returns a durable handle for long-running work, so a client can poll for status and collect the result later. That removes one of the older reasons teams reached past MCP for anything slow. Sampling and logging, by contrast, are now deprecated as client primitives.

Worth noting that tools are not the whole protocol either. Servers can expose resources and prompts as well, and clients can expose elicitation to ask the user for input mid-call. Comparing MCP to function calling only compares the tool-shaped slice of what MCP does.

So which one

Function calling MCP
Schema written by You, in your repository The server author, wherever that is
Schema arrives Build time, inside your request Runtime, via tools/list
Adding a tool Code change and deploy Server change and restart
Reusable across apps No, per integration Yes, any MCP client
Extra network hop No Yes, plus discovery at connect
Who reviewed the tool description Your team Possibly nobody on your team
Right when One app, few tools, private, latency-sensitive Many clients, shared or vendor tools, tools that change often

The decision is not about tool count on its own. It is about whether the tools have more than one consumer, and whether you trust whoever writes their descriptions.

Most real systems run both

The split is per tool, not per system. The test is simple: does anything other than this application need this tool?

Take an incident-response agent that needs four capabilities.

Tool Belongs in Why
Query the on-call alerting system MCP Every agent and dashboard in the company wants it
Search the runbook database MCP Shared across teams, changes without your deploy
Format a summary for one team's Slack channel Function calling Specific to this agent's output style
Record the agent's own reasoning steps Function calling Internal to this agent, nobody else consumes it

The first two are shared infrastructure and benefit from one place to update, one place to govern, one audit trail. The last two exist only inside this agent, and moving them to a server would add a process, a transport and a trust boundary for nothing.

Migration works the same way, one tool at a time. Start with the one that has been copy-pasted into three repositories. The function-calling versions keep working while you move the shared ones out, so there is no cutover event to schedule.

Frequently asked questions

Does MCP replace function calling?

No. A model connected through MCP still emits function calls. MCP governs how tools are discovered before that call and executed after it. Removing function calling would mean removing the model's ability to request a tool at all.

Does the model need to support MCP?

No, and this is the most persistent confusion about the protocol. The model only ever sees tool schemas and emits function calls, exactly as it always has. MCP support lives in the client, the application that connects to servers, assembles the tool list and executes the calls. Any model capable of tool calling works behind an MCP client, including older ones released before MCP existed.

Does MCP give me multi-step workflows?

No, and comparisons that credit it with chaining or conditional execution are overreaching. MCP standardises the discovery and execution of individual tool calls. Deciding to call three tools in sequence, branching on a result, or retrying a failure is your agent loop's job in both approaches. Choosing MCP does not buy you orchestration.

Is MCP slower than function calling?

Structurally it adds work: a discovery call on connect, and execution through a separate process rather than an in-process function. Whether that is perceptible depends on your tools and transport. A local stdio server behaves very differently from a remote HTTP one, so benchmark your own path rather than trusting a general figure.

Can I use both in the same application?

Yes, and it is common. Private, latency-sensitive tools stay as native function-calling definitions in your code, while shared or third-party tools come from MCP servers. The model sees one combined tool list and does not care where each entry came from.

What is the actual security difference?

With function calling, every tool description was written and reviewed by your team. With MCP, descriptions can come from a server you do not control, and the model treats them as instructions. That makes a third-party MCP server a supply-chain consideration, not just an integration.

When should I not use MCP?

When one application uses a small set of stable tools that nothing else needs. Runtime discovery earns its cost by serving multiple clients or absorbing frequent change. With neither, you are paying for a server, a transport and a trust boundary to reach functions that were already sitting in your codebase.

Sources

Ship AI agents that reach production

Only about 5% of enterprise agents ever go live. We build the evaluation, routing and cost control that get yours there.

Prefer email? [email protected]