Skip to content
academia.sh

Lesson 05 / 11

Server Capabilities

Counting the difference between declared and agreed capabilities: three fictional servers declare 5 tools, 2 resources and 1 prompt, the allowlist holds 4 tools, and the handshake yields 3. Both runs pay the same cost — 3 requests, 3 responses, 99 tokens, 5 round trips, 0 errors — but their results diverge: with aligned versions 4 tools are agreed and the allowlist drops kayit_yaz on purpose; with version skew the whole kayit server drops and bolge_ozet, which is on the allowlist, vanishes without any error. If olcum drops, the agreed tools fall to 2. Both losses give the same error text, one deliberate and one an accident.

Published Updated

In this lesson

  • Counts the tools, resources and prompts a server declares as the items of a single capability declaration.
  • Splits the gap between declared tools and the tools agreed in the handshake into version and allowlist filters.
  • Shows that version skew drops a whole server without producing any error.
  • Justifies, with the resolution arithmetic, why a count rather than a ratio is printed for a five-tool set.
Contents

The previous lesson counted the three message kinds of the data layer: request, response and notification. The eight-subscriber batch split into 48 messages and cost 640 tokens, 384 of which were envelope alone. But all of those messages worked on a set of tools that had already been agreed, and how that set came about was never asked.

This lesson builds that set. When a server connects, it declares what it can do; the declaration carries three items: tools, resources and prompts. The client does not take all of what is declared. What will be measured is the difference between declared and agreed, and in how many messages and with which error it arises. The lesson is also read in reverse: in the setup without the protocol all five tools were at hand; the protocol narrows that set, and part of the narrowing is deliberate, part an accident.

  • PS38. The environment, the three servers, the tools and the envelope count are fictional; no real server, client library or runtime is called. Seed 20260218. The handshake has no randomness: all the numbers in this lesson are independent of the run, and the two runs are two client configurations connecting to the same set of servers.
  • PS39. A capability declaration carries three items: a callable tool, a readable resource and a ready-made prompt. In this course only tools are called; resources and prompts are counted as declarations and no access pipeline is built.
  • PS40. Capability negotiation takes place in the handshake: the client queries each server for its capabilities, the server declares them, and the client passes them through two filters.
  • PS41. The first filter is version skew: if the version the client knows is smaller than the server’s version, the whole of that server drops.
  • PS42. The second filter is the allowlist: a tool not on the list is not taken, even if its server passes.
  • PS43. Both filters run after the response of the handshake; the narrowing happens after the cost is paid, and the narrowing produces no error.
  • PS44. This is the baseline without the protocol: all five tools are defined inside the lesson, there is no handshake, no tool drops.
  • PS45. The set is small: three servers, five declared tools, four allowed tools. In this set the smallest step of a ratio is 1/5 = 0.2000; a difference smaller than that is unmeasurable, so a count is printed instead of a ratio.
  • PS46. Capability negotiation is two-directional and the client declares its own capabilities too. This setup models only the server side, because what is measured is how much of what the server declared is taken.

What the Server Declares

A server’s declaration is not only a list of tools. It carries three items, and all three arrive inside the same response. A tool is a callable function; it has a name, fields and side-effect information. A resource is readable content; it is a document, table or list the server opens to the client. A prompt is a ready-made prompt template; the server hands the client a text that knows its own domain. The retrieval pipeline itself was measured in the Embeddings and Retrieval-Augmented Generation course; it is not built here. In this lesson resources and prompts are counted as declaration items, because when a server drops they drop too, and the drop happens in the same silence as the tool’s.

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


print("server  version transport tools resources prompts")
for name, s in SERVERS.items():
    print(f"{name:<7} {s['version']:>7} {s['transport']:<9} {len(s['tools']):>5}"
          f" {len(s['resources']):>9} {len(s['prompts']):>7}")
