---
title: 'What Is the Model Context Protocol'
source: 'https://academia.sh/en/courses/the-model-context-protocol-and-tool-ecosystem/what-is-the-model-context-protocol'
course: 'The Model Context Protocol and Tool Ecosystem'
language: en
updated: '2026-10-21T05:02:35+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines the host application, client and server roles of the Model Context Protocol on a fictional environment.'
    - 'Compares the number of requests, responses, envelope tokens and round trips of the same tool chain in the setup without the protocol and the setup with it.'
    - 'Shows by counting how capability negotiation narrows the declared tool set.'
    - 'Measures portability by the number of definitions written and the number of places that break when the interface changes.'
---

# What Is the Model Context Protocol

Counting what happens when the tool definition leaves the calling side for a server: three servers declare five tools, the allowlist carries four, negotiation leaves three. The same chain gives 844 with zero messages at the baseline; with the protocol it costs 6 requests, 6 responses, 179 envelope tokens and 9 round trips. Across four clients the setup without the protocol breaks four places, the setup with it one.

The AI Agents course built, equipped and measured an agent over twenty-two lessons: the order of
perception and action, the split between planner and executor, the step budget, the stopping
criterion, the four levels of tool definition, memory, the permission surface and evaluation.
One thing was silently assumed throughout: **every tool was defined by hand inside the lesson.**
The definition and the execution were under the same hand; the tool's name, schema and example
call sat right next to the agent's code.

This lesson removes that assumption and asks three questions: how is a tool **handed
over** to another agent, team or application, **whose hands** does the definition stay in, and
**who breaks** when the interface changes. The Model Context Protocol answers all three: it
takes the tool definition out of the calling side's code, moves it to a **server** on the other
side, and connects the two with a fixed message layout. The gain is portability. The cost is
countable: every message has an **envelope**, every remote request has a **round trip**. This
lesson puts the two side by side.

- **PS1.** The environment, servers and protocol simulator are **fictional**; no
  real server, client library or runtime is called. 60 fictional subscribers, three fictional
  servers, five declared tools. Seed **20260218**.
- **PS2.** The simulator does not implement the protocol; it counts the contract's **observable
  items**: requests, responses, notifications, envelope tokens and round trips.
- **PS3.** The envelope's **fixed cost is eight tokens**; the body's tokens come on top.
- **PS4.** The baseline is the **setup without the protocol**: the tool is defined inside the
  calling side, there is no handshake, no envelope is counted. All measurements of the AI Agents
  course were at this baseline.
- **PS5.** On local transport a request counts as **one** round trip, on remote transport as
  **two**.
- **PS6.** Capability negotiation sends each server one request and receives one response. On a
  version mismatch the **whole** server drops; a tool outside the allowlist is not taken.
- **PS7.** The set is small: **three** servers, **five** declared tools, an allowlist of **four**.
  Here the smallest step of a metric that prints a ratio is $1/5 = 0.2000$, and a smaller
  difference cannot be measured. This lesson prints **counts** instead of ratios.
- **PS8.** The counts do not depend on the run; the simulator has no nondeterminism.
  The second run executes the same chain for **another subscriber**.
- **PS9.** The tool's own accuracy was measured in the AI Agents course and is **not repeated**;
  the rise of the valid-call rate from 0.2803 to 0.8919 once a schema was added is taken from
  there as input. What is measured here is the cost of the definition's **location**.

## The Three Roles and the Fictional Environment

The **host application**
talks to the user and the language model: it holds the context, carries the allowlist, and asks
for approval on a call with a side effect. The **client** lives inside the host application and
is **one per server**: it sets up the session, runs the capability negotiation and wraps the
messages in envelopes. The **server** hosts the tool definitions, the **handler** behind each
one and, where present, the **resource** and **prompt** capabilities; it never sees the host
application, it talks only to the client.

