Introduction

Le Model Context Protocol (MCP) est un protocole permettant aux LLM d’interagir avec des services externes de manière standardisée. Dans ce tutoriel de 5 minutes, nous allons créer un serveur MCP en Go capable de lister des pods Kubernetes.

Prérequis :

  • Go 1.21+ installé
  • Accès à un cluster Kubernetes (minikube, kind, ou cluster distant)
  • Kubectl installé
  • Connaissances minimum en Go et Kubernetes

Étape 1 : Initialisation du projet

Créons notre projet MCP-Go :

mkdir mcp-k8s-server
cd mcp-k8s-server
go mod init github.com/votre-username/mcp-k8s-server

Installons les dépendances nécessaires :

# Client Kubernetes officiel
go get k8s.io/client-go@latest
go get k8s.io/api@latest
go get k8s.io/apimachinery@latest

# MCP SDK pour Go
go get github.com/modelcontextprotocol/go-sdk@latest

Étape 2 : Structure du projet

Créons la structure de base :

mkdir -p cmd/server
mkdir -p internal/k8s
mkdir -p internal/mcp

Structure finale :

mcp-k8s-server/
├── cmd/server/
│   └── main.go
├── internal/
│   ├── k8s/
│   │   └── client.go
│   └── mcp/
│       └── server.go
├── go.mod
└── go.sum

Étape 3 : Client Kubernetes

Créons le client Kubernetes dans internal/k8s/client.go :

package k8s

import (
    "context"
    "fmt"
    "os"

    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/util/homedir"
)

type Client struct {
    clientset *kubernetes.Clientset
}

// NewClient crée un nouveau client Kubernetes
func NewClient() (*Client, error) {
    config, err := getConfig()
    if err != nil {
        return nil, fmt.Errorf("erreur de configuration K8s: %w", err)
    }

    clientset, err := kubernetes.NewForConfig(config)
    if err != nil {
        return nil, fmt.Errorf("erreur création client K8s: %w", err)
    }

    return &Client{clientset: clientset}, nil
}

// getConfig récupère la configuration Kubernetes
func getConfig() (*rest.Config, error) {
    // Essayer la configuration in-cluster d'abord
    if config, err := rest.InClusterConfig(); err == nil {
        return config, nil
    }

    // Fallback vers kubeconfig local
    if home := homedir.HomeDir(); home != "" {
        kubeconfig := os.Getenv("KUBECONFIG")
        return clientcmd.BuildConfigFromFlags("", kubeconfig)
    }

    return nil, fmt.Errorf("impossible de trouver la configuration Kubernetes")
}

// PodInfo représente les informations d'un pod
type PodInfo struct {
    Name      string `json:"name"`
    Namespace string `json:"namespace"`
    Status    string `json:"status"`
    Ready     string `json:"ready"`
    Age       string `json:"age"`
    Node      string `json:"node"`
}

// ListPods liste tous les pods dans un namespace donné
func (c *Client) ListPods(ctx context.Context, namespace string) ([]PodInfo, error) {
    // Si namespace vide, lister tous les namespaces
    if namespace == "" {
        namespace = metav1.NamespaceAll
    }

    pods, err := c.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
    if err != nil {
        return nil, fmt.Errorf("erreur lors de la récupération des pods: %w", err)
    }

    var result []PodInfo
    for _, pod := range pods.Items {
        // Calculer le statut Ready
        ready := "0/0"
        readyCount := 0
        for _, condition := range pod.Status.Conditions {
            if condition.Type == "Ready" && condition.Status == "True" {
                readyCount++
            }
        }
        if len(pod.Spec.Containers) > 0 {
            ready = fmt.Sprintf("%d/%d", readyCount, len(pod.Spec.Containers))
        }

        // Calculer l'âge
        age := "Unknown"
        if !pod.CreationTimestamp.IsZero() {
            age = pod.CreationTimestamp.Time.Format("2006-01-02 15:04:05")
        }

        podInfo := PodInfo{
            Name:      pod.Name,
            Namespace: pod.Namespace,
            Status:    string(pod.Status.Phase),
            Ready:     ready,
            Age:       age,
            Node:      pod.Spec.NodeName,
        }
        result = append(result, podInfo)
    }

    return result, nil
}

