Comment créer un serveur MCP de zéro en Python, étape par étape

Un serveur MCP Python fonctionnel, depuis un dossier vide jusqu’à Claude Desktop. Écrivez des outils, des ressources et des prompts avec le SDK officiel 2.x, testez-les dans l’Inspector, ajoutez un vrai outil de génération d’images avec des appels API asynchrones, puis déployez-le via Streamable HTTP.

Comment créer un serveur MCP de zéro en Python, étape par étape
Cristian Da Conceicao
Fondateur de Picasso IA

La plupart des tutoriels MCP s’arrêtent à un outil météo qui renvoie une chaîne codée en dur. Celui-ci vous fait construire un serveur que vous pourrez réellement garder. Vous l’écrirez de zéro avec le SDK Python officiel, vous le testerez sans aucun client d’IA, vous le connecterez à Claude Desktop et Claude Code, et vous terminerez par un vrai outil qui appelle une API de génération d’images et attend le résultat. Tout ce qui suit nécessite Python 3.10 ou plus récent et correspond au SDK MCP Python 2.x (2.3.0 sur PyPI au moment de la rédaction, en octobre 2026).

Si vous avez copié du code d’un tutoriel de 2025 et que vous obtenez ModuleNotFoundError: No module named 'mcp.server.fastmcp', vous êtes au bon endroit. La classe principale a été renommée, et la section d’installation montre la correction en une ligne.

Ce que fait réellement un serveur MCP

Le Model Context Protocol (MCP) est une méthode standard qui permet à une application d’IA d’appeler votre code. Trois rôles comptent. L’hôte est l’application avec laquelle la personne interagit, comme Claude Desktop ou un IDE. Le client se trouve à l’intérieur de l’hôte et parle le protocole. Le serveur est la partie que vous construisez. Votre serveur ne communique jamais directement avec le modèle, il répond uniquement aux requêtes d’un client.

Trois primitives, trois responsables

Un serveur expose exactement trois types de capacités, et ce qui les distingue, c’est qui décide de les utiliser :

PrimitiveQui la déclencheCe que c’estExemple
OutilLe modèleUne fonction qui effectue une actionGénérer une image, écrire une ligne dans une base de données
RessourceL’applicationDes données chargées dans le contexte du modèleUn fichier, une configuration, un catalogue
PromptL’utilisateurUn modèle de message réutilisableUne commande slash

Si vous avez déjà construit une API web, la correspondance est rapide. Une ressource se comporte comme une GET, un outil se comporte comme une POST, et un prompt est une requête enregistrée que l’utilisateur lance par son nom.

Croquis à la main dans un carnet de trois blocs reliés, posé sur un bureau en chêne

💡 Règle empirique : si le modèle doit décider du moment où l’exécuter, faites-en un outil. Si l’application doit l’attacher, faites-en une ressource. Si une personne doit le choisir dans un menu, faites-en un prompt.

Choisir tôt son transport

Le transport correspond à la manière dont les données circulent entre le client et le serveur. Vous le choisissez avec un seul argument passé à mcp.run().

TransportFonctionnementUsage
stdioL’hôte lance votre fichier comme sous-processus et communique via son stdin et son stdoutServeurs locaux, et valeur par défaut
streamable-httpUn vrai serveur HTTP sur un port, avec le point d’accès à /mcpTout ce que vous déployez
sseL’ancien transport HTTPRien de nouveau, il a été remplacé dans la révision du protocole du 2025-03-26

Câbles réseau branchés sur un switch dans un petit local serveur

Commencez avec stdio. Vous passerez à Streamable HTTP vers la fin, et le code des outils restera exactement le même.

Configurer Python en cinq minutes

Mains d’un développeur tapant du code dans un éditeur, sur un ordinateur portable argenté

Installer uv et le SDK

Il vous faut Python 3.10 ou plus récent, ainsi que uv. Créez un projet et ajoutez le SDK :

uv init mcp-image-studio
cd mcp-image-studio
uv add "mcp[cli]" httpx

L’extra cli installe la commande mcp avec mcp dev, mcp run et mcp install. Un simple pip install "mcp[cli]" fonctionne aussi. L’Inspector est une application Node.js, donc npx doit être accessible depuis votre PATH.

Un renommage qui casse l’ancien code