In the AI Agents course these three roles were one file. The code below takes the same
measurement network setup and spreads the tools over three fictional servers: `olcum` (the
measurement server) reads meters with `sayac_oku` (read meter, the counterpart of `read_meter`
in the AI Agents course) and computes consumption with `tuketim` (consumption), `tarife` (the
tariff server) works out the charge with its `tarife` tool (tariff), and `kayit` (the records
server) returns a region summary with `bolge_ozet` (region summary) and writes records with
`kayit_yaz` (write record). Each server has a **version** and a **transport** mode. `message` is
the fictional envelope that counts every request, response and notification.

```python
SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def tokenize(m):
    b = []
    for s in str(m).lower().replace(",", " , ").split():
        while len(s) > 5:
            b.append(s[:5])
            s = s[5:]
        b.append(s)
    return b


# ---- FICTIONAL environment: subscriber records of the measurement network
REGION = ["dogu", "bati", "kuzey", "guney", "merkez"]
TYPE = ["mesken", "isyeri"]
SUBSCRIBERS = {}
for i in range(60):
    r = rng(SEED + 31 * i)
    previous = 10000 + int(r() * 80000)
    SUBSCRIBERS[f"A{i + 1:03d}"] = {"bolge": REGION[int(r() * 5)], "tip": TYPE[int(r() * 2)],
                                    "onceki": previous, "simdiki": previous + int(r() * 70)}
TIERS, ABOVE, COMMERCIAL_FACTOR = ((10, 4), (25, 7), (40, 11)), 16, 2


def _charge(m3, type_):
    remaining, lower, total = m3, 0, 0
    for upper, unit in TIERS:
        band = min(remaining, upper - lower)
        if band <= 0:
            break
        total += band * unit
        remaining -= band
        lower = upper
    return (total + remaining * ABOVE) * (COMMERCIAL_FACTOR if type_ == "isyeri" else 1)


def t_meter(abone=None, **_):
    a = SUBSCRIBERS.get(abone)
    return {"hata": "abone yok"} if a is None else {
        "onceki": a["onceki"], "simdiki": a["simdiki"], "tip": a["tip"]}


def t_consumption(onceki=None, simdiki=None, **_):
    if onceki is None or simdiki is None:
        return {"hata": "eksik alan"}
    f = simdiki - onceki
    return {"m3": f + 100000 if f < 0 else f}


def t_tariff(m3=None, tip="mesken", **_):
    return {"hata": "eksik alan"} if m3 is None else {"tutar": _charge(m3, tip)}


def t_region(bolge=None, **_):
    if bolge not in REGION:
        return {"hata": "bolge yok"}
    n = sum(1 for a in SUBSCRIBERS.values() if a["bolge"] == bolge)
    return {"bolge": bolge, "abone": n}


def t_record(abone=None, sutun=None, deger=None, **_):
    if not (abone and sutun and deger is not None):
        return {"hata": "eksik alan"}
    return {"yazildi": True}


# ---- FICTIONAL servers: each one declares its capabilities
SERVERS = {
    "olcum": {"version": 2, "transport": "local",
              "tools": {"sayac_oku": {"function": t_meter, "fields": ("abone",), "side_effect": False},
                        "tuketim": {"function": t_consumption, "fields": ("onceki", "simdiki"),
                                    "side_effect": False}},
              "resources": ("abone_listesi",), "prompts": ()},
    "tarife": {"version": 1, "transport": "remote",
               "tools": {"tarife": {"function": t_tariff, "fields": ("m3", "tip"),
                                    "side_effect": False}},
               "resources": ("basamak_tablosu",), "prompts": ("bedel_aciklamasi",)},
    "kayit": {"version": 3, "transport": "remote",
              "tools": {"bolge_ozet": {"function": t_region, "fields": ("bolge",),
                                       "side_effect": False},
                        "kayit_yaz": {"function": t_record, "fields": ("abone", "sutun", "deger"),
                                      "side_effect": True}},
              "resources": (), "prompts": ()},
}
CLIENT_VERSION = {"olcum": 2, "tarife": 1, "kayit": 2}      # the kayit server is ahead
ALLOW = ("sayac_oku", "tuketim", "tarife", "bolge_ozet")     # kayit_yaz is not on the allowlist

COUNT = {"request": 0, "response": 0, "notification": 0, "tokens": 0, "round_trips": 0}


def message(kind, body, transport="local"):
    """FICTIONAL envelope. No real protocol is implemented; what is counted is the cost of the envelope."""
    COUNT[kind] += 1
    b = len(tokenize(body)) + 8                          # envelope constant
    COUNT["tokens"] += b
    if kind == "request":
        COUNT["round_trips"] += 2 if transport == "remote" else 1
    return b


def handshake(client_version=None, allow=None):
    """Capability negotiation: a version mismatch and the allowlist narrow the tool set."""
    client_version = CLIENT_VERSION if client_version is None else client_version
    allow = ALLOW if allow is None else allow
    agreed, dropped = {}, []
    for name, s in SERVERS.items():
        message("request", f"yetenek sor {name}", s["transport"])
        message("response", f"yetenek bildir {name} surum {s['version']} "
                            + " ".join(s["tools"]) + " " + " ".join(s["resources"]))
        if client_version.get(name, 0) < s["version"]:
            dropped.append((name, "version", s["version"], client_version.get(name, 0)))
            continue
        for t, spec in s["tools"].items():
            if t not in allow:
                dropped.append((name, "allowlist", t, None))
                continue
            agreed[t] = dict(spec, server=name, transport=s["transport"])
    return agreed, dropped


def call(agreed, name, state):
    if name not in agreed:
        return {"hata": "arac bildirilmedi"}
    t = agreed[name]
    kw = {a: state[a] for a in t["fields"] if a in state}
    message("request", f"{name} " + " ".join(f"{k} {v}" for k, v in kw.items()), t["transport"])
    s = t["function"](**kw)
    message("response", " ".join(f"{k} {v}" for k, v in s.items()))
    return s


print("server   version  transport  tools                   resources prompts")
for name, s in SERVERS.items():
    print(f"{name:<8} {s['version']:>7}  {s['transport']:<9}  {', '.join(s['tools']):<23}"
          f" {len(s['resources']):>9} {len(s['prompts']):>7}")
print(f"declared tools {sum(len(s['tools']) for s in SERVERS.values())}"
      f"   allowlist {len(ALLOW)}   client's known version {CLIENT_VERSION}")
```

