docs: add P0 design spec for xdebug-mcp
This commit is contained in:
commit
52529919af
1 changed files with 147 additions and 0 deletions
147
docs/superpowers/specs/2026-05-12-xdebug-mcp-design.md
Normal file
147
docs/superpowers/specs/2026-05-12-xdebug-mcp-design.md
Normal 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]]`)
|
||||||
Loading…
Reference in a new issue