---
title: 'Data Layer'
source: 'https://academia.sh/en/courses/the-model-context-protocol-and-tool-ecosystem/data-layer'
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:
    - 'Separates request, response and notification messages on the axes of expecting a response and round trip cost.'
    - 'Compares the messages, envelope tokens and round trips the same batch pays without and with the protocol.'
    - 'Calculates the number of messages and round trips the notification saves by being responseless.'
    - 'Shows why a lost notification leaves no trace on the sender.'
---

# Data Layer

Counting the cost of request, response and notification messages: an eight-subscriber batch is 24 function calls and zero messages without the protocol, and 48 messages, 640 tokens and 32 round trips with it, 384 of the 640 tokens being envelope alone. The same progress information costs 1147 tokens and 80 round trips as request-response pairs and 976 tokens and 32 round trips as notifications. Its price is that five notifications dropped on the line leave no trace on the sender.

The previous three lessons measured **how the connection is established**: tying the tool
connection to a specification, the distribution of responsibility among host application, client
and server, the round trip difference between local and remote transport. In all three, what was
counted was the connection itself. **What passed** over it was never opened.

This lesson fills that gap. The Model Context Protocol's **data layer** sits on top of the
connection and recognizes only three message kinds: **request**, **response** and
**notification**. What will be measured is **what each of these three costs** — how many messages,
how many envelope tokens, how many round trips — and what one of them, the notification, gains and
makes invisible by being **responseless**.

- **PS31.** The environment, the three servers, the tools and the envelope count are
  **fictional**; no real server, client library or runtime is called. 60 fictional subscribers,
  three fictional servers, five declared tools. Seed **20260218**, second run **20260219**.
- **PS32.** There are three message kinds and every message carries an **envelope**. The envelope's
  fixed cost is **8 tokens**; the body is the field names and values passed through the fictional
  tokenizer.
- **PS33.** A **request** awaits a response and spends one round trip: one on local transport, two
  on remote transport. A **notification** awaits no response and spends no round trip on the
  counter.
- **PS34.** A **response** is not sent on its own, it is the answer to a request; it stands as a
  separate kind on the counter because its envelope is paid separately.
- **PS35.** This is the **baseline without the protocol**: the tool is defined inside the lesson,
  the call is a direct function call, there are no messages, no envelope is counted.
- **PS36.** The batch measured is the period charge of eight fictional subscribers; each
  subscriber goes through `sayac_oku` (read meter), `tuketim` (consumption) and `tarife` (tariff)
  in order.
- **PS37.** The fictional transport line drops a share of the messages. The loss share is taken as
  **0.20** and kept high so that the count is visible; the dropping decision is determined by the
  seed.

## The Three Message Kinds

The difference between a request and a notification is not a difference of form, it is a
difference of **expectation**. The sending side of a request expects a response in return; until
that response arrives the work is half done, and the waiting side pays the round trip. The sending
side of a notification expects nothing: the message is left on the line and the work goes on. The
response is not an independent kind between the two — it is not sent on its own, but since its
envelope is paid separately it stands apart on the counter.

The contract, versioning and error response of an interface were measured in the Web API Design
course; that procedure is not repeated here. This lesson counts not the **content** of the
contract but its **carrying cost**: into how many messages the same job is split and how many
tokens each split adds. The code below sets up the fictional environment, the three
servers and the envelope counter.

```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 and tools (same setup as the AI Agents course, moved into a server here)
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


agreed, dropped = handshake()
print("agreed tools :", " ".join(sorted(agreed)))
print("handshake    :", COUNT["request"], "requests,", COUNT["response"], "responses,",
      COUNT["tokens"], "tokens,", COUNT["round_trips"], "round trips")
```

```
agreed tools : sayac_oku tarife tuketim
handshake    : 3 requests, 3 responses, 99 tokens, 5 round trips
```

## The Cost of the Envelope

The envelope counter knows how to count by kind but does not separate the body from the envelope.
For measurement a thin wrapper is placed around the message: the wrapper writes the kind and the
body tokens into its own breakdown, then hands the job to the real counter. The eight-subscriber
batch is run with this breakdown.

