@clicbase/mcp 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/clicbase-mcp.mjs CHANGED
@@ -38,10 +38,112 @@ if (!CLE) {
38
38
  const out = (d) => ({ content: [{ type: "text", text: typeof d === "string" ? d : JSON.stringify(d, null, 2) }] });
39
39
  const fail = (e) => ({ isError: true, content: [{ type: "text", text: String(e?.message ?? e) }] });
40
40
 
41
+ // L'API de LECTURE des projets, derivee de celle de provisioning.
42
+ const API_LECTURE = API.replace(/\/databases\/?$/, "/admin/projects");
43
+
44
+ /**
45
+ * Traduit un statut HTTP en phrase utile.
46
+ *
47
+ * ⚠️ « Clicbase 403 » NE DIT RIEN, ET ON L'A VU COUTER UNE HEURE. Le
48
+ * 2026-09-17, une session a lu ce nombre, conclu que la cle etait morte, et
49
+ * cherche du mauvais cote. Mesure ensuite : la cle etait vivante, jamais
50
+ * revoquee, et cette route ne peut PAS repondre 403 (400 sans en-tete, 401 cle
51
+ * refusee, 429 debit, 500 provisioning). Le nombre seul envoie chercher au
52
+ * hasard.
53
+ */
54
+ /**
55
+ * Lit une reponse en echec SANS PERDRE CE QU'ELLE CONTENAIT.
56
+ *
57
+ * ⚠️ `res.json().catch(() => ({}))` EFFACE LA PREUVE. Le 2026-09-17, une session
58
+ * a rapporte « Clicbase 403: {} » sur trois tentatives. Ce `{}` ne voulait pas
59
+ * dire « corps vide » : il voulait dire « corps ILLISIBLE en JSON », donc pas
60
+ * notre application, qui rend toujours un champ `error`. L'information qui
61
+ * aurait tranche en dix secondes, l'en-tete `cf-ray` et les premiers octets du
62
+ * corps, avait ete jetee par le `catch`.
63
+ *
64
+ * On garde donc : le statut, l'origine declaree par le serveur, l'identifiant
65
+ * Cloudflare s'il existe, et le debut du corps tel quel.
66
+ */
67
+ async function lireEchec(res) {
68
+ const brut = await res.text().catch(() => "");
69
+ let json = null;
70
+ try {
71
+ json = JSON.parse(brut);
72
+ } catch {
73
+ /* pas du JSON : c'est precisement ce qu'on veut savoir */
74
+ }
75
+ const via = res.headers.get("cf-ray")
76
+ ? `Cloudflare (cf-ray ${res.headers.get("cf-ray")})`
77
+ : (res.headers.get("server") ?? "origine inconnue");
78
+ return {
79
+ json,
80
+ // ⚠️ UN CORPS NON-JSON EST LA SIGNATURE D'UN REFUS EN AMONT. On le dit en
81
+ // clair au lieu de rendre un objet vide qui ressemble a une reponse.
82
+ detail: json
83
+ ? JSON.stringify(json)
84
+ : brut.trim()
85
+ ? `reponse NON-JSON de ${via} : ${brut.replace(/\s+/g, " ").slice(0, 180)}`
86
+ : `corps vide, servi par ${via} — la requete n'a probablement pas atteint Clicbase.`,
87
+ };
88
+ }
89
+
90
+ function expliquer(statut) {
91
+ if (statut === 400) return "requete incomplete : le champ `name` est obligatoire.";
92
+ if (statut === 401)
93
+ return "cle refusee : invalide, expiree ou revoquee. Attention, le bouton « Rouler » du tableau de bord REMPLACE le secret : l'ancien meurt aussitot.";
94
+ if (statut === 403)
95
+ return "hors du perimetre de cette cle. Une cle de Docker ne voit que son Docker. Verifie sa portee sur /api/v1/admin/me.";
96
+ if (statut === 429) return "trop de requetes, attends une minute.";
97
+ if (statut === 500)
98
+ return "la plateforme a refuse l'operation. Les creations de projet sont actuellement fermees : un projet qui n'existe pas deja ne peut pas etre cree.";
99
+ return "reponse inattendue.";
100
+ }
101
+
102
+ /**
103
+ * Trouve un projet SANS RIEN CREER.
104
+ *
105
+ * ⚠️ IL ACCEPTE LE NOM **OU** LE SLUG, et c'est le coeur du correctif. Le
106
+ * projet « présidence » a pour slug `presidence` : un assistant qui tape le
107
+ * slug, ou qui perd l'accent en chemin, ne trouvait rien. Ici on cherche dans
108
+ * les deux, puis on rend le nom EXACT tel qu'il est en base.
109
+ */
110
+ async function trouver(name) {
111
+ const res = await fetch(API_LECTURE, {
112
+ headers: { authorization: `Bearer ${CLE}` },
113
+ });
114
+ if (!res.ok) {
115
+ const e = await lireEchec(res);
116
+ throw new Error(`Clicbase ${res.status} — ${expliquer(res.status)} ${e.detail}`);
117
+ }
118
+ const data = await res.json().catch(() => ({}));
119
+ const liste = Array.isArray(data.projects) ? data.projects : [];
120
+ const cible = String(name).trim().toLowerCase();
121
+ const p =
122
+ liste.find((x) => String(x.name ?? "").toLowerCase() === cible) ??
123
+ liste.find((x) => String(x.slug ?? "").toLowerCase() === cible);
124
+ if (!p) {
125
+ const noms = liste.map((x) => x.slug ?? x.name).filter(Boolean).join(", ");
126
+ throw new Error(
127
+ `Aucun projet « ${name} » dans le perimetre de cette cle.` +
128
+ (noms ? ` Projets visibles : ${noms}.` : " Cette cle ne voit aucun projet."),
129
+ );
130
+ }
131
+ return p;
132
+ }
133
+
41
134
  // Provisionne (ou récupère, idempotent) un projet et renvoie ses infos.
135
+ //
136
+ // ⚠️ `creer: false` POUR TOUT OUTIL DE LECTURE. Cet appel est un POST sur
137
+ // l'endpoint de provisioning : idempotent quand le projet existe, CREATEUR
138
+ // sinon. `list_tables` survit au mode lecture seule et passait par ici : la
139
+ // promesse « aucune ecriture » etait donc fausse au niveau du transport, pendant
140
+ // que `create_database` disparaissait de la liste des outils. On verifie
141
+ // l'existence par une LECTURE d'abord, et on ne poste qu'un nom deja connu.
42
142
  const cache = new Map();
43
- async function resolve(name, domain) {
143
+ async function resolve(name, domain, { creer = true } = {}) {
44
144
  if (cache.has(name)) return cache.get(name);
145
+ let nomExact = name;
146
+ if (!creer) nomExact = (await trouver(name)).name ?? name;
45
147
  const res = await fetch(API, {
46
148
  method: "POST",
47
149
  headers: {
@@ -49,10 +151,13 @@ async function resolve(name, domain) {
49
151
  // ⚠️ UNE CLE, PAS UN MOT DE PASSE. Voir l'en-tete du fichier.
50
152
  authorization: `Bearer ${CLE}`,
51
153
  },
52
- body: JSON.stringify({ name, domain }),
154
+ body: JSON.stringify({ name: nomExact, domain }),
53
155
  });
156
+ if (!res.ok) {
157
+ const e = await lireEchec(res);
158
+ throw new Error(`Clicbase ${res.status} — ${expliquer(res.status)} ${e.detail}`);
159
+ }
54
160
  const data = await res.json().catch(() => ({}));
55
- if (!res.ok) throw new Error(`Clicbase ${res.status}: ${JSON.stringify(data)}`);
56
161
  const info = {
57
162
  url: data.restApi?.url,
58
163
  anonKey: data.restApi?.anonKey,
@@ -65,8 +170,8 @@ async function resolve(name, domain) {
65
170
  }
66
171
 
67
172
  // Appel d'un endpoint du projet avec la clé service.
68
- async function call(name, path, method = "POST", body) {
69
- const p = await resolve(name);
173
+ async function call(name, path, method = "POST", body, opts) {
174
+ const p = await resolve(name, undefined, opts);
70
175
  if (!p.url || !p.serviceKey) throw new Error("Projet non résolu (clés manquantes).");
71
176
  const res = await fetch(`${p.url}${path}`, {
72
177
  method,
@@ -129,7 +234,20 @@ outils.tool(
129
234
  "Liste les tables du schéma public d'un projet.",
130
235
  { name: z.string() },
131
236
  async ({ name }) => {
132
- try { return out(await call(name, "/sql", "POST", { query: "select table_name from information_schema.tables where table_schema='public' order by table_name" })); } catch (e) { return fail(e); }
237
+ try {
238
+ // ⚠️ `creer: false` : un outil de lecture ne provisionne jamais.
239
+ return out(
240
+ await call(
241
+ name,
242
+ "/sql",
243
+ "POST",
244
+ { query: "select table_name from information_schema.tables where table_schema='public' order by table_name" },
245
+ { creer: false },
246
+ ),
247
+ );
248
+ } catch (e) {
249
+ return fail(e);
250
+ }
133
251
  },
134
252
  );
135
253
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicbase/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mcp": "clicbase-mcp.mjs",
package/savoir.mjs CHANGED
@@ -35,6 +35,8 @@ SIX PIÈGES QUI NE LÈVENT AUCUNE ERREUR. Ils répondent tous « 200 OK ».
35
35
  5. Une fonction Edge renvoie \`{ status, body }\`, jamais \`new Response(...)\`.
36
36
  6. Deux authentifications sans rapport coexistent. Celle du PROJET (tes utilisateurs finaux) vit dans le schéma \`auth\` de cette base et alimente \`auth.uid()\` dans les policies. Celle de la PLATEFORME (le compte Clicbase) ne s'écrit jamais dans une policy.
37
37
 
38
+ CE QUI N'EXISTE PAS, ET QUE DEUX ASSISTANTS ONT DEJA INVENTE. Il n'y a AUCUN registre Docker : ni \`docker login\`, ni \`docker pull\`, ni \`registry.*.clicbase.com\`. Ne devine aucun nom d'hote. Ce serveur MCP ne rend pas le code source d'un site heberge : pour recuperer des fichiers, c'est le SFTP du tableau de bord ou son gestionnaire de fichiers, et l'API d'administration ne fait que les ECRIRE (POST /sites/<id>/files), jamais les lire.
39
+
38
40
  APPELS REST : en-têtes \`apikey\` ET \`Authorization: Bearer\`, la même clé dans les deux. Filtres dans l'URL, façon PostgREST : \`?select=id,title&status=eq.published&order=created_at.desc\`.
39
41
 
40
42
  DEUX CLÉS À NE PAS CONFONDRE. \`anon\` est publique et soumise à la RLS, elle va dans le navigateur. \`service\` CONTOURNE la RLS et voit tout : serveur uniquement, jamais dans un front, jamais dans un dépôt.