Ein schreibgeschütztes MCP-Tool veröffentlichen

CloudflareBeginner
Jetzt üben

Einführung

Ein KI-Client sollte nicht für jede verwendete Anwendung eine eigene Integration benötigen. Das Model Context Protocol (MCP) bietet Clients eine standardisierte Möglichkeit, Tools zu entdecken, deren Eingabeverträge zu prüfen und sie aufzurufen. In diesem Lab bleibt das Tool bewusst klein: Es sucht einen synthetischen Supportfall und kann keine Daten ändern.

Sie erstellen den Server mit Cloudflares aktuellem zustandslosem MCP-Handler:

  1. Ein eigenes Cloudflare-KV-Namespace enthält den synthetischen Geschäftsdatenbestand. KV ist der ausdrücklich definierte Anwendungsspeicher und kein verborgener MCP-Sitzungsspeicher.
  2. Ein striktes Zod-Schema akzeptiert ausschließlich eine synthetische Ticket-ID und weist zusätzliche Felder zurück.
  3. McpServer.registerTool() veröffentlicht ein Tool mit schreibgeschützten, nicht destruktiven Annotationen.
  4. createMcpHandler() erstellt für jede Streamable-HTTP-Anfrage einen neuen Server.
  5. Der offizielle MCP-TypeScript-Client entdeckt und verwendet das Tool über unabhängige Verbindungen.
  6. Lokale und bereitgestellte Tests weisen eine gültige Abfrage, ein sicheres Verhalten bei fehlenden Datensätzen, die Ablehnung ungültiger Argumente und das Fehlen eines impliziten gemeinsamen Sitzungszustands nach.

Dieser Endpunkt ist nur deshalb absichtlich nicht authentifiziert, weil er genau einen löschbaren, synthetischen und schreibgeschützten Datensatz bereitstellt. Verwenden Sie dieses Muster nicht, um private Kundendaten zu veröffentlichen. Produktionsserver sollten vor dem Zugriff auf Mandantendaten Authentifizierung und Autorisierung ergänzen. Externe OAuth-Anbieter sind nicht Bestandteil dieses Einsteiger-Labs.

Das MCP-Ökosystem verwendete früher SSE-Endpunkte und zustandsbehaftete Server-Grundgerüste. Dieses Lab behandelt dieses veraltete Design nicht. Es verwendet Streamable HTTP und eine Factory für einen Server pro Anfrage. Das entspricht der aktuellen Cloudflare-Empfehlung für einen neuen Remote-Server.

Bevor Sie direkt an diesem Kurs teilnehmen, absolvieren Sie LabEx mit Ihrem Cloudflare-Konto verbinden. Jede neue LabEx-VM benötigt eine eigene Wrangler-Autorisierung. Frühere Labs des Kurses werden empfohlen, aber deren VMs und Ressourcen werden hier niemals wiederverwendet.

Die VM autorisieren und einen eigenen Katalog erstellen

In diesem Schritt autorisieren Sie diese neue VM, wählen das Lernkonto aus und erstellen einen einzigen löschbaren KV-Namespace. Durch die Trennung des Katalogs bleiben Besitzverhältnisse und Bereinigung eindeutig.

Wechseln Sie in das vorbereitete Projekt und prüfen Sie die festgelegten Tools:

cd /home/labex/project/read-only-mcp-tool
node --version
npx wrangler --version

Autorisieren Sie diese VM:

npx wrangler login

Öffnen Sie den angezeigten Geräte-Link im Browser, prüfen Sie die angeforderten Berechtigungen und autorisieren Sie Ihr eigenes Lernkonto. Wechseln Sie anschließend zurück zum Terminal, warten Sie den Abschluss ab und prüfen Sie die strukturierte Identität:

Wrangler fragt nach den Berechtigungen, die zum Verwalten des Lab-Workers und des KV-Namespace erforderlich sind

