docs: add P0 design spec for xdebug-mcp

This commit is contained in:
thibaud-leclere 2026-05-12 09:20:41 +02:00
commit 52529919af

View file

@ -0,0 +1,147 @@
# 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]]`)