Strona główna / Artykuły / Podłączanie narzędzi MCP do interfejsu chatowego w React z wbudowaną procedurą zatwierdzania przez ludzi

Podłączanie narzędzi MCP do interfejsu chatowego w React z wbudowaną procedurą zatwierdzania przez ludzi

Dowiedz się, jak Model Context Protocol integruje się z aplikacją React: dlaczego backend powinien hostować MCP, jak działa serwer narzędzi oraz jak przesyłać i zatwierdzać wywołania narzędzi w interfejsie użytkownika.

3444 słów

Cechy AI w aplikacjach React zazwyczaj rozwijane są po jednej spersonalizowanej integracji, przy czym każda z nich ma swój własny SDK, mechanizmy autoryzacji, obsługę błędów oraz mapowanie danych, wszystko to ściśle powiązane ze sobą. Model Context Protocol (MCP) – otwarty protokół wprowadzony przez Anthropic i obecnie powszechnie stosowany – zastępuje ten bałagan jedną standardową interfejsem pomiędzy aplikacjami AI a narzędziami oraz danymi, których używają; powszechną analogią jest tutaj port USB-C dla AI. Ten artykuł wyjaśnia, jakie miejsce zajmuje MCP w architekturze React, przedstawia prosty serwer narzędzi bazodanowych oraz host backendowy, a następnie opisuje tę część, którą tworzą sami deweloperzy React: interfejs czatowy, który przekazuje informacje o wywołaniach narzędzi i prosi użytkownika o ich zatwierdzenie.

Jeśli chcesz najpierw uzyskać podstawowe informacje na poziomie protokołu dotyczące odkrywania i wywoływania funkcji, przeczytaj jak MCP umożliwia agentom AI odkrywanie i wywoływanie narzędzi. Tutaj skupiamy się na aspektach związanych z aplikacją i interfejsem użytkownika.

Czym standardyzuje MCP

MCP określa, w jaki sposób aplikacje dostarczają kontekst i możliwości modelom dużych języków. Rozróżnia trzy role: host to twoja aplikacja, klient to element wewnątrz hosta, który utrzymuje sesję z jednym serwerem, a serwery udostępniają narzędzia i źródła danych. Host zazwyczaj uruchamia jeden klient na każdy serwer, z którym się łączy.

Bез MCP lista zależności aplikacji React wyposażonej w AI często wygląda mniej więcej tak:

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)

Każda pozycja posiada własne mechanizmy autoryzacji, obsługi błędów oraz mapowanie schematu; nowe źródło danych oznacza nowy punkt końcowy oraz nową usługę frontendu.

Dzięki MCP integracja sprowadza się do jednego wzorca:

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

Każdy serwer używa tego samego protokołu. Host nie musi znać PostgreSQL ani struktury API Slacka; zadaje dwa ogólne pytania: „Jakie narzędzia są dostępne?” oraz „Uruchom to narzędzie z tymi argumentami”, a protokół zajmuje się resztą. Zwróć uwagę na to, czego MCP nie robi: standaryzuje połączenie z narzędziami i danymi, a nie wybór modelu. Host nadal komunikuje się z dowolnym dostawcą LLM, którego używasz.

Trzy podstawowe elementy

Narzędzia

Narzędzia to funkcje, które model może wywołać. Każde z nich ma nazwę, opis oraz JSON Schema opisujące jego parametry. Gdy użytkownik pyta, ile kont zostało utworzonych wczoraj, model nie musi zgadywać: może znaleźć narzędzie takie jak query_user_signups, które przyjmuje wartość typu date, je wywołać i odpowiedzieć na podstawie uzyskanego rezultatu. To właśnie narzędzia łączą pytania użytkownika w języku potocznym z danymi znajdującymi się w aplikacji.

Zasoby

Zasoby to elementy kontekstu, które model może odczytać – na przykład plik, rekord w bazie danych lub wątek rozmowy. Każdy z nich jest adresowany za pomocą URI, na przykład file:///docs/spec.pdf lub db://users/123, a ich odczyt umożliwia modelowi opieranie swoich odpowiedzi na rzeczywistych danych, a nie tylko na treściach z czasu szkolenia.

Instrukcje

Wskazówki to wielokrotnie używalne szablony publikowane przez serwer. Serwer może oferować wskazówkę code_review, która przyjmuje argument file_path; host pobiera szablon, uzupełnia argument i wysyła wynik do modelu.

