---
title: 'Writing a Server'
source: 'https://academia.sh/en/courses/the-model-context-protocol-and-tool-ecosystem/writing-a-server'
course: 'The Model Context Protocol and Tool Ecosystem'
language: en
updated: '2026-10-21T05:02:34+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Separates a server's handlers from its capability declaration and produces the declaration at four levels."
    - 'Counts the negotiated tools, failed calls and completed tasks at each declaration level.'
    - 'Shows with numbers that a complete declaration narrows the tool set.'
    - 'Measures that an undeclared side effect cannot be stopped on the client side.'
---

# Writing a Server

Separating a server's handlers from its capability declaration and measuring the completeness of the declaration as callability: when the same three fictional servers and the same twelve tasks are run at four declaration levels, the cost of the handshake is 3 requests, 3 responses, 99 tokens and 5 round trips at all four levels, and the only thing that changes is what those three responses leave in the client's hands. At the level that declares only names, the negotiated tools number four but, because the client cannot fill in fields, 12 of 12 calls fail and the completed tasks are zero. Once fields are declared, calls rise to 30 and completed tasks to 9. Declaring the side effect moves no number on this allowlist. Declaring the version drops the negotiated tools from 4 to 3, lowers the completed tasks to 6 and pulls the envelope tokens from 816 to 744: the most complete declaration gives the smallest tool set. When the allowlist is widened to accept kayit_yaz, three write calls whose side effect is undeclared pass silently; when it is declared, the same three calls are stopped at the approval gate.

In the previous five lessons the Model Context Protocol was explained from the outside: the responsibility of the three roles was separated, the round trips of the transport were counted, the message types of the data layer were measured, and it was seen that of the five tools a server declared only three were negotiated. In all those measurements the server was something **given**. None of it was written.

This lesson writes the server. What gets written has two parts, and the distinction between them carries the rest of the course's measurements. The **handler** is the function that serves an incoming request and returns the result; it lives inside the server and the client never sees it. The **capability declaration** is the definition of that handler given to the outside, and it is the only thing the client sees. What a tool definition contributes was measured in the AI Agents course: with a definition given by name alone the valid call rate was 0.2803, with a schema 0.8919. That measurement questioned the **content** of the definition, and the definition was at hand. Here the definition is on the other side, and the question is its **completeness**: even though the handler stands in place, a field that is not declared does not exist in the client's hands at all.

- **IM1.** The environment, the three servers and the protocol simulator in the lesson 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**.
- **IM2.** At all four levels the handlers are **the same**; only the declaration changes: name only; name and fields; name, fields and side effect; full declaration (version added).
- **IM3.** The client uses only what is declared. If a field is not declared, the call goes out **without fields**.
- **IM4.** The envelope counts every request and response; its constant cost is 8 tokens, and on remote transport the round trip is doubled.
- **IM5.** The allowlist looks at the **name** and carries four tools; `kayit_yaz` (write record) is not on the list. The client knows version 2 of the `kayit` (records) server, not version 3.
- **IM6.** The task set is 12 fictional tasks: three of each of four types, each solved with a known tool chain. If a call returns an error, the chain is cut at that point.
- **IM7.** A completed task is when the result left by the chain equals the correct answer.
- **IM8.** A side-effecting tool asks for approval if its side effect is **declared**; if it is not declared, the client cannot build the approval gate.
- **IM9.** The baseline is the setup without the protocol: the tool defined inside the lesson, no handshake, the envelope not counted.
- **IM10.** The set is small: three servers, five declared tools. So **counts** are printed, not rates; the smallest step this set can measure is 1/5, that is, 0.2000.

## Handler and Declaration

The code below sets up the fictional environment, the three fictional servers and the envelope count, then adds two functions that separate the declaration from the handler: `declare` produces the definition the server gives to the outside at one level, and `negotiate` carries out capability negotiation with that definition.