```
server   version  transport  tools                   resources prompts
olcum          2  local      sayac_oku, tuketim              1       0
tarife         1  remote     tarife                          1       1
kayit          3  remote     bolge_ozet, kayit_yaz           0       0
declared tools 5   allowlist 4   client's known version {'olcum': 2, 'tarife': 1, 'kayit': 2}
```

This table is a **capability declaration**: each server says what it offers. Five tools are
declared, the allowlist recognizes four, and the `kayit` server's version is three while the
client knows two. What is declared and what can be used are not the same thing; the **capability
negotiation** decides the difference.

## Baseline Without the Protocol and Setup With It

Two setups are measured side by side. The `without_protocol` function calls the tools directly:
the definition sits in the same file, so no session is set up and no message is counted. The
`with_protocol` function does the same job through the protocol: first the capability
negotiation, then the three-tool chain. The second run executes the same chain for another
subscriber.

```python
def without_protocol(subscriber):
    """Baseline: the tool is defined in the lesson, no handshake, no envelope is counted."""
    a = t_meter(abone=subscriber)
    m = t_consumption(onceki=a["onceki"], simdiki=a["simdiki"])
    return t_tariff(m3=m["m3"], tip=a["tip"])["tutar"]


def with_protocol(subscriber):
    """Same job, definition on the other side: capability negotiation first, then the chain."""
    for k in COUNT:
        COUNT[k] = 0
    agreed, dropped = handshake()
    hs = dict(COUNT)
    state = {"abone": subscriber}
    for name in ("sayac_oku", "tuketim", "tarife"):
        state.update(call(agreed, name, state))
    return state["tutar"], agreed, dropped, hs, dict(COUNT)


for subscriber in ("A017", "A031"):
    amount, agreed, dropped, hs, total = with_protocol(subscriber)
    print(f"{subscriber}  baseline without protocol : result {without_protocol(subscriber):>4}"
          f"  requests 0  responses 0  envelope tokens 0  round trips 0")
    print(f"{subscriber}  handshake                 : requests {hs['request']}  responses {hs['response']}"
          f"  envelope tokens {hs['tokens']}  round trips {hs['round_trips']}")
    print(f"{subscriber}  three-tool chain          : requests {total['request'] - hs['request']}"
          f"  responses {total['response'] - hs['response']}"
          f"  envelope tokens {total['tokens'] - hs['tokens']}"
          f"  round trips {total['round_trips'] - hs['round_trips']}")
    print(f"{subscriber}  protocol total            : result {amount:>4}  requests {total['request']}"
          f"  responses {total['response']}  envelope tokens {total['tokens']}"
          f"  round trips {total['round_trips']}")
print("agreed tools", sorted(agreed), " dropped", dropped)
print("tool not on the allowlist :", call(agreed, "kayit_yaz",
                                          {"abone": "A017", "sutun": "bedel", "deger": 1}))
```

