Головна / Статті / Підключення інструментів MCP до інтерфейсу чату у React з вбудованою людською перевіркою

Підключення інструментів MCP до інтерфейсу чату у React з вбудованою людською перевіркою

Дізнайтеся, як Model Context Protocol інтегрується в додаток React: чому бекенд має хостувати MCP, як працює сервер інструментів, та як стрімувати та схвалювати виклики інструментів у користувацькому інтерфейсі.

3444 слів

Функції ШІ в додатку на React зазвичай розвиваються поступово, шляхом інтеграції окремих компонентів, кожен з яких має власний SDK, механізми автентифікації, обробку помилок та мапування даних, причому усі вони тісно пов’язані між собою. Протокол Model Context Protocol (MCP) — відкритий протокол, створений компанією Anthropic та зараз широко використовуваний — замінює цю складність єдиним стандартним інтерфейсом між додатками ШІ та інструментами та даними, які вони використовують; поширеною аналогією є порт USB-C для ШІ. У цій статті пояснюється місце MCP у архітектурі React, розглядається невеликий сервер для роботи з базою даних та хост-сервер, а потім створюється та частина, якою керують розробники React: інтерфейс чату, який транслює виклики до інструментів та просить користувача їх схвалити.

Якщо ви спочатку хочете ознайомитися з основами рівня протоколу щодо виявлення та виклику, перегляньте як MCP дозволяє агентам ШІ виявляти та викликати інструменти. Тут акцент робиться на стороні застосування та користувацького інтерфейсу.

Що стандартизує MCP

MCP визначає, як застосунки надають контекст та можливості великим мовним моделям. Він розділяє три ролі: хостом є ваш застосунок, клієнтом — з’єднувач усередині хоста, який підтримує сесію з одним сервером, а сервери відкривають доступ до інструментів та джерел даних. Хост зазвичай запускає по одному клієнту на кожен сервер, до якого підключається.

Без MCP список залежностей React-застосунку з підтримкою ШІ часто виглядає приблизно так:

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)

Кожен запис має власну систему автентифікації, обробку помилок та відповідність схемі; нове джерело даних означає наявність нового кінцевого пункту та нової сервісної частини фронтенду.

За допомогою MCP процес інтеграції зводиться до єдиного паттерну:

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

Кожен сервер використовує один і той самий протокол. Хосту не потрібно розуміти PostgreSQL чи структуру API Slack; він ставить два загальні запитання: «Які інструменти доступні?» та «Запустіть цей інструмент з цими аргументами», а протокол передає решту інформації. Зверніть увагу, що MCP не стандартизує спосіб підключення до інструментів та даних, а лише вибір моделі. Хост все одно спілкується з тим постачальником LLM, який ви використовуєте.

Три примітиви

Ваша React-додаток взаємодіє з трьома типами можливостей сервера — безпосередньо або, що частіше трапляється, через ваш бекенд.

Інструменти

Інструменти — це функції, які модель може викликати. Кожен з них має назву, опис та JSON Schema, що описує його параметри. Коли користувач запитує, скільки облікових записів було створено вчора, моделі не потрібно здогадуватися: вона може знайти інструмент на кшталт query_user_signups, який приймає параметр date, викликати його та отримати відповідь з результату. Саме інструменти пов’язують запит користувача мовою просторікої розмови з даними, що знаходяться у вашому додатку.

Ресурси

Ресурси — це елементи контексту, які модель може читати, такі як файл, запис бази даних або потік чату. Кожен з них має URI, наприклад file:///docs/spec.pdf або db://users/123, і читання їх дозволяє моделі ґрунтувати свої відповіді у реальних даних, а не лише у матеріалах навчання.

Запити

Шаблони запитів — це повторно використовувані формати, які публікуються сервером. Сервер може запропонувати шаблон code_review, який приймає аргумент file_path; хост завантажує цей шаблон, підставляє аргумент та надсилає результат моделі.

Чому фронтенд — це більше, ніж просто інтерфейс відображення

Фронтенд з прямою передачею даних

У багатьох додатках на основі ШІ клієнт React надсилає повідомлення користувача на бекенд на базі Node чи FastAPI, який передає його постачальнику моделі, чекає на відповідь та передає її назад. Якщо моделі потрібен певний інструмент, бекенд також цим займається. Фронтенд є пасивним рендерером тексту: він не має уявлення про те, що робить модель, і не може втручатися.

Фронтенд як інтерфейс керування

