Skip to content
academia.sh

Lesson 01 / 11

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.

Published Updated

In this lesson

  • 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.
Contents

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.20001/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.

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.

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.

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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close