Die Berechtigungsliste ist umfassender als für dieses einzelne Lab, weil Wrangler Cloudflares allgemeine CLI für die Entwicklung ist. Prüfen Sie vor der Bestätigung, dass auf der Seite Wrangler genannt wird, dass Sie das gewünschte Lernkonto verwenden und dass im Terminal kein Passwort oder Token angezeigt wird.

npx wrangler whoami --json

Bestätigen Sie loggedIn: true und den Namen des gewünschten Kontos, auch wenn die Ausgabe nur ein Konto enthält. Kopieren Sie die tatsächliche id dieses Kontos. Erzeugen Sie ein eindeutiges Präfix und speichern Sie die anfängliche Worker-Konfiguration. Ersetzen Sie zuvor den Platzhalter:

ACCOUNT_ID="paste-your-confirmed-account-id"
RUN="labex-c11-s07-$(openssl rand -hex 6)"
cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "$ACCOUNT_ID",
  "main": "src/server.ts",
  "compatibility_date": "2026-09-19",
  "compatibility_flags": ["nodejs_compat"],
  "workers_dev": true,
  "preview_urls": false,
  "observability": { "enabled": true }
}
JSON

Erstellen Sie den Namespace, ohne Wrangler die Datei automatisch ändern zu lassen:

npx wrangler kv namespace create "$RUN-cases" --update-config=false

Wenn Wrangler fragt, ob automatisch ein Binding hinzugefügt werden soll, wählen Sie No. Mit der nächsten Bearbeitung stellen Sie diese Verbindung ausdrücklich her. Kopieren Sie die 32-stellige Namespace-ID aus der Ausgabe und fügen Sie genau ein Binding hinzu:

NAMESPACE_ID="paste-the-created-namespace-id"
python3 - "$NAMESPACE_ID" <<'PY'
import json, sys
from pathlib import Path
path = Path('wrangler.jsonc')
data = json.loads(path.read_text())
data['kv_namespaces'] = [{'binding': 'SUPPORT_CASES', 'id': sys.argv[1]}]
path.write_text(json.dumps(data, indent=2) + '\n')
PY
npx wrangler kv namespace list
python3 .labex/verify.py authorization

Der Binding-Name SUPPORT_CASES ist die Kennung, die Ihr Code verwendet. Die Namespace-ID verweist auf die tatsächliche Ressource im bestätigten Konto. Es wurde noch nichts bereitgestellt.

Explizite synthetische Geschäftsdaten einfügen

In diesem Schritt legen Sie denselben bereitgestellten Datensatz in der lokalen und der entfernten KV ab. Der Datenspeicher ist ausdrücklich definiert: Eine MCP-Anfrage kann zustandslos sein, während die Anwendung weiterhin dauerhafte Geschäftsdaten anhand eines Schlüssels liest.

Prüfen Sie das Fixture, bevor Sie es hochladen:

cat fixtures/case.json

Das Präfix T-SYNTH-101 und die Markierung synthetic: true machen die Grenze der Demonstration sichtbar. Der Datensatz enthält keinen echten Kundennamen, keine echte E-Mail-Adresse, keine echte Nachricht und keine Zugangsdaten.

Befüllen Sie den lokalen Speicher, den wrangler dev verwendet:

npx wrangler kv key put case:T-SYNTH-101 --path fixtures/case.json --binding SUPPORT_CASES --local

Befüllen Sie den dedizierten Cloud-Namespace:

npx wrangler kv key put case:T-SYNTH-101 --path fixtures/case.json --binding SUPPORT_CASES --remote

Lesen Sie beide Kopien über das Binding:

npx wrangler kv key get case:T-SYNTH-101 --binding SUPPORT_CASES --local --text
npx wrangler kv key get case:T-SYNTH-101 --binding SUPPORT_CASES --remote --text
python3 .labex/verify.py catalog

Der Verifier prüft den Namespace anhand der Konto-ID, verlangt genau einen Schlüssel und vergleicht das entfernte JSON mit dem bereitgestellten synthetischen Fixture. KV kann zwischen verschiedenen Standorten letztlich konsistent werden. Falls beim ersten Lesen ein soeben geschriebener Wert kurzzeitig fehlt, warten Sie einige Sekunden und wiederholen Sie den Lesevorgang, statt mehrere Kopien zu schreiben.