За допомогою викликів інструментів у стилі MCP інтерфейс може відображати ці виклики у момент їх виконання та дозволяти користувачеві схвалити чи відхилити конфіденційні операції перед їх запуском. Незалежно від того, чи браузер сам підтримує з’єднання MCP, чи, що є більш поширеним, отримує структурований потік від сервера-хоста, React є місцем, де організація процесів залишається видимою та керованою.

Користувачі все більше очікують саме такого контролю: можливості бачити, що асистент збирається запитати їхні дані, та спочатку схвалити цей запит. Такий досвід реалізований у React.

Де має знаходитися хост MCP

Існує дві придатні архітектури. Виберіть одну з них залежно від ваших вимог щодо безпеки та затримки.

MCP через бекенд – стандартний варіант

У цій схемі додаток React взаємодіє лише з вашим бекендом, який є хостом MCP. Він підтримує відкритими з’єднання з серверами MCP, обробляє автентифікацію та передає інформацію про діяльність інструментів на фронтенд:

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

Переваги цього підходу є вирішальними для більшості продуктів:

  • Безпека: облікові дані серверів MCP залишаються на сервері та ніколи не потрапляють у браузер.
  • Стан: постійні сесії бази даних та файлової системи зберігаються на сервері, де їм і належить бути.
  • Підзвітність: кожен виклик інструменту може бути задокументований, обмежений за частотою та прив’язаний до конкретного користувача.

Фронтенд отримує структурований потік подій (фрагменти тексту, запити на виклик інструментів, результати їх роботи та кінцева відповідь) та свідомо відображає кожен елемент стану.

Примітка щодо транспорту: на діаграмі показано stdio між хостом та локальними серверами, а також SSE для віддалених серверів. Специфікація MCP з часом удосконалювала свій HTTP-транспорт, тому перевірте поточну специфікацію та документацію SDK щодо рекомендованого транспорту для віддалених серверів.

MCP, нативний для браузера, для обмежених випадків

Як альтернативу, додаток на React може безпосередньо під’єднуватися до віддалених серверів MCP через потокову передачу даних на основі HTTP. Цей підхід працює, але команди, що розробляють продукти для серійного використання, рідко його обирають, оскільки шар даних та відповідні облікові дані стають доступними з браузера. Використовуйте його лише для локальних інструментів розробника чи повністю клієнтських AI-додатків, які не обробляють конфіденційних даних.

Сервер інструментів для баз даних на TypeScript

Створення невеликого сервера — це найшвидший шлях до розуміння протоколу. У наведеному нижче прикладі таблиця PostgreSQL users розкривається через два інструменти за допомогою офіційного SDK TypeScript. Читайте його у трьох частинах. По-перше, створюється пул з’єднань та MCP Server, який оголошує про наявність функціоналу tools. По-друге, обробник ListToolsRequestSchema описує кожен інструмент за допомогою назви, опису та inputSchema, який використовується моделлю для вирішення, який інструмент викликати. По-третє, обробник CallToolRequestSchema направляє запит за назвою інструменту, виконує параметризований запит та повертає рядки у вигляді текстового контенту, або повертає isError: true разом із повідомленням у разі виникнення проблем. Нарешті, сервер підключається через stdio, тож хост може запустити його як дочірній процес.

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

Зверніть увагу на те, чого уникає ця архітектура: модель ніколи не надсилає сирий SQL-запит. Вона може обирати лише між вузькоспеціалізованими, іменованими операціями, а запити використовують місця заміни ($1, $2), тож аргументи не можуть вставити SQL-код. Повернення помилок у вигляді контенту за допомогою поля isError дозволяє моделі бачити та пояснювати причину помилки, замість того щоб вся сесія завершувалась збоєм.

Перш ніж використовувати щось подібне у реальних умовах, потрібно виправити кілька моментів. JSON Schema описує вхідні дані, але обробник все одно повинен перевіряти сам args (наприклад, за допомогою бібліотеки схем), оскільки він може бути відсутнім або мати неправильну структуру; також слід обмежити значення args.limit. Функція get_user_by_email виконує запит SELECT *, що передасть у модель усі стовпці, включаючи конфіденційну інформацію, таку як хеші паролів, тому краще вказувати конкретні стовпці. Крім того, у суворому режимі TypeScript значення error у блоку catch є типу unknown, тому його потрібно перевірити перед доступом до .message.

Хост бекенду на Python

