---
title: 'Server Capabilities'
source: 'https://academia.sh/en/courses/the-model-context-protocol-and-tool-ecosystem/server-capabilities'
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:
    - '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.'
---

# 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.

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.

```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


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.

```python
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.

```python
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.