Ein striktes schreibgeschütztes MCP-Tool registrieren

In diesem Schritt definieren Sie eine MCP-Server-Factory und ein schreibgeschütztes Abfragetool.

McpServer beschreibt die Protokolloberfläche. Die Factory erstellt für jede HTTP-Anfrage eine neue Instanz, während das Binding SUPPORT_CASES die ausdrücklich definierte Quelle der Geschäftsdaten bleibt. Erstellen Sie src/server.ts:

cat > src/server.ts <<'TS'
import { createMcpHandler } from "agents/mcp/server";
import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";

interface Env {
  SUPPORT_CASES: KVNamespace;
}

const lookupInput = z.object({
  ticketId: z.string().regex(/^T-SYNTH-[0-9]{3}$/, "use a synthetic ticket ID")
}).strict();

const storedCase = z.object({
  ticketId: z.string(),
  subject: z.string(),
  status: z.string(),
  priority: z.string(),
  product: z.string(),
  synthetic: z.literal(true)
}).strict();

function buildServer(env: Env): McpServer {
  const requestInstance = crypto.randomUUID();
  const server = new McpServer({
    name: "synthetic-support-catalog",
    version: "1.0.0"
  });

  server.registerTool("lookup_support_case", {
    title: "Look up a synthetic support case",
    description: "Read one synthetic demonstration case by its T-SYNTH identifier.",
    inputSchema: lookupInput,
    annotations: {
      readOnlyHint: true,
      destructiveHint: false,
      idempotentHint: true,
      openWorldHint: false
    }
  }, async ({ ticketId }) => {
    const raw = await env.SUPPORT_CASES.get(`case:${ticketId}`, "json");
    if (raw === null) {
      return {
        isError: true,
        content: [{ type: "text", text: `Synthetic case ${ticketId} was not found.` }]
      };
    }

    const record = storedCase.parse(raw);
    const result = { ...record, requestInstance };
    return {
      structuredContent: result,
      content: [{ type: "text", text: JSON.stringify(result) }]
    };
  });

  return server;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    if (url.pathname === "/health") {
      return Response.json({
        service: "synthetic-support-mcp",
        transport: "streamable-http",
        state: "stateless"
      });
    }
    if (url.pathname !== "/mcp") return new Response("Not found", { status: 404 });

    const handler = createMcpHandler(
      () => buildServer(env),
      { route: "/mcp", corsOptions: false, legacy: "stateless" }
    );
    return handler(request, env, ctx);
  }
};
TS
npm run check
python3 .labex/verify.py server

Hier sind drei Grenzen wichtig:

  • .strict() weist nicht deklarierte Felder zurück, statt sie stillschweigend zu akzeptieren.
  • Die Annotationen teilen Clients mit, dass das Tool einen abgeschlossenen synthetischen Katalog liest und keine destruktive Wirkung hat. Eine Annotation liefert nützliche Metadaten, ersetzt aber nicht die Codeprüfung, die bestätigt, dass kein put() oder delete() vorhanden ist.
  • requestInstance wird erzeugt, wenn die Factory einen Server erstellt. Verschiedene Protokollanfragen sollten unterschiedliche Kennungen zurückgeben. Dadurch wird der zustandslose Lebenszyklus sichtbar, ohne Sitzungsdaten zu speichern.

Die Kompatibilitätseinstellung legacy: "stateless" verwendet weiterhin Streamable HTTP. Sie erlaubt aktuellen Clients, die die Protokollfamilie von 2025 aushandeln, und stellt zugleich sicher, dass jede Anfrage eine neue Serverinstanz erhält. Es wird weder eine SSE-Route noch eine dauerhafte MCP-Sitzung erstellt.

Eine unabhängige MCP-Client-Prüfung erstellen