Додаток React ніколи не взаємодіє безпосередньо з цим сервером; це робить бекенд. Ось мінімальний клас хоста, створений за допомогою Python MCP SDK. Функція connect() описує спосіб запуску процесу сервера (command, args та середовище, яке передає DATABASE_URL), відкриває клієнта stdio, ініціює ClientSession, виконує процедуру обміну даними за протоколом за допомогою initialize(), а потім викликає list_tools(), щоб дізнатися, що пропонує сервер. Функція execute_tool() передає назву інструменту та аргументи, повертає перший текстовий елемент з результату, а close() закриває сесію та процес.

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

Цей фрагмент потребує деяких виправлень перед запуском. У ньому використовується os.getenv без імпорту модуля os. Він запускає node для обробки файлу з розширенням .ts, що працює лише тоді, коли версія Node дозволяє безпосередньо виконувати TypeScript; у іншому разі спочатку скомпілюйте сервер у JavaScript або використовуйте засіб для виконання коду TypeScript. Ручне викликання __aenter__ та __aexit__ працює, але використання блоків async with або об’єкта AsyncExitStack є безпечнішим, оскільки вони гарантують очищення при виникненні помилок. Також зверніть увагу, що середовище, передане дочірньому процесу, замінює середовище батьківського процесу, тому необхідно включити все інше, що потрібне серверу, наприклад PATH.

Сторона React: стрімінг та схвалення викликів інструментів

Саме тут розробники React виконують свою найбільш унікальну роботу. Бекенд надсилає потоки подій, які містять не лише текст, а й запити до інструментів та їх результати, а користувацький інтерфейс перетворює їх на інтерактивні елементи.

Компонент ChatInterface, наведений нижче, зберігає список повідомлень, кожне з яких може містити toolCalls із статусом pending, approved, rejected або completed. Коли користувач надсилає повідомлення, він додає його запис, відкриває EventSource до адреси /api/chat та формує повідомлення асистента у міру надходження подій. Подія text додається до контенту, подія tool_call додає запит до інструменту зі статусом pending, а подія tool_result фіксує результат та позначає відповідний запит як completed. Після кожної події він замінює повідомлення асистента у стані на нову копію, що змушує React переробити відображення. Функція approveToolCall надсилає рішення на адресу /api/chat/approve-tool та оптимістично змінює статус запиту на 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>
  );
}

Кожен виклик інструменту обробляється невеликою компонентою візуалізації, яка відображає назву інструменту, його статус, аргументи у форматі JSON та, поки виклик у процесі, кнопки «Схвалити» та «Відхилити»:

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

Саме тут проявляється користь від абстракції. Інтерфейс не знає, що функція query_users виконується проти PostgreSQL, і йому не доведеться змінюватися, якщо завтра з’явиться інструмент search_slack. Він знає лише те, що виклик інструменту у процесі, які аргументи він містить та що людина повинна прийняти рішення щодо нього.

Прогалини, які потрібно усунути перед випуском

