Skip to content

Le serveur MCP

This content is not available in your language yet.

À quoi sert ce serveur. Laisser un assistant lire et agir sur un club (planning, réservations, tarifs, caisse…) au nom de la personne qui l’autorise, avec exactement ses droits.

apps/mcp est un second adaptateur, à côté de GraphQL : il appelle les mêmes commandes et requêtes de packages/domain, sans aucune règle métier propre. Une réservation refusée par MCP l’est pour la même raison qu’à l’accueil (SlotTaken, OutsideOpeningHours…).

  • Le serveur répond sur /mcp (HTTP streamable, sans état). Le club vient de l’hôte de la requête, ou de ?club=<slug>.
  • Sans jeton valide, il répond 401 avec un en-tête WWW-Authenticate qui pointe vers /.well-known/oauth-protected-resource : le client découvre ainsi le serveur d’autorisation.
  • Le serveur d’autorisation est Better Auth, dans l’API, sur l’hôte du club. Les clients MCP s’enregistrent d’eux-mêmes (enregistrement dynamique, PKCE). Portées : openid, profile, email, offline_access.
  • Le jeton est un JWT vérifié avec les clés de l’API : émetteur = l’API de l’hôte du club, audience = le /mcp de ce même hôte. Un jeton d’un club ne sert pas sur un autre.

Pendant l’autorisation, le client ouvre la page de connexion du club, servie par l’API sur /oauth/connexion : lien par e-mail, code par SMS, ou mot de passe pour l’équipe (« Vous faites partie de l’équipe du club ? »).

La page Connexion du club servie par l'API : e-mail, téléphone, et l'accès équipeLa page Connexion du club servie par l'API : e-mail, téléphone, et l'accès équipe

Puis la page /oauth/consentement demande l’accord : « L’application … demande à agir en votre nom sur ce club », avec la liste de ce qu’elle pourra faire, et les boutons Autoriser et Refuser.

La page Autoriser l'accès : le nom de l'application, ce qu'elle pourra faire, Autoriser ou RefuserLa page Autoriser l'accès : le nom de l'application, ce qu'elle pourra faire, Autoriser ou Refuser

Les droits sont ceux du rôle de la personne connectée, vérifiés dans le domaine : un joueur ne voit du planning que les créneaux occupés et ses propres réservations ; les outils marqués « staff » ou « owner » sont refusés aux autres.

