If you have been building AI features over the last two years, you have probably wired up OpenAI function calling at least once. It was the first widely-adopted way to let an LLM invoke external code, and it is still the workhorse of many production AI features.
Then Anthropic introduced Model Context Protocol (MCP) in late 2024, and the picture got more nuanced. Both let an LLM call functions. Both use JSON Schema. Both have an active developer ecosystem. So which should you reach for in 2026?
This article walks through the honest differences — including the cases where OpenAI function calling is still the better choice, and where MCP wins decisively.
The 60-second summary
| Aspect | OpenAI Function Calling | MCP |
|---|---|---|
| Scope | One API request, one model | Persistent server, many clients |
| Lifecycle | Tools defined per request | Tools defined once at the server |
| Portability | OpenAI / Azure OpenAI only | Any MCP-aware client |
| Discovery | Pre-declared in the request body | Discovered at runtime from a server |
| Standard | OpenAI's proprietary API shape | Open spec (modelcontextprotocol.io) |
| Transport | HTTPS to OpenAI | stdio or Streamable HTTP |
| State | Stateless per call | Server can hold state across calls |
If your only client is the OpenAI API and your tools are simple, function calling is still excellent. If you need cross-client portability or persistent server-side capabilities, MCP is the better foundation.
What is OpenAI function calling?
OpenAI function calling is a feature of the OpenAI Chat Completions and Responses APIs that lets you declare functions inline with each request. The model decides whether to call one and, if so, returns a structured argument object.
A typical request looks like this:
import OpenAI from 'openai';
const client = new OpenAI();
const response = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'What is the order status for #4242?' }],
tools: [{
type: 'function',
function: {
name: 'get_order_status',
description: 'Look up the current status of an order',
parameters: {
type: 'object',
properties: { orderId: { type: 'string' } },
required: ['orderId'],
},
},
}],
});
const call = response.choices[0].message.tool_calls?.[0];
if (call) {
const { orderId } = JSON.parse(call.function.arguments);
const status = await fetchOrderStatus(orderId);
// Send the result back in a second call to get the final answer
}
The pattern works. It is well documented. Millions of production apps use it. But it has structural limits.
What is MCP?
MCP is an open protocol for connecting LLMs to external capabilities. Instead of declaring tools inline with each model call, you stand up an MCP server that exposes tools. Any compliant client — Claude Desktop, Cursor, Zed, a custom agent — can connect, discover the tools, and call them.
The same get_order_status capability, written as an MCP server tool:
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'orders', version: '1.0.0' });
server.tool(
'get_order_status',
'Look up the current status of an order',
{ orderId: z.string().describe('The numeric order ID, e.g. "4242"') },
async ({ orderId }) => {
const status = await fetchOrderStatus(orderId);
return { content: [{ type: 'text', text: `Order ${orderId} is currently ${status}.` }] };
}
);
await server.connect(new StdioServerTransport());
Notice what is missing: there is no OpenAI client, no chat.completions.create call, no message array. The MCP server does not know or care which LLM is on the other end.
The four key differences
1. Portability
OpenAI function calling is tied to OpenAI's API. If you want to swap to Claude, Gemini, or a local Llama model tomorrow, every tool definition has to be rewritten for the new model's tool-calling format (which is similar but not identical across providers).
MCP is model-agnostic. The same MCP server works in Claude Desktop, Cursor, and any custom agent regardless of which underlying LLM the agent uses. Build the server once; use it everywhere.
2. Lifecycle and state
OpenAI function calling is per-request. Tools are declared in the request body and exist only for that turn. There is no server-side state between calls (unless you bolt it on with session IDs, an external store, etc.).
An MCP server is a long-lived process that can hold state — caches, open database connections, in-progress jobs, subscriptions to data sources. This matters when:
- A tool benefits from warm connections (e.g., a Postgres pool)
- Multiple tool calls in one conversation need to share context
- The server needs to push proactive notifications (resources changed, progress updated)
3. Discovery
With function calling, the developer hand-picks which tools to send with each request. Want to add a tool? Edit the client code and redeploy.
With MCP, the client asks the server tools/list at runtime. Adding a tool means adding it to the server — every existing client gets it automatically on next connection, no code change.
4. Standardization
OpenAI function calling is a proprietary API surface. It works on OpenAI and Azure OpenAI. Other providers have copied the shape (Anthropic's tool use, Google's function declarations, etc.), but each has small dialect differences.
MCP is an open spec maintained by a community on GitHub. Anyone can implement a client or server, and they will interoperate. That is what makes the ecosystem effect possible — hundreds of community MCP servers exist for everything from Postgres to Notion to Linear, and they all just work with any MCP-aware client.
When OpenAI function calling is still the right tool
Despite all the above, function calling is the better pick when:
- You are building a single product on a single model. If your stack is OpenAI-only and you have no plans to change, function calling has fewer moving parts.
- Your tools are stateless and simple. A tool that takes inputs and returns outputs with no shared state, no streaming, no resource attachments — function calling is plenty.
- You want zero infrastructure. No subprocess, no separate server process, no MCP runtime. Function calling lives entirely inside your existing API request flow.
- You need the absolute lowest latency. MCP adds a small overhead from IPC or HTTP between the host and the server. For most apps this is negligible, but if you are squeezing milliseconds, it counts.
When MCP is the better foundation
Reach for MCP when:
- You want your tools to work across multiple AI clients. Build once, integrate everywhere — Claude Desktop, Cursor, Zed, internal agents.
- You are exposing tools to teammates who use different AI assistants. A shared MCP server beats five separate per-app integrations.
- Your tool needs state, streaming, or async progress. Database connections, long-running jobs, subscriptions — MCP has native support.
- You want a public ecosystem. The MCP registry already has hundreds of open-source servers; users discover and install them without your involvement.
- You are building a developer tool or IDE extension. Cursor, Continue, and Zed all use MCP — building for them is natural.
Can they work together?
Yes — and this is a common production pattern. Your application uses OpenAI (or any LLM provider) for the actual completions, and internally it talks to one or more MCP servers to actually execute tool calls.
The glue is small:
// Pseudo-code: bridge OpenAI function calling and an MCP server
const mcpClient = await connectToMcpServer('./orders-server.js');
const tools = await mcpClient.listTools();
const openaiTools = tools.map(t => ({
type: 'function',
function: { name: t.name, description: t.description, parameters: t.inputSchema },
}));
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages,
tools: openaiTools,
});
const call = response.choices[0].message.tool_calls?.[0];
if (call) {
const result = await mcpClient.callTool(call.function.name, JSON.parse(call.function.arguments));
// Feed result back into the conversation
}
This gives you the best of both worlds: OpenAI's mature completion API plus MCP's portable tool definition. Swap the LLM later? Only the bridge code changes — the MCP server is untouched.
Migration guidance
If you already have a working OpenAI function calling integration and you are considering MCP:
- Do not rewrite for the sake of rewriting. If your current setup works and your needs are static, leave it alone.
- Migrate when you feel a portability pinch. The day you need a tool to work in Claude Desktop and your custom OpenAI agent is the day MCP earns its complexity.
- Use the bridge pattern as a stepping stone. Define new tools as MCP servers; bridge them into your existing OpenAI flow. You incrementally build the MCP-shaped surface area without a big-bang rewrite.
Conclusion
OpenAI function calling and MCP are not competitors — they live at different layers of the stack. Function calling is a per-request feature of a specific completion API. MCP is a protocol for long-lived tool services that any LLM can use.
The right choice in 2026 depends on one question: how many AI clients will consume your tools?
- One client, one model → function calling is leaner.
- Many clients, many models, or unknown future clients → MCP is the durable bet.
If you are starting fresh in 2026, default to MCP and bridge it into your LLM provider of choice. The portability premium pays for itself the first time you swap models.
Try it yourself
Using the bridge pattern from earlier — OpenAI GPT-4o as the LLM, but tools sourced from an MCP server — the conversation looks identical to a pure-MCP setup:
get_order_status via the MCP bridgeOrder #4242 was shipped on May 9 via FedEx and is scheduled to arrive tomorrow before 6 PM. Tracking number: 1Z999AA10123456784.Same tool, two LLM runtimes. The MCP server has no idea whether GPT-4o, Claude, or a local model is on the other end — that is what the portability premium looks like in practice.