Dlaczego interfejs użytkownika to coś więcej niż tylko wyświetlacz

Interfejs typu pass-through

W wielu aplikacjach AI klient React wysyła wiadomość użytkownika do backendu typu Node lub FastAPI, który przekazuje ją dostawcy modelu, czeka na odpowiedź i przekazuje ją z powrotem. Jeśli model potrzebuje jakiegoś narzędzia, backend również się tym zajmuje. Interfejs użytkownika jest pasywnym rendererem tekstu: nie ma pojęcia o tym, co robi model, ani sposobu na interwencję.

Interfejs użytkownika jako powierzchnia sterowania

Dzięki wywołaniom narzędzi w stylu MCP interfejs użytkownika może pokazywać te wywołania w czasie rzeczywistym i umożliwiać użytkownikowi zatwierdzenie lub odrzucenie operacji wrażliwych przed ich uruchomieniem. Niezależnie od tego, czy przeglądarka sama utrzymuje połączenia MCP, czy – co jest częstsze – otrzymuje strukturyzowany strumień od serwera backendowego, React stanowi miejsce, w którym zarządzanie tym procesem jest widoczne i kontrolowalne.

Użytkownicy coraz częściej oczekują takiej kontroli: możliwości zobaczenia, że asystent zamierza uzyskać dostęp do ich danych, oraz możliwości ich uprzedniego zatwierdzenia. Taka funkcjonalność jest wbudowana w React.

Gdzie powinien znajdować się host MCP

Istnieją dwie sprawdzalne architektury. Wybierz jedną z nich w zależności od swoich wymagań dotyczących bezpieczeństwa i opóźnień.

MCP pośredniczone przez backend – domyślny wybór

W tym wzorcu aplikacja React komunikuje się wyłącznie z twoim backendem, który pełni rolę hosta MCP. Ten backend utrzymuje otwarte połączenia z serwerami MCP, zajmuje się autoryzacją oraz przekazuje informacje o działaniach narzędzi do frontendu:

React (Client)  <--SSE/WS-->  FastAPI/Node (MCP Host)  <--stdio/SSE-->  MCP Servers

Korzyści te są decydujące dla większości produktów:

  • Bезpieczeństwo: dane uwierzytelniające do serwerów MCP pozostają na serwerze i nigdy nie trafiają do przeglądarki.
  • Stan: trwałe sesje bazy danych i systemu plików są przechowywane na serwerze, tam gdzie powinny być.
  • Zdolność do audytu: każde wezwanie narzędzia może być rejestrowane, ograniczane pod względem częstotliwości oraz przypisywane konkretnemu użytkownikowi.

Frontend odbiera uporządkowany strumień zdarzeń (kawałki tekstu, żądania wezwania narzędzi, wyniki działań narzędzi oraz ostateczna odpowiedź) i świadomie renderuje każdy stan.

Uwaga dotycząca transportu: diagram pokazuje stdio pomiędzy serwerem host a lokalnymi serwerami oraz SSE dla tych zdalnych. Specyfikacja MCP z biegiem czasu ewoluowała pod względem transportu HTTP, dlatego sprawdź aktualną specyfikację oraz dokumentację SDK w celu ustalenia zalecanego transportu dla serwerów zdalnych.

MCP natywny w przeglądarce, do specyficznych przypadków

Jako alternatywę aplikacja React może łączyć się bezpośrednio z zdalnymi serwerami MCP za pomocą strumieniowania opartego na HTTP. To rozwiązanie działa, ale zespoły produkcyjne rzadko je wybierają, ponieważ warstwa danych i jej dane uwierzytelniające stają się dostępne z przeglądarki. Zachowaj je na narzędzia deweloperskie lokalne lub aplikacje AI całkowicie po stronie klienta, które nie przetwarzają żadnych danych wrażliwych.

Server narzędzia do bazy danych w TypeScript

