Serveurs MCP pour DevOps : ce qu’ils sont et deux modèles que vous pouvez créer cette semaine
Serveurs MCP pour DevOps : permettent à Claude d’accéder en temps réel à Kubernetes, AWS et aux documents de procédure. Les trois types de serveurs, un serveur MCP Kubernetes en lecture seule et les erreurs fréquentes.
Sources: What Is an MCP Server and Why Every DevOps Team Should Have One in 2026
MCP, le Model Context Protocol, est une norme ouverte d’Anthropic qui permet aux modèles d’IA de se connecter de manière structurée à des systèmes externes : outils, API, bases de données et services. Sans elle, Claude reste confiné et chaque information doit être introduite manuellement. Avec elle, Claude peut interroger directement ces systèmes.
Le « serveur » dans MCP n’est qu’un petit programme. Il expose des outils et des données à Claude, gère les échanges et s’exécute là où se trouvent les données, que ce soit sur un ordinateur portable, un cluster ou un serveur. Il ne s’agit pas d’une infrastructure lourde, et un serveur MCP fonctionnel peut être un script Python de 50 lignes.
Une analogie utile
Les intégrations Slack offrent une bonne comparaison. Avant leur apparition, quelqu’un devait manuellement publier le message « Déploiement en production commencé ». Après leur mise en place, le pipeline CI/CD le publie automatiquement, et Slack devient un centre de coordination qui suit l’évolution des événements. MCP fait de même pour Claude : auparavant, une personne devait fournir manuellement des informations sur l’infrastructure ; désormais, il existe une connexion directe qui permet à Claude de vérifier les informations par lui-même.
Pourquoi les équipes DevOps en tirent le plus de bénéfices
La plupart des documents relatifs à MCP s’adressent aux développeurs d’applications. Les professionnels DevOps, SRE et ingénieurs de plateforme en tirent davantage de bénéfices, car leur travail consiste à gérer des systèmes disposant déjà d’API : Kubernetes, AWS, Datadog ou Grafana, PagerDuty, Terraform Cloud, GitHub, Confluence et Notion. Chacun de ces systèmes peut être connecté à Claude via MCP. Une fois cela fait, il n’est plus nécessaire de passer d’un outil à l’autre pour collecter des informations ; le travail se résume alors à poser des questions.
Que font les équipes avec les serveurs MCP
- Questions sur les clusters. En demandant quels déploiements en production fonctionnent depuis plus de 30 jours sans mise à jour, Claude se connecte au serveur MCP Kubernetes, qui interroge le cluster et fournit la réponse. Aucun kubectl ni copier-coller n’est nécessaire.
- Questions sur les coûts. En demandant quels services AWS ont connu une augmentation cette semaine par rapport à la semaine précédente, Claude se connecte à un serveur AWS ainsi qu’à Cost Explorer, et la réponse est présentée en langage simple.
- Contexte des incidents. Une question concernant les incidents sur PagerDuty au cours des 24 dernières heures, et savoir s’il en reste en cours, permet de récupérer l’état actuel depuis un serveur PagerDuty et de le résumer.
- Recherche de guides opérationnels. En demandant le guide concernant l’épuisement du pool de connexions de base de données, Claude recherche dans la base de connaissances via un serveur Confluence et restitue le document exact.
- Audits de sécurité. En vérifiant si un groupe de sécurité autorise 0.0.0.0/0 sur des ports autres que 443, on fait en sorte qu’un serveur AWS examine ces groupes et signale les violations.
Trois types de serveurs
Les équipes qui commencent généralement finissent par créer trois types de serveurs.
Serveurs d’observabilité en lecture seule
Ces serveurs relient Claude aux API de surveillance et d’infrastructure afin qu’il puisse décrire l’état actuel des choses. Des exemples typiques sont un serveur d’état de cluster Kubernetes, un serveur d’inventaire AWS, un serveur de métriques Datadog et un serveur de journaux CloudWatch. Le risque est très faible car Claude ne peut que lire. C’est le point de départ idéal, car il permet de gagner en confiance avant d’accorder tout accès en écriture.
Serveurs de base de connaissances
Ces liens relient Claude à la documentation interne pour la recherche et le récupération : un serveur de manuels Confluence ou Notion, un serveur wiki GitHub, un dossier contenant des fichiers markdown locaux ou un stockage de documents relatifs aux décisions architecturales. Le risque est essentiellement nul, puisqu’il s’agit de recherches parmi des documents. Ils sont importants car ils donnent à Claude l’impression de vraiment connaître l’entreprise.
Serveurs d’actions
Ces derniers permettent à Claude d’effectuer des tâches telles que créer des tickets, déclencher des pipelines ou redémarrer des services. Parmi les exemples on trouve la création de tickets Jira, la gestion des incidents via PagerDuty, la création de demandes de pull GitHub et les notifications Slack. Le risque est de niveau moyen à élevé, car Claude modifie désormais des éléments. Des étapes d’approbation doivent toujours être présentes avant toute opération d’écriture, de sorte qu’aucun redémarrage de pod ne peut avoir lieu sans une étape de confirmation.
Création d’un serveur MCP Kubernetes
Ce qui suit est un serveur Kubernetes en lecture seule fonctionnel en Python. Installez d’abord le SDK :
pip install mcp kubernetes --break-system-packages
Ensuite, enregistrez le serveur sous le nom k8s_mcp_server.py :
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
from kubernetes import client, config as k8s_config
# Load kubeconfig
try:
k8s_config.load_incluster_config()
except:
k8s_config.load_kube_config()
v1 = client.CoreV1Api()
apps_v1 = client.AppsV1Api()
# Create MCP server
app = Server("k8s-devops-server")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
"""Tell Claude what tools are available."""
return [
types.Tool(
name="get_pods",
description="Get all pods in a namespace with their status, restarts, and age.",
inputSchema={
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Kubernetes namespace (e.g. production, staging)"
}
},
"required": ["namespace"]
}
),
types.Tool(
name="get_pod_logs",
description="Get recent logs from a pod or deployment.",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Pod or deployment name"
},
"namespace": {
"type": "string",
"description": "Kubernetes namespace"
},
"lines": {
"type": "integer",
"description": "Number of log lines to return (default 50)"
}
},
"required": ["name", "namespace"]
}
),
types.Tool(
name="get_deployments",
description="List all deployments with replica counts and status.",
inputSchema={
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Kubernetes namespace"
}
},
"required": ["namespace"]
}
),
types.Tool(
name="get_events",
description="Get recent Kubernetes events. Useful for diagnosing issues.",
inputSchema={
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Kubernetes namespace"
}
},
"required": ["namespace"]
}
),
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
"""Handle tool calls from Claude."""
if name == "get_pods":
namespace = arguments["namespace"]
pods = v1.list_namespaced_pod(namespace=namespace)
lines = [f"Pods in {namespace}:\n"]
for pod in pods.items:
phase = pod.status.phase
restarts = sum(
cs.restart_count
for cs in (pod.status.container_statuses or [])
)
name_str = pod.metadata.name
lines.append(f" {name_str}: {phase} | restarts: {restarts}")
return [types.TextContent(type="text", text="\n".join(lines))]
elif name == "get_pod_logs":
pod_name = arguments["name"]
namespace = arguments["namespace"]
lines = arguments.get("lines", 50)
import subprocess
result = subprocess.run(
f"kubectl logs {pod_name} -n {namespace} --tail={lines}",
shell=True, capture_output=True, text=True
)
output = result.stdout or result.stderr or "No logs found"
return [types.TextContent(type="text", text=output)]
elif name == "get_deployments":
namespace = arguments["namespace"]
deployments = apps_v1.list_namespaced_deployment(namespace=namespace)
lines = [f"Deployments in {namespace}:\n"]
for d in deployments.items:
desired = d.spec.replicas or 0
available = d.status.available_replicas or 0
status = "✅" if desired == available else "⚠️"
lines.append(
f" {status} {d.metadata.name}: {available}/{desired} replicas"
)
return [types.TextContent(type="text", text="\n".join(lines))]
elif name == "get_events":
namespace = arguments["namespace"]
events = v1.list_namespaced_event(namespace=namespace)
# Sort by last timestamp, show recent ones
sorted_events = sorted(
events.items,
key=lambda e: e.last_timestamp or e.event_time,
reverse=True
)[:20]
lines = [f"Recent events in {namespace}:\n"]
for e in sorted_events:
lines.append(
f" [{e.type}] {e.reason}: {e.message[:100]}"
)
return [types.TextContent(type="text", text="\n".join(lines))]
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(read_stream, write_stream, app.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
Pour le connecter à Claude Desktop, modifiez le fichier de configuration, qui se trouve dans l’une de ces chemins :
Mac: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
et ajoutez l’entrée du serveur :
{
"mcpServers": {
"kubernetes": {
"command": "python",
"args": ["/path/to/k8s_mcp_server.py"]
}
}
}
Redémarrez Claude Desktop et il sera prêt. En demandant combien de pods sont actuellement en cours d’exécution en production, Claude effectue réellement une vérification.
Création d’un serveur de runbooks
Celui-ci peut même être encore plus utile. Il oriente Claude vers les runbooks de l’équipe, de sorte que, au milieu d’un incident, Claude trouve celui qui convient plutôt que quelqu’un de devoir chercher manuellement dans Confluence.
import asyncio
import os
import glob
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import types
app = Server("runbook-server")
RUNBOOKS_PATH = os.path.expanduser("~/runbooks") # point to your runbooks folder
def load_all_runbooks() -> dict[str, str]:
"""Load all markdown runbooks from the folder."""
runbooks = {}
for filepath in glob.glob(f"{RUNBOOKS_PATH}/**/*.md", recursive=True):
with open(filepath, 'r', errors='replace') as f:
content = f.read()
name = os.path.relpath(filepath, RUNBOOKS_PATH)
runbooks[name] = content
return runbooks
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="search_runbooks",
description=(
"Search your team's runbooks for a specific topic, service, or error. "
"Use this when someone asks about a procedure, a known issue, "
"or how to fix something."
),
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "What to search for (e.g. 'database connection pool', 'nginx 502', 'SSL cert renewal')"
}
},
"required": ["query"]
}
),
types.Tool(
name="list_runbooks",
description="List all available runbooks by category.",
inputSchema={
"type": "object",
"properties": {}
}
),
types.Tool(
name="get_runbook",
description="Get the full content of a specific runbook by name.",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Runbook filename (from list_runbooks)"
}
},
"required": ["name"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
runbooks = load_all_runbooks()
if name == "list_runbooks":
names = sorted(runbooks.keys())
output = "Available runbooks:\n" + "\n".join(f" - {n}" for n in names)
return [types.TextContent(type="text", text=output)]
elif name == "search_runbooks":
query = arguments["query"].lower()
matches = []
for rb_name, content in runbooks.items():
if query in content.lower() or query in rb_name.lower():
# Return first 500 chars as preview
preview = content[:500].strip()
matches.append(f"📄 {rb_name}\n{preview}\n...")
if not matches:
return [types.TextContent(
type="text",
text=f"No runbooks found for '{query}'"
)]
return [types.TextContent(
type="text",
text=f"Found {len(matches)} runbook(s):\n\n" + "\n\n---\n\n".join(matches)
)]
elif name == "get_runbook":
rb_name = arguments["name"]
content = runbooks.get(rb_name)
if not content:
return [types.TextContent(type="text", text=f"Runbook not found: {rb_name}")]
return [types.TextContent(type="text", text=content)]
return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(read_stream, write_stream, app.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
Ajoutez-le à la configuration de Claude Desktop aux côtés des autres serveurs :
{
"mcpServers": {
"kubernetes": {
"command": "python",
"args": ["/path/to/k8s_mcp_server.py"]
},
"runbooks": {
"command": "python",
"args": ["/path/to/runbook_mcp_server.py"]
}
}
}
Par la suite, une question telle que « quel est le guide de procédure pour une utilisation élevée de la mémoire sur les pods API ? » renvoie au guide de procédure interne de l’équipe et non à un résultat de recherche générique.
Serveurs déjà existants
Tout ne doit pas être construit de zéro. La collection officielle de serveurs de référence sur GitHub, sous l’organisation modelcontextprotocol, couvre GitHub (repos, demandes de pull, problèmes et commits), Postgres, Slack, une recherche dans la base de connaissances d’AWS, l’accès au système de fichiers local et la recherche web Brave. Il existe également des serveurs développés par la communauté pour Datadog (métriques, tableaux de bord et moniteurs), PagerDuty (incidents et alertes) et Jira (tickets et sprints). Il est utile de consulter d’abord cette collection, car quelqu’un a peut-être déjà créé ce dont on a besoin.
Erreurs à éviter
- Accorder l’accès en écriture trop tôt. Commencez par un mode uniquement en lecture. Une fois la confiance acquise, ajoutez les outils d’écriture un par un.
- Créer un seul serveur géant. Les serveurs petits et spécialisés sont plus faciles à entretenir, à déboguer et à confier à des personnes spécifiques. Un
k8s-serverne doit contenir que des outils Kubernetes, unaws-serveruniquement des outils AWS et unrunbook-serveruniquement des documents. - Omettre la journalisation. Enregistrez chaque appel d’outil, y compris ce que Claude a consulté, quand et quelles ont été les réponses, surtout avant l’ajout des outils d’écriture.
- Omettre l’approbation pour les écritures. Tout ce qui crée un ticket, redémarre un service ou fusionne une demande de pull mérite une étape de confirmation, avec Claude qui montre d’abord ce qu’il s’apprête à faire.
Une matinée avec une configuration adéquate
Une session typique commence par une demande concernant l’état de la production. Claude vérifie le serveur Kubernetes et fait un rapport sur les déploiements et les pods, signale un pod présentant des redémarrages répétés, puis suit l’évolution du problème.
You: What's the state of production right now?
Claude: [checks kubernetes MCP server]
3 deployments all healthy, 47/47 pods running.
One pod in payments namespace has 8 restarts - api-worker-6d4f9b.
Everything else looks stable.
You: Why is that pod restarting?
Claude: [checks pod logs via MCP]
Logs show OOMKilled. Memory limit is 256Mi but it's hitting
248Mi regularly. Looks like it needs a bump to at least 512Mi.
Want me to check if we have a runbook for this?
You: Yes
Claude: [searches runbook MCP server]
Found it - "memory-limit-tuning.md"
The runbook says: increase limit in Helm values,
run helm upgrade, monitor for 10 minutes.
Last updated by Rahul on March 12th.
You: What did our AWS bill look like this week?
Claude: [checks AWS MCP server]
$4,823 this week vs $4,210 last week - up 14.5%.
Spike is from EC2 in us-east-1. Looks like
3 r6g.2xlarge instances were added on Tuesday.
Toute cette opération prend environ 90 secondes, sans changement de onglet, sans copier-coller et sans recherche.
Début cette semaine
Construire cinq serveurs en même temps est une erreur. En choisissez un. Pour la plupart des équipes DevOps, le meilleur choix initial est le serveur de manuels, car il est en lecture seule, apporte une valeur immédiate et fonctionne avec les documents déjà existants. Le démarrage se fait en trois étapes :
- Rassemblez les manuels de l’équipe dans un seul dossier, par exemple
~/runbooks/. - Prenez le code du serveur de manuels ci-dessus et pointez-le vers ce dossier.
- Ajoutez-le à la configuration de Claude Desktop et redémarrez.
Cela donne un assistant qui connaît les procédures de l’équipe. Lorsqu’il s’avère fiable, ajoutez le serveur Kubernetes, puis AWS, puis tout ce qui suivra.
Le contexte global
MCP est encore jeune et son écosystème se développe rapidement, mais certaines choses sont déjà évidentes. Les équipes disposant de serveurs MCP bénéficient d’une expérience IA nettement différente de celles qui n’en ont pas. Pour passer d’une fenêtre de chat à un outil fonctionnant comme un collègue opérationnel, il faut un serveur capable d’exposer les systèmes appropriés. Les équipes qui développent ces solutions aujourd’hui établissent les bases de l’utilisation de l’IA dans le travail d’infrastructure. Il ne s’agit pas d’un projet de recherche : ces solutions sont déjà mises en œuvre en production, et le serveur Kubernetes mentionné ci-dessus peut être opérationnel en environ 20 minutes.
Lectures complémentaires
- Un server MCP personnel qui permet à Claude de rechercher vos runbooks d’équipe — Indexez les runbooks et les analyses post-incident en markdown dans ChromaDB, exposez une recherche sémantique via un server MCP, et permettez à Claude de trouver les procédures d’incident en quelques secondes.
- Notes pratiques : kagent : Le framework agent-IA natif de Kubernetes que vous devez utiliser — Guide pas à pas de Notes pratiques : kagent : Le framework agent-IA natif de Kubernetes que vous devez utiliser : contrats, vérifications et emplacements de code prêts à l’emploi pour les équipes souhaitant adopter ce modèle.