MCP vs Direct Integration vs Function Calling
Direct API code is tightest and fastest, function calling adds model-driven invocation, MCP adds discovery and reuse across applications — each wins in a different N × M regime.
Three levels, not three competitors
These options are often presented as rivals; they are layers. Direct integration is plain code calling an API — no model involved in the decision. Function calling lets the model choose among functions you defined in the prompt, and your code executes them. MCP keeps function calling as the model-facing mechanism but moves the function definitions and execution behind a protocol so other hosts can reuse them.
So the real question is: who should decide when the call happens (code or model), and who should own the integration (this app or a shared server). The escalation ladder from Choosing the Right Abstraction applies — climb only when the lower rung fails.
- Coupling: direct = tight (compile-time); function calling = medium (schema in your repo); MCP = loose (schema discovered at runtime).
- Reuse: direct = none; function calling = within one codebase; MCP = across any compliant host.
- Discovery: direct = n/a; function calling = static list you maintain; MCP =
tools/listat runtime with change notifications. - Security surface: direct = your code only; function calling = model may pick wrong function or bad args; MCP = all of that plus third-party server code and its outputs in your trust boundary.
- Latency: direct ≈ upstream API; function calling = + one model turn; MCP = + one model turn + IPC/HTTP hop + serialization (typically 1–50 ms extra).
Worked example: "check whether the deploy finished"
A deploy status check is a fixed query with fixed arguments. Direct integration wins: GET /deploys/latest, parse, done — deterministic, testable, 40 ms. Wrapping it in function calling adds a model turn (~1 s and a few cents) to decide something you already knew.
Now the requirement becomes "answer arbitrary ops questions about our deploys, incidents and on-call". The model must pick between get_deploy_status, list_incidents and who_is_on_call based on the question. Function calling earns its keep.
Finally, three teams each build such an assistant on different stacks and all need the same PagerDuty and deploy access. Maintaining three copies of the tool code and schemas is where MCP pays: one ops-mcp-server, three clients.
1// 1. Direct: code decides, no model.2const status = await fetch('/api/deploys/latest').then(r => r.json())3 4// 2. Function calling: model decides, your code executes.5const tools = [{ name: 'get_deploy_status', description: '…', input_schema: {/*…*/} }]6if (toolCall.name === 'get_deploy_status') result = await getDeployStatus(toolCall.input)7 8// 3. MCP: model decides, a server you may not own executes; tools discovered at runtime.9const { tools: mcpTools } = await mcp.listTools()10const result2 = await mcp.callTool({ name: toolCall.name, arguments: toolCall.input })Decision rules
Choose the lowest level that satisfies the requirement, and revisit when N (hosts) or M (systems) grows. Migrating from function calling to MCP is mechanical — the schemas already exist — so there is no penalty for starting simple.
Whatever the level, the untrusted-content and least-privilege rules do not change. MCP simply makes it more likely that a tool you did not write is inside your loop, so the Tool Permissions and Least Privilege and Secrets and Untrusted Output disciplines matter more, not less.
- Fixed inputs, fixed sequence → direct code. Measure before adding a model.
- Model must choose among actions at runtime, single host → function calling.
- Multiple hosts or community servers, integrations should be pluggable → MCP.
- Latency budget under ~100 ms end to end → not a model-in-the-loop option at all.
Key points
- Direct, function calling and MCP are layers; MCP still uses function calling underneath.
- Direct code wins on latency, cost, determinism and testability whenever the decision is static.
- Function calling wins when the model must choose among actions in one application.
- MCP wins when several hosts share integrations or when discovery must be dynamic.
- Security surface grows at each level; controls must grow with it.
- Start low; migration upward is cheap because schemas carry over.
When to use — and when not to
- Use this comparison whenever someone proposes "add MCP" as the first step.
- Sizing an integration roadmap: count hosts and systems today and in 12 months.
- Justifying a direct integration in a design review.
- Do not pick MCP for a single-host prototype to "future-proof" it — you pay the trust and prompt-size cost now for a benefit you may never need.
- Do not pick direct code when the branching logic would become a 300-line intent classifier — that is what the model is for.
- Do not pick function calling for sub-100 ms hot paths.
Failure modes
- Model turn inserted into a deterministic path: slower, costlier, occasionally wrong.
- Function-calling schemas copied into three repos drift apart (
tool-argument-drift). - MCP adopted early; 12 servers and 180 tools later, tool selection accuracy collapses.
- Third-party server treated as trusted; injected output steers the agent.
Tradeoffs
Ratings describe the recommended "lowest sufficient level" choice; each step up adds about one point of complexity.