Accueil / Articles / Intégration des outils MCP dans une interface de chat React avec validation humaine intégrée

Intégration des outils MCP dans une interface de chat React avec validation humaine intégrée

Découvrez comment le Model Context Protocol s’intègre dans une application React : pourquoi le backend doit héberger MCP, comment fonctionne un serveur d’outils, et comment diffuser et valider les appels aux outils dans l’interface utilisateur.

3444 mots

Les fonctionnalités d’IA dans une application React se développent généralement intégration par intégration sur mesure, chacune disposant de son propre SDK, de mécanismes d’authentification, de gestion des erreurs et de mappage des données, tous étroitement liés entre eux. Le Model Context Protocol (MCP), un protocole ouvert introduit par Anthropic et désormais largement adopté, remplace ce enchevêtrement par une interface standard entre les applications d’IA et les outils ainsi que les données qu’elles utilisent ; l’analogie courante est celle d’un port USB-C pour l’IA. Cet article explique la place du MCP au sein d’une architecture React, présente en détail un petit serveur de outil de base de données et un hôte backend, puis construit la partie gérée par les développeurs React : une interface de chat qui diffuse les appels aux outils et demande à l’utilisateur de les valider.

Si vous souhaitez d’abord un guide de base au niveau du protocole sur la découverte et l’appel, consultez comment MCP permet aux agents AI de découvrir et d’appeler des outils. L’accent est mis ici sur la partie application et interface utilisateur.

Ce que standardise MCP

MCP définit la manière dont les applications fournissent du contexte et des capacités aux grands modèles de langage. Il distingue trois rôles : l’hôte correspond à votre application, le client est le connecteur intégré dans l’hôte qui maintient une session avec un serveur, et les serveurs exposent les outils et les sources de données. Un hôte exécute généralement un client par serveur auquel il est connecté.

Sans MCP, la liste des dépendances d’une application React dotée de fonctionnalités AI ressemble souvent à ceci :

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)

Chaque entrée dispose de sa propre gestion d’authentification, de traitement des erreurs et de mappage du schéma ; une nouvelle source de données signifie un nouveau point d’entrée ainsi qu’un nouveau service frontend.

Avec MCP, l’intégration se résume à un seul modèle :

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

Tous les serveurs utilisent le même protocole. L’hôte n’a pas besoin de comprendre PostgreSQL ni la structure de l’API de Slack ; il pose deux questions générales : « Quels outils sont disponibles ? » et « Exécutez cet outil avec ces arguments », le protocole s’occupant du reste. Notez ce que MCP ne fait pas : il standardise la connexion aux outils et aux données, mais non le choix du modèle. L’hôte continue de communiquer avec n’importe quel fournisseur de LLM que vous utilisez.

Les trois primitives

Votre application React interagit avec trois types de capacités serveur, directement ou, plus fréquemment, via votre backend.

Outils

Les outils sont des fonctions que le modèle peut invoquer. Chacun possède un nom, une description ainsi qu’un schéma JSON décrivant ses paramètres. Lorsqu’un utilisateur demande combien de comptes ont été créés hier, le modèle n’a pas besoin de deviner : il peut trouver un outil tel que query_user_signups qui accepte une valeur de type date, l’appeler et répondre en se basant sur le résultat. Ce sont les outils qui relient la question formulée en langage courant par l’utilisateur aux données au sein de votre application.

Ressources

Les ressources sont des éléments de contexte que le modèle peut lire, tels qu’un fichier, une entrée dans une base de données ou un fil de discussion. Chacun d’eux est identifié par une URI, par exemple file:///docs/spec.pdf ou db://users/123 ; en les lisant, le modèle peut fonder ses réponses sur des données réelles plutôt que sur son entraînement.

Prompt

Les prompts sont des modèles réutilisables publiés par un serveur. Un serveur peut proposer un prompt code_review qui accepte un argument file_path ; l’hôte récupère le modèle, y insère l’argument et envoie le résultat au modèle.

Pourquoi l’interface utilisateur va au-delà d’un simple afficheur

L’interface utilisateur de type passage direct

Dans de nombreuses applications d’IA, le client React envoie le message de l’utilisateur à un backend Node ou FastAPI, qui le transmet au fournisseur du modèle, attend la réponse et la renvoie. Si le modèle a besoin d’un outil, c’est également le backend qui s’en occupe. L’interface utilisateur n’est qu’un afficheur de texte passif : elle ne comprend pas ce que fait le modèle et n’a aucun moyen d’intervenir.

L’interface utilisateur en tant que surface de contrôle

