---
title: MCP Server
author: Frederic AOUSTIN
version: 1.0
---

# MCP Server

![category](developpement)
![tag](python)
![tag](ia)

L'IA avance très vite, ce n'est pas une grande nouvelle ... mais une chose importante est arrivée à me yeux: l'IA ne se contente plus d'être un LLM (un outil qui génère du texte et qui connait beaucoup de relation ce qui en faite un truc qui sait beaucoup de chose) c'est aussi maintenant une architecture qui permet d'intervenir,  d'intégrer des éléments autres et réels.

En sommes on peut via un **orchestrateur** faire des demandes à des services et utiliser le **LLM** pour comprendre des questions ou générer du texte.

Je vois le **MCP** comme une standardisation d'appel API pour des orchestrateurs IA.
Il ne s'agit pas seulement d'API type REST les échanges sont plus fins et permettent plus de choses.
Par exemple la norme MCP introduit la possibilité pour un orchestrateur de découvrir les APIs disponible.
Actuellement la norme est surtout leadé par *Antrophic* 

![20260531_mcp_integration.png](./upload/20260531_mcp_integration.png)

Par défaut un serveur MCP travail sur en mode *stdio* c'est à dire sur les sorties standards d'un script ... ce qui lui permet de faire des actions sur l'ordinateur qui porte l'orchestrateur.

```python
import sys
import logging


# ✅ Good (STDIO)
print("Error in Processing request", file=sys.stderr)

# ✅ Good (STDIO)
logging.info("Processing request")
```

Quand on prend l'outil claude, en effet il est installé sur l'ordinateur on peut donc lancer des commandes python ou autre et donc un serveur MCP.

Même si cette approche est simple pour des interactions locales pour moi elle a un principale inconvenient: c'est local !!! il faut donc pour chaque poste traiter les problématique d'installation et de paramétrage. Avec mon regarde de DSI je trouve cela peut pratique et laisse place à de grandes problématiques de maintenabilité ... mais c'est un autre sujet.

Pour ma part je préfère réaliser des serveurs MCP accessible via http ... comme cela je peux les appeler de n'importe où par n'importe quel orchestrateur et je peux gérer la maintenance et l'évolution de ces serveurs

Un serveur *MCP* peut fournir

- des outils (tools): pour faire simple une fonction python qu'on peut lancer et qui vous donne un résultat
- des prompts: il vous retourne depuis vos entrées un prompt que vous allez pouvoir ré-utiliser
- des assets

Nous allons voir par la suite la mise en place d'un serveur *MCP* ayant pour objectif de mettre à disposition:

- un service de localisation; vous indiquez un nom de ville et il retourne la longitude et la lattitude
- un service de météo: vous indiquez une ville et il vous donne la météo en temps réel (en passant par openweather)

## Le code

Comme d'habitude je souhaite que mon serveur comporte quelques éléments primordial

- des tokens de session: il faut sécuriser un minimum les appels
- une gestion centralisé des paramètres via les variables d'environnements ou un fichier *.env*
- une route pour la notion de version


