Главная / Статьи / Подключение инструментов 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 остаются на сервере и никогда не попадают в браузер.
  • Состояние: постоянные сессии базы данных и файловой системы хранятся на сервере, там, где им и положено.
  • Аудитируемость: каждый вызов инструмента может быть зарегистрирован, ограничен по частоте и привязан к конкретному пользователю.

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

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

Native MCP в браузере — для ограниченных случаев

В качестве альтернативы приложение на React может напрямую подключаться к удаленным серверам MCP с использованием потоковой передачи по протоколу HTTP. Этот метод работает, но команды, занимающиеся производственными задачами, редко им пользуются, поскольку слой обработки данных и соответствующие учетные данные становятся доступными из браузера. Его следует использовать только в локальных инструментах разработчика или полностью клиентских ИИ-приложениях, которые не обрабатывают конфиденциальные данные.

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

Создание небольшого сервера — это самый быстрый способ понять протокол. В приведённом ниже примере таблица users из PostgreSQL предоставляется через два инструмента с использованием официального 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. Он знает лишь о том, что вызов инструмента находится в ожидании, какие аргументы он содержит и что человек должен принять решение по нему.

Пробелы, которые необходимо устранить перед запуском в производство

Пример иллюстрирует структуру интерфейса, но есть несколько пробелов, которые стоит устранить:

  • Утверждение должно применяться на сервере. Бэкенд обязан задерживать вызов инструмента до получения утверждения для конкретного идентификатора вызова, связанного с аутентифицированным пользователем. Изменение статуса в интерфейсе является лишь обратной связью; согласно текущей реализации, ничто не мешает поступлению значения 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 на backend Fetch с помощью withFetch() — Узнайте, почему HttpClient в Angular переходит от XMLHttpRequest к методу fetch, как withFetch() обеспечивает работу на краевых серверах и потоковую передачу данных, и какое это влияние на интерцепторы.
  • Архитектура AI-чата в формате SaaS с использованием Next.js, FastAPI, системы учета кредитов и технологии SSE — Подробный обзор архитектуры AI-чата с использованием FastAPI в качестве шлюза, системы учета кредитов, ограничений скорости работы через Redis и потоковой передачи данных по протоколу SSE, а также основных недостатков, которые необходимо устранить в первую очередь.
  • Усиление безопасности сервера TypeScript MCP для реального сетевого трафика — Как преобразовать сервер MCP из формата stdio в Streamable HTTP с изоляцией по сессиям, аутентификацией через OAuth bearer, ограничениями скорости, механизмами обработки ошибок и защитой от внедрения кода-злоумышленника.