In diesem Schritt verwenden Sie die offizielle Client-Bibliothek, statt JSON-RPC manuell zu schreiben. Ein echter Client führt die Protokollinitialisierung, die Tool-Ermittlung und den Aufruf über StreamableHTTPClientTransport aus.

Erstellen Sie scripts/test-client.mjs:

cat > scripts/test-client.mjs <<'JS'
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";

const endpoint = process.argv[2];
if (!endpoint) throw new Error("usage: node scripts/test-client.mjs <mcp-url>");

async function withClient(label, action) {
  const transport = new StreamableHTTPClientTransport(new URL(endpoint));
  const client = new Client({ name: `labex-${label}`, version: "1.0.0" });
  try {
    await client.connect(transport);
    return await action(client);
  } finally {
    await client.close();
  }
}

const tools = await withClient("discovery", (client) => client.listTools());
const tool = tools.tools.find((item) => item.name === "lookup_support_case");
if (!tool || tool.annotations?.readOnlyHint !== true) {
  throw new Error("the read-only lookup tool was not discoverable");
}
console.log("DISCOVERED lookup_support_case");

async function lookup(ticketId) {
  return withClient(`lookup-${ticketId.toLowerCase()}`, (client) => client.callTool({
    name: "lookup_support_case",
    arguments: { ticketId }
  }));
}

const first = await lookup("T-SYNTH-101");
const second = await lookup("T-SYNTH-101");
const a = first.structuredContent;
const b = second.structuredContent;
if (!a || !b || a.synthetic !== true || a.status !== "investigating") {
  throw new Error("the valid synthetic record was not returned");
}
console.log(`VALID synthetic=${a.synthetic} status=${a.status}`);

const missing = await lookup("T-SYNTH-404");
console.log(`MISSING isError=${missing.isError === true}`);

let invalidRejected = false;
try {
  const invalid = await withClient("invalid", (client) => client.callTool({
    name: "lookup_support_case",
    arguments: { ticketId: "REAL-101", unexpected: "must-not-pass" }
  }));
  invalidRejected = invalid.isError === true;
} catch {
  invalidRejected = true;
}
console.log(`INVALID_REJECTED ${invalidRejected}`);

const stateless = typeof a.requestInstance === "string"
  && typeof b.requestInstance === "string"
  && a.requestInstance !== b.requestInstance;
console.log(`STATELESS ${stateless}`);

if (missing.isError !== true || !invalidRejected || !stateless) process.exitCode = 1;
JS
python3 .labex/verify.py client

Jeder Hilfsaufruf erstellt und schließt seinen eigenen Client-Transport. Die Ermittlung bestätigt, dass der Server den Tool-Vertrag bekannt gibt. Zwei gültige Aufrufe müssen denselben KV-Datensatz lesen, aber unterschiedliche Kennungen der Anfrageinstanz zurückgeben. Der fehlende Fall ist ein normaler Fehler auf Tool-Ebene, während eine ungültige Kennung vom Eingabeschema zurückgewiesen wird, bevor der Handler KV liest.

Den MCP-Vertrag lokal testen

In diesem Schritt starten Sie den Worker mit lokalem KV und führen die vollständige Client-Prüfung aus, bevor Sie den bereitgestellten Endpunkt verwenden.

Starten Sie den Entwicklungsserver:

npx wrangler dev --ip 127.0.0.1 --port 8787

Lassen Sie dieses Terminal geöffnet. Öffnen Sie ein zweites Terminal, wechseln Sie in dasselbe Projekt und prüfen Sie die kleine Health-Route:

cd /home/labex/project/read-only-mcp-tool
curl --fail --silent http://127.0.0.1:8787/health | python3 -m json.tool

Erwarten Sie transport: "streamable-http" und state: "stateless". Führen Sie nun den Protokoll-Client aus:

node scripts/test-client.mjs http://127.0.0.1:8787/mcp

Die fünf Nachweiszeilen sollten die Ermittlung, das gültige synthetische Ergebnis, einen sicheren Fehler für den fehlenden Fall, die Ablehnung ungültiger Eingaben und STATELESS true anzeigen. Wechseln Sie nach der Prüfung zum ersten Terminal und drücken Sie Ctrl+C.