Dans le SDK 1.x, la classe de haut niveau s’appelait FastMCP. Dans la 2.x, elle s’appelle MCPServer et se trouve dans un module différent :

# SDK 1.x, seen in older tutorials
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")

# SDK 2.x, used in this article
from mcp.server import MCPServer
mcp = MCPServer("Demo")

Un autre changement pose souvent problème : les paramètres de transport, comme port, ne sont plus passés au constructeur mais à run(). Transmettre port= à MCPServer(...) déclenche une TypeError.

Écrire votre premier serveur

Créez server.py. Un seul fichier, trois décorateurs, et chaque primitive est enregistrée :

from mcp.server 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 summarize(text: str) -> str:
    """Summarize a piece of text in one sentence."""
    return f"Summarize the following text in one sentence:\n\n{text}"


if __name__ == "__main__":
    mcp.run()

C’est un serveur fonctionnel. La garde if __name__ est importante : mcp dev, mcp run, mcp install et vos tests importent tous ce fichier, et un run() sans garde démarrerait un serveur dès que quelque chose le chargerait.

Ajouter un outil

Le SDK lit trois éléments dans votre fonction. Le nom devient le nom de l’outil, la docstring devient la description que voit le modèle, et les annotations de type deviennent le schéma des arguments. Vous n’avez pas de JSON Schema à écrire, car a: int, b: int est le schéma. Si un client envoie une chaîne là où vous avez déclaré un entier, le SDK rejette l’appel avant que votre fonction ne s’exécute.

Donnez une valeur par défaut à un paramètre et il devient facultatif. Pour des contraintes plus strictes, enveloppez le type dans Annotated avec un Field Pydantic :

from typing import Annotated, Literal
from pydantic import Field


@mcp.tool()
def search_books(
    query: str,
    limit: Annotated[int, Field(ge=1, le=50, description="Maximum results")] = 10,
    genre: Literal["fiction", "non-fiction", "poetry"] = "fiction",
) -> str:
    """Search the catalog by title or author."""
    return f"Found 3 books matching {query!r} (up to {limit})."

Les bornes apparaissent dans le schéma sous forme de minimum et maximum, et le Literal devient une énumération parmi laquelle le modèle doit choisir.

Ajouter une ressource et un prompt

Un {param} dans une URI de ressource en fait un modèle de ressource, si bien que greeting://{name} n’a aucune entrée unique à lister tant que quelqu’un n’a pas fourni un nom. Un prompt est encore plus simple : la chaîne qu’il renvoie devient un message utilisateur. Les deux lisent leur description dans la docstring, exactement comme les outils.

Lever des erreurs que le modèle peut lire

Quand un outil échoue, levez ToolError. Ne renvoyez jamais une chaîne d’erreur, car une chaîne renvoyée n’a pas is_error=False et ressemble à une réponse réussie.

from mcp.server.mcpserver.exceptions import ToolError

CATALOG = {"Dune": "Frank Herbert", "Neuromancer": "William Gibson"}


@mcp.tool()
def get_author(title: str) -> str:
    """Look up the author of a book in the catalog."""
    if title not in CATALOG:
        raise ToolError(f"No book titled {title!r} in the catalog.")
    return CATALOG[title]

Le modèle lit ce message, comprend qu’il a mal deviné le titre et rappelle l’outil avec un meilleur. Une seule raise vous donne un agent qui se corrige lui-même. Toute autre exception est traitée comme un plantage : le modèle voit seulement que l’appel a échoué, et votre journal reçoit la trace complète.

💡 Déclarez un outil async def chaque fois qu’il effectue des E/S, comme un appel API, une lecture de fichier ou une requête de base de données. Utilisez un simple def pour tout le reste.

Tester et connecter le serveur

Lancer le MCP Inspector

Avant qu’un client d’IA ne touche à votre serveur, lancez-le dans l’Inspector :

uv run mcp dev server.py

Ouvrez l’URL qui s’affiche. L’Inspector lance server.py comme sous-processus via stdio, exactement comme le ferait un hôte réel. Parcourez les onglets dans l’ordre :

  • Tools : add apparaît avec un formulaire construit à partir de vos annotations de type. Appelez-le avec a=1 et b=2 et vous obtenez 3.
  • Resources : la liste est vide, et greeting apparaît sous Resource Templates. Donnez-lui World et vous lisez Hello, World!.
  • Prompts : summarize a un argument text obligatoire et renvoie un seul message utilisateur.