// GetNamespaces récupère la liste des namespaces
func (c *Client) GetNamespaces(ctx context.Context) ([]string, error) {
    namespaces, err := c.clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
    if err != nil {
        return nil, fmt.Errorf("erreur lors de la récupération des namespaces: %w", err)
    }

    var result []string
    for _, ns := range namespaces.Items {
        result = append(result, ns.Name)
    }
    return result, nil
}

Étape 4 : Serveur MCP

Créons le serveur MCP dans internal/mcp/server.go :

package mcp

import (
    "context"
    "fmt"
    "log"

    "github.com/votre-username/mcp-k8s-server/internal/k8s"
    "github.com/modelcontextprotocol/go-sdk/mcp"
)

type Server struct {
    k8sClient *k8s.Client
    mcpServer *mcp.Server
}

// PodListParams représente les paramètres pour lister les pods
type PodListParams struct {
    Namespace string `json:"namespace"`
}

// NamespaceListParams est vide car on ne prend pas de paramètres
type NamespaceListParams struct{}

// NewServer crée un nouveau serveur MCP
func NewServer() (*Server, error) {
    // Initialiser le client Kubernetes
    k8sClient, err := k8s.NewClient()
    if err != nil {
        return nil, fmt.Errorf("erreur initialisation client K8s: %w", err)
    }

    // Créer le serveur MCP
    mcpServer := mcp.NewServer("kubernetes-pod-lister", "1.0.0", nil)

    server := &Server{
        k8sClient: k8sClient,
        mcpServer: mcpServer,
    }

    // Enregistrer les outils MCP
    server.registerTools()

    return server, nil
}

// registerTools enregistre les outils disponibles
func (s *Server) registerTools() {
    // Outil pour lister les pods
    listPodsHandler := func(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[PodListParams]) (*mcp.CallToolResultFor[any], error) {
        return s.handleListPods(ctx, ss, params)
    }

    // Outil pour lister les namespaces
    listNamespacesHandler := func(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[NamespaceListParams]) (*mcp.CallToolResultFor[any], error) {
        return s.handleListNamespaces(ctx, ss, params)
    }

    s.mcpServer.AddTools(
        mcp.NewServerTool("list_pods", "Liste les pods Kubernetes dans un namespace donné", listPodsHandler,
            mcp.Input(
                mcp.Property("namespace", mcp.Description("Namespace à explorer (vide = tous les namespaces)")),
            ),
        ),
        mcp.NewServerTool("list_namespaces", "Liste tous les namespaces Kubernetes disponibles", listNamespacesHandler),
    )
}

// handleListPods gère la requête de listage des pods
func (s *Server) handleListPods(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[PodListParams]) (*mcp.CallToolResultFor[any], error) {
    // Récupérer les pods
    pods, err := s.k8sClient.ListPods(ctx, params.Arguments.Namespace)
    if err != nil {
        return &mcp.CallToolResultFor[any]{
            Content: []mcp.Content{&mcp.TextContent{
                Text: fmt.Sprintf("Erreur: %v", err),
            }},
            IsError: true,
        }, nil
    }

    // Formater la réponse
    response := fmt.Sprintf("📋 **Pods trouvés dans le namespace '%s':** %d\n\n", 
        getNamespaceDisplay(params.Arguments.Namespace), len(pods))
    
    response += "| Nom | Namespace | Statut | Ready | Nœud |\n"
    response += "|-----|-----------|--------|-------|------|\n"
    
    for _, pod := range pods {
        response += fmt.Sprintf("| %s | %s | %s | %s | %s |\n",
            pod.Name, pod.Namespace, pod.Status, pod.Ready, pod.Node)
    }

    if len(pods) == 0 {
        response += "\n*Aucun pod trouvé.*"
    }

    return &mcp.CallToolResultFor[any]{
        Content: []mcp.Content{&mcp.TextContent{Text: response}},
    }, nil
}