```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 holds handlers and 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


# ---- Declaration levels: what the definition the server gives to the outside carries
LEVELS = ("name", "name + fields", "name + fields + side effect", "full")


def declare(s, level):
    """The server's declaration; the level decides which face of the handler is exposed."""
    b = {"tools": {}}
    for name, t in s["tools"].items():
        decl = {}
        if level >= 1:
            decl["fields"] = t["fields"]
        if level >= 2:
            decl["side_effect"] = t["side_effect"]
        b["tools"][name] = decl
    if level >= 3:
        b["version"] = s["version"]
    return b


def negotiate(level, allow=ALLOW):
    """Capability negotiation uses only what is declared: what is not declared cannot be negotiated."""
    agreed, dropped = {}, []
    for name, s in SERVERS.items():
        b = declare(s, level)
        message("request", f"yetenek sor {name}", s["transport"])
        message("response", f"yetenek bildir {name} surum {s['version']} "
                            + " ".join(s["tools"]) + " " + " ".join(s["resources"]))
        if "version" in b and CLIENT_VERSION.get(name, 0) < b["version"]:
            dropped += [(t, "version") for t in b["tools"]]
            continue
        for t, decl in b["tools"].items():
            if t not in allow:
                dropped.append((t, "allowlist"))
                continue
            agreed[t] = dict(decl, function=s["tools"][t]["function"], transport=s["transport"])
    return agreed, dropped


def call(agreed, name, state, approval=False):
    """The client fills in only the declared fields; a declared side effect asks for approval."""
    if name not in agreed:
        return {"hata": "arac bildirilmedi"}
    t = agreed[name]
    if t.get("side_effect") and not approval:
        return {"hata": "onay yok"}
    kw = {a: state[a] for a in t.get("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("level                         declaration of the olcum server ( sayac_oku )")
for level, name in enumerate(LEVELS):
    b = declare(SERVERS["olcum"], level)
    print(f"{level} {name:<27} version {b.get('version', '-')} , {b['tools']['sayac_oku']}")
print()
print("level                         negotiated dropped request response tokens round trips")
for level, name in enumerate(LEVELS):
    for k in COUNT:
        COUNT[k] = 0
    a, ds = negotiate(level)
    print(f"{level} {name:<27} {len(a):>10} {len(ds):>7} {COUNT['request']:>7} {COUNT['response']:>8}"
          f" {COUNT['tokens']:>6} {COUNT['round_trips']:>11}")
print("negotiated at full level :", sorted(negotiate(3)[0]))
```

```
level                         declaration of the olcum server ( sayac_oku )
0 name                        version - , {}
1 name + fields               version - , {'fields': ('abone',)}
2 name + fields + side effect version - , {'fields': ('abone',), 'side_effect': False}
3 full                        version 2 , {'fields': ('abone',), 'side_effect': False}

level                         negotiated dropped request response tokens round trips
0 name                                 4       1       3        3     99           5
1 name + fields                        4       1       3        3     99           5
2 name + fields + side effect          4       1       3        3     99           5
3 full                                 3       2       3        3     99           5
negotiated at full level : ['sayac_oku', 'tarife', 'tuketim']
```

In the four rows of the second table the cost of the handshake does not change: at every level **3 requests, 3 responses, 99 tokens, 5 round trips**. A complete declaration came free at discovery time; the only thing that changed was what those three responses left in the client's hands. At the first three levels the negotiated tools are **four** and the dropped tool is **one**: `kayit_yaz`, because its name is not on the allowlist. At the fourth level, the moment the version is declared, the client can make a comparison: the version it knows is 2, the one the server declares is 3. The whole `kayit` server is dropped and **`bolge_ozet` (region summary) goes with it**, even though it is on the allowlist. The negotiated tools fall from four to **three**. This is the counterpart in this lesson of the course's second claim: **the most complete declaration gave the smallest tool set.** A server that declares less negotiates more tools, because it has said nothing to compare against.

## The Count at Four Levels

The same twelve tasks are run at four levels. The task set has three of each of four types: charge, consumption, region and record. The only variable is the declaration level.

```python
CHAIN = {"charge": ("sayac_oku", "tuketim", "tarife"), "consumption": ("sayac_oku", "tuketim"),
         "region": ("bolge_ozet",), "record": ("sayac_oku", "tuketim", "tarife", "kayit_yaz")}


def tasks(seed=SEED):
    """12 FICTIONAL tasks: three of each of four types, subscriber and region picked by the seed."""
    g = []
    for i in range(12):
        r = rng(seed + 9973 * i)
        g.append({"type": list(CHAIN)[i % 4], "subscriber": f"A{1 + int(r() * 60):03d}",
                  "region": REGION[int(r() * 5)]})
    return g


def expected(g):
    a = SUBSCRIBERS[g["subscriber"]]
    f = a["simdiki"] - a["onceki"]
    m3 = f + 100000 if f < 0 else f
    if g["type"] == "consumption":
        return m3
    if g["type"] == "region":
        return sum(1 for x in SUBSCRIBERS.values() if x["bolge"] == g["region"])
    return True if g["type"] == "record" else _charge(m3, a["tip"])


def run(level, allow=ALLOW, approval=False, seed=SEED):
    """The same 12 tasks, one variable: the declaration level."""
    for k in COUNT:
        COUNT[k] = 0
    agreed = negotiate(level, allow)[0]
    s = {"calls": 0, "failed": 0, "completed": 0, "blocked": 0, "written": 0}
    for g in tasks(seed):
        state = {"abone": g["subscriber"], "bolge": g["region"], "sutun": "bedel"}
        result = None
        for name in CHAIN[g["type"]]:
            state["deger"] = result
            y = call(agreed, name, state, approval)
            s["calls"] += 1
            if "hata" in y:
                s["failed"] += 1
                s["blocked"] += y["hata"] == "onay yok"
                break
            state.update(y)
            s["written"] += y.get("yazildi") is True
            result = y.get("yazildi", y.get("tutar", y.get("m3", y.get("abone", result))))
        s["completed"] += result == expected(g)
    return dict(agreed=len(agreed), **s, **COUNT)


print("level                         negotiated calls failed completed request response tokens")
for level, name in enumerate(LEVELS):
    a = run(level)
    print(f"{level} {name:<27} {a['agreed']:>10} {a['calls']:>5} {a['failed']:>6}"
          f" {a['completed']:>9} {a['request']:>7} {a['response']:>8} {a['tokens']:>6}")
second = [run(level, seed=SEED + 1) for level in range(4)]
print("second run ( task seed 20260219 ) completed :", [x["completed"] for x in second],
      ", tokens :", [x["tokens"] for x in second])
print()
WIDE = ALLOW + ("kayit_yaz",)
print("wide allowlist ( kayit_yaz added ) negotiated blocked written completed")
for level, approval in ((1, False), (2, False), (2, True)):
    a = run(level, allow=WIDE, approval=approval)
    print(f"level {level} , approval {str(approval).lower():<5}           {a['agreed']:>10}"
          f" {a['blocked']:>7} {a['written']:>7} {a['completed']:>9}")
```