Deux ingénieurs examinant du code sur un ordinateur portable, debout devant un bureau

Écrire un test en mémoire

La classe Client du SDK permet aussi de se connecter en mémoire : il suffit de lui passer l’objet serveur, sans sous-processus ni port. Ajoutez pytest avec uv add --dev pytest, puis créez test_server.py :

import pytest
from mcp import Client

from server import mcp


@pytest.fixture
def anyio_backend():
    return "asyncio"


@pytest.mark.anyio
async def test_add():
    async with Client(mcp, raise_exceptions=True) as client:
        result = await client.call_tool("add", {"a": 1, "b": 2})
        assert result.structured_content == {"result": 3}

Gardez raise_exceptions=True uniquement dans les tests. Il rend visible le vrai message d’erreur, au lieu du message Internal server error épuré qu’un appelant distant verrait.

Connecter Claude Desktop et Claude Code

Chaque hôte a besoin de la même chose : la commande qui démarre votre serveur. Celle-ci fonctionne depuis n’importe quel répertoire, sans environnement virtuel à activer :

uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

Claude Desktop est le seul hôte que le SDK peut configurer pour vous :

uv run mcp install server.py

Cela ajoute une entrée dans claude_desktop_config.json, situé dans ~/Library/Application Support/Claude/ sous macOS et %APPDATA%\Claude\ sous Windows. Quittez complètement Claude Desktop, pas seulement la fenêtre, puis rouvrez-le. L’application démarre votre serveur avec son propre environnement, donc transmettez vos secrets via -v NAME=value ou -f .env.

Claude Code n’a besoin d’aucun fichier. Enregistrez le serveur avec la CLI, puis exécutez /mcp dans une session pour vérifier qu’il est bien connecté :

claude mcp add image-studio -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

Cursor lit .cursor/mcp.json sous le champ mcpServers, et VS Code lit .vscode/mcp.json sous servers avec "type": "stdio". La commande à l’intérieur est identique.

Ordinateur portable affichant une fenêtre de chat à côté d’un terminal, posé sur une table en bois

Corriger un serveur qui n’apparaît pas

Commencez par lancer vous-même la commande de démarrage. Un serveur stdio en bon état n’affiche rien et attend qu’un hôte lui parle en premier. Une trace d’erreur ou une sortie immédiate est votre vrai problème. S’il attend en silence, vérifiez ces trois causes :

SymptômeCauseCorrectif
Le serveur ne démarre jamaisUn chemin relatif, car l’hôte lance le serveur depuis son propre répertoire de travailUtilisez des chemins absolus, y compris celui vers uv (where uv sous Windows, which uv ailleurs)
Les modifications n’ont aucun effetLes hôtes lisent leur configuration au lancementQuittez complètement l’hôte et rouvrez-le
La connexion tombe aussitôtQuelque chose a écrit sur stdout, qui est le canal du protocoleJournalisez avec le module logging, qui écrit sur stderr, et ne vous fiez jamais à print()

Claude Desktop conserve un journal par serveur, nommé mcp-server-<NAME>.log, dans ~/Library/Logs/Claude sous macOS et %APPDATA%\Claude\logs sous Windows. Ce fichier correspond au stderr de votre serveur.

Construire un vrai outil de génération d’images

Un serveur mérite sa place quand un outil réalise un travail que le modèle ne peut pas faire seul. Celui-ci prend un prompt, demande à l’API de PicassoIA une image et renvoie l’URL.

Concevoir le contrat de l’outil

L’API fonctionne sur le modèle de Replicate : vous créez une prédiction, vous l’interrogez, puis vous lisez le résultat. Voici les éléments dont dépend l’outil :

DétailValeur
URL de basehttps://api.picassoia.com/v1
AuthentificationAuthorization: Bearer pia_sk_..., créé sur la page API
Créer une tâchePOST /v1/models/{owner}/{name}/predictions avec {"input": {"prompt": "..."}}
Modèle utilisé iciPicassoIA Image, slug picassoia/picassoia-image
Longueur du promptDe 1 à 4 000 caractères
Valeurs de statutstarting, processing, succeeded, failed, canceled
Concurrence5 prédictions par compte, partagées entre tous les tokens et toutes les connexions MCP

