Dass Fachsoftware einen eigenen MCP-Server braucht, lässt sich in einem Absatz begründen. Wie er entsteht, in keinem. Zwischen dem Argument und dem laufenden Dienst liegen Entscheidungen, die in keiner Protokollspezifikation stehen: welches Werkzeug zuerst, wo die Rechteprüfung sitzt, was der Agent zu sehen bekommt und was nicht, und wann er vor einer Aktion einen Menschen fragen muss. Dieser Beitrag geht den Weg einmal vollständig, an einem einzigen Fall.
Der Fall ist eine Kongressplattform eines Verbands. Eine Geschäftsführerin fragt ihren KI-Assistenten, wie viele Anmeldungen für die Jahrestagung vorliegen und bei wie vielen die Zahlung noch offen ist. Am Ende des Textes beantwortet ein Server diese Frage, verweigert die Auskunft über fremde Veranstaltungen, verschickt auf Wunsch eine Erinnerungsmail und lässt sich lokal testen. Der Code nutzt das offizielle TypeScript-SDK in der seit Juli stabilen Version 2, die zusammen mit der Spezifikation vom 28. Juli 2026 erschienen ist (1); wer mit dem älteren SDK arbeitet, findet in der Migrationsanleitung die Entsprechungen.
Angenommen also, die Kongressplattform bietet einen MCP-Server mit einer Handvoll Werkzeuge an: Anmeldestand abfragen, Referentenstatus prüfen, Raumbelegung lesen. Die Geschäftsführerin hat ihren Assistenten einmalig damit verbunden und den Zugriff über den vertrauten Anmeldedialog ihres Verbands freigegeben. Stellt sie nun ihre Frage, geschieht Folgendes: Der Assistent erkennt unter den angebotenen Werkzeugen eines, das passt, und ruft es mit der Kennung der Veranstaltung auf.
Der Server prüft, wer da fragt, holt die Zahlen aus der Plattform und liefert sie strukturiert zurück. Erst dann formuliert der Assistent seinen Satz: „Für die Jahrestagung liegen 428 bestätigte Anmeldungen vor, bei 37 ist die Zahlung noch offen.“ Kein Export, kein Copy-and-paste, kein zweites Browserfenster. Die Software bleibt, wo sie ist; ihre Daten sind nun im Gespräch abrufbar.
Auf der Leitung sieht dieser Aufruf erstaunlich unspektakulär aus. Es ist eine gewöhnliche HTTPS-Anfrage an einen einzigen Endpunkt:
POST /mcp HTTP/1.1
Host: kongress.example.org
Authorization: Bearer eyJhbGciOi…
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_registration_overview
{"jsonrpc": "2.0", "id": 7, "method": "tools/call",
"params": {"name": "get_registration_overview",
"arguments": {"event_id": "jt-2026"},
"_meta": {"io.modelcontextprotocol/clientInfo":
{"name": "assistent", "version": "1.0"}}}}
Die Antwort enthält, was die Frage verlangt, und nichts darüber hinaus:
{"jsonrpc": "2.0", "id": 7, "result": {
"resultType": "complete",
"content": [{"type": "text",
"text": "{\"event\":\"Jahrestagung 2026\",\"confirmed\":428,\"pending_payment\":37,\"as_of\":\"2026-09-27T08:15:00Z\"}"}],
"structuredContent": {
"event": "Jahrestagung 2026",
"confirmed": 428,
"pending_payment": 37,
"as_of": "2026-09-27T08:15:00Z"
}
}}
An diesen zwei Nachrichten lässt sich die Arbeitsteilung ablesen, um die es im Folgenden geht. MCP regelt, wie der Assistent das Werkzeug findet, aufruft und die Antwort versteht. Was eine Anmeldung ist, wer sie sehen darf und ob eine stornierte Anmeldung mitzählt, bleibt Sache der Kongressplattform. Gerade diese fachliche Schärfe gehört ins Werkzeug: Eine angelegte, eine bezahlte und eine stornierte Anmeldung sind drei verschiedene Zustände, und ein Assistent, der sie in einer einzigen Zahl zusammengezählt bekommt, wird der Geschäftsführerin mit großer Gewissheit etwas Falsches sagen.
Ein Werkzeug besteht aus einem Namen, einer Beschreibung in natürlicher Sprache, einem Eingabeschema und einer Funktion, die die Arbeit erledigt. Mit dem offiziellen TypeScript-SDK in der seit Juli stabilen Version 2 sieht das Werkzeug aus unserer Szene so aus:
import { McpServer, requireScopes } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
import { registrations, NotVisibleError } from './kongress-dienste.js';
export function buildServer(): McpServer {
const server = new McpServer({ name: 'kongress', version: '2.0.0' });
server.registerTool(
'get_registration_overview',
{
title: 'Anmeldestand abfragen',
description:
'Liefert für eine Veranstaltung die Zahl der bestätigten Anmeldungen ' +
'und der Anmeldungen mit offener Zahlung. Stornierungen zählen nicht mit. ' +
'Enthält keine Namen. Für Teilnehmerlisten list_participants verwenden.',
inputSchema: z.object({
event_id: z.string().max(64).describe('Kennung der Veranstaltung, z. B. jt-2026')
}),
outputSchema: z.object({
event: z.string(),
confirmed: z.number().int(),
pending_payment: z.number().int(),
as_of: z.string()
}),
annotations: { readOnlyHint: true },
scopeChallenge: requireScopes('registrations:read')
},
async ({ event_id }, ctx) => {
const caller = ctx.http?.authInfo;
if (!caller) {
return { content: [{ type: 'text', text: 'Nicht angemeldet.' }], isError: true };
}
try {
const overview = await registrations.overviewFor(event_id, caller);
return {
content: [{ type: 'text', text: JSON.stringify(overview) }],
structuredContent: overview
};
} catch (err) {
if (err instanceof NotVisibleError) {
return { content: [{ type: 'text', text: 'Veranstaltung nicht gefunden.' }], isError: true };
}
throw err;
}
}
);
return server;
}
Das Schema übernimmt dabei eine doppelte Aufgabe. Aus ihm leitet das SDK die Beschreibung ab, die der Agent zu sehen bekommt, und es prüft jeden Aufruf, bevor die Funktion überhaupt läuft; eine Kennung mit dreihundert Zeichen wird abgewiesen, ohne dass eine Zeile Geschäftslogik anspringt. Das Ausgabeschema verspricht dem Agenten eine feste Form der Antwort, die Annotation readOnlyHint teilt ihm mit, dass dieses Werkzeug nichts verändert, und scopeChallenge verlangt für genau dieses Werkzeug eine eigene Berechtigung im Token. Wer lesen darf, darf deshalb noch lange nicht schreiben.
Das eigentlich Bemerkenswerte ist die description. Sie ist kein Kommentar für Kollegen, sie ist die Schnittstelle selbst: Der Agent liest diesen Absatz, um zu entscheiden, ob und wie er das Werkzeug einsetzt. Man vergleiche zwei Fassungen. „Gibt Anmeldedaten zurück“ lässt offen, welche Daten, für welchen Zweck und ob Namen darunter sind; ein Agent, der das liest, wird es für Teilnehmerlisten ebenso verwenden wie für Zahlungsfragen.
Die Fassung im Code benennt die Zustände, schließt personenbezogene Daten ausdrücklich aus und verweist im letzten Satz auf das Nachbarwerkzeug. Dieser letzte Satz leistet mehr als jede Zeile Dokumentation, weil er den Agenten aktiv umleitet. Das Formulieren solcher Sätze ist Interface-Design mit sprachlichen Mitteln, näher an guter Microcopy als an API-Dokumentation.
Aus derselben Logik folgt der Zuschnitt. Als Entwurfsregel hat sich bewährt, lieber wenige, eng gefasste Werkzeugeanzubieten als ein Universalwerkzeug mit zwanzig Parametern, weil ein Sprachmodell über kleine, präzise beschriebene Einheiten verlässlicher urteilt (2). Ein Werkzeug query_database mit freiem Filterfeld wäre bequem zu schreiben und für den Agenten ein Labyrinth.
Wer das Codebeispiel aufmerksam liest, stellt die richtige Frage: Was hindert den Agenten daran, die Kennung einer fremden Veranstaltung einzusetzen, etwa die eines anderen Verbands, der dieselbe Plattform nutzt? Die ehrliche Antwort lautet: nichts, und genau so muss man planen. Ein Agent ist ein Client wie jeder andere, nur gesprächiger. Seine Werkzeugauswahl ist keine Sicherheitsschicht, und eine Beschreibung wie „bitte nur eigene Veranstaltungen abfragen“ wäre eine Bitte, kein Schutz. Die event_id wählt einen Datensatz aus; Rechte erteilt sie nicht.
Die Prüfung gehört deshalb in den Dienst, den der Handler aufruft, und zwar in denselben Dienst, den auch die Verwaltungsoberfläche der Plattform nutzt. Ein MCP-Server braucht keine zweite Kongresslogik; er ist eine dünne Übersetzungsschicht über der vorhandenen:
export async function overviewFor(eventId: string, caller: AuthInfo) {
// Wirft, wenn Mandant oder Person im geprüften Token fehlen
const { tenantId, userId } = requireIdentity(caller);
// Fachliche Sichtbarkeit: richtiger Mandant und eine Rolle, die diese Veranstaltung sehen darf
const event = await db.events.findVisibleTo({ eventId, tenantId, userId });
if (!event) throw new NotVisibleError(); // fremd, unbekannt oder nicht freigegeben: dieselbe Antwort
const counts = await db.registrations.countByStatus(event.id);
return { // nur, wonach gefragt wurde
event: event.title,
confirmed: counts.confirmed,
pending_payment: counts.pending_payment,
as_of: new Date().toISOString()
};
}
Drei Entscheidungen stecken in diesen Zeilen. Mandant und Person kommen aus dem geprüften Token, niemals aus den Parametern des Werkzeugs, und der Dienst weigert sich, ohne beides überhaupt anzufangen. Der OAuth-Scope hat zuvor nur festgelegt, welche Art von Zugriff das Token erlaubt; ob die Geschäftsführerin diese eine Veranstaltung sehen darf, entscheidet erst die fachliche Abfrage. Eine fremde, eine unbekannte und eine nicht freigegebene Veranstaltung erzeugen dieselbe Fehlermeldung, damit ein durchprobierender Client nicht einmal erfährt, welche Kennungen es gibt.
Und die Antwort nennt ausschließlich die Felder, die zur Frage gehören. Das hat einen Grund, den klassische APIs nicht kennen: Alles, was ein Werkzeug zurückgibt, kann im Kontext des Sprachmodells landen und dort weiterwirken. Ein Rohdatensatz mit Spalten, nach denen niemand gefragt hat, ist in diesem Kontext ein Risiko, und Datenminimierungwird damit zur Architekturentscheidung.
Ruft der Agent nun mit der Kennung eines anderen Verbands auf, bekommt er {„isError“: true, „content“: [{„type“: „text“, „text“: „Veranstaltung nicht gefunden.“}]} zurück und teilt der Geschäftsführerin mit, dass er diese Veranstaltung nicht finden kann. Mehr passiert nicht, und mehr soll auch nicht passieren. Hinzu kommen die Routinen, die für jede Schnittstelle gelten: Eingaben streng validieren (das Schema prüft Typen, keine Rechte), Werkzeuge nach dem Prinzip der minimalen Rechte zuschneiden, lesende Zugriffe vor schreibenden freigeben und eine Ratenbegrenzung gegen Clients setzen, die Kennungen durchzählen.
Bleibt die Frage, wie aus dieser Funktion ein Dienst im Netz wird. Seit der Spezifikation vom 28. Juli 2026 ist die Antwort deutlich einfacher geworden. Das Protokoll kennt keinen Handshake und keine Sitzungen mehr; jede Anfrage trägt Protokollversion, Client-Kennung und Fähigkeiten selbst mit sich. Für den Betrieb bedeutet das: Jede Anfrage kann auf jeder Instanz hinter einem gewöhnlichen Load Balancer landen, ohne gemeinsamen Sitzungsspeicher. Braucht ein Werkzeug doch einmal Zustand über mehrere Aufrufe hinweg, gibt es eine Kennung zurück, die der Agent beim nächsten Aufruf wieder mitschickt.
Der Transport heißt Streamable HTTP: ein einzelner Endpunkt, üblicherweise /mcp, über den Client und Server JSON-RPC-Nachrichten austauschen. Die Autorisierung schreibt der Standard für Remote-Server mit OAuth 2.1 vor; der Nutzer durchläuft einmalig den Freigabedialog, der Server prüft fortan bei jedem Aufruf das mitgelieferte Token samt Berechtigungen. Der Server selbst ist dabei ein OAuth-Resource Server: Er prüft Zugriffstoken, die ein Identitätsdienst ausgestellt hat, und stellt selbst keine aus. In Express sieht die gesamte Verdrahtung so aus:
import {
createMcpExpressApp, requireBearerAuth,
getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter
} from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler } from '@modelcontextprotocol/server';
import { buildServer } from './server.js';
import { verifyAccessToken, oauthMetadata } from './identitaet.js';
const mcpUrl = new URL('https://kongress.example.org/mcp');
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['kongress.example.org'] });
// Beschreibt Clients, wo sie ein Token bekommen
app.use(mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpUrl }));
// Ohne gültiges Token kein Aufruf: 401 bzw. 403 mit Hinweis auf den Identitätsdienst
const auth = requireBearerAuth({
verifier: { verifyAccessToken },
requiredScopes: ['mcp'],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpUrl)
});
const mcp = toNodeHandler(createMcpHandler(buildServer));
app.all('/mcp', auth, (req, res) => void mcp(req, res, req.body));
app.listen(3000);
Die Funktion verifyAccessToken liefert Ihr eigener Identitätsdienst zu, ob über lokale JWT-Prüfung oder Token-Introspection; Mandant und Person legt sie im Feld extra des geprüften Tokens ab, wo der Dienst aus dem vorigen Abschnitt sie erwartet. Bei einer Plattform, die bereits ein Single Sign-on betreibt, ist das in aller Regel die vorhandene Anmeldung, die um eine Freigabe für Assistenten erweitert wird. Die neue Spezifikation hat die Autorisierung zudem gehärtet und setzt für die Registrierung von Clients auf veröffentlichte Metadatendokumente statt auf die bisherige dynamische Registrierung. Wer die Autorisierung heute aufsetzt, sollte das von Beginn an berücksichtigen.
Der Rest ist angenehm unglamourös, denn ein MCP-Server ist eine Webanwendung: Reverse Proxy, TLS, Logging, Überwachung, dieselben Routinen, die jede Agentur beherrscht. Wer WordPress-Instanzen und Fachplattformen betreibt, betreibt auch das. Eine Neuerung erleichtert den Betrieb zusätzlich. Methode und Werkzeugname reisen jetzt als HTTP-Header mit (Mcp-Method, Mcp-Name), sodass Gateway, Firewall und Ratenbegrenzung pro Werkzeug entscheiden können, ohne den Inhalt der Anfrage zu zerlegen.
Lesende Werkzeuge sind der natürliche Anfang. Früher oder später wird die Geschäftsführerin aber mehr wollen: „Erinnere den Keynote-Referenten an sein Honorarformular.“ Damit verändert der Agent etwas in der Welt, und es stellt sich die Frage, wann ein Mensch entscheiden muss. Für solche Momente sieht die aktuelle Spezifikation eine Rückfrage mitten im Aufruf vor: Der Server antwortet zunächst mit input_required, der Client legt die Frage dem Menschen vor und wiederholt den Aufruf mit dessen Antwort. Das Muster ist elegant, verlangt aber Sorgfalt, denn die Antwort kommt vom Client zurück und ist aus Sicht des Servers so vertrauenswürdig wie jede andere Eingabe, nämlich gar nicht.
Die Ausschnitte in diesem Beitrag illustrieren jeweils einen Aspekt und ergeben keine kopierfertige Anwendung: SECRET_KEY, der Identitätsdienst und die Kongressdienste sind zu ergänzen, und der server unten ist derselbe, den buildServer() oben zurückgibt.
import {
acceptedContent, createRequestStateCodec, inputRequired, inputResponse, requireScopes
} from '@modelcontextprotocol/server';
// Signierter, kurzlebiger Zustand zwischen den beiden Runden,
// gebunden an Methode und geprüfte Person
const stateCodec = createRequestStateCodec<{ draftId: string; speakerId: string }>({
key: SECRET_KEY, // mindestens 32 Byte, auf allen Instanzen gleich
ttlSeconds: 300,
bind: ctx => `${ctx.mcpReq.method}\0${ctx.http?.authInfo?.clientId ?? ''}`
});
const server = new McpServer(
{ name: 'kongress', version: '2.0.0' },
{ requestState: { verify: stateCodec.verify } }
);
const confirmSchema = z.object({
confirm: z.boolean().meta({ title: 'Erinnerung jetzt versenden?' })
});
server.registerTool(
'send_speaker_reminder',
{
title: 'Referenten an Honorarformular erinnern',
description:
'Verschickt eine Erinnerungsmail an einen Referenten, der sein ' +
'Honorarformular noch nicht eingereicht hat. Fragt vor dem Versand nach.',
inputSchema: z.object({ speaker_id: z.string().max(64) }),
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
scopeChallenge: requireScopes('speakers:write')
},
async ({ speaker_id }, ctx) => {
const caller = ctx.http?.authInfo;
const state = ctx.mcpReq.requestState<{ draftId: string; speakerId: string }>();
if (!state) {
// Erste Runde: Entwurf serverseitig anlegen (prüft Rechte, speichert 5 Minuten), noch nichts versenden
const draft = await reminders.prepare(speaker_id, caller);
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: `Erinnerung an ${draft.recipientName} senden?`,
requestedSchema: confirmSchema
})
},
requestState: await stateCodec.mint({ draftId: draft.id, speakerId: speaker_id }, ctx)
});
}
// Zweite Runde: Argumente müssen zum Entwurf passen, und nur eine ausdrückliche Zustimmung gilt
const view = inputResponse(ctx.mcpReq.inputResponses, 'confirm');
const answer = acceptedContent(ctx.mcpReq.inputResponses, 'confirm', confirmSchema);
if (state.speakerId !== speaker_id
|| view.kind !== 'elicit' || view.action !== 'accept' || answer?.confirm !== true) {
await reminders.discard(state.draftId);
return { content: [{ type: 'text', text: 'Versand abgebrochen.' }], isError: true };
}
// Gehört der Entwurf dem Aufrufer, darf er noch, wurde er schon versendet? Alles prüft der Dienst.
const sent = await reminders.sendOnce(state.draftId, caller);
return { content: [{ type: 'text', text: `Erinnerung an ${sent.recipientName} verschickt.` }] };
}
);
Der Ablauf hat zwei Runden. Beim ersten Aufruf legt der Server einen Entwurf an, prüft die Rechte, verschickt aber nichts und fragt zurück. Die Geschäftsführerin sieht im Assistenten eine Bestätigung mit dem Namen des Empfängers, klickt auf Ja oder Nein, und erst beim zweiten Aufruf mit ihrer Antwort geht die Mail hinaus. Was den Versand absichert, ist nicht der Dialog, sondern das, was der Server um ihn herum festhält: Der Entwurf liegt auf dem Server, die Kennung reist signiert, mit Ablaufzeit und gebunden an Methode und geprüfte Person zwischen den Runden, der zweite Aufruf muss denselben Referenten nennen wie der erste, und der Versand prüft noch einmal, ob der Aufrufer der Eigentümer des Entwurfs ist, ob er noch darf und ob die Mail nicht längst unterwegs ist.
Eine Grenze bleibt, und sie gehört ausgesprochen: Der Server kann den Entwurf an die berechtigte Person binden und einen doppelten Versand verhindern. Ob die Bestätigung tatsächlich durch einen Menschen in der Oberfläche des Clients erfolgte, kann er aus inputResponses allein nicht beweisen. Wo eine Aktion eine nachweisbare Freigabe verlangt, braucht sie einen eigenen, authentifizierten Bestätigungsweg der Plattform, etwa einen Link, den nur die berechtigte Person öffnen kann.
Die Annotationen im Code sind Hinweise an den Client, und als solche sollte man sie auch lesen. Sie helfen einem Assistenten, schreibende Werkzeuge anders zu behandeln als lesende, ersetzen aber keine Prüfung auf dem Server. Folgenreiche Vorgänge wie eine Kündigung der Mitgliedschaft gehören ohnehin nicht in den ersten Werkzeugkasten, vielleicht auch in keinen späteren.
Die eigentliche Entwurfsarbeit liegt in der Auswahl. Ein MCP-Server muss nicht die gesamte Anwendung spiegeln; er exponiert eine kuratierte Auswahl von Fähigkeiten. Vier Fragen helfen beim Sortieren: Welche Auskünfte werden heute schon per Mail oder Telefon erfragt? Ist der Zugriff rein lesend? Ist eindeutig, wem die Daten gehören? Und wie groß ist der Schaden, wenn der Assistent eine Antwort falsch wiedergibt? Für eine typische Verbandsplattform ergibt sich daraus etwa diese Reihenfolge:
| Fähigkeit | Zugriff | Wer fragt heute | Risiko bei Fehlern |
|---|---|---|---|
| Anmeldestand einer Tagung | lesend, aggregiert, authentifiziert | Geschäftsstelle, Vorstand | gering |
| Eigene Fortbildungspunkte | lesend, personenbezogen | Mitglieder | mittel |
| Offene Rechnungen | lesend, personenbezogen | Buchhaltung | mittel |
| Erinnerung an Referenten | schreibend, mit Rückfrage | Kongressorganisation | mittel |
| Kündigung der Mitgliedschaft | schreibend, folgenreich | niemand über einen Agenten | hoch |
Ausprobieren lässt sich ein solcher Server, bevor ihn je ein Assistent zu sehen bekommt. Der MCP Inspector ist eine kleine lokale Anwendung, gestartet mit npx @modelcontextprotocol/inspector, die sich mit dem Server verbindet und seine Werkzeuge direkt aufrufen lässt. Vier Aufrufe genügen für den ersten Test, und sie prüfen zwei verschiedene Schutzebenen. Eine Kennung mit dreihundert Zeichen scheitert bereits am Eingabeschema: Der Inspector zeigt einen Validierungsfehler, und overviewFor wird nie erreicht.
Die drei anderen Fälle kommen bis in den Dienst: eine eigene Veranstaltung, eine fremde und schließlich der aufschlussreichste, ein gültiges Token mit registrations:read, dessen Inhaber diese eine Veranstaltung aber nicht sehen darf. Nur die eigene liefert die Zahlenübersicht; die fremde und die nicht freigegebene antworten mit derselben Meldung „Veranstaltung nicht gefunden“, und genau diese Ununterscheidbarkeit ist das gewünschte Ergebnis.
Für einen entfernten, mit OAuth geschützten Server verbindet sich der Inspector über dessen Adresse und durchläuft dabei selbst den Anmeldedialog; das unterscheidet sich vom lokalen Einstieg, bei dem er den Server als Prozess startet. Danach lässt sich der Server in Claude oder ChatGPT als eigener Connector eintragen; wie das für lokale Server in Claude funktioniert, zeigt die Anleitung zum MCP-Server einrichten.
Der Server aus diesen Abschnitten ist ein Gerüst. Zum Haus wird er erst mit Schichten, die keine Spezifikation liefert. Die Mandantentrennung, also die saubere Isolation der Daten verschiedener Verbände innerhalb eines Servers, steckt im Beispiel in einer einzigen Funktion und ist in einer echten Plattform ein eigenes Vorhaben. Dazu kommen Entscheidungsprotokolle, die festhalten, welcher Agent im Auftrag welcher Person was ausgelöst hat, und die Frage, wem die Kosten eines Aufrufs zugeordnet werden. Diese Schutzschichten sind der aufwendigere Teil, und sie sind auch der Teil, in dem sich eine Fachplattform von einer Demo unterscheidet.
Trotzdem lohnt der Anfang, weil er klein sein darf. Ein einziges lesendes Werkzeug, das eine Frage beantwortet, die heute per Mail in der Geschäftsstelle landet, ist ein vollständiger erster Anwendungsfall; Identität, Mandantenrechte und OAuth sind dabei meist der größere Teil der Arbeit, und sie sind einmal zu leisten. Alles, was danach kommt, folgt derselben Regel, die schon im ersten Handler gilt: Der Agent liest die Beschreibung. Was er bekommt, entscheidet der Server.