Avec les appels d’outils de type MCP, l’interface utilisateur peut afficher ces appels au fur et à mesure qu’ils se produisent, permettant à l’utilisateur d’approuver ou de rejeter les opérations sensibles avant qu’elles ne soient exécutées. Que le navigateur gère lui-même les connexions MCP ou, plus fréquemment, reçoive un flux structuré d’un hôte backend, React devient l’endroit où l’orchestration est visible et contrôlable.

Les utilisateurs attendent de plus en plus ce niveau de contrôle : voir que l’assistant s’apprête à interroger leurs données et pouvoir les approuver en premier. Cette expérience est intégrée dans React.

Où le hôte MCP doit être hébergé

Il existe deux architectures fonctionnelles. Choisissez-en une en fonction de vos exigences en matière de sécurité et de latence.

MCP médié par un backend, le choix par défaut

Dans ce modèle, l’application React ne communique qu’avec votre backend, qui est le hôte MCP. Celui-ci maintient les connexions vers les serveurs MCP ouvertes, gère l’authentification et transmet les activités des outils au frontend :

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

Les avantages sont décisifs pour la plupart des produits :

  • Sécurité : les identifiants des serveurs MCP restent sur le serveur et n’atteignent jamais le navigateur.
  • État : les sessions persistantes du système de base de données et du système de fichiers sont conservées sur le serveur, là où elles doivent être.
  • Auditable : chaque appel à un outil peut être enregistré, soumis à des limites de fréquence et attribué à un utilisateur spécifique.

Le frontend consomme un flux structuré d’événements (tranches de texte, demandes d’appel à des outils, résultats des outils et réponse finale) et affiche chaque état de manière ciblée.

Remarque sur les transports : le diagramme montre stdio entre l’hôte et les serveurs locaux, ainsi que SSE pour ceux distants. La spécification MCP a évolué au fil du temps en ce qui concerne son transport HTTP ; veuillez donc consulter la spécification actuelle ainsi que les documents du SDK pour connaître le transport distant recommandé.

MCP natif du navigateur, pour des cas limités

Par ailleurs, l’application React peut se connecter directement aux serveurs MCP distants via un streaming basé sur HTTP. Cette méthode fonctionne, mais les équipes de production l’utilisent rarement, car la couche de données et ses identifiants deviennent accessibles depuis le navigateur. Préservez-la pour les outils de développement locaux ou les applications d’IA entièrement côté client qui ne traitent aucune donnée sensible.

Un serveur d’outils de base de données en TypeScript

Construire un petit serveur est le moyen le plus rapide pour comprendre le protocole. L’exemple ci-dessous expose une table users de PostgreSQL à travers deux outils en utilisant le SDK officiel TypeScript. Il est décrit en trois parties. Tout d’abord, il crée un pool de connexions ainsi qu’un Server MCP qui annonce la capacité tools. Ensuite, le gestionnaire ListToolsRequestSchema décrit chaque outil en indiquant son nom, sa description et un inputSchema, c’est-à-dire ce que le modèle voit pour décider de quelle fonction appeler. Enfin, le gestionnaire CallToolRequestSchema envoie la requête selon le nom de l’outil, exécute une requête paramétrée et renvoie les lignes sous forme de contenu texte, ou bien renvoie isError: true accompagné d’un message en cas d’échec. En dernier ressort, le serveur se connecte via stdio, ce qui permet à un hôte de le lancer en tant que processus enfant.

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

Remarquez ce que le design évite : le modèle ne envoie jamais de SQL brut. Il ne peut choisir que parmi des opérations spécifiques et nommées, et les requêtes utilisent des placeholders ($1, $2) afin que les arguments ne puissent pas injecter de SQL. Le fait de retourner des erreurs sous forme de contenu via isError permet au modèle de détecter et d’expliquer l’échec, plutôt que de faire planter toute la session.

args lui-même (par exemple à l’aide d’une bibliothèque de schémas), car il peut être manquant ou mal formaté ; args.limit doit également faire l’objet d’une limite. La fonction get_user_by_email exécute SELECT *, ce qui transmettrait toutes les colonnes au modèle, y compris des informations sensibles comme les hachages de mot de passe ; il est donc préférable de sélectionner uniquement les colonnes nécessaires. De plus, en TypeScript strict, la variable error dans le bloc catch a le type unknown, il faut donc la vérifier avant d’accéder à sa propriété .message.

Hébergeur backend en Python

L’application React ne communique jamais directement avec ce serveur ; c’est le backend qui s’en charge. Voici une classe hôte minimale utilisant le SDK MCP en Python. connect() décrit comment lancer le processus du serveur (command, args ainsi qu’un environnement transmettant DATABASE_URL), ouvre un client stdio, initialise une ClientSession, effectue la mise en relation protocolaire via initialize(), puis appelle list_tools() pour découvrir ce que propose le serveur. execute_tool() envoie le nom d’une fonction et ses arguments, renvoie le premier élément texte du résultat, tandis que close() termine la session et arrête le processus.

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