Je n'ai pas intégré de gestion de LOGS pour cet exemple ... mais pour les futures versions j'intègrerai les logs à 7 niveaux (voir design pattern sur les [logs](https://blog.fraoustin.fr/?md=20260218145500))

mon fichier .env contient

```
API_TOKEN = "NENNAhVSBm4GGtVbKKASAWoU7oRXT5"
API_TOKEN_WEATHER = 'xxxxxxxxxxxxxxxxxxxxxxxxx'
```

mon fichier requirements.txt contient

```
mcp==1.27.2
pydantic_core==2.46.4
requests==2.34.2
```

J'utilise en effet la librairie mcp qui apporte beaucoup d'éléments pour créer un serveur MCP fonctionnel. Il permet ainsi de gérer tous les appels concernants la découverte des des prompts, tools, asset disponibles.
Cette librairie ce base pour faire cela sur la norme pydantic et sur les commentaires des classes et fonctions ... parfois cela est un peu lourds (notament dans la gestion des paramètres) mais permet un gain de temps important et une gestion documentaire simple.


mon fichier main.py

```python
import os
from dotenv import load_dotenv
from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
import uvicorn
import requests

__version__ = "1.0.0"

load_dotenv(override=True)

API_TOKEN = os.getenv('API_TOKEN', "NENNAhVSBm4GGtVbKKASAWoU7oRXT5")
API_TOKEN_WEATHER =os.getenv('API_TOKEN_WEATHER')
MCP_HOST = os.getenv('MCP_HOST', '0.0.0.0')
MCP_PORT = os.getenv('MCP_PORT', 8000)

mcp = FastMCP(
    "MCP Server Geography",
     host=MCP_HOST,
     port=MCP_PORT,
)

class GeoLocation(BaseModel):
    city: str
    latitude: float  = Field(description="Latitude")
    longitude: float = Field(description="Longitude")


class WeatherData(BaseModel):
    temperature: float = Field(description="Temperature in Celsius")
    humidity: float    = Field(description="Humidity percentage")
    condition: str
    wind_speed: float


@mcp.tool(
    name="get_coordinates",
    title="Get Coordinates",
    description="Get latitude and longitude for a city"
)
def get_coordinates(city: str, language: str = "fr") -> GeoLocation:
    params = {
        "name": city,
        "count": 1,
        "language": language
    }
    api_address='https://geocoding-api.open-meteo.com/v1/search?'
    results = requests.get(api_address, params=params).json()["results"][0]
    return GeoLocation(
        city=results["name"],
        latitude=float(results["latitude"]),
        longitude=float(results["longitude"]),
    )    


@mcp.tool(
    name="get_weather",
    title="Get Weather",
    description="Weather information structure"
)
def get_weather(city: str, language: str = 'fr') -> WeatherData:
    geo = get_coordinates(city, language)
    api_address='http://api.openweathermap.org/data/2.5/weather?'
    params = {
        'appid': API_TOKEN_WEATHER,
        'lat': geo.latitude,
        'lon': geo.longitude,
        'units': 'metric',
        'lang': language
    }
    data = requests.get(api_address, params=params).json()
    return WeatherData(
        temperature=data['main']['temp'],
        humidity=data['main']['humidity'],
        condition=data['weather'][0]["description"],
        wind_speed=data['wind']['speed']
    )


async def version_endpoint(request: Request):
    return JSONResponse({
        "name": "Geography Server",
        "version": __version__,
        "protocol": "2026-05-31",
    })


class TokenAuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        if request.url.path == "/version":
            return await call_next(request)
        if request.method == "OPTIONS":
            return await call_next(request)
        auth = request.headers.get("Authorization", "")
        if auth != f"Bearer {API_TOKEN}":
            return JSONResponse(
                {"error": "Unauthorized"},
                status_code=401
            )
        return await call_next(request)



if __name__ == "__main__":
    app = mcp.streamable_http_app()

    from starlette.routing import Route
    app.routes.append(Route("/version", version_endpoint))

    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
        expose_headers=["mcp-session-id"],
        allow_credentials=False,
    )
    app.add_middleware(TokenAuthMiddleware)
    print(f"Starting Geography Server v{__version__}")
    uvicorn.run(app, host=MCP_HOST, port=MCP_PORT)

```

On constate que je gère moi même la partie serveur uvicorn: en effet je ne fait pas un simple mcp.run() car je souhaite

- ajouter une route version
- gérer la partie CORS (important pour les outils de tests notamment)
- gérer la partie authentification

On expose ainsi les deux services

- get_weather
- get_coordinates

sur un lien http

Pour lancer mon serveur MCP rien de plus simple

> python main.py



## Les tests pour la chaine de CI/CD

Et bien sur il faut des fichiers de test **unittest** dans un répertoire *tests*.
Içi j'utilise beaucoup d'API externe, pour mes tests il faut pouvoir les mocker afin d'imiter leur fonctionnement sans lancer les appels

mon fichier test_mcp_geography.py

```python
# tests/test_mcp_geography.py

import unittest
from unittest.mock import patch, Mock

from main import (
    get_coordinates,
    get_weather,
    GeoLocation,
    WeatherData,
)


class TestGetCoordinates(unittest.TestCase):

    @patch("main.requests.get")
    def test_get_coordinates(self, mock_get):

        mock_response = Mock()
        mock_response.json.return_value = {
            "results": [
                {
                    "name": "Paris",
                    "latitude": 48.8566,
                    "longitude": 2.3522
                }
            ]
        }

        mock_get.return_value = mock_response

        result = get_coordinates("Paris")

        self.assertIsInstance(result, GeoLocation)
        self.assertEqual(result.city, "Paris")
        self.assertEqual(result.latitude, 48.8566)
        self.assertEqual(result.longitude, 2.3522)


class TestGetWeather(unittest.TestCase):

    @patch("main.requests.get")
    def test_get_weather(self, mock_get):

        # Réponse géocodage
        geo_response = Mock()
        geo_response.json.return_value = {
            "results": [
                {
                    "name": "Paris",
                    "latitude": 48.8566,
                    "longitude": 2.3522
                }
            ]
        }

        # Réponse météo
        weather_response = Mock()
        weather_response.json.return_value = {
            "main": {
                "temp": 23.5,
                "humidity": 65
            },
            "weather": [
                {
                    "description": "ciel dégagé"
                }
            ],
            "wind": {
                "speed": 4.8
            }
        }

        mock_get.side_effect = [
            geo_response,
            weather_response
        ]

        result = get_weather("Paris")

        self.assertIsInstance(result, WeatherData)
        self.assertEqual(result.temperature, 23.5)
        self.assertEqual(result.humidity, 65)
        self.assertEqual(result.condition, "ciel dégagé")
        self.assertEqual(result.wind_speed, 4.8)


if __name__ == "__main__":
    unittest.main()
```

il suffit par la suite de lancer les tests en réalisant

> python -m unittest discover -s tests


## Les outils pour interroger

A ma grande surprise il existe peut d'outil simple pour gérer des tests de serveur MCP avec authentification.

J'en ai comme mêm sélectionné trois avec des avantages différents

### curl

Comme nous sommes sur de l'http on peut réaliser l'ensemble des échanges de l'orchestrateur par curl.

un exemple sans gestion d'authentification

```bash
#!/bin/bash

SESSION=$(curl -s -X POST http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -D /dev/stderr \
  -d '{"jsonrpc":"2.0","method":"initialize","id":0,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
  2>&1 >/dev/null | grep -i mcp-session-id | awk '{print $2}' | tr -d '\r')

echo "Session: $SESSION"

echo "List tools"
curl -X POST http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'


echo "Test tools get_weather"
curl -X POST http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"get_weather","arguments":{"city":"Paris"}}}'

```

Avec authentification

```bash
#!/bin/bash

TOKEN="NENNAhVSBm4GGtVbKKASAWoU7oRXT5"

SESSION=$(curl -s -X POST http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer $TOKEN" \
  -D /dev/stderr \
  -d '{"jsonrpc":"2.0","method":"initialize","id":0,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}' \
  2>&1 >/dev/null | grep -i mcp-session-id | awk '{print $2}' | tr -d '\r')

echo "Session: $SESSION"

echo "List tools"
curl -X POST http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'


echo "Test tools get_weather"
curl -X POST http://127.0.0.1:8000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: $SESSION" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"get_weather","arguments":{"city":"Paris"}}}'

```

C'est rustique mais efficase et ne nécessite aucune installation

### inspector

Il s'agit d'un outil web qu'on peut lancer via docker

```bash
docker run --rm \
  -p 127.0.0.1:6274:6274 \
  -p 127.0.0.1:6277:6277 \
  -e HOST=0.0.0.0 \
  -e MCP_AUTO_OPEN_ENABLED=false \
  ghcr.io/modelcontextprotocol/inspector:latest
```

Comme il est dans un container il ne faut pas utiliser comme ip localhost mais la vraie IP de votre machine.
Au niveau de l'authentification il faut simplement rajouter 

> Bearer NENNAhVSBm4GGtVbKKASAWoU7oRXT5

J'ai trouvé un autre outil du même type mcp-use qui propose un outil en ligne https://inspector.mcp-use.com/inspector ou via docker "docker run -d -p 8080:8080 --name mcp-inspector mcpuse/inspector:latest"

### mcptools ... vive la ligne de commande

Il s'agit d'un outil qui peut être utilisé simplement en ligne de commande.
Il est écrit en go et comme je n'aime pas modifier mon pc local on peut le lancer via une image docker à construire à partir de ce Dockerfile

```
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
RUN git clone https://github.com/f/mcptools.git .
RUN go build -o /mcptools ./cmd/mcptools

FROM alpine:latest
COPY --from=builder /mcptools /usr/local/bin/mcp
ENTRYPOINT ["mcp"]
```

On doit créer l'image

> docker build -t mcptools .

puis l'usage

- docker run --rm mcptools tools http://ton-ip:8000/mcp
- docker run --rm mcptools call get_weather --params '{"city":"Paris"}' http://ton-ip:8000/mcp
- docker run --rm -it mcptools shell http://ton-ip:8000/mcp
- docker run --rm -it mcptools shell --auth-header "Bearer NENNAhVSBm4GGtVbKKASAWoU7oRXT5" http://ton-ip:8000/mcp (pour passage de token d'authorisation)

l'usage du shell est sympa car permet d'ouvrir un canal vers son serveur mcp puis de lancer les commandes qui nous interresse

on peut utiliser un raccourcit dans son .bashrc

> alias mcptools='docker run --rm -it mcptools'


## Docker pour déployer

Là encore je continue à utiliser ma chaine de CI/CD classique et donc je cherche à avoir ue image docker opérationnel

Pour cela j'utilise le Dockerfile suivant

```
FROM python:3.12-alpine

WORKDIR /app
RUN pip install --upgrade pip
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py main.py

ENV MCP_HOST=0.0.0.0
ENV MCP_PORT=8000
EXPOSE 8000

CMD ["python", "main.py"]
```

Comme d'habitude utilisation de l'image de bas ela plus petite, passage uniquement des sources de production. Tous cela pour limiter les risques de sécurité et avoir les éléments les plus portables.

il faut par la suite compiler

> docker build -t mcpgeography .

et la lancer

```bash
docker run -d \
  --name test \
  -p 8000:8000 \
  -e API_TOKEN="NENNAhVSBm4GGtVbKKASAWoU7oRXT5" \
  -e API_TOKEN_WEATHER="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -e MCP_HOST="0.0.0.0" \
  -e MCP_PORT="8000" \
  mcpgeography
```

On peut aussi utiliser un docker-compose.yml et un fichier .env

```yaml
services:
  mcpgeography:
    build: .
    container_name: mcpgeography

    ports:
      - "8000:8000"

    environment:
      API_TOKEN: ${API_TOKEN}
      API_TOKEN_WEATHER: ${API_TOKEN_WEATHER}
      MCP_HOST: 0.0.0.0
      MCP_PORT: 8000

    restart: unless-stopped
```

