Skip to main content
Glama

Zwei MCP-Server, ein Produkt, zwei Protokollrevisionen

Derselbe Warenkorb, zweimal implementiert: einmal auf der alten zustandsbehafteten MCP-Spezifikation (2025-11-25), einmal auf der neuen zustandslosen (2026-07-28). Führe sie nebeneinander aus und beobachte, wie einer von ihnen umkippt.

Das Ziel ist nicht funktionierender Code – es geht darum zu verstehen, warum sich die Spezifikation geändert hat. Jedes Experiment hier ist so gestaltet, dass der Fehler laut ist und der Grund auf der Leitung sichtbar wird.

Was ist MCP? (drei Sätze)

MCP – das Model Context Protocol – ist ein Standardweg, damit eine KI-Anwendung Werkzeuge aufrufen kann, die jemand anderes geschrieben hat. Es ersetzt N Anwendungen × M Integrationen durch N + M, so wie das Language Server Protocol ersetzt hat, dass jeder Editor seinen eigenen TypeScript-Support schrieb. Konkret sind es JSON-RPC-2.0-Nachrichten mit vereinbarten Methodennamen, gesendet über stdio oder HTTP.

Längere Version, falls das zu schnell vorbeiging: docs/01 – warum MCP existiert.

Related MCP server: Online Boutique AI Assistant MCP Server

Was dieses Repo demonstriert

Fünf Werkzeuge – catalog_list, cart_create, cart_add_item, cart_view, cart_checkout – mit identischen Namen in beiden Servern und identischer Geschäftslogik in einem gemeinsamen cart-core-Paket, das nichts von MCP weiß. Der einzige Unterschied zwischen den beiden Servern ist die Protokollebene, die genau das ist, was untersucht wird.

Vier Dinge, die du beobachten kannst:

  1. server-old kann seinen eigenen Handshake hinter einem einfachen Round-Robin-Lastverteiler nicht abschließen. server-new bemerkt nicht, dass der Lastverteiler existiert.

  2. Starte server-old mitten im Gespräch neu und der Warenkorb ist weg, dauerhaft, mit keiner Anfrage, die der Client senden kann, um ihn wiederherzustellen.

  3. „Diesen Betrag bestätigen?“ kostet server-old einen offen gehaltenen Socket für die gesamte menschliche Denkzeit (1522ms gemessen). server-new erledigt das in zwei unabhängigen Anfragen, 4ms + 15ms, und kann auf einer anderen Maschine fertig werden als auf der, auf der es begonnen hat.

  4. Stabile Listenreihenfolge und Cache-Hinweise – und die Rechnung, die zeigt, warum ein fehlendes .sort() etwa 4.200 $/Jahr wert ist.

Die Server sind bewusst nicht umgestaltet, um Protokollcode zu teilen. Es gibt Duplikation zwischen ihnen mit Absicht, damit du jeden direkt durchlesen und diffen kannst.

Stelle sicher, dass du die Leitung siehst

Beide Server drucken jede Anfrage auf HTTP-Ebene: Methode, Pfad, alle MCP-Header, die JSON-RPC-Methode und -Parameter, welche Instanz sie bearbeitet hat, und die Antwort einschließlich resultType. Nichts ist hinter SDK-Abstraktionen versteckt. Wenn du während eines Experiments nur eine Sache liest, lies die farbigen Logzeilen.

Voraussetzungen

  • Node.js 20 oder neuer (entwickelt auf 25.5). node -v zum Prüfen.

  • Ein Terminal, das ANSI-Farben darstellt – die Logs stützen sich stark darauf.

  • Ports 3000–3002, 3011, 3012 frei.

  • Keine Datenbank, kein Docker, kein Cloud-Konto. Gemeinsamer Zustand ist eine JSON-Datei.

Null Python irgendwo in diesem Repo.

Installation

git clone <this repo>
cd mcp-server
npm install
npm run typecheck    # should print nothing and exit 0

npm install richtet einen npm-Workspace ein, der zwei Generationen des MCP-SDK gleichzeitig enthält. Sie haben unterschiedliche Paketnamen, koexistieren also ohne Aliasing-Tricks:

Paket

Version

Verwendet von

@modelcontextprotocol/sdk

1.30.0

server-old, der alte Client

@modelcontextprotocol/{core,server,client,node}

2.0.0

server-new, der neue Client

Die vier Experimente, in Reihenfolge

Jedes ist ein Befehl. Jedes startet und stoppt seine eigenen Server – kein zweites Terminal nötig. Lies die verlinkte Ausarbeitung nach dem Ausführen; jedes erklärt, was du gerade gesehen hast und warum.

Reihenfolge

Befehl

Was es lehrt

1

npm run exp:01

Zwei Instanzen hinter einem Lastverteiler – der alte Server schafft es nicht einmal, Hallo zu sagen; der neue ist unbeeindruckt. Beginne hier.