Führen Sie die unabhängige Prüfung aus. Sie startet einen weiteren begrenzten lokalen Worker auf Port 8791, testet denselben importierten Code und beendet ihn automatisch:

python3 .labex/verify.py local

Den Remote-MCP-Endpunkt bereitstellen und testen

In diesem Schritt stellen Sie den Worker mit seinem ausdrücklich definierten KV-Binding bereit und führen denselben Client gegen den echten workers.dev-Endpunkt aus.

Stellen Sie den Worker anhand der Projektkonfiguration bereit:

npx wrangler deploy

Kopieren Sie die angezeigte Bereitstellungs-URL und speichern Sie sie ohne abschließenden Schrägstrich:

WORKER_URL="https://your-generated-worker.your-subdomain.workers.dev"

Prüfen Sie die Health-Route und verbinden Sie anschließend den MCP-Client mit /mcp:

curl --fail --silent "$WORKER_URL/health" | python3 -m json.tool
node scripts/test-client.mjs "$WORKER_URL/mcp"
python3 .labex/verify.py deployed

Der unabhängige Verifier leitet den Endpunkt aus dem ausgewählten Konto ab, statt der Shell-Variable zu vertrauen. Außerdem prüft er das bereitgestellte SUPPORT_CASES-Binding, den exakten entfernten Datensatz und alle fünf MCP-Verhaltensweisen. Eine erreichbare Health-Route allein genügt nicht: Ermittlung und Aufruf müssen über den Protokoll-Client erfolgreich sein.

Öffnen Sie Workers & Pages und wählen Sie den erstellten Worker aus. Die Übersicht sollte die workers.dev-Domain mit dem Worker verbinden und ein SUPPORT_CASES-KV-Binding anzeigen. Die folgenden Werte stammen beispielhaft aus dem getesteten Durchlauf. Ihre eindeutigen Ressourcennamen und Zählerstände weichen ab.

Der bereitgestellte MCP-Worker mit einem SUPPORT_CASES-KV-Binding

Die eigenen Ressourcen prüfen und entfernen

In diesem Schritt prüfen Sie den sichtbaren Cloud-Zustand und löschen anschließend ausschließlich den Worker und den KV-Namespace dieses Durchlaufs, solange Wrangler noch autorisiert ist.

Öffnen Sie das Cloudflare-Dashboard und wählen Sie dasselbe Lernkonto aus. Öffnen Sie unter Workers & Pages den Worker, dessen Name mit labex-c11-s07- beginnt. Bestätigen Sie, dass die letzte Bereitstellung fehlerfrei ist, die Beobachtung aktiviert ist und das SUPPORT_CASES-Binding auf die Namespace-ID in wrangler.jsonc verweist.

Öffnen Sie Storage & databases > KV, wählen Sie den passenden -cases-Namespace aus und prüfen Sie case:T-SYNTH-101. Der Wert ist das synthetische Fixture. Fügen Sie keine persönlichen Daten hinzu. Diese Dashboard-Ansichten dienen der Orientierung; der Client und der Verifier bleiben die maßgeblichen funktionalen Nachweise.

Die Ansicht „KV Pairs“ zeigt zunächst den exakten Schlüssel und eine Vorschau seines JSON-Werts:

Der dedizierte Namespace enthält nur den synthetischen Supportfall-Schlüssel

Klappen Sie die Zeile auf, um diesen Schlüssel mit den vom MCP-Tool zurückgegebenen Feldern zu verbinden. Das getestete Fixture verwendet status: investigating, priority: medium und synthetic: true.

Das aufgeklappte synthetische Supportfall-JSON in KV

Kehren Sie zum Worker zurück und öffnen Sie Observability. Erfolgreiche POST /mcp- und Transport-GET /mcp-Ereignisse zeigen, dass ein echter Remote-MCP-Client den bereitgestellten Worker erreicht hat. Im getesteten Durchlauf waren alle 42 erfassten Ereignisse erfolgreich und keines erzeugte einen Worker-Fehler. Ihre Anzahl von Anfragen kann abweichen.

