26 août 2025
Intégration d'un MCP dans une application React

8 minutes de lecture

La démocratisation de l'IA via les LLM a rendu l'accès à tout type d'information extrêmement facile et rapide, sans nécessiter de connaissances techniques avancées. Cependant dans certains cas, les modèles n'ont tout simplement pas le contexte nécessaire pour répondre à des demandes spécifiques. Par exemple, répondre à une question liée à de la facturation liée à un compte Stripe est impossible sans devoir développer un assistant spécifique, via l'intégration de tools. Pour palier à cela, Anthropic, à l'origine de Claude, a mis en place un protocole de communication permettant de fournir des informations contextuelles aux modèles sans nécessiter de développements spécifiques à un contexte particulier.
Model Context Protocol (MCP)
Model Context Protocol (MCP) est un standard de communication permettant de fournir un contexte à un LLM. En tant que développeur, vous êtes probablement familier avec MCP si vous utilisez un IDE supportant les agents IA (Cursor, Zed, etc.). Un serveur implémentant MCP peut fournir plusieurs types de données :
- un ensemble de tools à exécuter
- des prompts prédéfinis
- des données précises venant d'une source externe (BDD, API, etc.)
En réalité, l'usage le plus courant est d'utiliser les tools exposés par le serveur MCP.
Ce serveur peut ensuite être exposé soit via une URL, soit un exécutable.
Implémentation
Nous allons créer une petite application Next.js utilisant un MCP Postgres. Le but est de pouvoir répondre à une requête dans un langage naturel puis d'afficher les informations issues de la base de données.
bun create next-app
Installons les dépendances nécessaires :
bun add ai @ai-sdk/anthropic @modelcontextprotocol/sdk pg react-markdown
Implémentation du serveur MCP
Pour le serveur MCP, nous allons réutiliser le serveur Zed Postgres, que nous allons légèrement modifier.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import pg from "pg";
const { server } = new McpServer(
{
name: "postgres",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
},
);
const stdioTransport = new StdioServerTransport();
On initialise ici un serveur MCP qui sera en capacité d'exposer uniquement les données liées aux tools. D'autre types de capabilities sont disponibles: resources
, prompts
, completions
.
Nous utiliserons notre MCP sous forme d'exécutable. Par conséquent, la communication s'effectuera via la lecture et l'écriture sur stdin
et stdout
, d'où l'utilisation du transport StdioServerTransport
. Si l'on souhaite utiliser le transport HTTP, nous devront utiliser StreamableHTTPClientTransport
, et exposer notre serveur MCP à un port, en utilisant express
par exemple.
const dbUrl = Bun.env.PG_URL;
const resourceBaseUrl = new URL(dbUrl!);
resourceBaseUrl.protocol = "postgres:";
resourceBaseUrl.password = "";
const pool = new pg.Pool({
connectionString: Bun.env.PG_URL,
});
On initialise maintenant notre instance Postgres, afin d'établir une connexion plus tard.
Définissons maintenant l'accès à nos tools. Dans un premier temps nous devons en exposer la liste :
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "pg-schema",
description: "Returns the schema for a Postgres database.",
inputSchema: {
type: "object",
properties: {
mode: {
type: "string",
enum: ["all", "specific"],
description: "Mode of schema retrieval",
},
tableName: {
type: "string",
description:
"Name of the specific table (required if mode is 'specific')",
},
},
required: ["mode"],
if: {
properties: { mode: { const: "specific" } },
},
then: {
required: ["tableName"],
},
},
},
{
name: "query",
description: "Run a read-only SQL query",
inputSchema: {
type: "object",
properties: {
sql: { type: "string" },
},
},
},
],
};
});
ListToolsRequestSchema
est un schéma Zod. Si le serveur MCP reçoit une requête correspondant à ce schéma, alors il doit exécuter la fonction passée en second argument. Cette fonction doit retourner une liste de tools contenant un nom (servant d'identifiant théoriquement unique), une description qui sera lue par le modèle ainsi qu'un JSON Schema permettant d'indiquer au modèle la structure de donnée attendue pour l'exécution du tool.
Maintenant pour l'exécution de nos tools :
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "pg-schema") {
const mode = request.params.arguments?.mode;
const tableName = (() => {
switch (mode) {
case "specific": {
const tableName = request.params.arguments?.tableName;
if (typeof tableName !== "string" || tableName.length === 0) {
throw new Error(`Invalid tableName: ${tableName}`);
}
return tableName;
}
case "all": {
return ALL_TABLES;
}
default:
throw new Error(`Invalid mode: ${mode}`);
}
})();
const client = await pool.connect();
try {
const sql = await getSchema(client, tableName);
return {
content: [{ type: "text", text: sql }],
};
} finally {
client.release();
}
}
if (request.params.name === "query") {
const sql = request.params.arguments?.sql as string;
const client = await pool.connect();
try {
await client.query("BEGIN TRANSACTION READ ONLY");
// Force a prepared statement: Prevents multiple statements in the same query.
// Name is unique per session, but we use a single session per query.
const result = await client.query({
name: "sandboxed-statement",
text: sql,
values: [],
});
return {
content: [
{ type: "text", text: JSON.stringify(result.rows, undefined, 2) },
],
};
} finally {
client
.query("ROLLBACK")
.catch((error) =>
console.warn("Could not roll back transaction:", error),
);
// Destroy session to clean up resources.
client.release(true);
}
}
throw new Error("Tool not found");
});
/**
* @param tableNameOrAll {string}
*/
async function getSchema(client, tableNameOrAll) {
const select =
"SELECT column_name, data_type, is_nullable, column_default, table_name FROM information_schema.columns";
let result;
if (tableNameOrAll === ALL_TABLES) {
result = await client.query(
`${select} WHERE table_schema NOT IN ('pg_catalog', 'information_schema')`,
);
} else {
result = await client.query(`${select} WHERE table_name = $1`, [
tableNameOrAll,
]);
}
const allTableNames = Array.from(
new Set(result.rows.map((row) => row.table_name).sort()),
);
let sql = "```sql\n";
for (let i = 0, len = allTableNames.length; i < len; i++) {
const tableName = allTableNames[i];
if (i > 0) {
sql += "\n";
}
sql += [
`create table "${tableName}" (`,
result.rows
.filter((row) => row.table_name === tableName)
.map((row) => {
const notNull = row.is_nullable === "NO" ? "" : " not null";
const defaultValue =
row.column_default != null ? ` default ${row.column_default}` : "";
return ` "${row.column_name}" ${row.data_type}${notNull}${defaultValue}`;
})
.join(",\n"),
");",
].join("\n");
sql += "\n";
}
sql += "```";
return sql;
}
De la même manière que pour lister les tools, nous avons le schéma CallToolRequestSchema
pour intercepter la requête de leur exécution.
On peut maintenant démarrer notre serveur :
const main = async () => {
await server.connect(stdioTransport);
// Dans le cas d'un transport HTTP
// app.post("/mcp", async (req, res) => {
// await transport.handleRequest(req, res);
// });
// app.listen(Number(Bun.env.PORT ?? 3001));
};
main();
Implémentation Next (Client)
Créons maintenant notre page sur notre application Next.js, en utilisant le SDK AI de Vercel :
"use client";
import { useChat } from "ai/react";
import Markdown from "react-markdown";
export default function HomePage() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div className="flex flex-col h-screen max-w-4xl mx-auto p-4">
<div className="flex-1 overflow-y-auto mb-4">
{messages.map((message, index) => (
<div
key={index}
className={`mb-4 p-3 rounded-lg ${
message.role === "user" ? "bg-blue-100 ml-8" : "bg-gray-100 mr-8"
}`}
>
<div className="font-semibold text-sm text-gray-600 mb-1">
{message.role === "user" ? "You" : "Assistant"}
</div>
<div className="text-gray-800">
<Markdown>{message.content}</Markdown>
</div>
</div>
))}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Send
</button>
</form>
</div>
);
}
Implémentation Next (Serveur)
Créons maintenant notre route API /api/chat
, utilisée par le hook useChat
.
import { anthropic } from "@ai-sdk/anthropic";
import { experimental_createMCPClient, streamText } from "ai";
import { Experimental_StdioMCPTransport } from "ai/mcp-stdio";
import { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
const { messages } = await req.json();
const mcpTransport = new Experimental_StdioMCPTransport({
command: "bun",
args: ["pg:mcp"],
});
const mcpClient = await experimental_createMCPClient({
transport: mcpTransport,
name: "postgres-mcp-client",
});
try {
const tools = await mcpClient.tools();
const result = streamText({
model: anthropic("claude-4-sonnet-20250514"),
messages,
tools,
maxSteps: 3,
onError: async (error) => {
await mcpClient.close();
},
onFinish: async () => {
await mcpClient.close();
},
});
return result.toDataStreamResponse();
} catch (error) {
console.error("Error:", error);
}
}
Le SDK AI fournit des outils actuellement au stade expérimental permettant d'intéragir avec des serveurs MCP et d'en récupérer les tools.
const mcpTransport = new Experimental_StdioMCPTransport({
command: "bun",
args: ["pg:mcp"],
});
const mcpClient = await experimental_createMCPClient({
transport: mcpTransport,
name: "postgres-mcp-client",
});
Ici on crée un transport lié à notre sortie standard utilisée par le binaire de notre serveur MCP. Ce binaire est exécuté via la commande bun pg:mcp
que nous avons définie dans notre package.json
:
"pg:mcp": "dotenv -e .env -- bun ./mcp-server.ts"
L'appel à mcpClient.tools()
renvoie une liste de tools compatible avec les API de tools du SDK AI, avec le bon nom, la bonne description, les bons paramètres, ainsi qu'une fonction d'exécution. Il est bien évidemment possible de se brancher à plusieurs serveurs MCP en même temps et d'en combiner les tools. Toutefois, il faut faire attention aux potentielles collisions qui peuvent survenir pour les noms de chacun. La propriété maxSteps: 3
permet de continuer l'exécution de notre stream après l'appel à un tool. Par défaut, cette valeur est de 1. Ici on l'a définie à 3 pour permettre à nos tools de récupération du schéma et d'exécution de requêtes SQL d'être bien exécutés et renvoyés au client.
Testons maintenant l'ensemble ! Lançons notre serveur de développement Next :
bun dev
Puis rendons-nous sur http://localhost:3000 pour voir notre application en action. Une page avec un simple champ de saisie en bas apparait. Dans mon cas, je choisis de faire la demande suivante :
Donne moi la liste des 5 premiers email d'utilisateurs de ma base de données
Le LLM a bien été capable de me sortir des données dont elle n'a, de base, pas connaissance, tout ça grâce à l'utilisations de tools récupérés depuis notre serveur MCP.
Conclusion
MCP a en quelques sortes révolutionné notre manière d'utiliser les LLMs. Leur capacité de s'interfacer entre les modèles et des API tierces rend l'accès aux données encore plus puissant qu'il ne l'était déjà. Ainsi on peut imaginer des cas d'utilisation sur des back-offices par exemple, simplifiant l'aggrégation de données provenant de sources multiples, le tout à partir d'une simple requête dans un language naturel. Les serveurs MCP peuvent aussi être installés sur des clients qui les supportent, par exemples les IDE mais aussi les applications de LLM (Claude, Chat GPT). Une liste très exhaustive de serveurs est disponible sur mcp.so, et vous trouverez surement le MCP qui répond à vos besoins.
Vous souhaitez implémenter un serveur MCP pour votre application ? N'hésitez pas à nous contacter !