// handleListNamespaces gère la requête de listage des namespaces
func (s *Server) handleListNamespaces(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[NamespaceListParams]) (*mcp.CallToolResultFor[any], error) {
    namespaces, err := s.k8sClient.GetNamespaces(ctx)
    if err != nil {
        return &mcp.CallToolResultFor[any]{
            Content: []mcp.Content{&mcp.TextContent{
                Text: fmt.Sprintf("Erreur: %v", err),
            }},
            IsError: true,
        }, nil
    }

    response := fmt.Sprintf("🏷️ **Namespaces disponibles:** %d\n\n", len(namespaces))
    for _, ns := range namespaces {
        response += fmt.Sprintf("- `%s`\n", ns)
    }

    return &mcp.CallToolResultFor[any]{
        Content: []mcp.Content{&mcp.TextContent{Text: response}},
    }, nil
}

// Start démarre le serveur MCP
func (s *Server) Start() error {
    log.Println("🚀 Démarrage du serveur MCP Kubernetes...")
    return s.mcpServer.Run(context.Background(), mcp.NewStdioTransport())
}

// getNamespaceDisplay retourne un nom d'affichage pour le namespace
func getNamespaceDisplay(namespace string) string {
    if namespace == "" {
        return "tous les namespaces"
    }
    return namespace
}

Étape 5 : Point d’entrée principal

Créons le point d’entrée dans cmd/server/main.go :

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/votre-username/mcp-k8s-server/internal/mcp"
)

func main() {
    log.Println("🔧 Initialisation du serveur MCP Kubernetes...")

    // Créer le serveur MCP
    server, err := mcp.NewServer()
    if err != nil {
        log.Fatalf("❌ Erreur création serveur: %v", err)
    }

    // Gérer l'arrêt propre
    go func() {
        sigChan := make(chan os.Signal, 1)
        signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
        <-sigChan
        log.Println("🛑 Arrêt du serveur...")
        os.Exit(0)
    }()

    // Démarrer le serveur
    log.Println("🎯 Serveur MCP prêt à recevoir des connexions!")
    if err := server.Start(); err != nil {
        log.Fatalf("❌ Erreur serveur: %v", err)
    }
}

Étape 6 : Compilation et test

Compilons le projet :

# Télécharger les dépendances
go mod tidy

# Compiler
go build -o mcp-k8s-server ./cmd/server

# Tester la compilation
./mcp-k8s-server

Étape 7 : Configuration et utilisation

Configuration Kubernetes

Assurez-vous que kubectl fonctionne :

# Vérifier la connexion
kubectl cluster-info

# Lister les pods existants pour validation
kubectl get pods --all-namespaces

Lancement du serveur

# Lancer le serveur MCP
./mcp-k8s-server

Sortie attendue :

🔧 Initialisation du serveur MCP Kubernetes...
🚀 Démarrage du serveur MCP Kubernetes...
🎯 Serveur MCP prêt à recevoir des connexions!

Test avec le client MCP de vscode

Créer le fichier de configuration dans .vscode/mcp.json :

{
    "servers": {
      "k8s-mcp-server": {
        "command":"./mcp-k8s-server",
        "args": [],
        "env": {}
      }
    }
}

Entre la ligne 2 et 3, vous devriez avoir le bouton démarrer pour lancer le serveur.

Vous pouvez lui demander de lister les pods du namespaces kube-system.

Réponse exemple pour list_pods dans le namespace kube-system :

📋 **Pods trouvés dans le namespace 'kube-system':** 87

| Nom | Namespace | Statut | Ready | Nœud |
|-----|-----------|--------|-------|------|
| calico-node-5qgx7 | kube-system | Running | 1/1 | ******-36e5feec-jlf5 |
| calico-node-c27d5 | kube-system | Running | 1/1 | ******-a29caaa0-ftvv |
| calico-node-cgrmr | kube-system | Running | 1/1 | ******-f5f04122-qrdc |
| calico-node-k44k2 | kube-system | Running | 1/1 | ******-f5f04122-dm8c |
| calico-node-kbngs | kube-system | Running | 1/1 | ******-a29caaa0-dgz6 |
| calico-node-rrpfm | kube-system | Running | 1/1 | ******-36e5feec-cpsr |
| calico-node-vertical-autoscaler-554b66557c-vdll6 | kube-system | Running | 1/1 | 