La documentation de l’API indique qu’un plan Infinite est requis, et qu’une requête sans lui renvoie 403 plan_required. Les prédictions sont décrites comme gratuites et n’utilisent aucun crédit. Vérifiez votre plan avant de déboguer quoi que ce soit d’autre.

Femme dessinant un schéma de flux d’API sur un tableau blanc, dans un loft ensoleillé

Gardez le contrat simple : un outil, deux paramètres, une URL en retour. Chaque chemin d’échec lève ToolError, ainsi le modèle reçoit toujours un message lisible.

Gérer les tâches asynchrones sans bloquer

Comme la tâche s’exécute sur un GPU distant, l’outil doit attendre sans figer le serveur. Cela passe par async def, httpx.AsyncClient et asyncio.sleep :

import asyncio
import os
from typing import Literal

import httpx
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

API = "https://api.picassoia.com/v1"
MODEL = "picassoia/picassoia-image"
DONE = ("succeeded", "failed", "canceled")

mcp = MCPServer("Image Studio")
slots = asyncio.Semaphore(4)


@mcp.tool()
async def generate_image(
    prompt: str,
    aspect_ratio: Literal["1:1", "16:9", "9:16", "4:3"] = "16:9",
) -> str:
    """Generate an image from a text prompt and return its URL."""
    token = os.environ.get("PICASSOIA_API_TOKEN")
    if not token:
        raise ToolError("PICASSOIA_API_TOKEN is not set for this server.")

    headers = {"Authorization": f"Bearer {token}"}
    body = {"input": {"prompt": prompt, "aspect_ratio": aspect_ratio}}

    async with slots, httpx.AsyncClient(headers=headers, timeout=30) as http:
        response = await http.post(f"{API}/models/{MODEL}/predictions", json=body)
        prediction = response.json()
        if not response.is_success:
            raise ToolError(f"{prediction.get('code')}: {prediction.get('detail')}")

        while prediction["status"] not in DONE:
            eta = prediction.get("eta") or {}
            await asyncio.sleep(eta.get("next_poll_in_seconds", 2))
            prediction = (await http.get(prediction["urls"]["get"])).json()

    if prediction["status"] != "succeeded":
        raise ToolError(prediction.get("error") or prediction["status"])

    output = prediction["output"]
    return output[0] if isinstance(output, list) else output


if __name__ == "__main__":
    mcp.run()

Stylo pointant des notes de réponse d’API à côté d’un ordinateur portable

Trois détails permettent de le laisser tourner sans risque :

  1. Interrogez selon le rythme du serveur. La réponse contient eta.next_poll_in_seconds, donc vous attendez exactement le temps demandé par l’API.
  2. Limitez votre propre concurrence. Le Semaphore(4) empêche un modèle trop bavard d’occuper les 5 emplacements du compte.
  3. Lisez le token depuis l’environnement. Enregistrez-le avec mcp install server.py -v PICASSOIA_API_TOKEN=pia_sk_..., et ne le collez jamais dans le fichier.

💡 Le champ output peut être une liste d’URL, une seule URL, ou null. Les deux dernières lignes gèrent les deux premiers cas, et la vérification succeeded au-dessus exclut en pratique null.

Comment utiliser Sonnet 5 sur PicassoIA

L’écriture du code des outils est l’étape où un modèle de langage vous fait gagner le plus de temps. Claude Sonnet 5 sur PicassoIA gère les tâches de code en plusieurs étapes et d’usage d’outils, vous pouvez donc coller l’outil generate_image qui fonctionne et demander le suivant.

  1. Ouvrez la page du modèle de Claude Sonnet 5.
  2. Rédigez le prompt. Collez votre serveur et demandez : Ajoutez un second outil qui liste mes prédictions récentes avec GET /v1/predictions. Réutilisez la même gestion des erreurs. Le champ prompt est le seul obligatoire.
  3. Définissez un prompt système pour éviter le problème de renommage dès le départ : Vous écrivez du Python pour le SDK MCP 2.x. Importez MCPServer depuis mcp.server et n’utilisez jamais FastMCP.
  4. Choisissez l’effort. La valeur par défaut low n’active pas la réflexion étendue et est la plus rapide. Utilisez high ou max pour un bug qui touche plusieurs fichiers.
  5. Réglez les tokens de sortie maximum sur 8192 pour les fichiers de serveur complets, ou réduisez-les pour un court extrait.
  6. Joignez une capture d’écran d’une erreur de l’Inspector si vous en avez une. Le modèle lit les images, et max_image_resolution (0,5 mégapixel par défaut) les réduit.
  7. Générez, copiez et testez. Collez le résultat dans server.py et exécutez-le avec mcp dev avant de lui faire confiance.
