1. Was wir heute bauen und wie es in die Anwendung passt
Erinnern wir uns an unsere Übungsanwendung: Wir bauen einen Assistenten für die Geschenkauswahl. In den vorigen Modulen hatten wir bereits:
- ein Widget in ChatGPT (Next.js 16 + Apps SDK), das UI und Zustand anzeigt und callTool ausführen kann;
- ein einfaches Backend (über Apps SDK / Next.js‑Routen), das Geschenk‑Stubs zurückgab.
Jetzt möchten wir die „Intelligenz“ unseres Assistenten in einen separaten MCP‑Server auslagern. Am Ende sieht das so aus:
flowchart TD
subgraph ChatGPT
U[Benutzer
im Chat]
W["App-Widget
(Apps SDK)"]
end
subgraph MCP-Client
C[ChatGPT MCP client]
end
subgraph OurServer[Unser MCP-Server]
T1[Tool: suggest_gifts]
R1[Resource: gift_catalog]
P1[Prompt: birthday_template]
end
U --> W
W -- callTool --> C
C <-- JSON-RPC / HTTP --> OurServer
OurServer --> C
C --> W
Das bedeutet jetzt:
- Das Modell innerhalb von ChatGPT sieht unseren MCP‑Server als standardisierten Satz von tools/resources/prompts;
- callTool aus dem Widget wird logisch zu einem internen MCP‑Aufruf;
- unser Server beschreibt die Verträge (Schemata, Beschreibungen) und implementiert die Geschäftslogik.
Am Ende dieser Vorlesung sollten Sie ein separates Node/TypeScript‑Projekt mit einem MCP‑Server haben, der:
- lokal mit einem einzigen Befehl startet;
- mindestens ein Tool und eine Resource registriert;
- sinnvolle Daten zurückgibt (auch wenn es einfache Mocks sind);
- so strukturiert ist, dass er weiterentwickelt werden kann.
Dabei schreiben wir das bestehende Backend über Apps SDK/Next.js jetzt nicht um: Es bleibt wie es ist, und den MCP‑Server starten wir als separaten Dienst daneben. Später können Sie ihn an die ChatGPT App „andocken“ und schrittweise die Geschenklogik dorthin verlagern statt der alten Stubs.
2. Stack: TypeScript + MCP SDK + HTTP‑Transport
Wir schreiben den MCP‑Server in TypeScript unter Node.js. Das offizielle JS/TS‑SDK für MCP steckt im Paket @modelcontextprotocol/sdk. Es übernimmt die Routine rund um JSON‑RPC, Validierung und Schema‑Konvertierung: Sie beschreiben Argumente via Zod‑Schemata, und das SDK wandelt sie automatisch in JSON Schema um, das das Modell versteht.
Als Transport brauchen wir die HTTP‑Variante: ChatGPT kommuniziert mit entfernten MCP‑Servern über das Netz, nicht via stdio/lokal. Die MCP‑Spezifikation beschreibt ein standardisiertes Format für „Streaming‑HTTP“ – im Grunde eine Evolution des alten HTTP+SSE‑Schemas. In der Praxis ist das ein HTTP‑Endpoint, der Anfragen (POST/GET) verarbeitet und bei Bedarf die Antwort streamt. Im TypeScript‑SDK für MCP gibt es üblicherweise bereits einen fertigen Transport für dieses Format, der sich an Express oder Hono andocken lässt.
Um es schlank zu halten, gehen wir davon aus, dass wir haben:
- ein Server‑Objekt McpServer aus @modelcontextprotocol/sdk;
- einen HTTP‑Transport (z. B. StreamableHttpServerTransport oder ähnlich), der mit Express zusammenspielt.
Die genauen Klassennamen können sich zwischen SDK‑Versionen leicht ändern, architektonisch ist es aber immer:
- Sie erzeugen ein Objekt des MCP‑Servers;
- registrieren darauf tools/resources/prompts;
- koppeln den Transport an die HTTP‑Anwendung.
3. Projektstruktur und Vorbereitung
Wir legen einen eigenen Ordner für den MCP‑Server an. Er liegt praktisch neben der Frontend‑App, aber als separates Node‑Projekt:
chatgpt-gift-app/
app/ ← Next.js + Apps SDK (Widget)
mcp-server/ ← unser MCP-Server
Im Inneren von mcp-server:
mcp-server/
src/
server.ts ← Einstiegspunkt des MCP-Servers
gifts.ts ← Geschäftslogik zur Geschenkauswahl
package.json
tsconfig.json
Ein einfaches Beispiel gifts.ts bauen wir gleich, jetzt konzentrieren wir uns auf server.ts.
Angenommen, Sie haben das Projekt bereits initialisiert:
mkdir mcp-server
cd mcp-server
npm init -y
npm install typescript ts-node-dev zod express @modelcontextprotocol/sdk
tsconfig.json — ganz gewöhnlich (esnext modules, target node, strict). Sie können eine aus einem Ihrer TS‑Projekte übernehmen.
4. Geschäftslogik in ein separates Modul auslagern
Es ist verlockend, sofort server.registerTool(..., async () => {...}) zu schreiben und dort die gesamte Logik zu implementieren. Besser ist es jedoch von Anfang an zu trennen:
- ein Modul, das nichts weiß über MCP, JSON‑RPC und Co.;
- ein Modul, das nur MCP kennt, aber wenig über die Geschäftslogik.
In src/gifts.ts beschreiben wir eine einfache Funktion zur Geschenkauswahl:
// src/gifts.ts
export type GiftIdea = {
id: string;
title: string;
price: number;
occasion: string;
};
export type SuggestGiftsInput = {
age: number;
relationship: "friend" | "partner" | "child" | "coworker";
budget: number;
};
export function suggestGifts(input: SuggestGiftsInput): GiftIdea[] {
// vorerst nur Mocks
return [
{
id: "book-1",
title: "Buch zum Lieblingshobby",
price: Math.min(input.budget, 30),
occasion: "generic",
},
{
id: "game-1",
title: "Gesellschaftsspiel für die Gruppe",
price: Math.min(input.budget, 50),
occasion: "party",
},
];
}
Diese Funktion ist rein: Parameter rein, ein Array von Ideen raus. Sie lässt sich mit Unit‑Tests testen, an anderer Stelle wiederverwenden und hängt in keiner Weise von MCP ab. Genau so wird es empfohlen: Server‑Plumbing separat, Geschäftsfunktionen separat.
5. MCP‑Server erstellen und HTTP‑Transport anbinden
Nun der Einstiegspunkt src/server.ts. Schematisch brauchen wir:
- einen MCP‑Server instanziieren;
- darauf Tools, Ressourcen und Prompts registrieren;
- einen HTTP‑Server (z. B. Express) starten und den MCP‑Transport andocken.
Starten wir mit einem Gerüst:
// src/server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server";
import { StreamableHttpServerTransport } from "@modelcontextprotocol/sdk/transport/streamable-http";
const app = express();
// 1. MCP-Server erstellen
const mcpServer = new McpServer({
name: "gift-assistant-mcp",
version: "0.1.0",
});
// 2. Hier registrieren wir später tools/resources/prompts
// 3. Transport über HTTP konfigurieren
const transport = new StreamableHttpServerTransport({
path: "/mcp", // einziger MCP-Endpoint
app, // in die Express-App einbetten
});
transport.attach(mcpServer);
const PORT = process.env.PORT ?? 4000;
app.listen(PORT, () => {
console.log(`MCP server listening on http://localhost:${PORT}/mcp`);
});
Die konkreten Transport‑Klassennamen können abweichen, das Muster ist jedoch gleich: Sie richten einen HTTP‑Endpoint ein und binden daran den MCP‑Server als Handler für JSON‑RPC über HTTP/Stream.
In diesem Stadium macht der Server noch nichts Nützliches, aber er kann bereits:
- den MCP‑Handshake absolvieren;
- auf Basis‑Discovery‑Anfragen antworten (Liste von tools/resources/prompts — derzeit noch leer).
Als Nächstes registrieren wir das erste Tool.
6. Tool suggest_gifts über das MCP‑SDK registrieren
Das offizielle Apps SDK und die MCP‑Dokumentation zeigen dasselbe Muster zur Tool‑Registrierung: die Methode registerTool, der Sie Namen, Deskriptor (Titel, Beschreibung, Argument‑Schema) und Handler übergeben.
Wir haben den Typ SuggestGiftsInput in gifts.ts bereits beschrieben. Jetzt fügen wir ein Zod‑Schema hinzu, damit der Server die Eingabe validieren und automatisch ein korrektes JSON Schema an die LLM liefern kann.
// src/server.ts (Ausschnitt)
import { z } from "zod";
import { suggestGifts } from "./gifts";
const suggestGiftsInputSchema = z.object({
age: z.number().int().min(0).max(120),
relationship: z.enum(["friend", "partner", "child", "coworker"]),
budget: z.number().min(0),
});
Jetzt registrieren wir das Tool:
// weiterhin in server.ts
mcpServer.registerTool(
"suggest_gifts",
{
title: "Suggest gift ideas",
description:
"Wählt Geschenkideen basierend auf Alter, Beziehungstyp und Budget aus.",
// Das SDK konvertiert das Zod-Schema in JSON Schema für das Modell
inputSchema: suggestGiftsInputSchema,
},
async ({ input }) => {
const ideas = suggestGifts(input);
const text = ideas
.map(
(g) =>
`• ${g.title} — ~${g.price} USD (occasion: ${g.occasion}, id: ${g.id})`
)
.join("\n");
return {
content: [
{
type: "text",
text,
},
],
// structuredContent kann im Widget verwendet werden
structuredContent: {
ideas,
},
};
}
);
Schlüsselstellen:
- inputSchema — Zod‑Schema. Das TS‑SDK kann es in JSON Schema umwandeln und beschreibt so das Tool automatisch für das Modell.
- Der Handler erhält ein Objekt mit input (dessen Typ Sie aus dem Schema bekommen). Darin können Sie Ihre Geschäftslogik aufrufen.
- Im result geben Sie content zurück — Text, den das Modell als Ergebnis sieht — und optional structuredContent mit einer JSON‑Struktur, die Ihr Widget konsumieren kann.
Wenn Sie im vorigen Modul bereits ein Tool über das Apps SDK gebaut haben, wirkt dieser Code sehr vertraut: Das Muster ist identisch, nur lebt es jetzt in einem separaten MCP‑Server.
7. Ressource gift_catalog für Daten hinzufügen
Tools sind Aktionen. Manchmal möchte man zusätzlich Daten als Ressource bereitstellen, damit das Modell sie lesen, durchsuchen oder Ihr Widget Vorlagen, Komponenten usw. nachladen kann. MCP beschreibt dafür explizit die Konzepten von Ressourcen mit URI, MIME‑Typen und Inhalt.
Wir bauen eine einfache Ressource gift_catalog, die eine Liste verfügbarer Geschenke zurückgibt. Zunächst sind das Mocks, in echt könnte es ein Export aus einer Datenbank oder ein Product Feed sein.
Zuerst der Katalog selbst:
// src/gifts.ts (Ergänzung)
export const giftCatalog: GiftIdea[] = [
{
id: "book-1",
title: "Programmierbuch",
price: 25,
occasion: "learning",
},
{
id: "lego-1",
title: "LEGO-Set",
price: 60,
occasion: "fun",
},
];
Jetzt registrieren wir die Ressource auf dem Server:
// src/server.ts (Ausschnitt)
import { giftCatalog } from "./gifts";
mcpServer.registerResource(
"gift_catalog",
{
title: "Gift catalog",
description: "Einfacher Geschenk-Katalog für Demo und Debugging.",
mimeType: "application/json",
},
async () => {
return {
contents: [
{
uri: "mcp://gift-catalog",
mimeType: "application/json",
text: JSON.stringify(giftCatalog, null, 2),
},
],
};
}
);
Was hier logisch passiert:
- der Ressourcenname gift_catalog ist beim Discovery für den Client sichtbar (im MCP‑Inspector sehen Sie ihn später in der Ressourcenliste);
- der Deskriptor enthält eine menschenlesbare Beschreibung und den MIME‑Typ;
- der Handler liefert ein Array contents mit URI und Text — das ist das Standardformat für Ressourcen in MCP.
Später können Sie:
- diese Ressource vom Client aus lesen (z. B. Agent oder Inspector);
- sie als Vorlagen/Daten fürs UI verwenden;
- experimentieren, wie das Modell den Katalog nutzt, um dem Nutzer Optionen zu erklären.
8. Einen einfachen Prompt registrieren
Die dritte MCP‑Entität sind Prompts, vordefinierte Hinweise. Damit müssen lange System‑ oder Benutzerprompts nicht wiederholt, sondern mit Namen auf dem Server gespeichert werden.
Ein Mini‑Beispiel: der Prompt birthday_gift, den man als „vorausgefüllte Gesprächsvorlage zum Geburtstagsgeschenk“ aufrufen kann.
// src/server.ts (Ausschnitt)
mcpServer.registerPrompt("birthday_gift", {
title: "Birthday gift helper",
description: "Vorlage für eine Anfrage zur Auswahl eines Geburtstagsgeschenks.",
messages: [
{
role: "system",
content:
"Du bist ein Assistent für die Geschenksuche. Stelle Rückfragen und schlage mehrere Optionen vor.",
},
{
role: "user",
content:
"Ich brauche ein Geschenk zum Geburtstag. Stelle die nötigen Rückfragen und hilf bei der Auswahl.",
},
],
});
Unter der Haube ermöglicht MCP den Clients:
- eine Liste von Prompts abzurufen (im Inspector sehen Sie birthday_gift);
- ihren Inhalt zu laden und als Basis‑Hinweis für das Modell zu verwenden.
In einem separaten Modul zu System‑Prompts und Instruktionen besprechen wir im Detail, wie solche Prompts mit den globalen App‑Instruktionen zusammenspielen. Hier ist nur wichtig, sie als Teil des MCP‑Servers „zu sehen“.
9. Wie das zur Laufzeit zusammenwirkt
Setzen wir das Gesamtbild zusammen.
Wenn sich ein Client (z. B. MCP Inspector oder ChatGPT) mit unserem HTTP‑Endpoint /mcp verbindet:
- findet ein Handshake statt: Client und Server tauschen ihre Fähigkeiten aus (tools/resources/prompts etc.);
- der Client ruft Discovery‑Methoden auf: er erhält die Liste von Tools, Ressourcen und Prompts inklusive Beschreibungen und Schemata;
- wenn das Modell ein Tool aufrufen will, formt es eine JSON‑RPC‑Anfrage mit einer Methode wie tools/call o. ä. — das SDK auf Serverseite verwandelt dies in einen internen Aufruf des registerTool‑Handlers;
- der Handler führt die Geschäftslogik aus (bei uns suggestGifts oder die Ausgabe von giftCatalog) und liefert ein standardisiertes Ergebnis;
- das SDK serialisiert die Antwort zurück nach JSON‑RPC und sendet sie über denselben HTTP/Stream‑Transport an den Client.
Alle Details zu JSON‑RPC, Bildung von id, Methoden‑Routing usw. bleiben innerhalb von @modelcontextprotocol/sdk. Für Sie ähnelt das Interface stark dem Apps SDK: Sie arbeiten mit registerTool/registerResource/registerPrompt und deren Handlern, ohne über das Protokoll nachdenken zu müssen.
10. Lokaler Start und erster einfacher Test
Angenommen, Sie haben alles oben ergänzt. Jetzt starten.
In package.json können Sie ein Skript hinzufügen:
{
"scripts": {
"dev": "ts-node-dev src/server.ts"
}
}
Starten:
npm run dev
In der Konsole sollte etwas erscheinen wie:
MCP server listening on http://localhost:4000/mcp
Eine vollständige Inspektion und manuelle Tool‑Aufrufe machen wir in der nächsten Vorlesung über MCP Inspector / MCP Jam. Schon jetzt geht aber ein super‑einfacher Smoke‑Test via curl:
curl -X POST http://localhost:4000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Dieses curl ist rein fakultativ als Smoke‑Test für alle, die gern „rohe“ JSON‑Antworten sehen. In der Praxis sprechen Sie fast immer über das SDK mit dem MCP‑Server, statt JSON‑RPC‑Requests per Hand zu bauen.
Der genaue Methodenname hängt von Protokoll‑ und SDK‑Version ab, aber die Idee ist, dass Sie eine JSON‑Liste erhalten, in der unter tools suggest_gifts sichtbar ist. Falls die Methode abweicht — nicht schlimm: Ziel der Vorlesung ist nicht, alle Namen auswendig zu kennen, sondern dass Sie keine Scheu vor JSON‑Antworten haben und deren Struktur verstehen — dank der vorherigen Vorlesungen.
11. Anbindung an unsere ChatGPT App und weitere Entwicklung
Aktuell lebt der MCP‑Server für sich. In den nächsten Modulen werden Sie:
- ihn an den MCP Inspector anschließen und lernen, tools/resources/prompts isoliert zu debuggen, ohne ChatGPT anzufassen;
- die ChatGPT App so konfigurieren, dass sie diesen MCP‑Server als Tool‑Quelle sieht;
- einen Teil der Logik, die zuvor im Apps SDK steckte (z. B. über eingebaute Tools), in die MCP‑Schicht verschieben;
- Autorisierung, Logging, Streaming‑Szenarien hinzufügen — auf dem fertigen Grundgerüst.
Wichtig ist jetzt, dass:
- Sie einen separaten Service haben, der für die „Fähigkeiten“ und „Daten“ der Anwendung zuständig ist;
- dieser Service mit Clients über den MCP‑Standard spricht, nicht über custom REST;
- Sie bereits in der Lage sind, Tools, Ressourcen und Prompts manuell zu registrieren, ohne Angst vor dem Protokoll.
12. Ein wenig zur Codestruktur und Best Practices
Schon an diesem kleinen Beispiel lassen sich gute Gewohnheiten etablieren.
Erstens: halten Sie die Serverkonfiguration getrennt. Alles rund um Namen, Version, Logging, Transporteinstellungen (Port, Pfad /mcp) lässt sich in ein kleines Modul config.ts auslagern. Später, beim Deployment auf Vercel oder hinter ein MCP‑Gateway, kommen Umgebungsvariablen dazu – Sie werden sich bedanken.
Zweitens: bemühen Sie sich, dass die Methoden registerTool/registerResource/registerPrompt möglichst „dünn“ bleiben. Beschreibungen von Schemata, Texten und Geschäftslogik sehen in separaten Dateien besser aus:
- gifts.ts — Funktionen zur Geschenkauswahl;
- catalog.ts — Arbeit mit dem Warenkatalog;
- prompts.ts — Sammlung von Prompts.
Der server.ts wird dann zu einer Art „MCP‑Provider“, der alles zusammenführt.
Drittens: Denken Sie daran, dass der MCP‑Server seiner Natur nach reaktiv ist: Er wartet auf Client‑Verbindungen und deren Anfragen. Das bedeutet, dass blockierende oder sehr langlaufende Operationen in Tools direkt die UX in ChatGPT beeinflussen. In den kommenden Modulen sprechen wir über Timeouts, asynchrone Operationen und Streaming‑Antworten. Schon jetzt lohnt es sich zu überlegen, welche Operationen in den Hintergrund gehören und welche schnell antworten müssen.
Insight: ChatGPT unterstützt nur einen Teil von MCP
Wichtig zu verstehen: ChatGPT Apps nutzen MCP als Transport und Format, sind aber keine vollwertigen MCP-Clients. Liest man nur das Protokoll, entstehen leicht falsche Erwartungen an das Laufzeitverhalten.
Was „reines“ MCP verspricht:
- Ressourcen können dynamisch auf Anfrage des Clients gelesen werden, nicht nur einmalig;
- der Server kann resourceChanged/toolChanged-Benachrichtigungen senden und damit Updates ohne Client‑Neustart „durchschieben“;
- man kann ein flexibles System bauen, in dem der Satz an tools/resources/prompts per Configs oder externem Zustand gesteuert wird.
Im Kontext von ChatGPT Apps ist das nicht so. Für die App ist das Bild deutlich statischer:
- bei der Registrierung der App liest ChatGPT einmalig die Beschreibung aller tools und resources ein;
- anschließend wird diese Konfiguration faktisch als Teil der App‑Version gecacht;
- dynamische Updates per MCP-Benachrichtigungen werden nicht unterstützt — die Plattform ignoriert sie schlicht.
13. Typische Fehler beim Schreiben des ersten MCP‑Servers
Fehler Nr. 1: Die gesamte Geschäftslogik direkt in registerTool kippen.
Die Versuchung ist groß, „mal eben alles im Tool‑Handler“ zu bauen, gerade im Lernbeispiel. Das endet dann als unlesbarer Kombi, in dem Validierung, DB‑Zugriff und Antwortformatierung vermischt sind. Besser ist es, Geschäftsfunktionen (suggestGifts, Arbeit mit dem Katalog) direkt in separate Module auszulagern und im Handler nur zu „verkleben“.
Fehler Nr. 2: Sich hart an konkrete JSON‑Methodennamen von MCP binden.
Manche fangen an, if (method === "tools/list") zu schreiben und JSON per Hand zu parsen. Das ist unnötig: Das ist Aufgabe des SDK. Die MCP‑Spezifikation und Methodennamen können sich weiterentwickeln, und das SDK kümmert sich darum. Nutzen Sie registerTool, registerResource, registerPrompt und überlassen Sie der Bibliothek, wie das in JSON‑RPC aussieht.
Fehler Nr. 3: Den Transport ignorieren und versuchen, ChatGPT einen stdio‑Server zu geben.
Der stdio‑Transport ist ideal für lokale Clients wie Desktop‑Umgebungen, wo der Client den Server als Subprozess startet. ChatGPT kommuniziert jedoch via HTTPS und braucht einen HTTP/Streaming‑Endpoint. Der Versuch, „irgendwie stdio“ durch einen Tunnel zu reichen, endet schmerzhaft. Für die ChatGPT App direkt HTTP‑Transport (Streamable HTTP) bauen.
Fehler Nr. 4: MIME‑Typen und Ressourcenstruktur ignorieren.
Bei Ressourcen zählen nicht nur Inhalte, sondern auch Typ (mimeType) und URI. Wenn man überall text/plain schreibt und unbedacht JSON‑Strings wirft, wird es für Clients (und Inspector) schwerer zu verstehen, welche Daten das sind. Geben Sie korrekte MIME‑Typen an (application/json, text/html für UI‑Vorlagen usw.) und stabile URIs.
Fehler Nr. 5: Den MCP‑Server als „beliebiges HTTP‑API“ benutzen.
Es ist verführerisch: „Wenn ich schon Express habe, hänge ich noch /api/whatever dran und rede direkt damit.“ MCP‑Endpoint mit beliebigem REST zu mischen, ist selten eine gute Idee: Das verkompliziert Konfiguration, Routing und Sicherheit. Besser klar trennen: /mcp für MCP, andere Pfade für andere Zwecke — oder gleich ein anderer Service. Im Production‑Betrieb ist das besonders wichtig für Gateways und Auth. Verwandeln Sie den MCP‑Server also nicht in ein „beliebiges HTTP‑API“ — einen Haufen zufälliger HTTP‑Routen ohne MCP‑Vertrag.
Fehler Nr. 6: Eingehende und ausgehende MCP‑Nachrichten nicht loggen.
Ohne Logs wird der MCP‑Server zur Blackbox: „Irgendwas läuft nicht, aber keine Ahnung, was.“ Schon beim ersten Server lohnt es sich, zumindest in stderr kompakte strukturierte Logs zu schreiben: Tool‑Methode, Status, Laufzeit. Wichtig: keine sensiblen Daten oder Tokens loggen — dazu kommen wir später beim Thema Sicherheit.
Fehler Nr. 7: Alles sofort über ChatGPT debuggen, ohne einen Inspector zu haben.
Typisches Bild: Jemand baut den MCP‑Server, schließt ihn direkt an die ChatGPT App an — und „irgendwie bricht alles“. Der Inspector wurde nie gestartet. Dann ist schwer zu verstehen, ob das Problem im Protokoll, im Server, im Apps SDK oder im Modellverhalten liegt. Der richtige Weg: Erst sicherstellen, dass der MCP‑Server isoliert korrekt läuft (über MCP Jam / Inspector), und erst dann in die App einhängen.
GO TO FULL VERSION