```python
BATCH = ("A003", "A011", "A017", "A024", "A038", "A045", "A052", "A059")

# ---- measurement wrapper: body tokens per kind, on top of the same envelope counter
BREAKDOWN = {k: [0, 0] for k in ("request", "response", "notification")}
_envelope = message


def message(kind, body, transport="local"):
    BREAKDOWN[kind][0] += 1
    BREAKDOWN[kind][1] += len(tokenize(body))
    return _envelope(kind, body, transport)


def reset():
    for k in COUNT:
        COUNT[k] = 0
    for k in BREAKDOWN:
        BREAKDOWN[k] = [0, 0]


def chain(agreed, subscriber, progress=None):
    """Three-tool chain; after each step an optional progress message."""
    state = {"abone": subscriber}
    for name in ("sayac_oku", "tuketim", "tarife"):
        s = call(agreed, name, state)
        state.update({k: v for k, v in s.items() if k != "hata"})
        if progress is not None:
            progress(subscriber, name)
    return state.get("tutar")


agreed, _ = handshake()
reset()
print("eight-subscriber batch, amounts:", [chain(agreed, a) for a in BATCH])
print("kind         count  body envelope  total")
for k in BREAKDOWN:
    n, g = BREAKDOWN[k]
    print(f"{k:<12} {n:>5} {g:>5} {n * 8:>8} {g + n * 8:>6}")
print(f"with protocol    : {sum(BREAKDOWN[k][0] for k in BREAKDOWN)} messages,"
      f" {COUNT['tokens']} tokens, {COUNT['round_trips']} round trips")
print("without protocol : 24 function calls, 0 messages, 0 envelope tokens, 0 round trips")
```

```
eight-subscriber batch, amounts: [211, 16, 844, 710, 812, 876, 694, 470]
kind         count  body envelope  total
request         24   152      192    344
response        24   104      192    296
notification     0     0        0      0
with protocol    : 48 messages, 640 tokens, 32 round trips
without protocol : 24 function calls, 0 messages, 0 envelope tokens, 0 round trips
```

The last two lines are the lesson's first number. The same eight amounts come out at the baseline
without the protocol with **24 function calls**: no messages, no envelope, no round trips. With the
protocol the same job is split into **48 messages** and costs **640 tokens** and **32 round
trips**. **384** of the tokens are envelope alone — 192 in the 24 requests, 192 in the 24
responses — and the remaining 256 tokens are body. The envelope takes **one and a half times**
as much room as the information carried.

The shape of the split shows in the numbers too. The request body is 152 tokens, the response body
104; the request is more expensive because it carries the tool name and all fields as name and
value pairs, while the response carries only the result. The round trips are 32, not 24: the first
two steps of the eight chains go to the local `olcum` (the measurement server), the last step to a
remote server, and a remote request doubles the round trip. The notification row is zero for now;
the third kind was never used in this batch.

## What the Notification Gains

When a batch runs long, the calling side wants to know where it is. That information can be
carried in two designs. In the first, a **request** is sent at the end of every step and the other
side returns a confirming response. In the second, the same text is sent as a **notification** and
no response is awaited. Both designs run on the same fictional line, with the same seed and
dropping decisions.

```python
LOSS_SHARE = 0.20                    # FICTIONAL share of messages that do not reach the line


def line(seed):
    """FICTIONAL transport line: a share of the messages never reaches the other side."""
    r = rng(seed)

    def arrives():
        return r() >= LOSS_SHARE
    return arrives


def measure_progress(form, seed=SEED):
    """The same progress information in two designs: as a request-response pair or as a notification."""
    arrives, arrived = line(seed), [0]

    def report(subscriber, name):
        text = f"ilerleme {subscriber} {name} tamam"
        if form == "request-response":
            message("request", text, "remote")
            if arrives():
                arrived[0] += 1
                message("response", "onay")
        elif form == "notification":
            message("notification", text, "remote")
            if arrives():
                arrived[0] += 1

    agreed, _ = handshake()
    reset()
    for a in BATCH:
        chain(agreed, a, report)
    sent = 0 if form == "none" else 24
    seen = sent - arrived[0] if form == "request-response" else 0
    return (BREAKDOWN["request"][0], BREAKDOWN["response"][0], BREAKDOWN["notification"][0],
            COUNT["tokens"], COUNT["round_trips"], sent, arrived[0], seen)


print("design           run requests responses notifications tokens round trips"
      " sent arrived visible")
for form in ("none", "request-response", "notification"):
    for t in (SEED, SEED + 1):
        s = measure_progress(form, t)
        print(f"{form:<16} {'AB'[t - SEED]:>3} {s[0]:>8} {s[1]:>9} {s[2]:>13} {s[3]:>6}"
              f" {s[4]:>11} {s[5]:>4} {s[6]:>7} {s[7]:>7}")
```