```
level                         negotiated calls failed completed request response tokens
0 name                                 4    12     12         0      15       15    351
1 name + fields                        4    30      3         9      30       30    816
2 name + fields + side effect          4    30      3         9      30       30    816
3 full                                 3    30      6         6      27       27    744
second run ( task seed 20260219 ) completed : [0, 9, 9, 6] , tokens : [351, 818, 818, 744]

wide allowlist ( kayit_yaz added ) negotiated blocked written completed
level 1 , approval false                    5       0       3        12
level 2 , approval false                    5       3       0         9
level 2 , approval true                     5       0       3        12
```

In the first row the negotiated tools are four, so the client **knows** which tools exist; the handlers stand in place too. Even so, the completed tasks are **zero**. Because the client does not know which field to fill in, every call goes out without fields, `sayac_oku` returns `abone yok` (no such subscriber), and the chain is cut at the first step. Out of a total chain length of thirty steps, only **12 calls** can be made, all of them fail, and the run spends **351 tokens**. The cheapest run is the run that finishes no work.

In the second row the fields are declared and the chains work: calls rise to 30, completed tasks to **nine**. The three remaining failed calls are the last step of the record tasks; because `kayit_yaz` is not on the allowlist it returns `arac bildirilmedi` (tool not declared), and these three calls **spend no messages at all**: requests are 30, that is, 3 handshakes plus 27 calls sent. A call made to a tool that was not negotiated ends inside the client and never reaches the wire.

The third row is **identical** to the second. Declaring the side effect moved not a single number on this allowlist, because the only side-effecting tool was not on the list in the first place. A declaration that has not been tested has no measurable value. The fourth row is cheaper and worse: the envelope tokens fall from 816 to **744**, the completed tasks from nine to **six**. The three region tasks receive `arac bildirilmedi` in a single step because `bolge_ozet` was dropped.

In the second run the task set is drawn with a different seed, so different subscribers and different regions come up; the completed tasks stay the same at all four levels. The envelope tokens move from 816 to 818 at only two levels, because a region name that enters a body is one token longer. What is counted is a property of the **contract**, not of the data.

## The Undeclared Side Effect

The last three rows explain why the third level looked pointless. When `kayit_yaz` is added to the allowlist, the negotiated tools rise to five. While the side effect is undeclared (the first level), three write calls pass and all twelve tasks are completed; the client has **no field at all** that says these three calls differ from the others.

When the side effect is declared (the second level), the same three calls stop at the approval gate: **blocked 3**, written 0, completed 9. When approval is given, all three are written again. Whether approval will be given is the client's permission model and was measured in the AI Agents course; what is measured here is its **precondition**: an undeclared side effect cannot be stopped, because there is no question to ask. The three blocked calls are counted among the twelve tasks; this set is too small to print a rate.

## Summary

- The two faces of a server are separated: the handler stays inside and is never seen, and the capability declaration is the only thing in the client's hands.
- The cost of the handshake is the same at all four declaration levels: 3 requests, 3 responses, 99 tokens, 5 round trips; completeness is free at discovery time.
- At the level that declares only names, the negotiated tools are four but the completed tasks are zero: because all 12 calls go out without fields, 12 of 12 fail, and at 351 tokens the run is the cheapest.
- Once fields are declared, calls rise to 30 and completed tasks to 9; three calls made to a tool that was not negotiated spend no messages.
- Once the version is declared, the negotiated tools fall from 4 to 3 and the completed tasks from 9 to 6: the most complete declaration gives the smallest tool set.
- While the side effect is undeclared, three write calls pass silently; once it is declared, they are stopped.

## Next Step

In this lesson the server was written and the incompleteness of its declaration was counted as callability. The other side was assumed ready: that the handshake had already been made, that the negotiated set stood somewhere, and who asked when were never questioned. The next lesson writes the client. It measures what server discovery costs, the difference between renewing the same discovery at every task and paying for it once per session, and that the dropping of the `kayit` server **produces no error at all**: the discovery messages do not tell the dropped server from the one that was not dropped, and the only thing that makes the difference visible is counting.
