Tutorial MCP-Go : Lister des pods Kubernetes en 5 minutes
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.
É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
