# Serveur MCP

Deux façons de brancher un agent compatible MCP sur le téléphone :
- **En ligne, sans rien installer** (recommandé) : `https://mcp.dialyne.com/mcp` (Streamable HTTP).
  Deux façons de s'authentifier :
  - **OAuth 2.1 (Claude, ChatGPT, Cursor…)** : colle seulement l'URL `…/mcp` ; le client découvre l'autorisation tout
    seul, ouvre une page où tu te connectes (e-mail + mot de passe du compte, ou une clé API) et clique « Autoriser ».
  - **Clé API en en-tête** `Authorization: Bearer tel_…` (agents sans navigateur : Hermes, OpenClaw, n8n, scripts).
  La découverte (`initialize`, `tools/list`) répond sans authentification ; l'appel d'un outil sans jeton renvoie
  `401` + `WWW-Authenticate: Bearer resource_metadata="…"`, ce qui déclenche la connexion OAuth.
  Ne mets jamais la clé dans l'URL : l'ancienne forme `…/mcp/tel_…` est dépréciée et sera coupée.
- **En local** (`code/mcp_server/server.py`, transport stdio), mêmes outils.

## En ligne
Guides pas à pas : [Claude](guide-claude.md), [ChatGPT](guide-chatgpt.md), [Cursor](guide-cursor.md),
[n8n](guide-n8n.md), [Hermes](guide-hermes.md).

- **Claude (claude.ai / Desktop) › Réglages › Connecteurs › Ajouter un connecteur personnalisé** : URL `…/mcp`, puis
  « Se connecter » et « Autoriser ».
- **ChatGPT › Réglages › Connecteurs (mode développeur) › Créer** : URL `…/mcp`, authentification « OAuth »
  (enregistrement dynamique, rien à saisir).
- **Claude Code** : `claude mcp add --transport http dialyne https://mcp.dialyne.com/mcp` (OAuth au premier
  appel via `/mcp`), ou avec `--header "Authorization: Bearer tel_…"`.
- **Cursor** (`.cursor/mcp.json`) : `{"mcpServers": {"dialyne": {"url": "https://mcp.dialyne.com/mcp", "headers": {"Authorization": "Bearer tel_…"}}}}`.
- **API OpenAI (Responses)** : outil `{"type": "mcp", "server_label": "dialyne", "server_url": "https://mcp.dialyne.com/mcp", "headers": {"Authorization": "Bearer tel_…"}, "require_approval": "never"}`.

Le serveur s'appelle `dialyne`. Les outils sont en anglais (les modèles les choisissent mieux) ; ils renvoient un
résultat écrit, jamais d'audio, et des erreurs explicites (`isError: true`, code stable dans
`structuredContent.error` ; erreurs de limites et d'offre : [Offres et limites](offres.md#codes-d-erreur)).