La réponse correspond au formatage de réponse de notre code Go qui est transmis au LLM qui lui fait à son tour une réponse avec l’enrichissement du LLM.

exemple VSCode MCP

Étape 8 : Améliorations possibles

Gestion d’erreurs avancée

// Ajouter un timeout pour les requêtes
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

pods, err := s.k8sClient.ListPods(ctx, params.Arguments.Namespace)

Filtrage des pods

// Ajouter des filtres dans les paramètres
type PodListParams struct {
    Namespace string `json:"namespace"`
    Status    string `json:"status,omitempty"`    // Filtrer par statut
    LabelSelector string `json:"labelSelector,omitempty"` // Sélecteur de labels
}

// Dans NewServerTool pour list_pods
mcp.NewServerTool("list_pods", "Liste les pods Kubernetes", listPodsHandler,
    mcp.Input(
        mcp.Property("namespace", mcp.Description("Namespace à explorer")),
        mcp.Property("status", 
            mcp.Description("Filtrer par statut"), 
            mcp.Enum("Running", "Pending", "Failed", "Succeeded")),
        mcp.Property("labelSelector", mcp.Description("Sélecteur de labels K8s")),
    ),
)

Support HTTP/SSE

Dans notre exemple le serveur mcp tourne sur notre poste.
Pour le partager, 2 solutions possibles (parmis d’autres)

  • fournir une image Docker à démarrer sur son poste
  • ou fournir une url via l’exemple suivant.
// Pour exposer via HTTP au lieu de stdio
func main() {
    server := mcp.NewServer("kubernetes-pod-list", "1.0.0", nil)
    // ... register tools ...
    
    handler := mcp.NewSSEHandler(func(*http.Request) *mcp.Server {
        return server
    })
    
    log.Println("Serveur MCP disponible sur http://localhost:8080")
    http.ListenAndServe(":8080", handler)
}

Conclusion

En 5 minutes, nous avons créé un serveur MCP en Go capable de :

  • ✅ Se connecter à un cluster Kubernetes
  • ✅ Lister les namespaces disponibles
  • ✅ Lister les pods avec leurs informations détaillées
  • ✅ Gérer les erreurs et timeouts
  • ✅ Fournir une interface MCP standardisée

Prochaines étapes :

  • Ajouter d’autres ressources K8s (services, deployments, vos customs ressources)
  • Ajouter des métriques et logs structurés
  • Déployer le serveur mpc dans le cluster pour partager son usage

Ressources utiles :

Le serveur MCP est maintenant prêt à être intégré avec vos outils d’IA pour une interaction amélioré avec votre serveur Kubernetes ! 🐳

Bonus

Je n’ai pas personellement pris le temps de l’essayer mais si vous êtes plus à l’aise en Python, il y a aussi un client officiel

La communication avec le serveur MCP se fait en json-rpc 2.0, nous pouvons donc tester facilement le serveur depuis un terminal, voici ma commande de debug pour lister les tools disponibles.

echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"test","version":"1.0.0"}}}' | ./mcp-k8s-server

⚠️ Attention à la sécurité

Attention aux permissions ! Avant de déployer ce serveur MCP en production, prenez en compte les points suivants :

  • 🔐 Permissions RBAC : Limitez les droits du service account aux actions strictement nécessaires (lecture seule pour ce tutoriel)
  • 🚫 Attention au delete : Évitez d’exposer des opérations destructives (delete, update) via MCP sans confirmation de l’utilisateur, ou en étant consient du risque associé.
  • 🔑 Tokens sensibles : Ne jamais exposer les tokens Kubernetes en clair dans les logs ou les tokens de configuration de serveur MCP tierce
  • 🌐 Exposition réseau : Si vous exposez le serveur via HTTP, utilisez HTTPS et une authentification appropriée
  • 📋 Audit : Activez les logs d’audit Kubernetes pour tracer les actions du serveur MCP