2

npm run exp:02

Neustart mitten im Gespräch – wo der Warenkorb tatsächlich lebte und warum „einfach Redis hinzufügen“ nur halb funktioniert.

3

npm run exp:03

Vor dem Checkout bestätigen – 1522ms gehaltener Socket vs. zwei 4ms-Anfragen und warum der alte Weg nie serverless laufen kann.

4

npm run exp:04

Cache-Hinweise und stabile Reihenfolge – Cache-Treffer per Zähler statt Stoppuhr beweisen und das Geldargument für .sort().

Lies dann die Architekturdokumente, die die vier verbinden:

Manuell steuern

Es lohnt sich, mindestens einmal zu tun, weil du das Tempo wählst und jede Logzeile lesen kannst, während sie erscheint.

# Old server (port 3001)
npm run old:server
npm run client -- --target old --scenario basic
npm run client -- --target old --scenario checkout
npm run client -- --target old --scenario checkout --decline

# New server (port 3002)
npm run new:server
npm run client -- --target new --scenario basic
npm run client -- --target new --scenario checkout
npm run client -- --target new --scenario discover    # server/discover — new spec only

Zwei Instanzen plus ein Lastverteiler, manuell:

PORT=3002 INSTANCE_ID=A npm run new:server
PORT=3012 INSTANCE_ID=B npm run new:server
PORT=3000 TARGETS=http://localhost:3002,http://localhost:3012 npm run lb
npm run client -- --target new --url http://localhost:3000/mcp --scenario basic

Beobachte beide Server-Terminals: Dieselbe Warenkorb-ID erscheint in Anfragen, die von jeder bearbeitet werden, und keiner kümmert sich darum.

Mit curl herumstochern

Der direkteste Weg, zu fühlen, wie selbstbeschreibend eine Anfrage der neuen Spezifikation ist. Starte npm run new:server, dann:

# A complete, valid request — note how much has to be in it
curl -s -X POST http://localhost:3002/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H 'mcp-protocol-version: 2026-07-28' \
  -H 'mcp-method: tools/call' \
  -H 'mcp-name: catalog_list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
        "name":"catalog_list","arguments":{},
        "_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28",
                 "io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1"},
                 "io.modelcontextprotocol/clientCapabilities":{}}}}'

Jetzt brich es Stück für Stück und beobachte, wie sich der Fehler ändert:

Änderung

Erwartet

-H 'mcp-method: tools/list' (Body bleibt tools/call)

-32020 HeaderMismatch

-H 'mcp-name: cart_view'

-32020, benennt die Abweichung

den mcp-method-Header weglassen

-32020, „der erforderliche Mcp-Method-Header fehlt“

den _meta-Block weglassen

-32602, listet die fehlenden Envelope-Schlüssel

mcp-protocol-version: 2099-01-01 in Header und _meta

-32022 Nicht unterstützte Protokollversion

curl http://localhost:3002/mcp (ein GET)

405 – der GET-Endpunkt ist weg

Mach dasselbe gegen den alten Server und dir wird gesagt, du sollst zuerst initialize aufrufen.

Repo-Struktur

packages/
  cart-core/     the actual product. zero MCP knowledge. shared by both servers.
  server-old/    MCP 2025-11-25. sessions, handshake, held-open streams.
  server-new/    MCP 2026-07-28. stateless, handles, MRTR, cache hints.
  client-demo/   both clients — one per SDK generation.
  round-robin/   ~50-line load balancer. no stickiness, on purpose.
experiments/     four runnable scripts + a write-up each.
docs/            the four architecture notes.
.cart-store/     server-new's shared state. a JSON file. delete it freely.

Lesereihenfolge für den Code: cart-core/src/cart.ts (was das Produkt tut) → server-old/src/index.tsserver-new/src/index.ts. Der Protokollcode in beiden Servern ist Zeile für Zeile kommentiert; die Verkabelung nicht.

npm run clean entfernt Build-Ausgabe und .cart-store.


Glossar

Begriffe, die überall verwendet werden, in der Reihenfolge, in der sie dich beißen werden.

Lastverteiler – eine Box vor mehreren identischen Kopien deines Servers, die eingehende Anfragen auf sie verteilt. Die Standardrichtlinie ist Round-Robin: sende jede Anfrage an die nächste Kopie in der Liste. Sie nimmt an, dass jede Kopie jede Anfrage beantworten kann, was genau die Annahme ist, die die alte MCP-Spezifikation brach. Hier ist es packages/round-robin, etwa 50 Zeilen.

Session – eine serverseitige Erinnerung an einen Client, die mehrere Anfragen umfasst. Am 2025-11-25 prägte der Server eine Mcp-Session-Id während des Handshakes, der Client wiederholte sie bei jeder Anfrage, und der Server verwendete sie als Schlüssel in eine In-Memory-Karte. Die Session-ID ist ein Zeiger in den Heap eines Prozesses, was die Quelle allen Ärgers ist.