Budowa małego serwera to najszybsza droga do zrozumienia protokołu. Poniższy przykład udostępnia tabelę users w PostgreSQL za pomocą dwóch narzędzi wykorzystujących oficjalne SDK TypeScript. Przykład ten jest podzielony na trzy części. Po pierwsze, tworzone jest zbiór połączeń oraz MCP Server, który reklamuje możliwość używania tools. Po drugie, obsługa ListToolsRequestSchema opisuje każde narzędzie podając jego nazwę, opis oraz inputSchema – to właśnie ten element jest widoczny dla modelu przy decydowaniu, jakie narzędzie wywołać. Po trzecie, obsługa CallToolRequestSchema kieruje żądanie według nazwy narzędzia, wykonywa zapytanie z parametrami i zwraca wyniki w formie tekstu, albo zwraca isError: true wraz z komunikatem w przypadku błędu. Na koniec serwer łączy się przez stdio, dzięki czemu host może uruchomić go jako proces potomny.

// 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);

Zwróć uwagę na to, czego unika ten projekt: model nigdy nie wysyła surowych zapytań SQL. Może wybierać jedynie wąsko określone, nazwane operacje, a zapytania używają zmiennych miejscowych ($1, $2), dzięki czemu argumenty nie mogą wprowadzić kodu SQL. Zwracanie błędów wraz z flagą isError pozwala modelowi zobaczyć i wyjaśnić przyczynę awarii, zamiast by cała sesja się załamała.

Zanim zaczniesz używać czegoś takiego w praktyce, należy sprawdzić kilka kwestii. JSON Schema opisuje dane wejściowe, ale obsługa powinna nadal weryfikować samą wartość args (np. za pomocą biblioteki do schematów), ponieważ może być brakująca lub nieprawidłowo sformatowana; wartość args.limit również powinna mieć określony limit. Funkcja get_user_by_email wykonywa instrukcję SELECT *, co przekazałoby modelowi wszystkie kolumny, w tym dane wrażliwe takie jak hasła, dlatego lepiej wybrać konkretne kolumny. Ponadto w ścisłym TypeScriptie wartość error w bloku catch ma typ unknown, więc należy ją sprawdzić przed odczytaniem pola .message.

Hosting backendu w Pythonie

Aplikacja React nigdy nie komunikuje się bezpośrednio z tym serwerem; to backend to robi. Oto minimalna klasa hostująca wykorzystująca Python MCP SDK. Funkcja connect() opisuje, jak uruchomić proces serwera (command, args oraz środowisko przekazujące DATABASE_URL), otworzyć klienta stdio, rozpocząć ClientSession, przeprowadzić procedurę nawiązywania połączenia za pomocą initialize(), a następnie wywołać list_tools() w celu odkrycia oferowanych przez serwer funkcji. Funkcja execute_tool() przekazuje nazwę narzędzia i argumenty, zwracając pierwszy element tekstowy z wyniku, natomiast close() zamyka sesję i proces.

# 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)

Kod wymaga kilku poprawek przed uruchomieniem. Wykorzystuje funkcję os.getenv bez wcześniejszego importowania modułu os. Uruchamia narzędzie node na pliku o rozszerzeniu .ts, co działa tylko wtedy, gdy wersja Node umożliwia bezpośrednie uruchamianie TypeScript; w przeciwnym razie należy najpierw skompilować kod na JavaScript lub użyć narzędzia obsługującego TypeScript. Ręczne wywoływanie funkcji __aenter__ i __aexit__ jest możliwe, ale użycie bloków async with lub struktury AsyncExitStack jest bezpieczniejsze, ponieważ gwarantują one wykonywanie czynności porządkowych w przypadku błędów. Należy również pamiętać, że środowisko przekazane procesowi potomnemu zastępuje to rodzicielskie, więc trzeba dodać wszystko inne, czego potrzebuje serwer, np. wartość zmiennej PATH.

Część React: transmisja danych w czasie rzeczywistym i zatwierdzanie wywołań narzędzi

Tutaj deweloperzy React realizują swoją najbardziej charakterystyczną pracę. Serwer backend przesyła strumienie zdarzeń, które zawierają nie tylko tekst, ale także żądania wywołania narzędzi oraz wyniki ich działania, a interfejs użytkownika przekształca je w elementy interaktywne.

Komponent ChatInterface przechowuje listę wiadomości, z których każda może zawierać toolCalls o statusie pending, approved, rejected lub completed. Gdy użytkownik wysyła wiadomość, dodaje jego wpis, otwiera EventSource skierowany na adres /api/chat i buduje wiadomość asystenta w miarę przychodzenia zdarzeń. Zdarzenie text dodaje treść do wiadomości, zdarzenie tool_call dodaje oczekującą prośbę o użycie narzędzia, a zdarzenie tool_result zapisuje wynik i oznacza odpowiadającą prośbę jako completed. Po każdym zdarzeniu zastępuje aktualną wiadomość asystenta nową kopią, dzięki czemu React ponownie renderuje interfejs. Funkcja approveToolCall wysyła decyzję na adres /api/chat/approve-tool i optymistycznie zmienia status prośby na 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>
  );
}

