Exemples de serveurs MCP en Python et TypeScript (code GitHub) qui fonctionnent avec les SDK actuels
Des exemples de serveurs MCP en Python et TypeScript, prêts à copier, écrits d'après les dépôts SDK officiels sur GitHub. Chaque exemple fonctionne via stdio ou Streamable HTTP, et les deux derniers encapsulent une API d'images et de vidéos pour qu'un assistant génère des médias depuis une fenêtre de chat.
Un serveur MCP est un petit programme qui fournit à un assistant d’IA une liste d’outils qu’il peut appeler. Le moyen le plus rapide de comprendre son fonctionnement est de lire quelques serveurs déjà opérationnels. Cet article rassemble des exemples de serveurs MCP en Python et TypeScript, écrits à partir des dépôts SDK officiels sur GitHub. Vous pouvez donc copier un fichier, le lancer et voir un assistant l’utiliser en quelques minutes.
Tout ce qui suit s’appuie sur la documentation des SDK en date d’octobre 2026, ce qui compte, car les deux SDK ont récemment atteint une deuxième version majeure. En Python, FastMCP est devenu MCPServer. En TypeScript, le code du serveur a été déplacé dans son propre paquet @modelcontextprotocol/server. La première moitié de l’article construit un serveur simple dans chaque langage. La seconde ajoute des outils qui génèrent des images et des vidéos, ce qui fait passer MCP du stade de démo à celui d’un outil qui fait gagner un vrai temps de travail.
Ce que fait un serveur MCP
Le Model Context Protocol (MCP) est un standard ouvert qui permet à un client d’IA, comme une application de chat, un IDE ou un agent, d’appeler du code que vous avez écrit. Le client ouvre une connexion, demande à votre serveur ce qu’il propose, et laisse le modèle décider quand utiliser chaque élément. Les messages circulent en JSON-RPC, et votre serveur n’appelle jamais le modèle lui-même. Il attend qu’on le sollicite, exécute la fonction et renvoie un résultat.
Outils, ressources et prompts
Chaque serveur est construit à partir de trois types de briques :
Les outils sont des fonctions que le modèle peut appeler, comme add ou generate_image. Ils peuvent avoir des effets de bord, c’est pourquoi ce sont eux qu’il faut concevoir avec le plus de soin.
Les ressources sont des données en lecture seule identifiées par une URI, comme greeting://alice ou un chemin de fichier. Le client les lit pour donner du contexte au modèle.
Les prompts sont des modèles de messages réutilisables qu’une personne choisit dans un menu, par exemple une demande de revue de code.
💡 Astuce : commencez par les outils. La plupart des serveurs sur GitHub n’exposent que des outils, et un modèle choisit plus sûrement le bon outil dans une courte liste d’outils aux noms clairs que dans une longue liste d’outils vagues.
Quelle branche du SDK installer
Les deux SDK officiels proposent désormais une branche actuelle et une branche de maintenance. Choisissez la bonne avant de copier du code, car les imports diffèrent.
Python
TypeScript
Actuelle (v2)
pip install "mcp[cli]", classe MCPServer
npm install @modelcontextprotocol/server
Maintenance (v1.x)
pip install "mcp[cli]<2", classe FastMCP
npm install @modelcontextprotocol/sdk zod
Dépôt GitHub
modelcontextprotocol/python-sdk
modelcontextprotocol/typescript-sdk
La branche Python v1 ne reçoit plus que des correctifs de sécurité. Les nouveaux projets doivent donc démarrer sur la v2, et c’est ce que font les exemples Python ci-dessous. Si vous maintenez un serveur Python ancien, la migration consiste à remplacer from mcp.server.fastmcp import FastMCP par from mcp.server.mcpserver import MCPServer et à renommer l’appel au constructeur. Les décorateurs restent les mêmes.
Pour TypeScript, les exemples complets utilisent le paquet v1.x que la plupart des serveurs existants importent aujourd’hui. Une version courte en v2 du même serveur suit, pour que vous voyiez précisément ce qui change.
Exemple Python avec MCPServer
Python offre le chemin le plus court entre rien et un serveur fonctionnel, car les annotations de type et les docstrings deviennent le schéma de l’outil. Il n’y a aucun JSON Schema à écrire à la main.
Le fichier du serveur
Installez avec uv add "mcp[cli]" (ou pip install "mcp[cli]"), puis enregistrez ceci sous server.py :
from mcp.server.mcpserver import MCPServer
mcp = MCPServer("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
@mcp.prompt()
def review_code(code: str) -> str:
"""Ask for a code review that lists bugs first."""
return f"Please review this code and list bugs first:\n\n{code}"
if __name__ == "__main__":
mcp.run(transport="stdio")
La fonction add devient un outil dont le schéma d’entrée est généré à partir de a: int, b: int. La docstring devient la description que le modèle lit pour décider s’il doit l’appeler. greeting est un modèle de ressource : un client qui lit greeting://Ada reçoit en retour Hello, Ada!.
Lancer et inspecter
uv run mcp dev server.py # opens the MCP Inspector in your browser
uv run mcp run server.py # plain stdio, for a client to launch
uv run mcp run server.py --transport streamable-http # HTTP instead
Commencez par l’Inspector. Il liste chaque outil, vous propose un formulaire pour les arguments et affiche le trafic JSON-RPC brut, ce qui est la façon la plus rapide de repérer un mauvais schéma. Une fois que tout fonctionne, enregistrez le serveur auprès d’un client. Dans Claude Code, cela tient en une ligne :
claude mcp add demo -- uv run mcp run server.py
Exemple TypeScript avec Zod
TypeScript demande un peu plus de formalisme, car vous décrivez les entrées avec Zod au lieu des annotations de type. En contrepartie, vous obtenez une validation à l’exécution et des arguments typés dans votre gestionnaire.
Le fichier du serveur en v1.x
Installez le paquet avec npm install @modelcontextprotocol/sdk zod et définissez "type": "module" dans package.json. Écrivez le serveur comme une fonction afin que les deux transports puissent la réutiliser. Enregistrez ceci sous src/build-server.ts :
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function buildServer(): McpServer {
const server = new McpServer({ name: "demo", version: "1.0.0" });
server.registerTool(
"add",
{
title: "Add numbers",
description: "Add two numbers",
inputSchema: { a: z.number(), b: z.number() },
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
return server;
}
Puis un point d’entrée de trois lignes pour stdio, dans src/stdio.ts :
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { buildServer } from "./build-server.js";
await buildServer().connect(new StdioServerTransport());
Compilez avec tsc, puis vérifiez avec npx @modelcontextprotocol/inspector node dist/stdio.js. Les ressources et les prompts suivent la même forme via registerResource et registerPrompt. Le dépôt v1.x fournit aussi src/examples/server/simpleStreamableHttp.ts, un exemple complet avec outils, ressources, prompts, journalisation et OAuth optionnel, qui mérite d’être lu une fois que votre serveur dépasse le simple jouet.
Le même serveur en v2
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";
const server = new McpServer({ name: "demo", version: "1.0.0" });
server.registerTool(
"add",
{
description: "Add two numbers",
inputSchema: z.object({ a: z.number(), b: z.number() }),
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
await server.connect(new StdioServerTransport());
Trois éléments changent : le nom du paquet, le sous-chemin d’import /stdio, et le schéma, qui devient un z.object(...) complet issu de zod/v4 au lieu d’un simple objet de champs. Le corps du gestionnaire reste identique. Le service via HTTP en v2 passe par de petits paquets d’adaptation, comme @modelcontextprotocol/express : lisez donc le README de ce paquet avant de porter un serveur HTTP.
Stdio ou Streamable HTTP
Le transport est la seule décision qui change la façon de déployer. Le code des outils reste identique.
stdio
Streamable HTTP
Qui lance le serveur
Le client le lance comme processus enfant
Vous le faites tourner, les clients se connectent par URL
Idéal pour
Outils locaux, IDE, applications de bureau
Serveurs partagés ou distants, équipes
Authentification
Hérite de votre utilisateur et de votre environnement
Vous l’ajoutez (jetons ou OAuth)
Journalisation
stderr uniquement
La sortie normale convient
Mise à l’échelle
Un processus par client
Mise à l’échelle web classique
Processus local via stdio
Un serveur stdio est le bon choix par défaut pour tout ce qui touche à votre propre machine, comme des fichiers, une base de données locale ou un script. Le client le démarre, communique via son stdin et son stdout, et l’arrête à la fin de la session. Il n’y a pas de port à sécuriser.
Serveur distant via HTTP
Streamable HTTP permet à un seul serveur en fonctionnement de répondre à de nombreux clients. Cette version Express sans état crée un nouveau serveur pour chaque requête, ce qui évite un état de session partagé. Enregistrez-la sous src/http.ts :
import express from "express";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { buildServer } from "./build-server.js";
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const server = buildServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, "127.0.0.1");
Enregistrez-le avec claude mcp add --transport http demo http://127.0.0.1:3000/mcp. En Python, l’équivalent tient en une ligne : mcp.run(transport="streamable-http", host="127.0.0.1", port=9000). Liez-le à localhost, sauf si quelque chose devant le serveur gère l’authentification, car un port MCP ouvert est une porte ouverte sur chaque outil que vous avez enregistré.
Exemple : des outils qui génèrent des images
Les outils deviennent plus intéressants lorsque le résultat n’est pas un nombre. La génération d’images et de vidéos fait de bons cas d’étude, car elle est lente, asynchrone et renvoie une URL plutôt que du texte. Les exemples ci-dessous appellent l’API PicassoIA, qui suit un style proche de Replicate : créer une prédiction, l’interroger, lire le résultat.
URL de base et authentification :https://api.picassoia.com/v1 avec l’en-tête Authorization: Bearer pia_sk_...
Création :POST /models/{owner}/{name}/predictions avec le corps {"input": {...}}
Interrogation :GET /predictions/{id} jusqu’à ce que le statut soit succeeded, failed ou canceled
Délais : la réponse de création inclut eta.next_poll_in_seconds, un intervalle d’interrogation qu’il vaut la peine de respecter
Outil Python avec interrogation
import asyncio
import os
import httpx
from mcp.server.mcpserver import MCPServer
API = "https://api.picassoia.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['PICASSOIA_API_TOKEN']}"}
mcp = MCPServer("picassoia-media")
slots = asyncio.Semaphore(5) # the account allows 5 concurrent predictions
async def run_prediction(model: str, payload: dict, timeout_s: int = 600) -> list[str]:
async with slots, httpx.AsyncClient(headers=HEADERS, timeout=30) as http:
created = await http.post(
f"{API}/models/{model}/predictions", json={"input": payload}
)
created.raise_for_status()
prediction = created.json()
waited = 0
while prediction["status"] in ("starting", "processing"):
if waited >= timeout_s:
raise TimeoutError(f"Prediction {prediction['id']} is still running")
delay = (prediction.get("eta") or {}).get("next_poll_in_seconds", 3)
await asyncio.sleep(delay)
waited += delay
polled = await http.get(f"{API}/predictions/{prediction['id']}")
polled.raise_for_status()
prediction = polled.json()
if prediction["status"] != "succeeded":
raise RuntimeError(f"Prediction {prediction['status']}: {prediction.get('error')}")
output = prediction["output"]
return output if isinstance(output, list) else [output]
@mcp.tool()
async def generate_image(prompt: str, aspect_ratio: str = "16:9") -> str:
"""Generate one image from a text prompt and return its URL."""
urls = await run_prediction(
"picassoia/picassoia-image",
{"prompt": prompt, "aspect_ratio": aspect_ratio, "num_outputs": 1},
)
return urls[0]
if __name__ == "__main__":
mcp.run(transport="stdio")
Deux détails comptent ici. La boucle utilise asyncio.sleep, afin que le serveur reste réactif aux autres requêtes pendant qu’une tâche tourne, et elle suit l’intervalle suggéré par l’API au lieu de la solliciter en continu. Le sémaphore vous maintient sous la limite de concurrence du compte. Le modèle PicassoIA Image accepte prompt, aspect_ratio, seed, num_outputs (1 ou 2), output_format et output_quality. Pour les retouches, faites pointer le même helper vers PicassoIA Image Editor Pro.
Outil TypeScript pour la vidéo
Le même schéma fonctionne en TypeScript. Cette version ajoute un outil vidéo à la fonction buildServer vue précédemment :
const API = "https://api.picassoia.com/v1";
const headers = {
Authorization: `Bearer ${process.env.PICASSOIA_API_TOKEN}`,
"Content-Type": "application/json",
};
async function runPrediction(model: string, input: Record<string, unknown>) {
const created = await fetch(`${API}/models/${model}/predictions`, {
method: "POST",
headers,
body: JSON.stringify({ input }),
});
if (!created.ok) throw new Error(`Create failed: ${created.status}`);
let prediction = await created.json();
while (["starting", "processing"].includes(prediction.status)) {
const delay = prediction.eta?.next_poll_in_seconds ?? 5;
await new Promise((resolve) => setTimeout(resolve, delay * 1000));
const polled = await fetch(`${API}/predictions/${prediction.id}`, { headers });
prediction = await polled.json();
}
if (prediction.status !== "succeeded") {
throw new Error(`Prediction ${prediction.status}`);
}
return [prediction.output].flat() as string[];
}
server.registerTool(
"generate_video",
{
title: "Generate video",
description: "Make a short clip with synchronized audio from a text prompt",
inputSchema: {
prompt: z.string().max(4000),
duration: z.union([z.literal(5), z.literal(10)]).default(5),
resolution: z.enum(["480p", "720p"]).default("720p"),
},
},
async ({ prompt, duration, resolution }) => {
const [url] = await runPrediction("picassoia/seedance-2.5-lite", {
prompt,
duration,
resolution,
});
return { content: [{ type: "text", text: url }] };
}
);
La vidéo est plus lente. La page du modèle Seedance 2.5 Lite indique des exemples d’exécution d’environ 100 à 190 secondes, un délai suffisamment long pour que certains clients abandonnent un appel d’outil unique. Une conception plus sûre répartit le travail en deux : start_video renvoie immédiatement la prédiction id, et check_video prend cet identifiant et renvoie soit le statut, soit l’URL finale.
C’est ainsi que se comporte le connecteur PicassoIA pour claude.ai. Ses outils de génération renvoient un predict_id et un délai d’attente suggéré, et get_generation est appelé jusqu’à ce que la tâche réussisse ou échoue. Le même modèle accepte aussi un image facultatif comme première image, un seed, un aspect_ratio et un indicateur save_audio pour les clips sans son.
💡 Astuce : gardez le résultat de l’outil léger. Renvoyez l’URL et un résumé d’une ligne, pas le fichier. L’assistant n’a besoin que d’un lien à afficher ou à transmettre.
Utiliser PicassoIA avec MCP
Il existe deux façons d’atteindre PicassoIA depuis un assistant : le connecteur prêt à l’emploi, ou votre propre serveur qui encapsule l’API, comme dans les exemples ci-dessus.
Installation pas à pas
Choisissez la voie. Pour un client de chat, ajoutez le connecteur PicassoIA dans vos paramètres claude.ai. Il expose generate_image, edit_image, generate_video_picassoia, generate_video_seedance, get_generation, cancel_generation, list_models, get_account et list_generations. Pour votre propre code d’agent, utilisez la voie API.
Créez un token API. Connectez-vous à la page API de PicassoIA et créez-en un. Il commence par pia_sk_, et un compte en détient au maximum deux. Consultez la page des tarifs pour savoir quel forfait inclut l’accès à l’API avant de construire dessus.
Stockez-le dans l’environnement. Exécutez export PICASSOIA_API_TOKEN=pia_sk_... dans votre shell. Gardez-le hors des fichiers source et de toute configuration client que vous versionnez.
Choisissez un modèle dans le tableau ci-dessous.
Encapsulez et testez. Collez un outil issu des exemples, lancez-le dans l’Inspector avec un prompt simple, puis enregistrez-le auprès de votre client.
Texte ou image vers vidéo avec audio synchronisé, 5 ou 10 secondes
Quel modèle décide quand appeler vos outils ? N’importe quel LLM capable d’appeler des outils. Le catalogue de grands modèles de langage de PicassoIA propose notamment Claude Sonnet 5, GPT 5.6 Sol, Kimi K2.6 et Gemini 3.5 Flash, parmi beaucoup d’autres. Essayez-en plusieurs sur le même serveur et comparez leur fiabilité pour choisir le bon outil.
Les limites à connaître
5 prédictions simultanées par compte, partagées entre les jetons et les connexions MCP
10 Mo de taille maximale du corps de requête
4 000 caractères maximum par prompt
3 heures avant qu’une prédiction n’expire
2 tokens API par compte
Les erreurs qui cassent les serveurs MCP
La plupart des premiers essais échoués remontent à l’un de ces trois problèmes. Chacun est facile à éviter une fois qu’on sait où regarder.
Journaliser sur stdout
Avec stdio, la sortie standard est le canal du protocole. Un print() ou console.log() égaré corrompt le flux JSON-RPC, et le client signale une erreur d’analyse ou un serveur qui ne s’est jamais connecté. Envoyez plutôt les journaux vers la sortie d’erreur standard :
En TypeScript, utilisez console.error("polling prediction").
Appels bloquants et noms vagues
Un time.sleep(30) dans un outil asynchrone fige toutes les autres requêtes du même serveur. Utilisez await asyncio.sleep en Python et une minuterie utilisée avec await en TypeScript, comme le font les exemples. Les noms comptent tout autant : un outil appelé do_task ne donne rien au modèle pour choisir, alors que generate_image avec une docstring claire lui indique exactement quand faire appel à l’outil.
Secrets et charges volumineuses
Lisez les tokens depuis les variables d’environnement, jamais depuis les fichiers source, et ne les renvoyez jamais dans un résultat d’outil. Pour la sortie, renvoyez des URL plutôt que du base64. Une seule image en base64 peut atteindre plusieurs mégaoctets et saturer la fenêtre de contexte du modèle, et l’API PicassoIA limite de toute façon les corps de requête à 10 Mo.
Essayez dès aujourd’hui sur PicassoIA
Copiez server.py ou la paire TypeScript, lancez-le dans l’Inspector, et vous disposez d’un serveur MCP fonctionnel en moins de dix minutes. Donnez-lui ensuite quelque chose de visuel à faire. Les serveurs de référence officiels sur GitHub sont un bon endroit pour découvrir d’autres schémas, et les deux dépôts SDK contiennent un dossier d’exemples.
Ouvrez PicassoIA Image et rédigez votre propre prompt, retouchez une photo avec Image Editor Pro, ou animez une image avec Seedance 2.5 Lite. Quoi que vous construisiez, la boucle reste la même : décrire, soumettre, interroger, vérifier.
Essayez de créer vos propres images avec Picasso IA, et lorsqu’un prompt fonctionne, encapsulez-le dans un outil afin que votre assistant puisse le reproduire à la demande.