print(f"declared: {sum(len(s['tools']) for s in SERVERS.values())} tools,"
      f" {sum(len(s['resources']) for s in SERVERS.values())} resources,"
      f" {sum(len(s['prompts']) for s in SERVERS.values())} prompts;"
      f" allowlist {len(ALLOW)} tools")
server  version transport tools resources prompts
olcum         2 local         2         1       0
tarife        1 remote        1         1       1
kayit         3 remote        2         0       0
declared: 5 tools, 2 resources, 1 prompts; allowlist 4 tools

The baseline without the protocol is the whole of this table: had the tools been defined inside the lesson, all five would be at hand. The allowlist approves four. The one tool in between, kayit_yaz (write record), is the only tool with a side effect, and leaving it out is a decision — in the AI Agents course the same list had stopped 20 calls, and that procedure is not repeated here.

Declared and Agreed

The handshake sends each server one request, receives one capability response, and runs the two filters after the response. In the two runs below the only difference is the version the client knows for the kayit server. The versioning and backward compatibility procedure was measured in the Web API Design course; it is not repeated here, only what the skew does to the tool set is counted.

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


ALIGNED = {"olcum": 2, "tarife": 1, "kayit": 3}     # the client also knows the third server's version

for label, ver in (("aligned", ALIGNED), ("skewed", CLIENT_VERSION)):
    reset()
    agreed, dropped = handshake(ver)
    print(f"client knows kayit server version {ver['kayit']}  ({label})")
    print(f"  handshake : {COUNT['request']} requests, {COUNT['response']} responses,"
          f" {COUNT['tokens']} tokens, {COUNT['round_trips']} round trips, 0 errors")
    print(f"  agreed {len(agreed)} : {' '.join(sorted(agreed))}")
    for server_name, kind, a, b in dropped:
        reason = f"tool {a} not on the allowlist" if kind == "allowlist" else \
            f"server version {a}, client version {b}"
        print(f"  dropped   : {server_name} server, {reason}")
client knows kayit server version 3  (aligned)
  handshake : 3 requests, 3 responses, 99 tokens, 5 round trips, 0 errors
  agreed 4 : bolge_ozet sayac_oku tarife tuketim
  dropped   : kayit server, tool kayit_yaz not on the allowlist
client knows kayit server version 2  (skewed)
  handshake : 3 requests, 3 responses, 99 tokens, 5 round trips, 0 errors
  agreed 3 : sayac_oku tarife tuketim
  dropped   : kayit server, server version 3, client version 2

The cost of the two runs is exactly the same: 3 requests, 3 responses, 99 tokens, 5 round trips and zero errors in both runs. Their results diverge. With aligned versions the agreed tools are four, with skew three. The tool that vanished is bolge_ozet (region summary), and that tool is on the allowlist — the client explicitly approved using it. It was not taken because its server’s version did not match.

The two dropped lines stand in the same set but they are not the same thing. The kayit_yaz drop is deliberate: the tool with a side effect was not put on the allowlist and the protocol enforced that decision. The bolge_ozet drop is an accident: nobody decided to leave this tool out, the version number the client knew was old. Both happened in the same silence; the handshake reported zero errors for both. This is the second example of the course’s second claim: standardizing also breaks. In the setup without the protocol five tools were at hand, in the setup with the protocol three remained, and one of the two that dropped is an unwanted loss.

What Goes When a Server Drops

The skew drops not a tool but a server. What is measured is what goes when each server’s version is known one short. The same call is tried in both configurations; the error text is the same.

print("dropped server lost tools resources prompts agreed tools left")
for name, s in SERVERS.items():
    agreed, _ = handshake(dict(ALIGNED, **{name: s["version"] - 1}))
    print(f"{name:<14} {len(s['tools']):>10} {len(s['resources']):>9} {len(s['prompts']):>7}"
          f" {len(agreed):>17}")