| Outil | Rôle |
|---|---|
| `place_phone_call(phone_number, task, language?, scheduled_at?, voice?, idempotency_key?, ai_disclosure?, callee_informed_confirmation?, outside_hours?)` | Lance (ou planifie) un appel en France ; renvoie `call_id` tout de suite (l'appel n'est pas fini). `ai_disclosure="callee_already_informed"` (+ confirmation de l'utilisateur) retire la mention IA de l'accroche pour une personne déjà prévenue ; `outside_hours` est obsolète (accepté, ignoré) : aucune plage horaire imposée |
| `get_call(call_id, wait_seconds=25, after_event_id=0, response_format="concise")` | Statut et résultat ; attend jusqu'à 50 s un événement important (décroché, attente, question, transfert, fin) |
| `answer_call_question(call_id, answer, question_id?)` | Répond à la question de l'agent pendant l'appel (45 s ; échéance dans `pending_question.expires_at` et `seconds_left`) |
| `hold_call_question(call_id, question_id?)` | Demande 60 s de plus, une seule fois par question : l'agent vocal demande à l'interlocuteur s'il veut bien patienter, et le délai n'est prolongé que s'il accepte (`can_hold` dans `pending_question`). Voir [Questions en direct](questions.md) |
| `transfer_call_to_user(call_id, user_phone_number)` | Passe l'appel en cours à l'utilisateur sans coupure |
| `schedule_callback(call_id, callback_at)` | Programme un rappel d'un appel terminé (14 jours max, 3 rappels) |
| `cancel_call(call_id)` | Annule un appel en cours ou planifié |
| `list_calls(query="", outcome?, limit=10)` | Appels passés et planifiés (recherche dans la mission, le résumé, la transcription, le numéro) |
| `get_account_balance()` | Minutes de conversation restantes, offre (`plan`), prix à la minute (`price_per_minute_eur`) et limites (`limits`) |
| `place_batch_calls(phone_numbers, task, language?, voice?, scheduled_at?, name?)` | Pro et Entreprise : une même mission vers une liste de numéros (100 au plus). Voir [Appels en lot](lots.md) |
| `get_batch_calls(batch_id)` | Avancement et résultats d'un lot |
| `configure_agent_identity(first_name?, tone?, opening_line?, who_is_calling_answer?, introduction?, ai_mention_in_opening?, reset_to_recommended?, accept_responsibility?, warning_version?)` | Lit ou modifie l'identité de l'agent (prénom, ton, ouverture, « de la part de qui », présentation, mention IA). Un écart aux réglages recommandés renvoie d'abord l'avertissement (`responsibility_acceptance_required`) : l'agent client le montre à l'utilisateur et ne l'accepte qu'avec son accord explicite (journalisé). L'agent vocal ne nie jamais être une IA |

Statuts : `scheduled`, `in_progress`, `retry_scheduled`, `completed`, `cancelled`, `failed`. Résultats : `success`,
`failed`, `voicemail`, `no_answer`, `busy`, `callback_requested`.

## OAuth (détails techniques)
Conforme à la spécification MCP « Authorization » (2025-06-18) :
- `401` sur `tools/call` sans jeton → `WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp"`.
- Ressource protégée (RFC 9728) : `GET /.well-known/oauth-protected-resource/mcp` ; serveur d'autorisation (RFC 8414) :
  `GET /.well-known/oauth-authorization-server`.
- Enregistrement dynamique (RFC 7591) : `POST /oauth/register` (`redirect_uris` en https, `http://localhost`/`127.0.0.1`
  ou schéma d'application ; client public `none` par défaut, ou `client_secret_post` / `client_secret_basic`).
- `GET /oauth/authorize` (code + **PKCE S256 obligatoire**, `state`, `resource` = `…/mcp`), `POST /oauth/token`
  (`authorization_code`, `refresh_token`), `POST /oauth/revoke`.
- Jetons liés au compte : accès `oat_…` valable 1 h, rafraîchissement `ort_…` 30 jours avec rotation (un ancien jeton
  réutilisé révoque toute l'autorisation). Le jeton d'accès vaut une clé API pour l'API REST du même compte.
- Applications connectées : `GET /v1/applications`, révocation `DELETE /v1/applications/{client_id}`.

## Installation (stdio)
```bash
cd code/mcp_server
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
```

## Claude Desktop / Claude Code
`claude_desktop_config.json` (ou `.mcp.json`) :
```json
{
  "mcpServers": {
    "dialyne": {
      "command": "/chemin/vers/code/mcp_server/.venv/bin/python",
      "args": ["/chemin/vers/code/mcp_server/server.py"],
      "env": {"DIALYNE_API_KEY": "tel_xxx", "DIALYNE_API_URL": "https://api.dialyne.com"}
    }
  }
}
```
(`TELEPHONE_API_KEY` / `TELEPHONE_API_URL` restent lues si les nouvelles variables sont absentes.)

## Cursor
`~/.cursor/mcp.json` : même bloc `mcpServers` que ci-dessus.

## Exemple de dialogue
> « Appelle le 01 42 00 00 00 et réserve une table pour 4 vendredi 20 h au nom de Moha. »

1. L'agent appelle `place_phone_call("+33142000000", "Réserve une table pour 4 vendredi 20 h au nom de Moha")`.
2. Il suit avec `get_call(call_id, wait_seconds=25)` ; si `pending_question` est rempli (« Terrasse ou salle ? »), il
   répond avec `answer_call_question`. S'il doit te demander, il appelle d'abord `hold_call_question` (l'interlocuteur
   patiente une minute s'il l'accepte), puis te pose la question et répond.
3. À `done = true`, il te donne `outcome` et `summary`.

## Tester sans client MCP
```bash
npx @modelcontextprotocol/inspector python server.py
```
