All articles

Déployer un MCP sécurisé en entreprise

Amrltqt
··6 min read
Déployer un MCP sécurisé en entreprise

On parle beaucoup de MCP, mais en entreprise à part pour connecter des services génériques genre la suite Atlassian, drive, slack, etc.. c'est finalement pas vraiment adopté.

Développer des applications d'entreprise ça nécessite d'échanger des informations sensibles qu'on ne souhaite pas transmettre au modèle de langage.

MCP est vu comme l'usb-c du monde agentique et c'est très cool, mais comment on fait quand on a besoin de passer des tokens, des informations utilisateurs et que ces informations ne doivent pas passer par le modèle de langage?

Si des informations sensibles transitent par le modèle de langage, alors il y a un risque qu'un utilisateur mal intentionné puisse contourner toutes les instructions et éventuellement les modifier. Et ça c'est pas génial.

Dans la suite de l'article on va montrer une approche avec un client utilisant OpenAI agent SDK et un serveur fastmcp. Le client et le serveur MCP tournent en local, mais dans l'idée, les deux tournent dans un environment que vous maîtrisez (pas sur le poste de l'utilisateur).

Un peu de contexte.

Je travaille pour une marketplace, je veux aider nos clients à retrouver leur commande. J'ai des APIs pour ça et j'aimerais bien que mon agent puisse gérer des questions comme "Où en est ma commande #123 ?".

Un agent est capable de comprendre la question, de détecter le numéro de commande et d'appeler un outil MCP pour renvoyer les informations.

Voyons un peu de code tout cru. On retrouve côté serveur quelque chose comme ça:

text
ORDERS = [
    {
        "id": "#12345",
        "user_id": "user-123",
        "status": "shipped",
        "customer": "Jean Dupont",
        "address": "12 rue de la soif, Toulouse, France"
    },
    {
        "id": "#67890",
        "user_id": "user-456",
        "status": "delivered",
        "customer": "Jeanne Martin",
        "address": "46 avenue de la joie, Bordeaux, France"
    }
]

@mcp.tool
def find_order(order_id: str) -> str:
    for order in ORDERS:
        if order["id"] == order_id:
            return f"Order {order_id} is {order['status']} to {order['customer']} at {order['address']}"
    return f"Order {order_id} not found"

Et côté client quelque chose comme ça:

text
USER = "user-123"

async with MCPServerStreamableHttp(
    name="Streamable HTTP Python Server",
    params={
        "url": "http://localhost:8000/mcp",
        "timeout": 10,
    },
) as server:
    agent = Agent(
        name="My Agent",
        instructions="You're a customer support agent, use your tools to find the order status.",
        mcp_servers=[server],
        model_settings=ModelSettings(tool_choice="required"),
        model="gpt-4.1-mini"
    )

    result = await Runner.run(agent, "Where is my order #67890")
    print(result.final_output)py

Si vous avez une galère pour comprendre ce code ou des interrogation sur comment démarrer, mettez un commentaire sous l'article.

Si vous utilisez votre agent avec un serveur MCP en local vous verrez que ça va bien marcher, mais peut être un peu trop car qu'importe si la commande est lié à l'utilisateur du client. Aucun contrôle n'est fait côté serveur pour le moment.

Les options

Plusieurs pistes sont envisageables.

  • Utiliser les query params d'abord lors de la définition du MCPServerStreamableHTTP et récupérer les informations côté serveur en inspectant l'url*.*
  • Créer les paramètres côté serveur et les réécrire à la volée en utilisant un tool côté client qui s'occupe d'initialiser et d'appeler MCP lui même.
  • Signer un token JWT, passer les informations en claims et utiliser le header authorization pour passer les infos.

Le premier c'est ok, mais pas hyper securisé. Le second c'est totalement contreproductif, pourquoi utiliser MCP si on part là dessus. Le troisième si le contexte à partager est important c'est pas génial, mais c'est securisé et plutôt bien supporté. On part là dessus!

Correction du code serveur

On va utiliser un code HMAC pour sécuriser la transmission des informations. Pour ça côté serveur on va simplement réutiliser ce que fastmcp propose à savoir un JWTVerfier.

text
SECRET = os.getenv("JWT_SECRET", "default-secret-key-change-me")

verifier = JWTVerifier(
    public_key=SECRET,
    issuer="order-agent",
    audience="order-api",
    algorithm="HS256"
)

mcp = FastMCP("MCP Order Server", auth=verifier)

Et on modifie le tool pour tirer partie de l'access_token qui va être décodé par le verifier. A chaque appel, ce middleware va vérifier qu'il y a bien un JWT transmis, vérifier qu'il est fiable et transmettre les infos pour que les handlers puisse y accéder.

On va donc aussi modifier le code du tool MCP pour vérifier que l'utilisateur est bien fourni.

text
@mcp.tool
def find_order(order_id: str) -> str:
    access_token = get_access_token()
    if not access_token:
        raise ValueError("Access token is missing")

    claims = access_token.claims
    for order in ORDERS:
        if order["id"] == order_id and claims["sub"] == order["user_id"]:
            return f"Order {order_id} is {order['status']} to {order['customer']} at {order['address']}"
    return f"Order {order_id} not found"

A noter que les informations qu'on veut transmettre sont dans les claims et qu'on peut maintenant faire confiance au client. C'est la clé secrete, qu'il faut partager secrètement avec le client, qui garantie que les informations transmises sont fiables.

Correction du client

Côté client on va simplement chiffrer les informations que l'on veut transmettre. Ici on va utiliser le user_id qu'on va mettre dans le champ sub vu qu'on soumet la demande pour cet utilisateur.

text
SECRET = os.getenv("JWT_SECRET", "default-secret-key-change-me")
USER = "user-123"

payload = {
    "sub": USER,
    "exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(hours=1),
    "aud": "order-api",
    "iss": "order-agent"
}

token = jwt.encode(payload, SECRET, algorithm="HS256")

Ce token qu'on a créé, on va simplement le passer en header de l'appel HTTP.

text
async with MCPServerStreamableHttp(
    name="Streamable HTTP Python Server",
    params={
        "url": "http://localhost:8000/mcp",
        "timeout": 10,
        "headers": {"Authorization": f"Bearer {token}"}
    },
)

Et voilà, c'est tout.

Conclusion

Le design est ok, c'est pas foufou car on doit chiffrer un token à chaque appel, si on veut faire de la réutilisation on peut aussi mais ça demande un peu plus de logique.

  1. Par contre c'est complètement stateless, il suffit de partager la clé secrète par variable d'environnement et on n'a aucune autre dépendance. Pas de session, rien.
  2. On peut embarquer le context utilisateur dans les claims au-delà du sub, très pratique si vous avez des informations qui ne sont pas forcément sensible mais qui n'ont pas d'intérêt à être transmis dans le contexte du modèle de langage comme des rôles ou des préférences utilisateur.
  3. FastMCP fait le job, très peu de lignes de code.

Alors, c'est pas différent de ce qu'on peut faire en SOA, donc c'est pas très novateur. Par contre, maintenant vous avez une solution si vous avez besoin de transmettre des informations de contexte entre votre client et un serveur MCP!

Stay in the loop

Get new articles delivered directly to your inbox. No spam, unsubscribe anytime.