Verbindung von MCP-Tools mit einer React-Chat-Oberfläche unter Einbeziehung einer integrierten menschlichen Überprüfung
Erfahren Sie, wie das Model Context Protocol in eine React-Anwendung passt: Warum der Backend-Server MCP hosten sollte, wie ein Tool Server funktioniert und wie Anrufe von Tools in der Benutzeroberfläche gestreamt und genehmigt werden können.
Die KI-Funktionen in einer React-Anwendung entwickeln sich in der Regel Schritt für Schritt durch individuelle Integrationen – jede mit eigenem SDK, Authentifizierungsmechanismus, Fehlerbehandlung und Datenkoppelung, wobei alle Elemente eng miteinander verbunden sind. Das Model Context Protocol (MCP), ein offenes Protokoll, das von Anthropic eingeführt und heute weit verbreitet wird, ersetzt dieses Verwirrungspotenzial durch eine einheitliche Schnittstelle zwischen KI-Anwendungen und den Tools sowie Daten, die sie nutzen; eine gängige Analogie ist hierfür ein USB-C-Anschluss für KI. Dieser Artikel erläutert, welche Rolle MCP in einer React-Architektur einnimmt, zeigt einen kleinen Datenbank-Tool-Server sowie einen Backend-Host und entwickelt anschließend den Teil, der bei React-Entwicklern liegt: eine Chat-Schnittstelle, die Tool-Aufrufe streamt und den Benutzer um Genehmigung bittet.
Falls Sie zunächst einen Einführungstext auf Protokollebene zu Entdeckung und Aufruf benötigen, lesen Sie wie MCP es KI-Agenten ermöglicht, Tools zu entdecken und aufzurufen. Der Fokus liegt hier auf der Anwendungs- und Benutzeroberflächenseite.
Was MCP standardisiert
MCP definiert, wie Anwendungen Kontext und Funktionen an große Sprachmodelle bereitstellen. Es unterscheidet drei Rollen: Der Host ist Ihre Anwendung, ein Client ist der Connector innerhalb des Hosts, der eine Sitzung mit einem Server aufrechterhält, und die Server stellen die Tools sowie Datenquellen bereit. Ein Host führt in der Regel einen Client pro verbundenem Server aus.
Ohne MCP sieht die Abhängigkeitsliste einer mit KI ausgestatteten React-Anwendung oft so aus:
React App
├── OpenAI SDK (for chat)
├── Anthropic SDK (for reasoning)
├── LangChain (for RAG)
├── Custom API Client (for your database)
└── Custom API Client (for your CRM)
Jeder Eintrag verfügt über seine eigene Authentifizierung, Fehlerbehandlung und Schemamapping; eine neue Datenquelle bedeutet einen neuen Endpunkt sowie einen neuen Frontend-Service.
Mit MCP reduziert sich die Integration auf ein einziges Muster:
React App (Host)
└── MCP Client
├── MCP Server: File System
├── MCP Server: PostgreSQL
├── MCP Server: Slack
├── MCP Server: Your Internal API
└── MCP Server: Any Future Tool
Jeder Server verwendet dasselbe Protokoll. Der Host muss weder PostgreSQL noch die Struktur der Slack-API verstehen; er stellt zwei allgemeine Fragen: „Welche Tools sind verfügbar?“ und „Führe dieses Tool mit diesen Argumenten aus?“, wobei das Protokoll den Rest übernimmt. Beachten Sie, was MCP nicht tut: Es standardisiert die Verbindung zu Tools und Daten, nicht jedoch die Wahl des Modells. Der Host kommuniziert weiterhin mit dem jeweiligen LLM-Anbieter, den Sie verwenden.
Die drei Primitiven
Ihre React-Anwendung interagiert mit drei Arten von Serverfunktionen – direkt oder, was häufiger der Fall ist, über Ihren Backend-Service.
Tools
Tools sind Funktionen, die ein Modell aufrufen kann. Jeder Tool hat einen Namen, eine Beschreibung sowie ein JSON Schema, das seine Parameter beschreibt. Wenn ein Benutzer fragt, wie viele Konten gestern erstellt wurden, muss das Modell nicht raten: Es kann ein Tool wie query_user_signups finden, das einen date-Wert entgegennimmt, es aufrufen und auf der Grundlage des Ergebnisses antworten. Tools sind es, die eine Frage des Benutzers in Alltagssprache mit den Daten hinter Ihrer Anwendung verbinden.
Ressourcen
Ressourcen sind Kontextelemente, die das Modell lesen kann – beispielsweise eine Datei, ein Datenbankeintrag oder ein Chat-Thread. Jede Ressource wird durch eine URI adressiert, wie zum Beispiel file:///docs/spec.pdf oder db://users/123; das Lesen dieser Ressourcen ermöglicht es dem Modell, seine Antworten auf tatsächlichen Daten statt auf seiner Trainingsdatenbasis zu stützen.
Aufforderungen
Prompts sind wiederverwendbare Vorlagen, die von einem Server veröffentlicht werden. Ein Server kann beispielsweise einen code_review-Prompt anbieten, der ein Argument file_path erfordert; der Host lädt die Vorlage herunter, füllt das Argument ein und sendet das Ergebnis an das Modell.
Warum die Frontend-Plattform mehr ist als nur ein Anzeigefeld
Die Durchleitungs-Frontend-Plattform
In vielen KI-Apps sendet der React-Klient die Nachricht des Benutzers an eine Node- oder FastAPI-Backend-Plattform, welche sie an den Modellanbieter weiterleitet, auf die Antwort wartet und diese anschließend zurücksendet. Falls das Modell ein Werkzeug benötigt, kümmert sich die Backend-Plattform ebenfalls darum. Die Frontend-Plattform ist ein passiver Textanzeiger: Sie hat keine Kenntnis davon, was das Modell tut, und kann nicht eingreifen.
Die Frontend-Plattform als Steueroberfläche
Durch Tool-Aufrufe im MCP-Stil kann die Benutzeroberfläche diese in Echtzeit anzeigen und dem Benutzer ermöglichen, sensible Operationen vor deren Ausführung zu genehmigen oder abzulehnen. Egal, ob der Browser die MCP-Verbindungen selbst hostet oder, was häufiger der Fall ist, einen strukturierten Datenstrom von einem Backend-Host erhält – React sorgt dafür, dass die Orchestrierung sichtbar und steuerbar bleibt.
Benutzer erwarten zunehmend diese Kontrolle: Sie möchten sehen, dass der Assistent ihre Daten abfragen will, und diese zuerst genehmigen. Diese Funktionalität ist in React integriert.
Wo der MCP-Host platziert werden sollte
Es gibt zwei praktikable Architekturen. Wählen Sie je nach Sicherheits- und Latenzanforderungen zwischen ihnen.
MCP über ein Backend – die Standardlösung
In diesem Muster kommuniziert die React-App nur mit Ihrem Backend, wobei das Backend der MCP-Host ist. Er hält Verbindungen zu den MCP-Servern offen, kümmert sich um die Authentifizierung und leitet die Aktivitäten der Tools an das Frontend weiter:
React (Client) <--SSE/WS--> FastAPI/Node (MCP Host) <--stdio/SSE--> MCP Servers
Die Vorteile sind für die meisten Produkte entscheidend:
- Sicherheit: Die Anmeldeinformationen für die MCP-Server bleiben auf dem Server und erreichen niemals den Browser.
- Zustand: Persistente Datenbank- und Dateisystem-Sessions werden auf dem Server aufbewahrt, wo sie hingehören.
- Auditorien: Jeder Tool-Aufruf kann protokolliert, mit einer Aufrufrate begrenzt und einem bestimmten Benutzer zugeordnet werden.
Das Frontend verarbeitet einen strukturierten Stream von Ereignissen (Textblöcke, Tool-Aufrufanfragen, Tool-Ergebnisse sowie die endgültige Antwort) und rendernt jeden Zustand gezielt.
Hinweis zu den Transportmethoden: Das Diagramm zeigt stdio zwischen Host- und lokalen Servern sowie SSE für entfernte Server. Die MCP-Spezifikation hat im Laufe der Zeit ihren HTTP-Transport weiterentwickelt, daher sollten Sie die aktuelle Spezifikation sowie die SDK-Dokumente prüfen, um den empfohlenen Transport für entfernte Server zu erfahren.
Browser-eigener MCP – für spezielle Fälle
Alternativ kann die React-App direkt über HTTP-basiertes Streaming mit entfernten MCP-Servern verbunden werden. Dies funktioniert zwar, wird aber von Produktionsteams selten gewählt, da die Datenschicht sowie die dazugehörigen Zugangsdaten vom Browser aus erreichbar sind. Verwenden Sie diese Methode lieber für lokale Entwicklerwerkzeuge oder vollständig clientseitige KI-Apps, die keine sensiblen Daten verarbeiten.
Ein Datenbankwerkzeug-Server in TypeScript
Das Erstellen eines kleinen Servers ist der schnellste Weg, um das Protokoll zu verstehen. Das untenstehende Beispiel stellt eine PostgreSQL users-Tabelle über zwei Tools mithilfe des offiziellen TypeScript SDK zur Verfügung. Es ist in drei Teile unterteilt. Zunächst wird ein Connection Pool sowie ein MCP Server erstellt, der die tools-Funktionalität anbietet. Anschließend beschreibt der Handler ListToolsRequestSchema jedes Tool mit einem Namen, einer Beschreibung sowie einem inputSchema, das vom Modell zur Entscheidung darüber herangezogen wird, welches Tool aufgerufen werden soll. Drittens leitet der Handler CallToolRequestSchema die Anfrage anhand des Toolnamens weiter, führt eine parametrisierte Abfrage aus und gibt die Ergebniszeilen als Textinhalt zurück – oder gibt bei einem Fehler isError: true zusammen mit einer Nachricht aus. Schließlich verbindet sich der Server über stdio, sodass ein Host ihn als Kindprozess starten kann.
// mcp-servers/database-server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const server = new Server(
{
name: "postgres-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query_users",
description: "Query the users table with filters",
inputSchema: {
type: "object",
properties: {
limit: { type: "number", description: "Max results" },
status: { type: "string", enum: ["active", "inactive"] },
},
required: ["limit"],
},
},
{
name: "get_user_by_email",
description: "Find a user by their email address",
inputSchema: {
type: "object",
properties: {
email: { type: "string" },
},
required: ["email"],
},
},
],
};
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
if (name === "query_users") {
const result = await pool.query(
"SELECT id, email, status, created_at FROM users WHERE status = $1 LIMIT $2",
[args.status || "active", args.limit]
);
return {
content: [
{
type: "text",
text: JSON.stringify(result.rows, null, 2),
},
],
};
}
if (name === "get_user_by_email") {
const result = await pool.query(
"SELECT * FROM users WHERE email = $1",
[args.email]
);
return {
content: [
{
type: "text",
text: JSON.stringify(result.rows[0] || null, null, 2),
},
],
};
}
throw new Error(`Unknown tool: ${name}`);
} catch (error) {
return {
content: [
{
type: "text",
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
Achten Sie darauf, was das Design vermeidet: Das Modell sendet niemals rohen SQL-Code. Es kann nur zwischen spezifischen, benannten Operationen wählen, und die Abfragen verwenden Platzhalter ($1, $2), sodass Argumente keinen SQL-Code injizieren können. Das Zurückgeben von Fehlern zusammen mit isError ermöglicht es dem Modell, den Fehler zu erkennen und zu erklären, anstatt dass die gesamte Sitzung abstürzt.
Vor dem echten Einsatz solcher Lösungen sollten einige Aspekte überprüft werden. Das JSON Schema beschreibt die Eingaben, doch der Handler sollte dennoch args selbst validieren (zum Beispiel mit einer Schema-Bibliothek), da dieser fehlen oder fehlerhaft sein könnte; außerdem sollte args.limit begrenzt werden. Die Funktion get_user_by_email führt SELECT * aus, wodurch jede Spalte an das Modell übergeben wird – einschließlich sensibler Daten wie Passworthashes – daher sollten stattdessen explizit nur die benötigten Spalten ausgewählt werden. In strengem TypeScript ist error im Catch-Block vom Typ unknown, weshalb dieser vor dem Auslesen von .message überprüft werden muss.
Ein Backend-Host in Python
Die React-App kommuniziert niemals direkt mit diesem Server – das übernimmt der Backend. Hier ist eine minimale Host-Klasse, die das Python MCP SDK verwendet. connect() beschreibt, wie der Serverprozess gestartet wird (command, args sowie eine Umgebung, die DATABASE_URL übermittelt), wie ein stdio-Client geöffnet wird, eine ClientSession initiiert wird, wie durch initialize() die Protokollverbindung hergestellt wird und anschließend list_tools() aufgerufen wird, um herauszufinden, was der Server anbietet. execute_tool() leitet einen Tool-Name sowie Argumente weiter und gibt das erste Textelement aus dem Ergebnis zurück, während close() die Session sowie den Prozess beendet.
# backend/mcp_host.py
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
import asyncio
import json
class MCPHost:
def __init__(self):
self.session = None
self.tools = []
async def connect(self):
server_params = StdioServerParameters(
command="node",
args=["mcp-servers/database-server.ts"],
env={"DATABASE_URL": os.getenv("DATABASE_URL")}
)
self._client = stdio_client(server_params)
self._read, self._write = await self._client.__aenter__()
self.session = await ClientSession(self._read, self._write).__aenter__()
await self.session.initialize()
# Discover available tools
tools_result = await self.session.list_tools()
self.tools = [tool.name for tool in tools_result.tools]
async def execute_tool(self, tool_name: str, arguments: dict):
result = await self.session.call_tool(tool_name, arguments)
return result.content[0].text if result.content else None
async def close(self):
await self.session.__aexit__(None, None, None)
await self._client.__aexit__(None, None, None)
Der Codeausschnitt muss vor dem Ausführen repariert werden. Er verwendet os.getenv, ohne os zu importieren. Er startet node mit einer .ts-Datei, was nur funktioniert, wenn Ihre Node-Version TypeScript direkt ausführen kann; andernfalls muss der Server zunächst in JavaScript kompiliert werden oder ein für TypeScript geeigneter Runner verwendet werden. Das Manuellaufrufen von __aenter__ und __aexit__ funktioniert zwar, doch async with-Blöcke oder ein AsyncExitStack sind sicherer, da sie eine saubere Aufräumung bei Fehlern gewährleisten. Beachten Sie außerdem, dass die dem Kindprozess übergebene Umgebung die des Elternprozesses ersetzt, weshalb alle weiteren benötigten Elemente wie PATH mit übergeben werden müssen.
Die React-Seite: Streaming und Freigabe von Toolaufrufen
Hier erledigen React-Entwickler ihre einzigartigsten Arbeiten. Der Backend streamt Ereignisse, die nicht nur Text enthalten, sondern auch Anfragen an Werkzeuge sowie deren Ergebnisse, und die Benutzeroberfläche wandelt sie in interaktive Elemente um.
Die untenstehende Komponente ChatInterface speichert eine Liste von Nachrichten, wobei jede Nachricht toolCalls enthalten kann, deren Status entweder pending, approved, rejected oder completed ist. Wenn der Benutzer eine Nachricht sendet, fügt sie die Eingabe des Benutzers hinzu, öffnet einen EventSource zu /api/chat und erstellt im Laufe des Eintreffens von Ereignissen eine Assistentenantwort. Ein text-Ereignis fügt Inhalt hinzu, ein tool_call-Ereignis fügt einen ausstehenden Toolaufruf hinzu, und ein tool_result-Ereignis speichert das Ergebnis und markiert den entsprechenden Aufruf als completed. Nach jedem Ereignis ersetzt sie die Assistentenantwort im Zustand durch eine neue Kopie, sodass React neu gerendert wird. approveToolCall sendet die Entscheidung an /api/chat/approve-tool und setzt den Status des Aufrufs optimistisch auf approved.
// components/ChatInterface.tsx
"use client";
import { useState, useRef, useCallback } from "react";
import { ToolCallCard } from "./ToolCallCard";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
toolCalls?: ToolCall[];
toolResults?: ToolResult[];
}
interface ToolCall {
id: string;
name: string;
arguments: Record<string, any>;
status: "pending" | "approved" | "rejected" | "completed";
}
export function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const eventSourceRef = useRef<EventSource | null>(null);
const sendMessage = useCallback(async (content: string) => {
// Add user message
const userMsg: Message = {
id: `user-${Date.now()}`,
role: "user",
content,
};
setMessages((prev) => [...prev, userMsg]);
// Open SSE connection to backend
const es = new EventSource(
`/api/chat?message=${encodeURIComponent(content)}`
);
eventSourceRef.current = es;
let assistantMsg: Message = {
id: `assistant-${Date.now()}`,
role: "assistant",
content: "",
toolCalls: [],
};
es.onmessage = (event) => {
const chunk = JSON.parse(event.data);
switch (chunk.type) {
case "text":
assistantMsg.content += chunk.text;
setMessages((prev) => {
const filtered = prev.filter((m) => m.id !== assistantMsg.id);
return [...filtered, { ...assistantMsg }];
});
break;
case "tool_call":
// Model wants to call a tool
assistantMsg.toolCalls = [
...(assistantMsg.toolCalls || []),
{
id: chunk.tool_call_id,
name: chunk.name,
arguments: chunk.arguments,
status: "pending",
},
];
setMessages((prev) => {
const filtered = prev.filter((m) => m.id !== assistantMsg.id);
return [...filtered, { ...assistantMsg }];
});
break;
case "tool_result":
// Tool execution completed
assistantMsg.toolResults = [
...(assistantMsg.toolResults || []),
{
toolCallId: chunk.tool_call_id,
result: chunk.result,
},
];
// Update the specific tool call status
assistantMsg.toolCalls = assistantMsg.toolCalls?.map((tc) =>
tc.id === chunk.tool_call_id
? { ...tc, status: "completed" }
: tc
);
setMessages((prev) => {
const filtered = prev.filter((m) => m.id !== assistantMsg.id);
return [...filtered, { ...assistantMsg }];
});
break;
}
};
es.onerror = () => {
es.close();
};
}, []);
const approveToolCall = useCallback(
async (messageId: string, toolCallId: string) => {
// Send approval to backend
await fetch("/api/chat/approve-tool", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messageId, toolCallId }),
});
// Optimistically update UI
setMessages((prev) =>
prev.map((msg) => {
if (msg.id !== messageId) return msg;
return {
...msg,
toolCalls: msg.toolCalls?.map((tc) =>
tc.id === toolCallId ? { ...tc, status: "approved" } : tc
),
};
})
);
},
[]
);
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto">
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${
msg.role === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-lg p-4 ${
msg.role === "user"
? "bg-blue-600 text-white"
: "bg-gray-100 text-gray-900"
}`}
>
<p className="whitespace-pre-wrap">{msg.content}</p>
{msg.toolCalls?.map((tool) => (
<ToolCallCard
key={tool.id}
tool={tool}
onApprove={() => approveToolCall(msg.id, tool.id)}
/>
))}
</div>
</div>
))}
</div>
<div className="border-t p-4">
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage(input);
setInput("");
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about your data..."
className="w-full rounded-lg border px-4 py-2"
/>
</form>
</div>
</div>
);
}
Jeder Tool-Aufruf wird durch ein kleines Präsentationskomponente dargestellt, das den Namen des Tools, seinen Status, die Argumente in formatiertem JSON-Format sowie – solange der Aufruf aussteht – die Schaltflächen „Zustimmen“ und „Ablehnen“ anzeigt:
// components/ToolCallCard.tsx
interface ToolCallCardProps {
tool: {
name: string;
arguments: Record<string, any>;
status: string;
};
onApprove: () => void;
}
export function ToolCallCard({ tool, onApprove }: ToolCallCardProps) {
return (
<div className="mt-3 rounded border border-yellow-300 bg-yellow-50 p-3">
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-yellow-800">
🔧 Tool Request: {tool.name}
</span>
<span className="text-xs text-yellow-600 uppercase">
{tool.status}
</span>
</div>
<pre className="mt-2 text-xs bg-white p-2 rounded overflow-x-auto">
{JSON.stringify(tool.arguments, null, 2)}
</pre>
{tool.status === "pending" && (
<div className="mt-3 flex gap-2">
<button
onClick={onApprove}
className="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700"
>
Approve
</button>
<button className="px-3 py-1 bg-red-600 text-white text-sm rounded hover:bg-red-700">
Reject
</button>
</div>
)}
</div>
);
}
Genau hier zeigt sich der Nutzen der Abstraktion. Die Benutzeroberfläche hat keine Ahnung, dass query_users gegen PostgreSQL ausgeführt wird, und sie muss sich auch nicht ändern, wenn morgen ein Tool namens search_slack hinzukommt. Sie weiß lediglich, dass ein Tool-Aufruf wartet, welche Argumente er enthält und dass ein Mensch eine Entscheidung treffen muss.
Lücken, die vor der Produktion geschlossen werden müssen
Das Beispiel veranschaulicht die Struktur der Benutzeroberfläche, doch es gibt einige Lücken, die geschlossen werden sollten:
- Die Genehmigung muss auf dem Server durchgesetzt werden. Der Backend muss den Tool-Aufruf zurückhalten, bis er eine Genehmigung für genau diesen Aufruf-ID erhält, der mit dem authentifizierten Benutzer verknüpft ist. Die optimistische Statusänderung in der UI dient lediglich als Feedback; wie beschrieben, hindert nichts daran, dass ein
tool_resultunabhängig vom Button eingehend wird. - Die Schaltfläche „Ablehnen“ verfügt über keinen Handler. Verbinden Sie sie mit einem Endpoint, der dem Host mitteilt, den Aufruf abzubrechen und es dem Modell ermöglicht, ohne Ergebnis weiterzuarbeiten.
EventSourcesendet nur GET-Anfragen, wodurch die Nachricht des Benutzers in der Abfragezeichenkette übertragen wird, wo sie den URL-Längenbeschränkungen unterliegt und in Server- sowie Proxy-Logs landen kann. Eine POST-Anfrage, die mitfetcheinen gestreamten Antwortkörper liest, vermeidet beide Probleme.
onerror-Fehler an, anstatt sie stumm zu schließen.Checkliste für die MCP-Integration in React-Teams
Bereinigen Sie diese architektonischen Fragen vor der MCP-Integration:
Wer ist für den MCP-Client verantwortlich?
In der Produktion ist es der Backend. MCP-Server benötigen in der Regel Anmeldeinformationen, persistente Verbindungen sowie stateful Sessions. Die React-App sollte einen für die UI konzipierten, strukturierten Ereignisstrom erhalten und nicht rohe Protokollnachrichten.
Wie werden Tool-Aufrufe genehmigt?
Lassen Sie das Modell niemals zerstörerische Tools ausführen, ohne ausdrückliche Bestätigung. Wenn es beispielsweise delete_user anfordert, muss die Benutzeroberfläche einen BestätigungsSchritt anzeigen. Dabei geht es genauso sehr um das Vertrauen der Nutzer wie um die Sicherheit. Entwerfen Sie den Chat so, dass das Streamen pausiert, sobald ein Toolaufruf eingeht, und erst wieder fortgesetzt wird, nachdem der Nutzer zugestimmt hat – und wie bereits erwähnt, muss diese Pause auf dem Server durchgesetzt werden.
Wie werden teilweise Zustände gestreamt?
Verwenden Sie SSE oder WebSockets. Eine einzige Antwort durchläuft mehrere Phasen: Das Modell überlegt, fordert ein Tool an, wartet darauf und setzt anschließend fort. Die Benutzeroberfläche sollte jede Phase klar darstellen, indem sie einen Fortschrittsanzeiger, Karten für die Toolaufrufe sowie die Ergebnisse der Tools als strukturierte Daten statt als unstrukturierten Text anzeigt.
Wie werden Fehler angezeigt?
MCP-Server versagen: Die Datenbankverbindungen werden unterbrochen und die Dateisystemserver weisen Berechtigungsfehler auf. Die Benutzeroberfläche sollte diese als strukturierte Fehlerereignisse erhalten und sie als lösbare Probleme darstellen, niemals als kaputten Bildschirm.
Wie werden Tools entdeckt?
Die Anwendung sollte sich an die verfügbaren Tools anpassen. Wenn der Host mit einem neuen Server verbunden wird, sollte er die aktualisierte Tool-Liste an die Benutzeroberfläche übermitteln, die anschließend den aktuellen Funktionsumfang für die Benutzer auflisten kann – beispielsweise das Abfragen von Konten, das Suchen in Dokumenten oder das Ausführen von Analyseabfragen.
Welche Änderungen bringt MCP für die Arbeit der Benutzeroberfläche?
Ohne ein gemeinsames Protokoll benötigt jede Datenquelle, Modulintegration und jedes Tool eigene Verbindungsmechanismen – wie ein Schubfach voller nicht kompatibler Ladegeräte. MCP standardisiert diese Verbindungen: Daten werden zu durch Schemata beschriebenen Tools, die Hosts konsumieren sie über eine einzige Schnittstelle, und die Benutzeroberfläche präsentiert jeden Aufruf als interaktives Element. In der Praxis:
- Neuere Funktionen können ohne Änderungen am Frontend hinzugefügt werden. Man verbindet einfach einen neuen MCP-Server mit dem Host, und eine generische Tool-Aufruf-Benutzeroberfläche kann die Tools sofort anzeigen.
- Die Benutzeroberfläche ist vom Modell getrennt. Da sie einen stabilen Ereignisstrom darstellt, ist das Wechseln der Modellanbieter eine Sache des Backends; MCP hält die Tool-Seite konstant, während der Host die anbieterbezogenen Modellaufrufe handhabt.
- Ein Chat-Komponent, der Tool-Aufrufe und Genehmigungen versteht, leistet weitaus mehr als einer, der nur Markdown darstellt.
Kernpunkte
- Hosten Sie MCP im Backend, wo Zugangsdaten, Verbindungen und Prüfprotokolle gespeichert werden, und streamen Sie strukturierte Ereignisse an React.
- Bauen Sie Tool-Server aus eng definierten, validierten und parametrisierten Abläufen, die nur das liefern, was das Modell benötigt.
- Setzen Sie Genehmigungen auf der Serverseite durch; der UI-Status dient lediglich als Rückmeldung, nicht als Zugangskontrolle.
- Modellieren Sie den Chat nach expliziten Zuständen (Text, ausstehender Aufruf, Ergebnis, Fehler), sodass neue Tools keinen neuen UI-Code erfordern.
Verwandte Artikel
- Eine Python LangChain-Agentur mit sieben integrierten Middlewares absichern — Erfahren Sie, wie die Middlewares von LangChain 1.0 Summarisierung, Aufruflimits, Wiederholungsversuche, Modell-Fallbacks, PII-Redaktion sowie menschliche Genehmigungen zu einer Gemini-Agentur hinzufügen, ohne deren Kernlogik anzutasten.