Un refus revient en erreur d’outil, error: suivi du JSON de l’erreur du domaine (son nom en _tag, ses champs) : les mêmes codes que l’API GraphQL, présentés de la même façon. Une erreur d’infrastructure ou un bug ne donne qu’un message court (database error, internal error, ou le nom de l’erreur) ; le détail va dans les journaux du serveur.

  • Planning et réservations : planning, availability, booking, create_booking, confirm_booking, cancel_booking, cancel_series, move_booking, change_booking_players, check_in, undo_check_in, name_participant, create_client, mark_no_show, mark_participant_no_show, undo_participant_no_show, create_block, release_block, create_series.
  • Horaires et créneaux : opening_hours, schedule_settings, schedule_change_preview, save_schedule, delete_schedule, set_site_default_schedule, assign_schedule, set_schedule_assignment_dates, unassign_schedule (horaires datés par site ou terrain, avec la liste des réservations laissées hors horaires avant d’enregistrer), add_closure, remove_closure, sport_configs, sport_step_preview, save_sport_step, save_sport_limits (fenêtre de réservation, matchs à venir par joueur, annulation gratuite propre au sport), my_booking_limits (les miennes, celles de ma formule si elle les relève).
  • Terrains : resource_settings, save_resource (ajouter un terrain, le renommer, lui donner un partenaire : nom, lien, image envoyée depuis l’accueil ; son site et son sport ne changent plus).
  • Tarifs et périodes : sports, tariffs, quote, quotes, create_tariff, update_tariff, delete_tariff, periods, period_week, create_period, update_period, delete_period, create_default_periods.
  • Formules et droits : formules, create_formule, update_formule, sell_formule, formule_sales, entitlements, grant_entitlement, settle_share_with_entitlement, share_offers et choose_share_offer (le prix de la part du titulaire : tarif ou une de ses remises), licences, set_licence, remove_licence.
  • Abonnements : subscriptions, subscription, subscribable_formules, start_subscription, buy_subscription, cancel_subscription, undo_cancel_subscription, pause_subscription, resume_subscription, reactivate_subscription, collect_subscription, retry_subscription, change_subscription_method, start_card_update, set_subscription_settings.
  • Suppléments et cours : addons, create_addon, update_addon, booking_items, add_booking_items, remove_booking_item, products, create_product, update_product, product_stock, restock_product, count_stock, coaching_series, create_coaching_series, coaching_series_for, enrol_in_coaching_series, record_coaching_attendance.
  • Notes de comptoir : list_tabs, tab_of_booking, tab (ses lignes, ses réservations avec leurs parts et suppléments, payé et reste à régler), open_tab, close_tab (refusé tant qu’il reste à régler ; ses réservations s’en détachent), add_tab_line (le prix et la TVA du produit, le stock suivi bouge), cancel_tab_line (refusé une fois payée, le stock revient), attach_booking_to_tab, move_booking_to_tab, detach_booking_from_tab (les parts restent dues par leurs joueurs), pay_ticket (un encaissement sur plusieurs lignes : le montant se répartit selon ce qui est dû sur chacune ; le porte-monnaie débité est celui du payeur choisi ; la note se ferme quand plus rien n’est dû), ticket (ce que montre son reçu), create_sub_tab (une sous-note : des lignes ou une partie, prises sur ce qui reste à régler), split_tab (partager en N parts, « Part 1/N », au centime près), cancel_sub_tab (ses lignes reviennent dans ce qui reste) ; pay_ticket encaisse aussi une ou plusieurs sous-notes, et un montant plus petit laisse le reste à régler.
  • Événements : club_events, club_event (les inscriptions : joueurs, licences, réponses, rang en liste d’attente), save_event, cancel_event, promote_event_entry (staff) ; register_event_entry, cancel_event_entry (staff, ou un joueur pour sa propre inscription jusqu’à la clôture, jusqu’au début si elle vient de la liste d’attente ; une inscription faite par l’accueil ne s’annule qu’à l’accueil) ; events_board, event_page (ce qu’un joueur voit : places, son inscription, les noms des inscrits selon le choix du club).
  • Clients et pilotage : clients, client_bookings, client_file (la fiche client : solde, à régler, mouvements d’argent, réservations, formules, abonnements, notes, messages), add_client_note, remove_client_note, pilotage (le tableau de bord : activité, argent, membres, comparés à la période précédente), occupancy_by_day.
  • Argent : booking_money, wallet, till, closings, close_day, verify_ledger, check_money (les règles de l’argent du club : commandes, porte-monnaie, parts réglées, réservations libérées, chaîne du journal), export_fec, collect_share, collect_booking, create_order_for_share, create_order_for_booking, record_payment, pay_with_wallet, create_top_up, set_top_up_tiers, stray_payments (l’argent reçu en trop, à rendre), provider_discrepancies (les écarts avec Stripe trouvés chaque nuit), acknowledge_discrepancy, record_discrepancy_charge, refund_payment, cancel_order.
  • Import (propriétaire) : import_specs, import_start, import_map, import_rows, import_apply, import_report, import_jobs.
  • Club et équipe (propriétaire) : who_am_i, invite_staff, staff, invitations, revoke_invitation, change_team_role, remove_from_team (le dernier responsable reste responsable), club_settings, set_block_presets, update_club_settings, club_payment_account (équipe : le compte Stripe du club et son statut), club_domains, add_club_domain, verify_club_domain, remove_club_domain.
  • Retours au pilote (administrateurs de la plateforme, dans le club de l’hôte) : feedback_notes (les retours envoyés depuis l’accueil et l’application joueur, les plus récents d’abord, ouverts par défaut), feedback_note (un retour entier : chaque élément pointé, son chemin avec son data-testid, le texte autour), mark_feedback_handled.
  • Messages : client_notifications, resend_notification, set_notify_channel, notification_settings, update_notification_settings.
  • Côté joueur (les mêmes commandes que l’application joueur) : my_bookings, book, open_match, join_open_match, buy_formule, search_members. open_match lit une partie ouverte par l’identifiant de son lien ; join_open_match la rejoint et règle la part (porte-monnaie ou droit tout de suite, carte avec un checkoutUrl, la place gardée quelques minutes) ; addPartner ajoute l’organisateur à ses partenaires quand open_match renvoie partnerOffer. book place une pré-réservation puis règle la part du joueur (ou tout) par porte-monnaie, carte (il renvoie un checkoutUrl à ouvrir) ou droit. search_members ne trouve, pour un joueur, que les profils publics.
  • Partenaires (joueur) : my_network, send_partner_request, invite_partner (par e-mail ou téléphone, membre ou non), withdraw_partner_invitation, answer_partner_request, withdraw_partner_request, remove_partner_link, block_member, unblock_member, set_public_profile, set_joiners_become_partners (sur sa partie ouverte) ; le propriétaire autorise ou non les profils publics avec set_public_profiles_allowed.

En local, chaque club vit sur <slug>.localhost en http. Or les clients MCP (Claude Code en tête) refusent d’envoyer un jeton à un point d’accès http qui n’est pas exactement localhost (« OAuth token requests MUST use TLS »). D’où --club : il sert un club sur l’hôte nu localhost, exempté de TLS.

  1. Lancez la pile avec un club sur l’hôte nu : pnpm bo dev --club bo. L’API, le serveur MCP, l’accueil et l’appli joueur répondent alors aussi sur http://localhost:<port> pour le club bo.
  2. Ajoutez le serveur à Claude Code : claude mcp add --transport http bo http://localhost:<MCP_PORT>/mcp (pnpm bo auth login thibault@bo.test affiche la commande avec le bon port ; pnpm bo doctor confirme « dev club on localhost: bo »).
  3. Dans Claude Code, /mcp → bo → Authenticate. La page de connexion du club s’ouvre sur localhost ; connectez-vous (mot de passe de dev booker-dev pour l’équipe), puis Autoriser.
  4. Appelez who_am_i : il doit répondre Bø, avec votre rôle.

Quelques précisions :

  • Seule la recherche du club change : l’hôte reste localhost. Émetteur http://localhost:<API_PORT>/auth, audience http://localhost:<MCP_PORT>/mcp, cookies sur localhost. Une session ouverte sur localhost ne vaut pas sur bo.localhost, et inversement.
  • Un seul club sur localhost : l’en-tête x-bo-club-host et les vrais hôtes de club gardent la priorité, mais ?club=<slug> ne sert pas sur l’hôte nu (le client y obtiendrait un jeton pour localhost, refusé pour <slug>.localhost). Pour un autre club, relancez avec --club <slug>.
  • Après un changement de --club, réauthentifiez le client : le jeton et la session restent ceux de localhost, quel que soit le club.
  • BO_DEV_CLUB (ce que pose --club) est refusé au démarrage si BO_BASE_DOMAIN n’est pas localhost : jamais en production.
  • Un client qui accepte http partout peut toujours utiliser http://bo.localhost:<MCP_PORT>/mcp sans --club.
  • La vraie réponse est TLS en local (mkcert + Caddy, un futur bo dev --tls) : prévue, pas encore disponible.