Zustandslos – der Server behält nichts zwischen Anfragen. Jede Anfrage trägt alles, was nötig ist, um sie zu bedienen. Beachte, was das nicht bedeutet: Es gibt immer noch einen Warenkorb, und er wird immer noch gespeichert. Was verschwunden ist, ist Zustand, der in einem bestimmten Prozess gehalten wird, implizit, schlüsselbasiert auf Verbindung. Anwendungszustand in einer gemeinsamen Datenbank ist perfekt kompatibel mit einem zustandslosen Protokoll.

Sticky Session (Sitzungsaffinität) – den Lastverteiler so konfigurieren, dass alle Anfragen von einem Client zur selben Serverkopie zurückkehren, normalerweise durch Hashen eines Cookies oder eines Headers. Der Standard-Workaround für ein zustandsbehaftetes Protokoll. Es funktioniert, und es kostet dich gleichmäßige Lastverteilung, schmerzlose Deployments, nützliches Autoscaling und einen Lastverteiler, der dein Anwendungsprotokoll nicht verstehen muss. Kostenliste in docs/02.

Elicitation – ein Server, der den Endbenutzer mitten in einer Operation eine Frage stellt („Summe ist $180,36, bestätigen?“). In der alten Spezifikation sendete der Server seine eigene Anfrage an den Client über einen offen gehaltenen Stream und blockierte innerhalb des Tool-Handlers, während ein Mensch darüber nachdachte. Diese einzelne Funktion erforderte einen laufenden Prozess, einen offenen Socket und garantierte Weiterleitung zurück zur selben Box.

MRTR (Multi Round-Trip Requests) – wie 2026-07-28 Elicitation stattdessen macht. Der Server gibt ein normales 200 mit resultType: "input_required" zurück, die Fragen in inputRequests und einen undurchsichtigen signierten requestState. Diese Anfrage ist vorbei – nichts wird gehalten. Der Client sammelt die Antworten und sendet eine neue Anfrage (neue JSON-RPC-ID), die inputResponses und denselben requestState trägt. Der In-Flight-Zustand reiste durch den Client, statt in einem Prozess zu sitzen, weshalb Runde 2 von einer völlig anderen Maschine bedient werden kann.

Handle – eine vom Server geprägte Kennung, die als gewöhnliche Tool-Ausgabe zurückgegeben und dann als gewöhnliches Argument zurückgegeben wird. cart_create gibt cartId zurück; cart_add_item nimmt es entgegen. So ersetzt 2026-07-28 Session-Zustand, und der Unterschied zum alten Design ist, wer den Schlüssel hält: der Transport, unsichtbar, versus der Client, in einem Wert, den das Modell lesen und weitergeben kann. Einschränkung: Ein Handle allein ist ein Inhaber-Token – es muss auf einen authentifizierten Benutzer beschränkt werden, was docs/04 ehrlich behandelt.

Prompt-Caching – LLM-Anbieter cachen das Präfix eines Prompts: Sende dieselben Anfangsbytes erneut und der Anbieter verwendet seinen berechneten Zustand wieder, statt diese Token erneut zu verarbeiten, zu etwa einem Zehntel des Eingabepreises. Zwei Eigenschaften machen es fragil: Der Abgleich erfolgt auf exakte Bytes, und es ist positional von vorne. Wenn also deine Tool-Liste oder dein Katalog im Präfix sitzt und zwei Einträge die Plätze tauschen, verlierst du den Rabatt auf jedes Token nach dem Tausch. Deshalb sagt 2026-07-28, dass Server Listen in einer deterministischen Reihenfolge zurückgeben SOLLEN, und warum listProducts() nach einer eindeutigen id sortiert statt nach Name oder Preis – ein eindeutiger Schlüssel ergibt eine totale Ordnung ohne Gleichstände, die eine Sortierimplementierung unterschiedlich auflösen könnte. Durchgerechnetes Beispiel, mit Preisen: Experiment 04.


Wenn du dir nur drei Dinge merkst

  1. „Zustandslos“ bedeutet nicht „kein Zustand“ – es bedeutet keinen Zustand, der an einen Prozess gebunden ist. Der Warenkorb existiert weiterhin. Er ist dorthin gezogen, wo jede Instanz ihn erreichen kann.

  2. Sticky Sessions waren eine echte Lösung mit echten Kosten, und eine dieser Kosten war, dass deine Infrastruktur dein Anwendungsprotokoll parsen musste.

  3. MRTR, nicht Zustandslosigkeit, hat Serverless freigeschaltet. Zustandslosigkeit brachte MCP hinter einen Lastverteiler. Elicitation brauchte immer noch einen Prozess, der am Leben blieb, während ein Mensch einen Dialog las – und genau das hat Serverless entfernt.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ritik913553/mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server