ParamètreObligatoirePar défautRôle
promptOuiaucunVotre requête
system_promptNonvideFixe le rôle et les contraintes de la session
effortNonlowProfondeur de réflexion, du plus rapide au plus approfondi
max_tokensNon8192Plafond de longueur de sortie
imageNonaucunUne capture d’écran ou un schéma comme contexte

Vous préférez un autre modèle ? Kimi K2.6 fait partie de la même catégorie et est décrit comme adapté à la création d’agents et à l’écriture de code.

Déployer via HTTP

Technicien marchant dans un couloir de baies serveur

Changer le transport

Modifiez une ligne en bas de server.py :

if __name__ == "__main__":
    mcp.run(transport="streamable-http", port=3001)

Les clients se connectent maintenant à http://127.0.0.1:3001/mcp. Vous pouvez aussi laisser le fichier tel quel et exécuter uv run mcp run server.py --transport streamable-http. L’appel run() accepte ces options :

  • host et port, avec pour valeurs par défaut 127.0.0.1 et 8000
  • streamable_http_path, avec pour valeur par défaut /mcp
  • json_response=True pour répondre à chaque POST par un seul corps JSON
  • stateless_http=True pour un transport neuf à chaque requête

Enregistrez le serveur distant dans Claude Code avec claude mcp add --transport http image-studio https://mcp.example.com/mcp.

Le verrouiller

Dès que votre serveur quitte localhost, trois choses changent :

  • La liste d’autorisation des hôtes. Par défaut, seuls 127.0.0.1, localhost et [::1] sont acceptés. Derrière un vrai nom d’hôte, chaque requête échoue avec 421 Misdirected Request et Invalid Host header. Corrigez cela avec transport_security= et ajoutez à allowed_hosts à la fois "mcp.example.com" et "mcp.example.com:*".
  • L’autorisation. Votre serveur est un serveur de ressources OAuth 2.1. Implémentez TokenVerifier avec une méthode asynchrone verify_token qui renvoie un token d’accès ou None, et transmettez token_verifier= avec auth=.
  • TLS derrière un proxy. Lorsqu’un load balancer termine le TLS, lancez uvicorn avec --proxy-headers pour qu’il fasse confiance aux en-têtes transmis.

💡 Une 421 est une simple réponse HTTP, pas une erreur de protocole : le client n’affiche donc qu’un échec de transport générique. Le nom d’hôte fautif apparaît dans le journal du serveur. Un serveur fraîchement déployé qui refuse toutes les connexions relève d’un problème de liste d’autorisation des hôtes, jusqu’à preuve du contraire.

Créez vos propres images avec Picasso IA

Développeur renversé dans son fauteuil après avoir terminé son travail

Vous disposez maintenant d’un serveur qui enregistre des outils, des ressources et des prompts, réussit un test en mémoire, fonctionne dans Claude et peut être déployé derrière un vrai nom d’hôte. Ce qui mérite votre prochaine heure, c’est l’outil lui-même : changez le slug du modèle, ajoutez un outil edit_image, ou branchez à côté un outil vidéo.

Essayez le modèle que votre serveur vient d’appeler. PicassoIA Image transforme un prompt en image finie en quelques secondes, et vous pouvez tester n’importe quel prompt dans le navigateur avant de l’automatiser. Quand une image fixe ne suffit pas, PicassoIA Video et Seedance 2.5 Lite animent un prompt ou une photo en courts clips.

Ouvrez Picasso IA, choisissez un modèle et lancez le même prompt que celui que vous donneriez à votre outil. Ensuite, branchez-le à votre serveur et laissez Claude cliquer à votre place.

Partager cet article

Choisissez votre langue