Cloudflare Observability zeigt erfolgreiche Remote-MCP-Anfragen ohne Fehler

Führen Sie vor dem Löschen noch eine unabhängige Beobachtungsprüfung aus:

python3 .labex/verify.py observed
cat wrangler.jsonc

Bestätigen Sie den exakten eindeutigen Workernamen und die Namespace-ID. Löschen Sie anschließend den Worker:

npx wrangler delete

Wenn Sie dazu aufgefordert werden, prüfen Sie den angezeigten Workernamen und antworten Sie mit y. Löschen Sie ausschließlich den Namespace, der vom SUPPORT_CASES-Binding ausgewählt wird:

npx wrangler kv namespace delete --binding SUPPORT_CASES
npx wrangler kv namespace list
python3 .labex/verify.py deleted

Aktualisieren Sie die Listen der Worker sowie der KV-Ressourcen im Dashboard. Beide Ressourcen mit dem Präfix labex-c11-s07-... sollten fehlen, während nicht zugehörige Ressourcen erhalten bleiben. Eine fehlgeschlagene Anfrage an den Endpunkt beweist keine Löschung. Der Verifier prüft die Inventare des autorisierten Kontos direkt.

Suchen Sie nach dem exakt erzeugten Workernamen. Ein leeres Ergebnis bestätigt, dass das Dashboard den Worker nicht mehr auflistet:

Workers and Pages zeigt kein Projekt mit dem Namen des entfernten Lab-Workers

Suchen Sie unter Workers KV nach dem exakten -cases-Namespace. Der leere Zustand und 0 B aktueller Speicher bestätigen, dass der löschbare Katalog ebenfalls aus diesem sauberen Testkonto entfernt wurde:

Workers KV zeigt keinen Namespace mit dem Namen des entfernten synthetischen Katalogs

Die Autorisierung dieser VM widerrufen

In diesem Schritt widerrufen Sie die temporäre Autorisierung der VM, nachdem das Fehlen der Ressourcen nachgewiesen wurde.

npx wrangler logout
npx wrangler whoami --json || true

Das strukturierte Ergebnis sollte loggedIn: false melden. Alternativ kann Wrangler ein Ergebnis ohne Authentifizierung mit einem von null verschiedenen Exit-Code zurückgeben. Die Abmeldung erfolgt absichtlich zuletzt: Für die Löschprüfung ist noch Lesezugriff auf das ausgewählte Konto erforderlich, während die löschbare VM ihn nicht mehr benötigt.

Zusammenfassung

Sie haben einen begrenzten schreibgeschützten MCP-Dienst auf Cloudflare veröffentlicht und entfernt. Sie haben:

  • synthetische Geschäftsdaten in einem eigenen KV-Namespace statt in einem impliziten MCP-Sitzungszustand gespeichert;
  • ein auffindbares Tool mit strikter Eingabevalidierung und schreibgeschützten Annotationen registriert;
  • es über den aktuellen zustandslosen Streamable-HTTP-Handler bereitgestellt;
  • einen echten MCP-Client für Ermittlung, gültige Abfrage sowie Tests auf fehlende Datensätze und ungültige Eingaben verwendet;
  • nachgewiesen, dass unabhängige Anfragen neue Serverinstanzen erhalten und dabei dieselben ausdrücklich gespeicherten Daten lesen;
  • den Zustand von Worker und KV geprüft, beide eigenen Ressourcen gelöscht und die VM-Autorisierung widerrufen.

Die wichtigste Design-Erkenntnis lautet: Zustandsloser Transport bedeutet nicht, dass die Anwendung datenfrei ist. Es bedeutet, dass Protokollanfragen nicht von verborgenem Sitzungszustand abhängen. Dauerhafte Geschäftsdaten bleiben ausdrücklich definiert, abgegrenzt und unabhängig verwaltet.