```
A017  baseline without protocol : result  844  requests 0  responses 0  envelope tokens 0  round trips 0
A017  handshake                 : requests 3  responses 3  envelope tokens 99  round trips 5
A017  three-tool chain          : requests 3  responses 3  envelope tokens 80  round trips 4
A017  protocol total            : result  844  requests 6  responses 6  envelope tokens 179  round trips 9
A031  baseline without protocol : result   89  requests 0  responses 0  envelope tokens 0  round trips 0
A031  handshake                 : requests 3  responses 3  envelope tokens 99  round trips 5
A031  three-tool chain          : requests 3  responses 3  envelope tokens 80  round trips 4
A031  protocol total            : result   89  requests 6  responses 6  envelope tokens 179  round trips 9
agreed tools ['sayac_oku', 'tarife', 'tuketim']  dropped [('kayit', 'version', 3, 2)]
tool not on the allowlist : {'hata': 'arac bildirilmedi'}
```

The setups give **the same result**: 844 for A017, 89 for A031. The computation, the tools'
code and the order did not change. The only change is where the definition sits, and its cost is
written in four columns: **6 requests, 6 responses, 179 envelope tokens and 9 round trips**. At
the baseline without the protocol all four numbers are zero.

The breakdown of the cost is the opposite of what one would expect. The three calls that do the
work cost **80 tokens**, the handshake that sets up the connection **99 tokens**; setting up the
connection is **19 tokens more expensive** than doing the work, and five of the nine round trips
are spent there. The reason is that the handshake is done **with each server separately**: the
chain uses three tools, and the negotiation talks to three servers.

The two runs give the same numbers. This is no coincidence but a consequence of what the
envelope measures: the message body changes between A017 and A031 while the token count stays
the same, because what is counted is not the data but **the shape of the contract**. The count of
a tool connection is the number of contracts, not the number of tools; this is the rule of the
course, and it holds across eleven lessons.

The last two lines are the first loss. After negotiation the agreed set is **three tools**,
while five were declared. The `dropped` list has a single row: the `kayit` server declares
version three, the client knows two, and the **whole** server drops. This is paid for twice.
First, when `kayit_yaz` is called, the tool is treated as if it never existed. Second, and more
silently, the handshake's **2 messages and 2 round trips** went to a server that gained no tool.
The negotiation cost is paid even when the negotiation fails.

## The Count of Portability

What the cost buys is portability, and that can be counted too. The measure: **in how many
places** the same five tool definitions are written, and **how many places break** when the
interface changes.

