@clicbase/mcp 0.1.1 → 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/README.md +27 -0
- package/clicbase-db-mcp.mjs +17 -1
- package/clicbase-mcp.mjs +141 -7
- package/package.json +2 -1
- package/portee.mjs +5 -0
- package/savoir.mjs +122 -0
package/README.md
CHANGED
|
@@ -80,6 +80,33 @@ projet** (`/dashboard?studio=<id>§ion=api`) : « API REST » donne l'URL,
|
|
|
80
80
|
« Cle service » la cle. Ce n'est PAS une cle `cbk_` : celles-la servent au
|
|
81
81
|
serveur tout-en-un et n'apparaissent que pour qui possede un VPS ou un Docker.
|
|
82
82
|
|
|
83
|
+
## Ce que l'assistant sait avant son premier appel
|
|
84
|
+
|
|
85
|
+
Un serveur MCP qui n'expose que des outils laisse le modele deviner les
|
|
86
|
+
conventions de la plateforme. Depuis la 0.1.2, celui-ci remplit le champ
|
|
87
|
+
`instructions` du protocole : le client le pose dans le contexte du modele A LA
|
|
88
|
+
CONNEXION, avant tout appel. On ne peut donc pas oublier de le lire.
|
|
89
|
+
|
|
90
|
+
Six pieges y sont enumeres, et ils ont en commun de repondre **200 OK** :
|
|
91
|
+
|
|
92
|
+
1. `with check` n'est pas `using`. Sur INSERT et UPDATE, PostgreSQL n'evalue
|
|
93
|
+
QUE `with check`.
|
|
94
|
+
2. Chaque nouvelle table accorde le CRUD a `authenticated` : un `grant select`
|
|
95
|
+
ne restreint rien, il faut `revoke`.
|
|
96
|
+
3. Toute DDL se termine par `notify pgrst, 'reload schema';`.
|
|
97
|
+
4. `service_role` a besoin de `BYPASSRLS`, sinon 200 OK et un tableau vide.
|
|
98
|
+
5. Une fonction Edge rend `{ status, body }`, jamais `new Response(...)`.
|
|
99
|
+
6. L'auth du PROJET (schema `auth` de la base, `auth.uid()`) ne se confond pas
|
|
100
|
+
avec celle de la PLATEFORME.
|
|
101
|
+
|
|
102
|
+
L'outil `clicbase_conventions` en rend le detail a la demande : formes exactes
|
|
103
|
+
des policies, ordre `revoke` puis `grant`, operateurs PostgREST. Il est classe
|
|
104
|
+
en LECTURE, donc il survit au mode restreint · c'est ce mode qui en a le plus
|
|
105
|
+
besoin, puisqu'il ne laisse que trois autres outils.
|
|
106
|
+
|
|
107
|
+
Le texte vit dans `savoir.mjs`, sans aucun import, pour rester chargeable par
|
|
108
|
+
les tests du depot.
|
|
109
|
+
|
|
83
110
|
## Quand l'utiliser
|
|
84
111
|
|
|
85
112
|
`run_sql` exécute du SQL arbitraire avec les droits du propriétaire de la base,
|
package/clicbase-db-mcp.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { filtre, lectureSeule } from "./portee.mjs";
|
|
15
|
+
import { CONVENTIONS, INSTRUCTIONS } from "./savoir.mjs";
|
|
15
16
|
|
|
16
17
|
const BASE = process.env.CLICBASE_DB_URL ?? process.env.ROROUIRA_DB_URL;
|
|
17
18
|
const KEY = process.env.CLICBASE_SERVICE_KEY ?? process.env.ROROUIRA_SERVICE_KEY;
|
|
@@ -51,7 +52,15 @@ async function api(path, method = "GET", body) {
|
|
|
51
52
|
const out = (d) => ({ content: [{ type: "text", text: JSON.stringify(d, null, 2) }] });
|
|
52
53
|
const fail = (e) => ({ isError: true, content: [{ type: "text", text: String(e?.message ?? e) }] });
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
// ⚠️ `instructions` EST LU A LA CONNEXION, AVANT LE PREMIER APPEL. C'est le
|
|
56
|
+
// seul endroit qu'un assistant ne peut pas oublier de consulter : le client
|
|
57
|
+
// MCP le pose dans le contexte du modele. Sans lui, un assistant recevait
|
|
58
|
+
// TROIS outils et trente mots de description, puis devinait les conventions
|
|
59
|
+
// de la plateforme. Voir savoir.mjs.
|
|
60
|
+
const server = new McpServer(
|
|
61
|
+
{ name: "clicbase-db", version: "1.0.0" },
|
|
62
|
+
{ instructions: INSTRUCTIONS },
|
|
63
|
+
);
|
|
55
64
|
|
|
56
65
|
// ⚠️ EN LECTURE SEULE, LES OUTILS QUI ECRIVENT NE SONT PAS ENREGISTRES. Voir
|
|
57
66
|
// portee.mjs : un outil absent n'est jamais demande, un outil present qui
|
|
@@ -59,6 +68,13 @@ const server = new McpServer({ name: "clicbase-db", version: "1.0.0" });
|
|
|
59
68
|
const SEULEMENT_LECTURE = lectureSeule();
|
|
60
69
|
const outils = filtre(server, SEULEMENT_LECTURE);
|
|
61
70
|
|
|
71
|
+
outils.tool(
|
|
72
|
+
"clicbase_conventions",
|
|
73
|
+
"Les conventions Clicbase : formes exactes des policies RLS, droits par defaut, rechargement du schema, appels REST, fonctions Edge. A lire AVANT d'ecrire du SQL ou une policy.",
|
|
74
|
+
{},
|
|
75
|
+
async () => ({ content: [{ type: "text", text: CONVENTIONS }] }),
|
|
76
|
+
);
|
|
77
|
+
|
|
62
78
|
outils.tool(
|
|
63
79
|
"run_sql",
|
|
64
80
|
"Exécute du SQL (DDL/DML/RLS) sur la base. Après un changement de schéma, termine par: NOTIFY pgrst, 'reload schema';",
|
package/clicbase-mcp.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
21
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
import { filtre, lectureSeule } from "./portee.mjs";
|
|
24
|
+
import { CONVENTIONS, INSTRUCTIONS } from "./savoir.mjs";
|
|
24
25
|
|
|
25
26
|
const API = process.env.CLICBASE_API ?? process.env.ROROUIRA_API ?? "https://clicbase.com/api/v1/databases";
|
|
26
27
|
|
|
@@ -37,10 +38,112 @@ if (!CLE) {
|
|
|
37
38
|
const out = (d) => ({ content: [{ type: "text", text: typeof d === "string" ? d : JSON.stringify(d, null, 2) }] });
|
|
38
39
|
const fail = (e) => ({ isError: true, content: [{ type: "text", text: String(e?.message ?? e) }] });
|
|
39
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
|
+
|
|
40
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.
|
|
41
142
|
const cache = new Map();
|
|
42
|
-
async function resolve(name, domain) {
|
|
143
|
+
async function resolve(name, domain, { creer = true } = {}) {
|
|
43
144
|
if (cache.has(name)) return cache.get(name);
|
|
145
|
+
let nomExact = name;
|
|
146
|
+
if (!creer) nomExact = (await trouver(name)).name ?? name;
|
|
44
147
|
const res = await fetch(API, {
|
|
45
148
|
method: "POST",
|
|
46
149
|
headers: {
|
|
@@ -48,10 +151,13 @@ async function resolve(name, domain) {
|
|
|
48
151
|
// ⚠️ UNE CLE, PAS UN MOT DE PASSE. Voir l'en-tete du fichier.
|
|
49
152
|
authorization: `Bearer ${CLE}`,
|
|
50
153
|
},
|
|
51
|
-
body: JSON.stringify({ name, domain }),
|
|
154
|
+
body: JSON.stringify({ name: nomExact, domain }),
|
|
52
155
|
});
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const e = await lireEchec(res);
|
|
158
|
+
throw new Error(`Clicbase ${res.status} — ${expliquer(res.status)} ${e.detail}`);
|
|
159
|
+
}
|
|
53
160
|
const data = await res.json().catch(() => ({}));
|
|
54
|
-
if (!res.ok) throw new Error(`Clicbase ${res.status}: ${JSON.stringify(data)}`);
|
|
55
161
|
const info = {
|
|
56
162
|
url: data.restApi?.url,
|
|
57
163
|
anonKey: data.restApi?.anonKey,
|
|
@@ -64,8 +170,8 @@ async function resolve(name, domain) {
|
|
|
64
170
|
}
|
|
65
171
|
|
|
66
172
|
// Appel d'un endpoint du projet avec la clé service.
|
|
67
|
-
async function call(name, path, method = "POST", body) {
|
|
68
|
-
const p = await resolve(name);
|
|
173
|
+
async function call(name, path, method = "POST", body, opts) {
|
|
174
|
+
const p = await resolve(name, undefined, opts);
|
|
69
175
|
if (!p.url || !p.serviceKey) throw new Error("Projet non résolu (clés manquantes).");
|
|
70
176
|
const res = await fetch(`${p.url}${path}`, {
|
|
71
177
|
method,
|
|
@@ -77,7 +183,15 @@ async function call(name, path, method = "POST", body) {
|
|
|
77
183
|
return data;
|
|
78
184
|
}
|
|
79
185
|
|
|
80
|
-
|
|
186
|
+
// ⚠️ `instructions` EST LU A LA CONNEXION, AVANT LE PREMIER APPEL. C'est le
|
|
187
|
+
// seul endroit qu'un assistant ne peut pas oublier de consulter : le client
|
|
188
|
+
// MCP le pose dans le contexte du modele. Sans lui, un assistant recevait
|
|
189
|
+
// TROIS outils et trente mots de description, puis devinait les conventions
|
|
190
|
+
// de la plateforme. Voir savoir.mjs.
|
|
191
|
+
const server = new McpServer(
|
|
192
|
+
{ name: "clicbase", version: "2.0.0" },
|
|
193
|
+
{ instructions: INSTRUCTIONS },
|
|
194
|
+
);
|
|
81
195
|
|
|
82
196
|
// ⚠️ EN LECTURE SEULE, LES OUTILS QUI ECRIVENT NE SONT PAS ENREGISTRES. Voir
|
|
83
197
|
// portee.mjs : un outil absent n'est jamais demande, un outil present qui
|
|
@@ -85,6 +199,13 @@ const server = new McpServer({ name: "clicbase", version: "2.0.0" });
|
|
|
85
199
|
const SEULEMENT_LECTURE = lectureSeule();
|
|
86
200
|
const outils = filtre(server, SEULEMENT_LECTURE);
|
|
87
201
|
|
|
202
|
+
outils.tool(
|
|
203
|
+
"clicbase_conventions",
|
|
204
|
+
"Les conventions Clicbase : formes exactes des policies RLS, droits par defaut, rechargement du schema, appels REST, fonctions Edge. A lire AVANT d'ecrire du SQL ou une policy.",
|
|
205
|
+
{},
|
|
206
|
+
async () => ({ content: [{ type: "text", text: CONVENTIONS }] }),
|
|
207
|
+
);
|
|
208
|
+
|
|
88
209
|
outils.tool(
|
|
89
210
|
"create_database",
|
|
90
211
|
"Crée (ou récupère, idempotent) une base Clicbase pour un site. Renvoie la chaîne de connexion PostgreSQL, l'URL de l'API REST et les clés anon/service.",
|
|
@@ -113,7 +234,20 @@ outils.tool(
|
|
|
113
234
|
"Liste les tables du schéma public d'un projet.",
|
|
114
235
|
{ name: z.string() },
|
|
115
236
|
async ({ name }) => {
|
|
116
|
-
try {
|
|
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
|
+
}
|
|
117
251
|
},
|
|
118
252
|
);
|
|
119
253
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clicbase/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mcp": "clicbase-mcp.mjs",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"clicbase-mcp.mjs",
|
|
36
36
|
"clicbase-db-mcp.mjs",
|
|
37
37
|
"portee.mjs",
|
|
38
|
+
"savoir.mjs",
|
|
38
39
|
"README.md",
|
|
39
40
|
"LICENSE"
|
|
40
41
|
],
|
package/portee.mjs
CHANGED
|
@@ -43,6 +43,11 @@ export const OUTILS = {
|
|
|
43
43
|
enable_rls: "ecriture",
|
|
44
44
|
create_policy: "ecriture",
|
|
45
45
|
get_oauth_provider: "lecture",
|
|
46
|
+
// ⚠️ RENDU EN LECTURE SEULE, ET C'EST TOUT L'INTERET. Cet outil ne rend que
|
|
47
|
+
// du texte : les conventions de la plateforme. C'est precisement le mode
|
|
48
|
+
// restreint qui en a le plus besoin, puisqu'il ne reste alors que trois
|
|
49
|
+
// outils et qu'un assistant doit deviner le reste.
|
|
50
|
+
clicbase_conventions: "lecture",
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
/** Ce qu'on garde en lecture seule. */
|
package/savoir.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// CE QUE L'ASSISTANT DOIT SAVOIR, ET QU'AUCUN OUTIL NE LUI APPREND.
|
|
2
|
+
//
|
|
3
|
+
// Question de Teiki le 2026-09-17 : « quand un client ajoute la commande MCP à
|
|
4
|
+
// son IA, elle va tout comprendre ? » Mesuré dans le paquet publié, la réponse
|
|
5
|
+
// était non. En lecture seule, un assistant reçoit TROIS outils et une
|
|
6
|
+
// trentaine de mots de description. Il sait explorer un schéma. Il ignore tout
|
|
7
|
+
// ce qui fait perdre des journées ici.
|
|
8
|
+
//
|
|
9
|
+
// ⚠️ LES SIX PIÈGES CI-DESSOUS SONT RÉELS, PAS THÉORIQUES. Ils viennent de la
|
|
10
|
+
// section « Pièges qui ont déjà coûté cher » du dépôt, écrite après coup à
|
|
11
|
+
// chaque fois. Ils ont en commun de ne produire NI exception NI ligne de
|
|
12
|
+
// journal : une policy qui ne protège rien répond 200, un grant qui ne
|
|
13
|
+
// restreint pas répond 200, une clé service sans BYPASSRLS répond 200 avec un
|
|
14
|
+
// tableau vide. Un assistant qui les ignore croit avoir réussi.
|
|
15
|
+
//
|
|
16
|
+
// ⚠️ DEUX CANAUX, ET CE N'EST PAS UNE REDONDANCE. `INSTRUCTIONS` part dans le
|
|
17
|
+
// champ prévu par le protocole : le client MCP le pose dans le contexte du
|
|
18
|
+
// modèle À LA CONNEXION, avant le premier appel, et on ne peut donc pas
|
|
19
|
+
// oublier de le lire. `CONVENTIONS` est le détail, rendu par un outil quand
|
|
20
|
+
// l'assistant en a besoin. Tout mettre dans le premier gonflerait chaque
|
|
21
|
+
// conversation ; tout mettre dans le second laisserait l'assistant écrire une
|
|
22
|
+
// policy fausse sans avoir jamais su qu'il fallait demander.
|
|
23
|
+
//
|
|
24
|
+
// ⚠️ CE FICHIER N'IMPORTE RIEN, exprès : le lanceur de tests du dépôt charge
|
|
25
|
+
// les modules voisins avec leur extension, et un import casserait sa lecture.
|
|
26
|
+
|
|
27
|
+
/** Posé dans le contexte du modèle dès la connexion. Court par obligation. */
|
|
28
|
+
export const INSTRUCTIONS = `Tu es connecté à un projet Clicbase : une base PostgreSQL managée, exposée en API REST par PostgREST.
|
|
29
|
+
|
|
30
|
+
SIX PIÈGES QUI NE LÈVENT AUCUNE ERREUR. Ils répondent tous « 200 OK ».
|
|
31
|
+
1. \`with check\` n'est PAS \`using\`. Sur INSERT et UPDATE, PostgreSQL n'évalue QUE \`with check\`. Une policy qui n'a qu'un \`using\` ne protège pas l'écriture.
|
|
32
|
+
2. Chaque nouvelle table accorde le CRUD à \`authenticated\` par défaut. Un \`grant select\` ne restreint RIEN : il faut \`revoke\`.
|
|
33
|
+
3. Après tout changement de schéma, termine par \`notify pgrst, 'reload schema';\` sinon l'API continue de servir l'ancien schéma.
|
|
34
|
+
4. Le rôle \`service_role\` a besoin de \`BYPASSRLS\`. Sans lui : 200 OK et un tableau vide, sans explication.
|
|
35
|
+
5. Une fonction Edge renvoie \`{ status, body }\`, jamais \`new Response(...)\`.
|
|
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
|
+
|
|
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
|
+
|
|
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\`.
|
|
41
|
+
|
|
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.
|
|
43
|
+
|
|
44
|
+
Appelle \`clicbase_conventions\` avant d'écrire du SQL ou une policy : tu y trouveras les formes exactes.`;
|
|
45
|
+
|
|
46
|
+
/** Le détail, rendu à la demande par un outil de lecture. */
|
|
47
|
+
export const CONVENTIONS = `# Conventions Clicbase
|
|
48
|
+
|
|
49
|
+
## Row Level Security
|
|
50
|
+
|
|
51
|
+
\`\`\`sql
|
|
52
|
+
alter table articles enable row level security;
|
|
53
|
+
|
|
54
|
+
-- LECTURE : using suffit.
|
|
55
|
+
create policy "lecture_publique" on articles
|
|
56
|
+
for select to anon, authenticated using (status = 'published');
|
|
57
|
+
|
|
58
|
+
-- ÉCRITURE : with check est OBLIGATOIRE, using ne sert pas.
|
|
59
|
+
create policy "chacun_ses_lignes" on articles
|
|
60
|
+
for insert to authenticated with check (user_id = auth.uid());
|
|
61
|
+
|
|
62
|
+
create policy "modifier_les_siennes" on articles
|
|
63
|
+
for update to authenticated
|
|
64
|
+
using (user_id = auth.uid()) -- quelles lignes il peut viser
|
|
65
|
+
with check (user_id = auth.uid()); -- ce qu'il peut y écrire
|
|
66
|
+
|
|
67
|
+
notify pgrst, 'reload schema';
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
⚠️ Sur UPDATE, les deux clauses ont des rôles différents : \`using\` choisit les
|
|
71
|
+
lignes visées, \`with check\` valide le résultat. Omettre la seconde laisse
|
|
72
|
+
réécrire \`user_id\` vers quelqu'un d'autre.
|
|
73
|
+
|
|
74
|
+
## Les droits, avant la RLS
|
|
75
|
+
|
|
76
|
+
La RLS filtre les lignes ; les GRANT décident si la table est atteignable. Une
|
|
77
|
+
nouvelle table est ouverte en CRUD à \`authenticated\` par défaut, donc :
|
|
78
|
+
|
|
79
|
+
\`\`\`sql
|
|
80
|
+
revoke all on articles from anon, authenticated;
|
|
81
|
+
grant select on articles to anon, authenticated;
|
|
82
|
+
grant insert, update, delete on articles to authenticated;
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
Un \`grant select\` seul n'enlève rien : il ajoute. C'est \`revoke\` qui ferme.
|
|
86
|
+
|
|
87
|
+
## auth.uid()
|
|
88
|
+
|
|
89
|
+
Il rend le \`sub\` du JWT du projet, donc l'identifiant de l'utilisateur final
|
|
90
|
+
connecté. Il vaut NULL avec la clé \`anon\`. Une policy qui doit exiger un compte
|
|
91
|
+
écrit \`auth.uid() is not null\`, pas \`true\`.
|
|
92
|
+
|
|
93
|
+
## API REST
|
|
94
|
+
|
|
95
|
+
\`\`\`
|
|
96
|
+
GET /db/<slug>/articles?select=id,title&status=eq.published&order=created_at.desc
|
|
97
|
+
en-têtes : apikey: <clé> et Authorization: Bearer <clé>
|
|
98
|
+
\`\`\`
|
|
99
|
+
|
|
100
|
+
Opérateurs : \`eq.\`, \`neq.\`, \`gt.\`, \`gte.\`, \`lt.\`, \`lte.\`, \`like.\`, \`ilike.\`,
|
|
101
|
+
\`in.(a,b)\`, \`is.null\`. Les relations se tirent par \`select=*,auteur(*)\`.
|
|
102
|
+
|
|
103
|
+
## Après toute DDL
|
|
104
|
+
|
|
105
|
+
\`notify pgrst, 'reload schema';\` dans la même exécution. Sans cela, la table
|
|
106
|
+
existe en base et l'API répond 404 : le défaut ressemble à une erreur de nom.
|
|
107
|
+
|
|
108
|
+
## Fonctions Edge
|
|
109
|
+
|
|
110
|
+
\`\`\`js
|
|
111
|
+
export default async function (req) {
|
|
112
|
+
return { status: 200, body: { ok: true } };
|
|
113
|
+
}
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
Jamais \`new Response(...)\` : l'hôte attend un objet, pas une réponse HTTP.
|
|
117
|
+
|
|
118
|
+
## Ce qu'aucun outil ne fera pour toi
|
|
119
|
+
|
|
120
|
+
Distinguer un \`drop table\` prévu d'un \`drop table\` accidentel. Le DDL est la
|
|
121
|
+
raison d'être de \`run_sql\`, et aucune liste de mots interdits ne sépare les
|
|
122
|
+
deux. Avant un ordre destructeur, dis ce que tu vas faire et demande.`;
|