```
design           run requests responses notifications tokens round trips sent arrived visible
none               A       24        24             0    640          32    0       0       0
none               B       24        24             0    640          32    0       0       0
request-response   A       48        43             0   1147          80   24      19       5
request-response   B       48        44             0   1156          80   24      20       4
notification       A       24        24            24    976          32   24      19       0
notification       B       24        24            24    976          32   24      20       0
```

The first two rows are the baseline without progress, the same in both runs. In the first
run the request-response design raises the batch's message count from 48 to **91**, its tokens
from 640 to **1147** and its round trips from 32 to **80**. The notification design carries the
same information with **72 messages**, **976 tokens** and **32 round trips**. The notification's
gain reads in three columns at once: **19 messages**, **171 tokens** and **48 round trips**.

The three numbers have separate sources. The message gain is the responses to the requests that
arrived; in the first run 19 requests arrived, 19 confirmations came back and in the notification
design those 19 confirmations were never written. The token gain is the envelope and body of
those 19 responses, nine tokens per message. The round trip gain is the largest and has nothing to
do with arrival: since all 24 progress messages pass over remote transport, each of them spent
**two round trips** in the request design and **none** in the notification design. When there is
no waiting side, there is nothing awaited on the line either.

## What the Notification Makes Invisible

The last three columns of the same table carry the price. The fictional line passed **19** of the
24 progress messages in the first run and **20** in the second. In the request-response design the
loss the sender sees is exactly this difference: **5** in the first run, **4** in the second. In
the notification design the same line, with the same seed and the same dropping decisions, lost the
same number of messages, but the loss the sender sees is **zero** in both runs.

The difference comes from the response itself. In a lost request the awaited response does not
come and the end of the wait is an event on the sending side — it can be counted, written to a log,
retried. In a lost notification nothing is awaited; the sender's counter says 24, the receiving
side's counter says 19, and the five messages between them **produce an error nowhere**. In a setup
that does not put the two counters side by side, this difference is invisible.

The rule that follows decides the choice of message kind. No information the other side **has to
act on** and the sender **has to know the result of** is carried by a notification: a tool call, a
capability query, a write operation. Information whose loss does not break the job suits a
notification: progress, a log line, an announcement of a status change. The difference between the
two runs confirms this split too — the number of lost messages moves between 5 and 4 from run to
run, but the loss **seen** in the notification design stays zero in both. What changes is the loss,
what does not change is the blindness.

## Summary

- The data layer recognizes three message kinds: a request awaits a response and spends a round
  trip, a response is the answer to a request, a notification awaits no response and spends no
  round trip on the counter.
- The eight-subscriber batch is 24 function calls, zero messages and zero envelope tokens at the
  baseline without the protocol, and 48 messages, 640 tokens and 32 round trips with it.
- 384 of the 640 tokens are envelope, and it takes up one and a half times the room of the 256
  tokens of body carried.
- The same progress information costs 91 messages, 1147 tokens and 80 round trips as
  request-response pairs in the first run, and 72 messages, 976 tokens and 32 round trips as
  notifications. The notification's gain is 19 messages, 171 tokens and 48 round trips.
- The fictional line dropped 5 and 4 messages in the two runs; in the request-response design the
  sender saw all of this loss, in the notification design it saw zero in both runs.
- No information the other side has to act on and whose result has to be known is carried by a
  notification; a notification suits only information whose loss does not break the job.

## Next Step

This lesson counted the cost of messages, but all the messages counted worked on a set of tools
that had already been **agreed**. The single line in the first block's output quietly gives this
away: three tools came out of the handshake, yet the number of tools the three fictional servers
declare is larger. What the server declared, how much of it the client took, and **in which
message with which error** the gap between them arose was never asked. The next lesson measures
server capabilities — tools, resources and prompts — by the gap between declared and agreed, and
counts how that gap arises without producing any error message.