```python
def definition_text(s):
    return " ".join(f"{a} ( {' , '.join(t['fields'])} )" for a, t in s["tools"].items())


DEFINITION_LINES = sum(len(s["tools"]) for s in SERVERS.values())
DEFINITION_TOKENS = sum(len(tokenize(definition_text(s))) for s in SERVERS.values())
for k in COUNT:
    COUNT[k] = 0
handshake()
HS_MESSAGES, HS_TOKENS = COUNT["request"] + COUNT["response"], COUNT["tokens"]

print(f"five tool definitions {DEFINITION_LINES} lines, {DEFINITION_TOKENS} tokens;"
      f" one session's handshake {HS_MESSAGES} messages, {HS_TOKENS} envelope tokens")
print("clients   without protocol: definitions  breaks   with protocol: definitions  breaks"
      "  messages  envelope tokens")
for k in (1, 2, 3, 4):
    print(f"{k:>7}   {k * DEFINITION_LINES:>29}  {k:>6}   {DEFINITION_LINES:>26}  {0:>6}"
          f"  {k * HS_MESSAGES:>8}  {k * HS_TOKENS:>15}")
```

```
five tool definitions 5 lines, 35 tokens; one session's handshake 6 messages, 99 envelope tokens
clients   without protocol: definitions  breaks   with protocol: definitions  breaks  messages  envelope tokens
      1                               5       1                            5       0         6               99
      2                              10       2                            5       0        12              198
      3                              15       3                            5       0        18              297
      4                              20       4                            5       0        24              396
```

With a single client the protocol **gains nothing**: the definition is five lines in both
setups, and one place breaks in both, one in the client, the other in the server. The only
additions are six messages and ninety-nine tokens. If a tool will be used in a single
application, the protocol is pure cost.

The difference opens up with the number of clients. With four clients the setup without the
protocol writes **20 tool definitions** and breaks **four places** when the field names of the
`tuketim` tool change; with the protocol the definition stays in **five lines and a single
place**, and there is nothing to break in the clients. In return the four clients pay **24
messages and 396 envelope tokens**: portability has a fixed per-session cost, and that cost
grows linearly with the number of clients.

A zero in the "breaks" column **does not mean no errors**. The version skew of the
`kayit` server showed this: there was no line to fix in the client, and two tools vanished
anyway. An interface for which it is not written who breaks when it changes counts as
unmeasured; the protocol only moves that question to **a single place**.

## Summary

- The Model Context Protocol defines three roles: the host application holds the context, the
  allowlist and the approval; the client sets up one session per server and wraps the messages in
  envelopes; the server hosts the tool definitions and their handlers.
- Three fictional servers declare five tools, the allowlist carries four, and after capability
  negotiation the agreed set stays at three; the `kayit` server drops entirely through version
  skew.
- The same three-tool chain gives 844 at the baseline without the protocol with zero messages;
  with the protocol the same result costs 6 requests, 6 responses, 179 envelope tokens and 9
  round trips.
- Setting up the connection costs more than doing the work: the handshake is 99 tokens, the chain
  80; the handshake's 2 messages and 2 round trips go to a server that gains no tool.
- The runs of two subscribers give the same envelope count; what is counted is the shape of the
  contract, not the data.
- Portability is a count: across four clients the setup without the protocol writes 20
  definitions and breaks four places, while the setup with the protocol keeps 5 definitions in
  one place for 6 messages and 99 envelope tokens per client.

## Next Step

This lesson named the three roles and counted the messages between them, but did not lay out
which item sits in which role. Does the allowlist sit in the host application or the client, what
is the single right place for the tool definition, who holds the session, who asks for approval?
The answer is a cost, not a preference: when an item sits in the wrong role, the two sides
must explain it to each other, and every explanation is a message. The next lesson distributes
ten items across the three roles and counts how many messages each item crossing a role boundary
adds.