print("\nsame error text, two different causes:")
for label, ver in (("aligned", ALIGNED), ("skewed ", CLIENT_VERSION)):
    agreed, _ = handshake(ver)
    print(f"  {label}  bolge_ozet -> {call(agreed, 'bolge_ozet', {'bolge': 'dogu'})}")
    print(f"  {label}  kayit_yaz  -> "
          f"{call(agreed, 'kayit_yaz', {'abone': 'A017', 'sutun': 'bedel', 'deger': 844})}")
dropped server lost tools resources prompts agreed tools left
olcum                   2         1       0                 2
tarife                  1         1       1                 3
kayit                   2         0       0                 3

same error text, two different causes:
  aligned  bolge_ozet -> {'bolge': 'dogu', 'abone': 11}
  aligned  kayit_yaz  -> {'hata': 'arac bildirilmedi'}
  skewed   bolge_ozet -> {'hata': 'arac bildirilmedi'}
  skewed   kayit_yaz  -> {'hata': 'arac bildirilmedi'}

The table shows the unit of the drop. When a server’s version does not match, not only one tool but the server’s whole declaration goes: if olcum drops, two tools and one resource; if tarife drops, one tool, one resource and one prompt. Resources and prompts are not called in this course, but they stand in the count because the lost capability is not only the callable kind. The harshest row is the first: olcum is the server that runs on local transport and is used the most, and when it drops the agreed tools fall from four to two.

The last four lines show how the silence looks. With aligned versions the bolge_ozet call returns 11 subscribers in the east region. In the skewed configuration the same call, the same code and the same client returns {'hata': 'arac bildirilmedi'} — the same text the kayit_yaz call gives. A deliberate drop and an accidental drop are indistinguishable at call time; the only thing that distinguishes them is the handshake’s dropped list, and that list becomes visible only if it is counted.

The form of the number is also a decision. The three agreed tools are 0.6000 of the five declared, but the smallest step of a ratio in this set is 1/5 = 0.2000: a single tool coming and going moves the ratio in steps of two hundred units, and no difference smaller than that can be measured. That is why this lesson prints a count, not a ratio — five declared, four allowed, three agreed, one deliberate drop, one accidental drop.

Whom this count falls to is also a decision. The server knows what it declared, the client knows what it took; if no party puts the two side by side, the difference is recorded nowhere. The course’s rule reads here: the number of a tool connection is not the number of tools but the number of contracts, and an interface that does not write down who will break when it changes is unmeasured. In this lesson the party that breaks is the client, the thing that breaks it is the server’s version number, and there is no error message in between. The only remedy the client has is to count both ends of the handshake: how many tools were declared, how many were agreed, and which of the drops was a permission decision and which a version skew.

Summary

  • A capability declaration carries three items: a callable tool, a readable resource and a ready-made prompt; all three arrive in the same response, and when a server drops, all three go together.
  • The three fictional servers declare 5 tools, 2 resources and 1 prompt; the allowlist approves 4 tools and 3 tools come out of the handshake.
  • The two configurations pay the same cost — 3 requests, 3 responses, 99 tokens, 5 round trips and 0 errors — but with aligned versions 4 tools are agreed, with skew 3.
  • Version skew drops not a tool but a server: bolge_ozet, which is on the allowlist, vanishes without any error; had olcum dropped, the agreed tools would have fallen to 2.
  • Of the two tools that dropped, one is deliberate and one an accident, and at call time both give the same error text; only the handshake’s dropped list shows the distinction.
  • In a five-tool set the smallest step of a ratio is 0.2000; this is why a count is printed rather than a ratio.

Next Step

In five lessons the Model Context Protocol was described from the outside. What standardization gains, the responsibilities of the three roles, the round trip difference of the transport layer, the three message kinds of the data layer and the capabilities the server declares were measured; next to each measurement the baseline without the protocol and the cost were written. But all of these numbers were read on a setup that had been built ready-made: no server and no client was written. Which handlers must be written for a server to really declare its capabilities, how a client discovers a server and opens a session, and on which line of the client code the silent drop counted in this lesson arises were never asked. The next topic moves from describing the protocol to implementing it, and writes the server side first.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close