# xdebug-mcp — Design P0 ## Objectif Serveur MCP en Go capable de lire et d'analyser des fichiers de sortie de profilage Xdebug (format cachegrind, compressés gzip). Exposé via stdio conformément au protocole MCP. ## Stack - **Langage** : Go - **Protocole MCP** : `mark3labs/mcp-go` - **Framework CLI/config** : `forge.lclr.dev/AI/mcp-framework` + package généré `mcpgen/` - **Format d'entrée** : cachegrind v1 (xdebug 3.x), fichiers `.gz` ou texte brut ## Structure du projet ``` xdebug-mcp/ ├── cmd/xdebug-mcp/main.go # Point d'entrée, bootstrap CLI ├── internal/ │ ├── app/app.go # Wiring MCP server │ ├── cachegrind/ │ │ ├── parser.go # Parser streaming du format cachegrind │ │ └── model.go # Structures de données (Profile, Function, Call) │ ├── cache/ │ │ └── lru.go # Cache LRU keyed par chemin fichier absolu │ └── tools/ │ ├── analyze.go # Tool analyze_profile │ ├── callers.go # Tool get_callers │ └── callees.go # Tool get_callees ├── mcpgen/ # Généré par `mcp-framework generate` │ └── manifest.go ├── mcp.toml └── go.mod ``` ## Modèle de données (`cachegrind/model.go`) ```go type Profile struct { Cmd string Events []string // ex: ["Time_(10ns)", "Memory_(bytes)"] Functions []*Function ByName map[string][]*Function // une clé peut matcher plusieurs fichiers } type Function struct { Name string File string Costs []int64 // indexé sur Profile.Events Calls []*Call // appels sortants (callees) CalledBy []*Call // appels entrants (callers) } type Call struct { Caller *Function Callee *Function Count int64 Costs []int64 } ``` Les coûts sont agrégés : si une fonction apparaît dans plusieurs blocs `fl=/fn=`, ses coûts sont sommés. `CalledBy` est reconstruit à partir des directives `calls=` pendant le parse. Le parser maintient une table de résolution interne des alias numériques (`fl=(N)`, `fn=(N)`). ## Tools MCP Le chemin du fichier est toujours un paramètre direct de l'outil — pas de configuration globale de dossier. ### `analyze_profile` | Paramètre | Type | Défaut | Description | |---|---|---|---| | `file_path` | string | requis | Chemin absolu ou relatif du fichier cachegrind (.gz ou texte) | | `top_n` | int | 20 | Nombre de fonctions à retourner | Retourne : stats globales (commande profilée, événements, nombre total de fonctions) + top N fonctions triées par `Costs[0]` (Time), avec coûts absolus et pourcentage du total. ### `get_callers` | Paramètre | Type | Défaut | Description | |---|---|---|---| | `file_path` | string | requis | Chemin du fichier cachegrind | | `function_name` | string | requis | Nom exact ou sous-chaîne de la fonction cible | | `top_n` | int | 10 | Nombre de callers à retourner | Retourne : liste des fonctions qui appellent `function_name`, avec nombre d'appels et coûts associés, triés par coût décroissant. ### `get_callees` | Paramètre | Type | Défaut | Description | |---|---|---|---| | `file_path` | string | requis | Chemin du fichier cachegrind | | `function_name` | string | requis | Nom exact ou sous-chaîne de la fonction cible | | `top_n` | int | 10 | Nombre de callees à retourner | Retourne : liste des fonctions appelées par `function_name`, avec nombre d'appels et coûts associés, triés par coût décroissant. **Résolution du nom** : recherche exacte d'abord (`ByName`), puis recherche `contains` si aucun résultat exact. Si plusieurs fonctions matchent en mode contains, toutes sont listées avec un avertissement. ## Cache LRU (`cache/lru.go`) - Capacité : **2 fichiers** simultanés (constante `DefaultCapacity = 2`) - Clé : chemin absolu du fichier - Invalidation : comparaison de `os.FileInfo.ModTime()` à chaque accès - Thread-safe : `sync.Mutex` - Structure : `map[string]*entry` + `container/list` stdlib pour l'ordre d'éviction - Pas de dépendance externe ## Gestion d'erreurs | Situation | Comportement | |---|---| | Fichier introuvable | Erreur avec chemin complet | | Fichier non gzippé | Tentative de lecture en texte brut | | Format cachegrind invalide | Erreur avec numéro de ligne | | Fonction non trouvée (exact) | Message clair + liste des matches contains si disponible | | Fichier très gros / OOM | Non géré à P0 — limite documentée | ## Wiring **`mcp.toml`** ```toml binary_name = "xdebug-mcp" [bootstrap] description = "MCP server for Xdebug profiling files" [profiles] default = "prod" known = ["dev", "prod"] ``` **`cmd/xdebug-mcp/main.go`** utilise `bootstrap.Run` avec uniquement le hook `MCP` branché sur `app.RunMCP`. La `Description` est soit fournie par `mcpgen` (à vérifier après génération), soit hardcodée. **`internal/app/app.go`** : 1. Instancie `cache.New(cache.DefaultCapacity)` 2. Crée le serveur `mcp-go` 3. Enregistre les trois tools 4. Lance `server.ServeStdio()` `mcpgen/` est généré via `mcp-framework generate` et doit être regénéré après toute modification de `mcp.toml`. Il expose au minimum `mcpgen.BinaryName`. ## Limites P0 connues - Pas de guard mémoire : un fichier de 325 Mo décompressé est chargé intégralement en RAM - Pas de list_profiles : l'appelant doit connaître le chemin du fichier - Pas d'auto-update configuré dans `mcp.toml` - Pas de secrets ni de config persistante (pas de `[[config.fields]]`)