# Exploitation du serveur MCP Infoparcelle

> Rédigé le 2026-08-19, à la suite de la panne « la connexion ne marche plus du tout ».
> Ce document explique **pourquoi** le service était tombé, ce qui a changé, et
> comment le démarrer en développement comme en production.

---

## 1. Ce qui s'était passé (et qui ne peut plus se reproduire)

Le serveur n'avait pas de bug fonctionnel : **il était devenu injoignable, et il
annonçait une adresse qui n'existait plus.**

| Constat mesuré | Conséquence |
|---|---|
| `MCP_PUBLIC_URL` était figée au démarrage, sur un tunnel ngrok éphémère | À chaque renouvellement du tunnel, l'adresse annoncée devenait fausse |
| Dans le conteneur, cette variable valait la valeur de repli `http://localhost:3000` | La découverte OAuth envoyait Claude et ChatGPT vers `localhost` : connexion impossible |
| Les `redirect_uri` enregistrés côté Passport portaient ces anciennes URL | 37 clients OAuth pointaient vers 3 tunnels morts, dont 10 encore actifs |
| Les réponses 401 ne portaient pas d'en-tête `WWW-Authenticate` | Le client ne pouvait pas découvrir où s'authentifier : refus muet, connecteur affiché « déconnecté » |

**Le correctif de fond** : le serveur ne lit plus une adresse figée, il **dérive son
URL publique de l'hôte par lequel on l'appelle** (`src/utils/public-url.ts`).
Derrière ngrok comme derrière le vhost de production, les métadonnées OAuth sont
justes sans aucune reconfiguration. Une URL ngrok qui change ne casse plus rien.

`MCP_PUBLIC_URL` reste acceptée mais **ne peut plus imposer une adresse qui
contredit la requête reçue** : si elle diverge, elle est ignorée et un
avertissement est journalisé. C'est exactement le scénario qui avait provoqué la
panne.

---

## 2. Démarrage en développement

### 2.1 Le serveur MCP

```bash
cd /home/julio/www/Infoparcelle-MCP
npm run build          # après toute modification de src/
npm start              # écoute sur le PORT du .env (3000)
```

Réglages du `.env` :

| Variable | Valeur de dev | Remarque |
|---|---|---|
| `LARAVEL_APP_URL` | `https://35bd82292b7d5177.ngrok.app` | ⚠ **le domaine ngrok, pas `app.devinfoparcelle.fr`** : la session Laravel est portée par un cookie dont le domaine est `SESSION_DOMAIN`. Redirigé vers `app.devinfoparcelle.fr`, l'utilisateur n'est jamais reconnu comme connecté et boucle sur `/login` pendant l'autorisation. |
| `MCP_PUBLIC_URL` | *(vide)* | Réglage recommandé : l'adresse est déduite de l'appel. |
| `MCP_TRANSPORT` | `http` | `stdio` pour un client local. |

### 2.2 Le tunnel ngrok

⚠ **Le compte ngrok n'autorise qu'un seul agent simultané** (mesuré le
2026-08-19 : lancer un `ngrok http` isolé à côté d'un `ngrok start --all` fait
tomber les autres tunnels en 404, sans aucun message). Il faut donc **un agent
unique portant tous les endpoints**.

```bash
./scripts/ngrok-mcp.sh          # remplace `ngrok start --all`
```

Ce script lance un agent unique avec les endpoints de développement **et** le
MCP. Il refuse de démarrer si un agent tourne déjà, pour ne pas couper les
tunnels existants.

⚠ `~/.config/ngrok/ngrok.yml` **n'est pas modifié**. Mais quand plusieurs
fichiers sont passés avec `--config`, ngrok fusionne les valeurs simples
(l'authtoken) et **remplace** la liste `endpoints` par celle du dernier fichier :
`ngrok-mcp.yml` doit donc redéclarer tous les endpoints. **Si vous ajoutez un
endpoint à votre configuration personnelle, reportez-le dans `ngrok-mcp.yml`.**

Récupérer l'URL publique du MCP :

```bash
curl -s http://127.0.0.1:4040/api/tunnels \
  | python3 -c "import json,sys;print([t['public_url'] for t in json.load(sys.stdin)['tunnels'] if t['name']=='mcp'][0])"
```

### 2.3 Brancher le connecteur

Dans Claude ou ChatGPT, l'URL du connecteur est `<url-ngrok>/mcp`. Tout le reste
(client OAuth, `redirect_uri`) est créé automatiquement par enregistrement
dynamique.

⚠ **Après un changement d'URL ngrok, reconnectez le connecteur** : le client
OAuth enregistré chez Passport porte l'ancienne adresse. C'est le seul geste
manuel restant — et il disparaît avec un domaine ngrok réservé (à déclarer via
`url:` dans `ngrok-mcp.yml`) ou en production.

---

## 3. Production — `mcp.infoparcelle.fr`

**État constaté le 2026-08-19 : le service répond 500 sur toutes les routes.**
Le DNS (162.19.28.220) et le certificat sont en place, mais la réponse est la
page d'erreur Apache par défaut, sans en-tête `x-powered-by` — y compris sur un
fichier statique. Le vhost est chargé mais sa configuration échoue avant
d'atteindre un quelconque backend.

Le vhost prêt à déployer est fourni : **`deploy/mcp.infoparcelle.fr.conf`**. Il
contient la procédure de diagnostic du 500 et, surtout, les trois en-têtes
**indispensables** :

```apache
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Host  "mcp.infoparcelle.fr"
RequestHeader set X-Forwarded-Port  "443"
```

Sans eux, le serveur ne peut pas connaître son adresse publique et la découverte
OAuth redevient fausse.

---

## 4. Vérifier que tout fonctionne

```bash
U=https://<url-du-mcp>

# 1. Le serveur répond
curl -s $U/health

# 2. Il s'annonce sous la BONNE adresse (le défaut d'origine)
curl -s $U/.well-known/oauth-authorization-server | grep -o '"issuer":"[^"]*"'

# 3. Le 401 indique où s'authentifier (indispensable à Claude)
curl -s -D - -o /dev/null -X POST $U/mcp -H 'Content-Type: application/json' -d '{}' \
  | grep -i www-authenticate
```

Les trois doivent répondre. Si le point 2 renvoie `localhost` ou une ancienne
URL, c'est que `MCP_PUBLIC_URL` a été renseignée à tort — la retirer.

---

## 5. Points connus, non traités

- **37 clients OAuth pointent vers des tunnels ngrok morts, dont 10 non révoqués.**
  Sans effet sur le fonctionnement. À révoquer par hygiène : un tiers qui
  obtiendrait l'un de ces sous-domaines ngrok pourrait intercepter un code
  d'autorisation émis pour ces `client_id`.
- **`/oauth/userinfo` ne rend qu'un `sub`.** L'API Infoparcelle n'expose aucun
  endpoint d'identité (`/api/user` répond 404). Les claims `email`/`name` ne sont
  plus inventés (ils valaient `user@example.com`, marqué comme vérifié, pour
  **tous** les comptes). Ils pourront être renseignés le jour où Laravel exposera
  l'utilisateur courant.
- **L'`id_token` est non signé (`alg: none`).** Suffisant pour les clients
  actuels, à remplacer par un RS256 avec `jwks_uri` réel si un client strict
  l'exige.
- **Le `docker-compose` n'a pas été modifié** (fichier appartenant à `root`) :
  patch fourni dans `deploy/docker-compose.mcp.patch`.