Każde wezwanie narzędzia jest renderowane przez mały komponent prezentacyjny, który pokazuje nazwę narzędzia, jego stan, argumenty w formacie JSON oraz, dopóki wezwanie jest w trakcie przetwarzania, przyciski „Zatwierdź” i „Odrzuć”:

// 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>
  );
}

Tutaj abstrakcja przynosi korzyści. Interfejs nie wie, że funkcja query_users działa z bazą danych PostgreSQL, i nie będzie musiał ulegać zmianie, gdy jutro pojawi się narzędzie search_slack. Wie jedynie, że wezwanie narzędzia jest w oczekiwaniu, jakie argumenty zawiera oraz że człowiek musi podjąć decyzję w tej sprawie.

Luki do zamknięcia przed wprowadzeniem do produkcji

Przykład ilustruje strukturę interfejsu, ale istnieje kilka luk, które warto zamknąć:

  • Autorizacja musi być egzekwowana na serwerze. Część backendowa powinna przechowywać żądanie narzędzia do chwili otrzymania potwierdzenia dla dokładnie tego identyfikatora żądania, powiązanego z zalogowanym użytkownikiem. Optymistyczna zmiana stanu w interfejsie użytkownika jest jedynie informacją zwrotną; zgodnie z obecnym układem nic nie powstrzymuje przybycia wartości tool_result, niezależnie od użytego przycisku.
  • Przycisk „Odrzuć” nie ma obsługi. Należy go połączyć z punktem końcowym, który poinstruuje serwer o anulowaniu żądania i pozwoli modelowi kontynuować pracę bez wyniku.
  • EventSource wysyła wyłącznie żądania typu GET, więc wiadomość użytkownika jest przekazywana w łańcuchu zapytania, co podlega ograniczeniom dotyczącym długości adresu URL i może trafić do logów serwera oraz proxy. Żądanie typu POST, które używa fetch do odczytu strumieniowanego ciała odpowiedzi, unika obu tych problemów.
  • Strumień powinien zostać wyraźnie zakończony. Zamknij połączenie podczas ostatniego zdarzenia „done”, zamknij je, gdy komponent zostanie usunięty, oraz pokaż użytkownikowi informację z onerror zamiast zamknąć je w tajemnicy.
  • Lista kontrolna integracji MCP dla zespołów React

    Rozwiąż te kwestie architektoniczne przed integracją MCP:

    Kto jest odpowiedzialny za klienta MCP?

    W środowisku produkcyjnym – backend. Serwery MCP zazwyczaj wymagają uprawnień, trwałych połączeń oraz sesji z pamięcią stanu. Aplikacja React powinna otrzymywać ustrukturyzowany strumień zdarzeń przeznaczony do interfejsu użytkownika, a nie surowe wiadomości protokołu.

    Jak zatwierdzane są wywołania narzędzi?

    Nigdy nie pozwól modelowi używać narzędzi destruktywnych bez wyraźnej potwierdzenia. Jeśli poprosi o coś takiego jak delete_user, interfejs musi pokazać krok potwierdzenia. Chodzi tu zarówno o bezpieczeństwo, jak i zaufanie użytkowników. Zaprojektuj czat tak, aby transmisja była wstrzymywana po otrzymaniu żądania do narzędzia i wznowiona dopiero po zatwierdzeniu przez użytkownika, a także – jak wspomniano powyżej – zapewnij to wstrzymanie na serwerze.

    Jak przekazywane są częściowe stany?

    Użyj SSE lub WebSockets. Jedna odpowiedź przechodzi przez kilka faz: model analizuje sytuację, żąda użycia narzędzia, czeka na jego wynik, a następnie kontynuuje pracę. Interfejs powinien wyraźnie przedstawiać każdą fazę za pomocą wskaźnika postępów, kart z żądaniami narzędzi oraz wyników tych narzędzi prezentowanych jako dane strukturyzowane, a nie jako nierozróżniony tekst.

    Jak pokazywane są błędy?

    Serwery MCP przestają działać: połączenia z bazą danych zostają przerwane, a serwery systemu plików wykazują błędy uprawnień. Frontend powinien otrzymywać te informacje jako ustrukturyzowane zdarzenia błędów i prezentować je jako problemy, które da się naprawić, a nie jako zepsuty ekran.

    Jak odkrywane są narzędzia?

    Aplikacja powinna dostosowywać się do dostępnych narzędzi. Gdy host łączy się z nowym serwerem, powinien przekazać zaktualizowaną listę narzędzi do frontendu, który następnie może przedstawić użytkownikom aktualne możliwości, takie jak wyszukiwanie kont, przeszukiwanie dokumentów lub wykonywanie zapytań analitycznych.

    Jakie zmiany w MCP wpływają na pracę frontendu

    Bез wspólnego protokołu każde źródło danych, integracja modeli oraz narzędzia wymagają własnego rozwiązania łączącego, podobnie jak szuflada pełna niepasujących ładowarek. Standard MCP standaryzuje połączenie: dane stają się narzędziami opisanymi za pomocą schematu, hosty konsumują je przez jedną interfejs, a interfejs użytkownika przedstawia każde wezwanie jako element interaktywny. W praktyce:

    • Nowe funkcjonalności mogą pojawić się bez zmian w interfejsie użytkownika. Wystarczy podłączyć nowy serwer MCP do hosta, a uniwersalny interfejs wezwań narzędzi może natychmiast pokazać dostępne narzędzia.
    • Interfejs użytkownika jest oddzielony od modelu. Ponieważ renderuje stabilny strumień zdarzeń, zmiana dostawców modeli to kwestia backendu; MCP utrzymuje niezmienioną stronę narzędzi, podczas gdy host zajmuje się wezwaniami modelu specyficznymi dla danego dostawcy.
    • Komponent czatu, który rozumie wezwania narzędzi i akceptacje, robi o wiele więcej niż taki, który renderuje markdown.

    Główne wnioski

    • Zainstaluj MCP w warstwie backendowej, gdzie znajdują się dane uwierzytelniające, połączenia oraz logi audytowe, i przesyłaj strukturyzowane zdarzenia do Reacta.
    • Buduj serwery narzędzi na podstawie wąskich, zweryfikowanych i sparametryzowanych operacji, które zwracają tylko to, czego potrzebuje model.
    • Wymagaj zatwierdzenia na serwerze; stan interfejsu to jedynie informacja zwrotna, a nie brama kontrolna.
    • Zamodeluj rozmowę wokół wyraźnych stanów (tekst, czekająca próba połączenia, wynik, błąd), aby nowe narzędzia nie wymagały dodatkowego kodu interfejsu.

    Literatura pokrewna

  • Koordynacja wywołań narzędzi LLM w Node.js za pomocą Promise.withResolvers() — Zobacz, jak Promise.withResolvers() uporządkowuje procesy wywołań narzędzi w kontekście funkcji Lambda w Node.js, które łączą się z Claude na platformie Bedrock, a także jak radzi sobie z czasem oczekiwania, próbami ponownego wysłania żądania i ograniczeniami, których nie obejmuje.
  • DI dla frontendu niezależne od frameworku przy użyciu InversifyJS i Composition Root — Jak wybrać kontener DI dla TypeScript, zapakować każdą domenę jako ContainerModule, połączyć wszystko w jednym Composition Root oraz stworzyć most łączący go z React, Vue i Angular.
  • Przenoszenie Angular HttpClient do backendu Fetch za pomocą withFetch() — Dowiedz się, dlaczego Angular’s HttpClient przechodzi z XMLHttpRequest na fetch, jak withFetch() umożliwia edge SSR i transmisję strumieniową oraz co to oznacza dla twoich interceptorów.
  • Projektowanie SaaS do rozmów z AI przy użyciu Next.js, FastAPI, Credits i SSE — Przejrzyj architekturę pełnoskalowej rozmowy z AI z bramką FastAPI, rejestrem kredytów, ograniczeniami szybkości w Redis oraz transmisją strumieniową SSE, a także luki, które należy najpierw zamknąć.
  • Wzmacnianie serwera MCP w TypeScript dla rzeczywistego ruchu sieciowego — Jak przenieść serwer MCP z stdio na Streamable HTTP z izolacją na poziomie sesji, autoryzacją OAuth bearer, ograniczeniami szybkości, granicami błędów oraz mechanizmami obrony przed iniekcjami.