Tool Calling Is the Primitive. MCP Is the Layer Above It.

Euael Eshete ยท August 14, 2026

TL;DR

  • A model cannot run code. Tool calling is a structured request the model emits, and your own code executes it. This is true with MCP and without it.
  • The raw loop has four steps. You send tool schemas. The model returns a tool_use block. Your code runs the function and returns a tool_result.
  • MCP does not replace that loop. The model never sees MCP. The client converts MCP tool definitions into the same API tool schemas, then calls the server when the model asks.
  • What MCP adds is everything around the call: runtime discovery, a process boundary, per-user authorization, and reuse across clients.
  • The cost is a network hop, a server to operate, and a protocol version to track. One application with five in-process functions does not need any of it.

The claim that confuses people

People say MCP lets a model use tools. Models could use tools before MCP existed. Function calling shipped in 2023, and every major API has supported it since.

MCP solves a different problem. To see which one, you need to know what a tool call is at the API level first.

What a tool call actually is

Start with the primitive. You pass a tools array in the request. Each entry is a name, a description, and a JSON Schema for the input:

{
  "name": "check_availability",
  "description": "Check open slots for a service on a given date",
  "input_schema": {
    "type": "object",
    "properties": {
      "service_id": { "type": "string" },
      "date": { "type": "string", "format": "date" }
    },
    "required": ["service_id", "date"]
  }
}

The model reads that schema as part of its context. When it decides the tool applies, it stops generating prose and returns a structured block instead:

{
  "type": "tool_use",
  "id": "toolu_01A9",
  "name": "check_availability",
  "input": { "service_id": "svc_44", "date": "2026-08-20" }
}

The response carries stop_reason: "tool_use". Nothing ran. The model produced JSON that says what it wants, and then it stopped.

Your code runs the function. You then send the answer back as a new message, tagged with the same tool identifier:

{
  "type": "tool_result",
  "tool_use_id": "toolu_01A9",
  "content": "[{\"start\":\"09:00\"},{\"start\":\"14:30\"}]"
}

The model continues from there. That is the whole mechanism.

sequenceDiagram participant A as Your Application participant M as Model API participant F as Your Function A->>M: messages + tools[] schemas M-->>A: stop_reason tool_use, name + input A->>F: run the function yourself F-->>A: return value A->>M: messages + tool_result M-->>A: final text

Two facts matter more than the syntax. The model is a planner, not an executor. The tool list also cannot change mid-request. It is a field in the request body.

What the primitive leaves to you

The loop above works. It also leaves four jobs on your side of the line.

You hardcode the catalog. The tool schemas live in your application source. A new tool means a code change and a deploy.

You own execution and authorization. Your handler decides which user made the request and what that user may touch. The model has no concept of a user. It only sees a name and an input object.

You own the failure path. A bad input, a timeout, or an exception must return as a tool_result the model can read. Silence stalls the conversation.

You repeat all of it per application. A second application that needs the same tools writes its own copy. Two applications and three tools produce six integrations, each maintained separately.

That last one is the N times M problem. It is the reason MCP exists.

What MCP changes, and what it does not

MCP changes nothing about the loop. It moves the tool definitions and the execution out of your application and behind a protocol.

flowchart LR M[Model API] <-->|tool_use / tool_result| C[MCP Client in your app] C <-->|tools/list, tools/call| S1[MCP Server: Convia] C <-->|tools/list, tools/call| S2[MCP Server: Mizan] S1 --> D1[(Convia data)] S2 --> D2[(Mizan data)]

The client does the translation, and the translation is short:

  1. The client calls tools/list on the server and gets back tool definitions with JSON Schema inputs.
  2. The client maps each one into the API tools array. The schema field is the same JSON Schema in both places.
  3. The model returns a tool_use block, exactly as before.
  4. The client calls tools/call on the server with that name and input instead of running a local function.
  5. The client wraps the response as a tool_result and sends it back.

Read step 3 again. The model never learns about MCP. MCP concerns the client and the server only. The wire format between your application and the model does not change.

So what do you actually get

Four things, and each one maps to a job the primitive left to you.

Runtime discovery instead of a hardcoded list. The catalog comes from tools/list at connection time. Ship a new tool on the server and every connected client sees it without a client deploy.

A real process boundary. The server runs as its own service with its own dependencies and its own database access. Your application does not need a driver for every system the model can reach.

Per-user authorization. The current specification aligns the authorization model with OAuth 2.1 patterns. The useful guarantee is not that the model may call a tool. It is that the model calls the tool as one named user, with that user's permissions, for a bounded time.

Reuse across clients. One server answers any compatible client. Your own product, a desktop assistant, and an internal automation script all use the same interface. You write the integration once instead of once per client.

MCP also exposes two primitives that raw tool calling has no equivalent for. Resources are readable data the model loads into context without calling an action. Prompts are parameterized templates the server offers. They stop every connected client from rebuilding the same workflow.

Side by side

Concern Raw tool calling MCP
Tool catalog A literal array in your request body Fetched from the server with tools/list
Adding a tool Code change and deploy in every client Server change only
Execution A function in your process A call across a transport to a server
Authorization Whatever your handler implements OAuth 2.1 aligned, scoped per user
Read-only data Another tool that returns text A resource primitive
Second client A second full integration Connect and go
Failure surface Your own exception handling Your handling, plus transport and server availability
Operational cost None beyond the function A service to deploy, monitor, and version

When the primitive is the right answer

Do not add a protocol to solve a function-call problem. That choice costs a week and returns nothing. Stay with raw tool calling when all of these hold:

  • One application consumes the tools, and no other client will.
  • The functions already run in that application's process.
  • The tool list changes on the same cadence as the application deploy.
  • Every caller shares one permission scope.

Reach for MCP when any one of them breaks. The most common break is the second client. The second most common is a permission model that has to differ per user.

How this shapes what we build

Our products expose MCP servers because the second client is not hypothetical for us. Convia, Mizan, and the platform this blog runs on each have a dashboard that a person operates directly. Each also has an MCP server that offers the same actions to an AI client.

The rule we hold to is that the server does not get a parallel, weaker permission model. A tool call runs through the same authorization path as the dashboard request that performs the same action. If the two ever diverge, the MCP server becomes a bypass rather than an interface.

Two related posts cover the other half of this. What "MCP-Native" Actually Means covers the architecture claim. MCP Just Went Stateless covers the current specification revision.

The short version

Tool calling is the primitive. The model emits a typed request, your code executes it, and you hand the result back. MCP sits above that primitive. It answers a distribution question. Who defines the tools, who runs them, and under whose permissions. If you have one application and five local functions, the primitive is enough. The moment a second client needs the same actions, writing the integration twice costs more than running a server.