Cette partie nécessite des corrections avant de pouvoir fonctionner. Elle utilise os.getenv sans importer le module os. Elle lance node sur un fichier .ts, ce qui ne fonctionne que si la version de Node permet d’exécuter directement TypeScript ; dans le cas contraire, il faut compiler le serveur en JavaScript au préalable ou utiliser un exécuteur compatible avec TypeScript. Appeler manuellement __aenter__ et __aexit__ fonctionne, mais les blocs async with ou un AsyncExitStack sont plus sûrs car ils garantissent le nettoyage en cas d’erreur. Notez également que l’environnement transmis au processus enfant remplace celui du processus parent, il faut donc y inclure tout ce dont le serveur a besoin, comme par exemple PATH.

Le côté React : streaming et validation des appels aux outils

C’est ici que les développeurs React réalisent leur travail le plus original. Le backend diffuse des événements contenant non seulement du texte, mais aussi des demandes d’appel d’outils et leurs résultats, et l’interface les transforme en éléments interactifs.

Le composant ChatInterface ci-dessous conserve une liste de messages, chacun pouvant contenir des toolCalls ayant un statut de pending, approved, rejected ou completed. Lorsque l’utilisateur envoie un message, il ajoute cette entrée, ouvre un EventSource vers /api/chat et construit progressivement un message de l’assistant au fur et à mesure que des événements arrivent. Un événement text ajoute du contenu, un événement tool_call insère une demande d’outil en attente, et un événement tool_result enregistre le résultat et marque la demande correspondante comme completed. Après chaque événement, il remplace le message de l’assistant dans l’état par une copie fraîche, ce qui pousse React à se rérender. La fonction approveToolCall envoie la décision vers /api/chat/approve-tool et met optimistiquement à jour le statut de la demande en 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>
  );
}

Chaque appel d’outil est affiché par un petit composant de présentation qui montre le nom de l’outil, son état, les arguments au format JSON, ainsi que des boutons « Approuver » et « Rejeter » tant que l’appel est en attente :

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

C’est là que l’abstraction porte ses fruits. L’interface utilisateur n’a aucune idée que query_users s’exécute sur PostgreSQL, et elle n’aura pas besoin de changer lorsque l’outil search_slack apparaîtra demain. Elle sait simplement qu’un appel d’outil est en attente, quels arguments il contient et qu’une personne doit prendre une décision à son sujet.

Points à résoudre avant la mise en production