Приклад ілюструє структуру інтерфейсу, але є кілька прогалин, які варто закрити:

  • Схвалення має бути забезпечене на сервері. Бекенд повинен утримувати запит до інструменту доки не отримає схвалення для саме цього ID запиту, пов’язаного з автентифікованим користувачем. Оптимістична зміна статусу в інтерфейсі є лише фідбеком; згідно з описом, ніщо не заважає отриманню tool_result, незалежно від того, яку кнопку було натиснуто.
  • У кнопки „Відхилити“ немає обробника. Під’єднайте її до ендпоїнта, який накаже серверу скасувати запит та дозволить моделі продовжити роботу без результату.
  • EventSource відправляє лише запити типу GET, тому повідомлення користувача передається у рядку запиту, де воно підлягає обмеженням за довжиною URL та може потрапити до журналів сервера та проксі. Запит типу POST, який використовує fetch для отримання потокового тіла відповіді, усуває обидві проблеми.
  • Стрім має завершуватися явно. Закривайте з’єднання після останньої події „done“, закривайте його, коли компонент видаляється, та показуйте користувачеві значення onerror замість того, щоб мовчки закривати його.
  • Чек-лист інтеграції MCP для команд React

    Вирішіть ці архітектурні питання перед інтеграцією MCP:

    Хто відповідає за клієнт MCP?

    У продакшені — бекенд. Сервери MCP зазвичай потребують облікових даних, постійних з’єднань та сеансів із станом. Додаток React має отримувати структурований потік подій, призначений для інтерфейсу користувача, а не сирі повідомлення протоколу.

    Як схвалюються виклики інструментів?

    Ніколи не дозволяйте моделі використовувати руйнівні інструменти без чіткого підтвердження. Якщо вона просить щось на кшталт delete_user, інтерфейс має показувати крок підтвердження. Це стосується як довіри користувача, так і безпеки. Сконструюйте чат так, щоб трансляція призупинялась під час виклику інструменту та продовжувалась лише після схвалення користувачем, причому, як зазначалося вище, ця призупинка має здійснюватися на сервері.

    Як транслюються часткові стани?

    Використовуйте SSE або WebSockets. Одна відповідь проходить кілька етапів: модель аналізує ситуацію, запитує інструмент, чекає на його результати, а потім продовжує роботу. Інтерфейс має чітко відображати кожен етап за допомогою індикатора прогресу, карток викликів інструментів та результатів, які подаються у вигляді структурованих даних, а не нерозбірливого тексту.

    Як відображаються помилки?

    Сервери MCP не працюють: з’єднання з базою даних втрачаються, а сервери файлової системи повідомляють про помилки дозволів. Фронтенд має отримувати ці повідомлення у вигляді структурованих подій помилок та представляти їх як проблеми, які можна вирішити, а не як зламаний інтерфейс.

    Як виявляються інструменти?

    Додаток має адаптуватися до наявних інструментів. Коли хост під’єднується до нового сервера, він має надати оновлений список інструментів фронтенду, який потім може відображати поточні функції для користувачів, такі як пошук облікових записів, пошук документів чи виконання аналітичних запитів.

    Які зміни MCP впливають на роботу фронтенду

    Без спільного протоколу кожне джерело даних, інтеграція моделей та інструменти потребують власних механізмів з’єднання, наче шухляда, наповнена непідходящими зарядними пристроями. MCP стандартизує це з’єднання: дані перетворюються на інструменти, описані схемою, хости споживають їх через один інтерфейс, а користувацький інтерфейс представляє кожен виклик у вигляді інтерактивного елемента. На практиці:

    • Нові функції можуть з’явитися без змін у фронтенді. Достатньо під’єднати новий сервер MCP до хоста, і універсальний користувацький інтерфейс для викликів інструментів може негайно показати їх.
    • Користувацький інтерфейс відокремлений від моделі. Оскільки він відображає стабільний потік подій, зміна постачальників моделей є справою бекенду; MCP зберігає стабільність інструментів, тоді як хост обробляє виклики моделей, специфічні для конкретного постачальника.
    • Компонент чату, який розуміє виклики інструментів та процедури схвалення, виконує набагато більше функцій, ніж той, що відображає Markdown.

    Основні висновки

    • Розмістіть MCP на бекенді, де знаходяться облікові дані, підключення та журнали аудиту, і передавайте структуровані події до React.
    • Створюйте сервери інструментів на основі вузьких, перевірених та параметризованих операцій, які повертають лише те, що потрібно моделі.
    • Забезпечуйте схвалення на сервері; статус інтерфейсу є лише зворотним зв’язком, а не механізмом контролю.
    • Моделюйте чат за допомогою чітких станів (текст, очікування дзвінка, результат, помилка), щоб новим інструментам не потрібен був новий код інтерфейсу.

    Пов’язана література

  • Координація викликів інструментів LLM у Node.js за допомогою Promise.withResolvers() — Дізнайтеся, як Promise.withResolvers() допомагає оптимізувати оркестрацію викликів інструментів під час роботи з Node.js Lambda, який викликає Claude на платформі Bedrock, а також про таймаути, повторні спроби та обмеження, які він не покриває.
  • Фронтенд-DI, незалежний від фреймворку, з InversifyJS та Composition Root — Як вибрати контейнер DI для TypeScript, запакувати кожну сферу функціоналу як ContainerModule, з’єднати все в один Composition Root та інтегрувати його з React, Vue та Angular.
  • Перенесення Angular HttpClient до серверного бекенду Fetch за допомогою withFetch() — Дізнайтеся, чому HttpClient у Angular переходить від XMLHttpRequest до fetch, як withFetch() дозволяє реалізувати edge SSR та стрімінг, і що це означає для ваших інтерцепторів.
  • Архітектура AI-чату у форматі SaaS з використанням Next.js, FastAPI, Credits та SSE — Детальний огляд архітектури AI-чату на всьому рівні стеку з використанням гейткіпера FastAPI, обліку кредитів, обмежень швидкості через Redis та стрімінгу SSE, а також проблем, які потрібно спочатку вирішити.
  • Зміцнення сервера TypeScript MCP для реального мережевого трафіку — Як перетворити сервер MCP з формату stdio на Streamable HTTP із ізоляцією за сеансом, автентифікацією через OAuth bearer, обмеженнями швидкості, механізмами обробки помилок та захистом від втручань.