const emptyInputSchema = Object.freeze({ type: 'object', properties: {}, additionalProperties: false, }); export function readPublicSiteData(doc = globalThis.document) { const source = doc?.getElementById?.('public-site-data'); if (!source?.textContent) return null; try { return JSON.parse(source.textContent); } catch { return null; } } export function selectReviews(data, source = 'all', liveGoogle = null) { const selectedSource = ['all', 'google', 'superprof'].includes(source) ? source : 'all'; const result = {}; if (selectedSource === 'all' || selectedSource === 'google') { result.google = liveGoogle || { available: false, profileUrl: data.reviews.google.profileUrl, message: 'Les données Google en direct ne sont pas disponibles. Consultez le profil Google.', fallbackReviews: data.reviews.google.fallbackItems || [], }; } if (selectedSource === 'all' || selectedSource === 'superprof') { result.superprof = { available: true, profileUrl: data.reviews.superprof.profileUrl, reviews: data.reviews.superprof.items, }; } return result; } export function createWebMcpTools(data, options = {}) { if (!data?.business || !data?.lesson || !data?.reviews) { throw new TypeError('Public site data is incomplete.'); } const readOnly = Object.freeze({ readOnlyHint: true, untrustedContentHint: false }); const getLiveGoogle = () => options.liveGoogle || globalThis.__coursDeChantGoogleReviews || null; return [ { name: 'get_lesson_information', title: 'Informations sur les cours de chant', description: 'Donne les informations publiques et vérifiées sur le cours de chant individuel à Bruxelles : public, durée, prix, lieu, formats et sujets travaillés. À utiliser pour comprendre l’offre, jamais pour envoyer une demande.', inputSchema: emptyInputSchema, annotations: readOnly, execute: async () => ({ service: 'Cours de chant individuel', ...data.lesson, enquiryPage: '/reserver/', }), }, { name: 'get_public_speaking_information', title: 'Informations sur la prise de parole', description: 'Donne les informations publiques et vérifiées sur l’accompagnement de la prise de parole : situations, axes de travail, durée, prix et lieu. À utiliser pour s’informer, jamais pour envoyer une demande.', inputSchema: emptyInputSchema, annotations: readOnly, execute: async () => ({ service: 'Prise de parole', ...data.speaking, informationPage: '/coaching-vocal/', enquiryPage: '/reserver/', }), }, { name: 'get_karaoke_workshop_information', title: 'Informations sur l’atelier karaoké', description: 'Donne uniquement les informations publiques confirmées sur l’atelier karaoké pour les groupes. Les détails non encore convenus sont signalés comme à confirmer et ne doivent pas être inventés.', inputSchema: emptyInputSchema, annotations: readOnly, execute: async () => ({ service: data.workshop.name, ...data.workshop, informationPage: '/ateliers-karaoke/', quoteRequestPage: '/ateliers-karaoke/#devis', }), }, { name: 'get_contact_information', title: 'Coordonnées de Rem Sari', description: 'Retourne les coordonnées publiques de Rem Sari, l’adresse des cours et les pages de demande. À utiliser lorsqu’une personne demande où ont lieu les cours ou comment contacter le professeur.', inputSchema: emptyInputSchema, annotations: readOnly, execute: async () => ({ ...data.business, lessonEnquiryPage: '/reserver/', workshopQuotePage: '/ateliers-karaoke/#devis', }), }, { name: 'view_student_reviews', title: 'Consulter les avis élèves', description: 'Consulte les avis actuellement publiés par source. Google et Superprof restent séparés. Une absence de données Google en direct doit être présentée comme indisponible, jamais remplacée par une note ou un total inventé.', inputSchema: { type: 'object', properties: { source: { type: 'string', enum: ['all', 'google', 'superprof'], description: 'Source à consulter : all, google ou superprof.', }, }, additionalProperties: false, }, annotations: readOnly, execute: async ({ source = 'all' } = {}) => selectReviews(data, source, getLiveGoogle()), }, ]; } export async function registerWebMcpTools(doc = globalThis.document) { if (!doc?.modelContext || typeof doc.modelContext.registerTool !== 'function') { return { supported: false, registered: [] }; } const data = readPublicSiteData(doc); if (!data) return { supported: true, registered: [], error: 'public_data_unavailable' }; const liveGoogle = globalThis.__coursDeChantGoogleReviews || null; const tools = createWebMcpTools(data, { liveGoogle }); const registered = []; for (const tool of tools) { try { await doc.modelContext.registerTool(tool); registered.push(tool.name); } catch { // Registration failures are intentionally non-fatal: the normal website remains complete. } } return { supported: true, registered }; } if (typeof document !== 'undefined') { registerWebMcpTools(document); }