L’exemple illustre la structure de l’interface, mais quelques lacunes méritent d’être comblées :

  • L’approbation doit être appliquée sur le serveur. Le backend doit retenir la demande d’outil jusqu’à ce qu’il reçoive une approbation pour cet ID de demande précis, lié à l’utilisateur authentifié. Le changement d’état optimiste de l’interface utilisateur n’est qu’un retour d’information ; tel quel, rien ne empêche l’arrivée d’un tool_result quel que soit le bouton utilisé.
  • Le bouton « Rejeter » ne dispose d’aucun gestionnaire. Le connecter à une extrémité de réseau qui indique à l’hôte d’annuler la demande permet au modèle de continuer sans le résultat.
  • EventSource ne fait que des requêtes GET, de sorte que le message de l’utilisateur est transmis dans la chaîne de requête, ce qui le soumet aux limites de longueur des URL et peut le faire apparaître dans les journaux du serveur et du proxy. Une requête POST qui lit un corps de réponse en flux avec fetch évite ces deux problèmes.
  • Le flux doit se terminer de manière explicite. Fermez la connexion lors d’un événement final « done », fermez-la lorsque le composant est démonté, et affichez l’événement onerror à l’utilisateur au lieu de la fermer en silence.
  • Liste de vérification pour l’intégration MCP aux équipes React

    Résolvez ces questions architecturales avant d’intégrer MCP :

    Qui est responsable du client MCP ?

    Dans un environnement de production, c’est le backend. Les serveurs MCP nécessitent généralement des identifiants, des connexions persistantes et des sessions à état. L’application React doit recevoir un flux d’événements structuré conçu pour l’interface utilisateur, et non des messages de protocole bruts.

    Comment les appels aux outils sont-ils approuvés ?

    Ne permettez jamais au modèle d’exécuter des outils destructeurs sans confirmation explicite. Si celui-ci demande quelque chose comme delete_user, l’interface doit afficher une étape de confirmation. Il s’agit autant de la confiance des utilisateurs que de leur sécurité. Concevez le chat de manière à ce que la diffusion soit interrompue lorsqu’une requête d’outil arrive et ne reprend que lorsque l’utilisateur donne son accord, et, comme indiqué ci-dessus, assurez cette interruption sur le serveur.

    Comment les états partiels sont-ils diffusés ?

    Utilisez SSE ou WebSockets. Une seule réponse passe par plusieurs phases : le modèle réfléchit, demande un outil, l’attend puis continue. L’interface doit représenter clairement chaque phase grâce à un indicateur de progression, des cartes indiquant les requêtes d’outil, et des résultats affichés sous forme de données structurées plutôt que de texte non différencié.

    Comment les erreurs sont-elles affichées ?

    Les serveurs MCP tombent en panne : les connexions à la base de données sont interrompues et les serveurs du système de fichiers rencontrent des erreurs de permission. L’interface utilisateur doit recevoir ces événements d’erreur structurés et les présenter comme des problèmes pouvant être résolus, jamais comme un écran défectueux.

    Comment les outils sont-ils découverts ?

    L’application doit s’adapter aux outils disponibles. Lorsque l’hôte se connecte à un nouveau serveur, il doit transmettre la liste mise à jour des outils à l’interface utilisateur, qui pourra alors afficher les fonctionnalités actuelles aux utilisateurs, telles que la recherche de comptes, la consultation de documents ou l’exécution de requêtes analytiques.

    Quelles modifications MCP pour le travail avec l’interface utilisateur ?

    En l’absence d’un protocole commun, chaque source de données, intégration de modèle et outil nécessite son propre mécanisme de liaison, comme un tiroir rempli de chargeurs incompatibles. Le standard MCP normalise ces connexions : les données deviennent des outils décrits par un schéma, les hôtes les consomment via une seule interface, et l’interface utilisateur présente chaque appel comme un élément interactif. En pratique :

    • De nouvelles fonctionnalités peuvent être ajoutées sans modification de l’interface utilisateur. Il suffit de connecter un nouveau serveur MCP à l’hôte, et une interface d’appel d’outils générique peut afficher immédiatement ces outils.
    • L’interface utilisateur est déconnectée du modèle. Comme elle affiche un flux d’événements stable, le changement de fournisseur de modèle relève du côté backend ; MCP maintient la partie outil constante, tandis que l’hôte gère les appels de modèle spécifiques au fournisseur.
    • Un composant de chat qui comprend les appels d’outils et les validations permet bien plus que celui qui se contente d’afficher du Markdown.

    Points clés

    • Héberger MCP en arrière-plan, où se trouvent les identifiants, les connexions et les journaux d’audit, puis diffuser des événements structurés vers React.
    • Créer des serveurs d’outils à partir d’opérations restreintes, validées et paramétrées qui ne retournent que ce dont le modèle a besoin.
    • Imposer une approbation sur le serveur ; l’état affiché dans l’interface est un retour d’information, et non le mécanisme de contrôle.
    • Modéliser la conversation autour d’états explicites (texte, appel en attente, résultat, erreur) afin que de nouveaux outils n’aient pas besoin de code d’interface supplémentaire.

    Lectures complémentaires

    • Renforcer un agent Python LangChain avec sept middlewares intégrés — Découvrez comment les middlewares de LangChain 1.0 ajoutent des fonctionnalités telles que la synthèse, les limites d’appel, les tentatives de réessai, le recours à un modèle de secours, la suppression des données personnelles et l’approbation humaine à un agent Gemini sans toucher à sa logique de base.
  • Coordiner les appels des outils LLM dans Node.js avec Promise.withResolvers() — Découvrez comment Promise.withResolvers() simplifie l’orchestration des appels d’outils dans un Lambda Node.js appelant Claude sur Bedrock, ainsi que les temps d’attente, les tentatives de réessai et les limites qu’il ne couvre pas.
  • DI frontend indépendant du framework avec InversifyJS et un root de composition — Comment choisir un conteneur DI pour TypeScript, emballer chaque domaine en tant que ContainerModule, relier tout cela dans un seul root de composition et le connecter à React, Vue et Angular.
  • Déplacer Angular HttpClient vers le backend Fetch avec withFetch() — Découvrez pourquoi l’HttpClient d’Angular passe de XMLHttpRequest à fetch, comment withFetch() permet le SSR aux frontières et la diffusion en flux, ainsi que ses conséquences pour vos intercepteurs.
  • Architecturer un chat AI SaaS avec Next.js, FastAPI, Credits et SSE — Découvrez en détail une architecture de chat AI full-stack utilisant un gatekeeper FastAPI, un registre des crédits, des limites de vitesse Redis et la diffusion en flux SSE, ainsi que les points à résoudre en priorité.
  • Renforcer un server MCP en TypeScript pour un trafic réseau réel — Comment passer d’un server MCP basé sur stdio à un serveur HTTP Streamable avec isolation par session, authentification OAuth bearer, limites de débit, frontières d’erreur et protections contre les injections.