@gemmein/mcp 0.2.0

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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/index.js +379 -0
  4. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gemmein Limited (Company No. 17339623, England and Wales)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @gemmein/mcp
2
+
3
+ The [Gemmein](https://gemmein.com) MCP server — gives your coding agent the
4
+ whole Gemmein contract as tools, straight in the editor.
5
+
6
+ Gemmein is the backend for AI-built web apps: passwordless auth, structured
7
+ storage with plain-English safety rules, and built-in Stripe subscription
8
+ handling. This server is **read-only**: it never creates, edits, or deletes
9
+ anything on the platform.
10
+
11
+ ## Setup
12
+
13
+ Claude Code:
14
+
15
+ ```sh
16
+ claude mcp add gemmein -- npx -y @gemmein/mcp
17
+ ```
18
+
19
+ Cursor / any MCP client (`mcpServers` config):
20
+
21
+ ```json
22
+ { "gemmein": { "command": "npx", "args": ["-y", "@gemmein/mcp"] } }
23
+ ```
24
+
25
+ ## Tools
26
+
27
+ - **`guide`** — the full builder's guide (auth flow, the seven collection
28
+ safety rules, record shapes, links, uploads, contention patterns, payments).
29
+ - **`reference`** — the exact SDK API reference: every method, signature,
30
+ return shape, error code.
31
+ - **`search_docs`** — targeted search over both documents.
32
+ - **`explain_rule`** — any safety rule's contract, what it's right for, and
33
+ the mistakes to avoid (or a cheat-sheet of all seven).
34
+ - **`explain_error`** — what a `GemmeinError` code means and exactly what to do.
35
+ - **`validate_collection_name`** — catch a bad collection name at planning
36
+ time (a bad name throws synchronously and can blank an app silently).
37
+ - **`reaffirm_template`** — the ready-to-edit CI harness that proves an app's
38
+ boundaries on every deploy.
39
+ - **`check_integration`** — run those boundary checks live against your own
40
+ app right now: anonymous access refused where it must be, cross-user
41
+ isolation proven with throwaway dev test sessions, structured pass/fail
42
+ back. Never pass a live secret key — `sk_live` is refused by design.
43
+
44
+ ## The companion SDK
45
+
46
+ Your app talks to Gemmein through [`@gemmein/sdk`](https://www.npmjs.com/package/@gemmein/sdk).
47
+ This server teaches your agent to use it correctly.
48
+
49
+ MIT © Gemmein Limited
package/dist/index.js ADDED
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+ // @gemmein/mcp — the Gemmein MCP server (phase 1: read-only).
3
+ //
4
+ // Gives a coding agent the whole Gemmein contract as tools: the guide
5
+ // (llms.txt), the API reference (REFERENCE.md), targeted search over both,
6
+ // deterministic explainers for safety rules and error codes, the reaffirm
7
+ // harness template, and `check_integration` — the reaffirm boundary checks
8
+ // run live against the caller's own app.
9
+ //
10
+ // Phase-1 law: NOTHING here creates, mutates, or deletes platform state.
11
+ // The only writes anywhere are check_integration's Tier-B probe records in
12
+ // the caller's own DEV environment (created and deleted by the check, the
13
+ // same as reaffirm.mjs in CI). Provisioning (create app / mint keys) is
14
+ // phase 2, gated behind launch signup unlock.
15
+ //
16
+ // The guide/reference/template are read from the installed @gemmein/sdk
17
+ // package — one source of truth, no copies to drift.
18
+ import { readFileSync } from "node:fs";
19
+ import { createRequire } from "node:module";
20
+ import { dirname, join } from "node:path";
21
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
24
+ import { gemmein, gemmeinServer } from "@gemmein/sdk";
25
+ const require = createRequire(import.meta.url);
26
+ function sdkFile(name) {
27
+ try {
28
+ return readFileSync(require.resolve(`@gemmein/sdk/${name}`), "utf8");
29
+ }
30
+ catch {
31
+ // sdk 0.1.0 shipped the files in its tarball but without subpath
32
+ // exports, so Node refuses the pretty specifier. Resolve the entry
33
+ // point (dist/index.js) and read from the package root instead.
34
+ return readFileSync(join(dirname(require.resolve("@gemmein/sdk")), "..", name), "utf8");
35
+ }
36
+ }
37
+ // Same law as the SDK's assertCollectionName — kept in lockstep by test.
38
+ const COLLECTION_NAME_RE = /^[a-z][a-z0-9_]{1,62}$/;
39
+ // ── Safety rules (distilled from llms.txt; the guide stays the source of
40
+ // truth for prose — these are the decision-shaped versions) ─────────────
41
+ const RULES = {
42
+ private: {
43
+ contract: "Each signed-in user sees and edits ONLY their own records. Anonymous access is refused (denied). The app owner (signed in with their dashboard email) can read everyone's records — that is how admin views work, read-only.",
44
+ rightFor: "notes, tasks, saved games — anything personal to one user.",
45
+ cautions: "No expand/links here — join in memory. Touching someone else's record returns 404 not_found (existence is never leaked).",
46
+ },
47
+ shared: {
48
+ contract: "Every signed-in user can read AND write every record. Anonymous access is refused.",
49
+ rightFor: "a team board every user edits together.",
50
+ cautions: "WRONG for personal data — it leaks to every user. Render other users' content as text (never innerHTML). Use { ifVersion } on updates to avoid silently clobbering concurrent edits.",
51
+ },
52
+ admin_write: {
53
+ contract: "Everyone signed in can read; ONLY the app owner writes. One record that all users read.",
54
+ rightFor: "announcements, app settings, feature flags your human curates.",
55
+ cautions: "Non-owner writes get 403 forbidden — never retry a forbidden. If each user should get their OWN copy, that is `addressed`, not admin_write.",
56
+ },
57
+ public_read: {
58
+ contract: "Readable WITHOUT signing in; only the app owner writes. Strangers can never inject records.",
59
+ rightFor: "catalogs, menus, single-author blogs.",
60
+ cautions: "Everything in it is public — no secrets, ever. Drafts: create with option { published: false }, publish with update(id, {}, { published: true }). No expand/links.",
61
+ },
62
+ community: {
63
+ contract: "Readable without signing in; any signed-in user creates and edits their OWN records. The public multi-author surface.",
64
+ rightFor: "multi-author blogs, public boards, user profiles.",
65
+ cautions: "Everything is PUBLIC — keep record data minimal. Plain text only: HTML in string fields is refused (400 html_not_allowed). Links learn + expand here. One-record-per-user (profiles) = keyed create: create(data, { key: 'profile:' + user.userId }). Drafts supported.",
66
+ },
67
+ addressed: {
68
+ contract: "The app sends to one user: the OWNER creates records naming a recipient — create(data, { for: userId }). Each user's .list() returns only records addressed to them (their inbox). Users never write.",
69
+ rightFor: "notifications, order status, invoices, results, purchase receipts.",
70
+ cautions: "Recipient is server-stamped (record.audienceUserId), never a data field. Same message for everyone = admin_write instead. Plain text only. 400 invalid_audience = the recipient isn't a user of this app.",
71
+ },
72
+ direct: {
73
+ contract: "Users send to each other: any signed-in user creates records naming a recipient; only the author and that recipient can read them. The app owner can read directs too (owner reads reach everything).",
74
+ rightFor: "messages, sharing, requests between users.",
75
+ cautions: "NEVER present as private or encrypted chat (the owner can read it). Plain text only. 403 reply_only = this collection only allows replying to someone who wrote first; 403 sends_disabled = the owner turned in-app sends off.",
76
+ },
77
+ };
78
+ const RULES_FOOTER = "Cross-cutting law: collections are created by the app owner in their dashboard, never by the SDK. " +
79
+ "There is NO team/group/workspace scope and no per-user visibility inside a rule — if the app needs that shape, stop and tell your human it isn't supported yet (never approximate it by client-side filtering a shared collection). " +
80
+ "Record fields always live under record.data; ownerUserId/version/published are server-derived and top-level.";
81
+ // invalid_collection_name is thrown client-side by the SDK, so it is not in
82
+ // the REFERENCE server-error table — appended here.
83
+ const EXTRA_ERRORS = {
84
+ invalid_collection_name: {
85
+ meaning: "the collection name breaks the naming law (lowercase letters, numbers, underscores; must start with a letter; 2-63 chars). Thrown synchronously by g.collection(name), before any network call.",
86
+ fix: "rename the collection (e.g. saved_games, never savedGames) — validate with the validate_collection_name tool",
87
+ },
88
+ };
89
+ function parseErrorTable() {
90
+ const out = {};
91
+ for (const line of sdkFile("REFERENCE.md").split("\n")) {
92
+ const m = line.match(/^\|\s*`([a-z_ /`]+?)`?\s*(?:\/\s*`([a-z_]+)`)?\s*\|(.+)\|(.+)\|\s*$/);
93
+ if (!m)
94
+ continue;
95
+ const cells = line.split("|").map((c) => c.trim());
96
+ // cells[1] = code cell, cells[2] = meaning, cells[3] = do
97
+ const codes = (cells[1].match(/`([a-z_]+)`/g) ?? []).map((c) => c.slice(1, -1));
98
+ for (const code of codes) {
99
+ out[code] = { meaning: cells[2], fix: cells[3].replace(/\*\*/g, "") };
100
+ }
101
+ }
102
+ return { ...out, ...EXTRA_ERRORS };
103
+ }
104
+ // ── search over the two docs ───────────────────────────────────────────────
105
+ function searchDocs(query) {
106
+ const q = query.toLowerCase().trim();
107
+ const terms = q.split(/\s+/).filter(Boolean);
108
+ if (!terms.length)
109
+ return "Empty query.";
110
+ const files = [
111
+ ["llms.txt (the guide)", sdkFile("llms.txt")],
112
+ ["REFERENCE.md (the API reference)", sdkFile("REFERENCE.md")],
113
+ ];
114
+ const blocks = [];
115
+ for (const [label, text] of files) {
116
+ const lines = text.split("\n");
117
+ const hits = [];
118
+ lines.forEach((line, i) => {
119
+ const l = line.toLowerCase();
120
+ if (l.includes(q) || terms.every((t) => l.includes(t)))
121
+ hits.push(i);
122
+ });
123
+ // merge hits into windows of ±3 lines
124
+ let win = null;
125
+ const windows = [];
126
+ for (const h of hits) {
127
+ const lo = Math.max(0, h - 3);
128
+ const hi = Math.min(lines.length - 1, h + 3);
129
+ if (win && lo <= win[1] + 1)
130
+ win[1] = hi;
131
+ else
132
+ windows.push((win = [lo, hi]));
133
+ }
134
+ for (const [lo, hi] of windows.slice(0, 6)) {
135
+ blocks.push(`── ${label}, lines ${lo + 1}-${hi + 1} ──\n` + lines.slice(lo, hi + 1).join("\n"));
136
+ }
137
+ if (windows.length > 6)
138
+ blocks.push(`… ${windows.length - 6} more match block(s) in ${label} — narrow the query.`);
139
+ }
140
+ return blocks.length
141
+ ? blocks.join("\n\n")
142
+ : `No matches for "${query}". Try a term from the contract vocabulary (rule names, error codes, method names) — or read the full guide/reference tools.`;
143
+ }
144
+ async function runIntegrationChecks(input) {
145
+ const checks = [];
146
+ const notes = [];
147
+ const opts = input.apiUrl ? { apiUrl: input.apiUrl } : {};
148
+ const push = (label, pass, detail) => checks.push({ label, pass, ...(detail ? { detail } : {}) });
149
+ const refuse = async (label, code, fn) => {
150
+ try {
151
+ await fn();
152
+ push(label, false, `expected error "${code}" but the call succeeded — the boundary is OPEN`);
153
+ }
154
+ catch (e) {
155
+ const got = e.code ?? e.message;
156
+ push(label, got === code, got === code ? undefined : `expected "${code}", got "${got}"`);
157
+ }
158
+ };
159
+ const g = gemmein(input.publicKey, opts);
160
+ // ── Tier A — anonymous + shape. Safe against any environment, live included.
161
+ try {
162
+ g.collection(input.privateCollection);
163
+ push(`collection name "${input.privateCollection}" is valid`, true);
164
+ }
165
+ catch (e) {
166
+ push(`collection name "${input.privateCollection}" is valid`, false, e.message);
167
+ return { checks, notes }; // nothing downstream can run
168
+ }
169
+ await refuse(`anonymous CANNOT read the "${input.privateCollection}" collection`, "denied", () => g.collection(input.privateCollection).list());
170
+ await refuse(`anonymous CANNOT write the "${input.privateCollection}" collection`, "denied", () => g.collection(input.privateCollection).create({ probe: "x" }));
171
+ if (input.publicCollection) {
172
+ try {
173
+ const open = await g.collection(input.publicCollection).list();
174
+ push(`"${input.publicCollection}" is anonymously readable (as its rule intends)`, true);
175
+ notes.push(`"${input.publicCollection}" is public by rule — ${open.records.length} record(s) visible to ANYONE. Never put secrets or personal data in it.`);
176
+ }
177
+ catch (e) {
178
+ push(`"${input.publicCollection}" is anonymously readable (as its rule intends)`, false, `got "${e.code ?? e.message}" — is its rule really community/public_read?`);
179
+ }
180
+ }
181
+ // ── Tier B — cross-user isolation. Dev environments only, by design.
182
+ const sk = input.secretKey;
183
+ if (!sk) {
184
+ notes.push("Tier B (cross-user isolation) skipped — pass secretKey (sk_dev) to prove one user can't read another's private records. Dev and live enforce the same rules, so isolation proven in dev holds in live.");
185
+ return { checks, notes };
186
+ }
187
+ if (sk.startsWith("sk_live")) {
188
+ notes.push("Tier B skipped — sk_live can never mint test sessions (by design; never point test tooling at live user data). Use the sk_dev key.");
189
+ return { checks, notes };
190
+ }
191
+ const users = input.testUsers?.length === 2 ? input.testUsers : ["reaffirm-a@test.dev", "reaffirm-b@test.dev"];
192
+ const srv = gemmeinServer(sk, opts);
193
+ const [a, b] = await Promise.all(users.map((e) => srv.testSession(e)));
194
+ const asUser = (token) => gemmein(input.publicKey, {
195
+ ...opts,
196
+ tokenStore: { get: async () => token, set: async () => { }, clear: async () => { } },
197
+ });
198
+ const A = asUser(a.token);
199
+ const B = asUser(b.token);
200
+ const note = await A.collection(input.privateCollection).create({ probe: "a-secret" });
201
+ try {
202
+ await refuse(`user B CANNOT read user A's private record (404-shaped, existence not leaked)`, "not_found", () => B.collection(input.privateCollection).get(note.id));
203
+ const bSees = await B.collection(input.privateCollection).list();
204
+ push("user B's private list contains none of user A's records", !bSees.records.some((r) => r.id === note.id), bSees.records.some((r) => r.id === note.id) ? "ISOLATION BREACH — B's list contains A's record" : undefined);
205
+ const who = await A.auth.currentUser();
206
+ push("currentUser() exposes userId (not id) — the shape your UI must read", !!who.userId);
207
+ push("record fields live under .data", note.data?.probe === "a-secret");
208
+ }
209
+ finally {
210
+ try {
211
+ await A.collection(input.privateCollection).delete(note.id);
212
+ }
213
+ catch { /* leave nothing behind on a best-effort basis */ }
214
+ }
215
+ return { checks, notes };
216
+ }
217
+ // ── the MCP server ─────────────────────────────────────────────────────────
218
+ const TOOLS = [
219
+ {
220
+ name: "guide",
221
+ description: "The Gemmein guide (llms.txt): what the platform is, the full contract an AI builder follows — auth flow, the seven collection safety rules, record shapes, links/expand, uploads, contention patterns, payments (g.subscriptions.checkout/g.payments.buy), drafts, error philosophy, pricing. Read this FIRST when building on Gemmein.",
222
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
223
+ },
224
+ {
225
+ name: "reference",
226
+ description: "The Gemmein SDK API reference (REFERENCE.md): every method, exact signature, return shape, and the stable error-code table. Use when you need a precise signature or shape; use `guide` for how the model works.",
227
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
228
+ },
229
+ {
230
+ name: "search_docs",
231
+ description: "Search the Gemmein guide and API reference for a term or phrase (e.g. 'keyed create', 'ifVersion', 'addressed', 'expand'). Returns matching passages with a few lines of context. Cheaper than reading both documents when you need one fact.",
232
+ inputSchema: {
233
+ type: "object",
234
+ properties: { query: { type: "string", description: "term or phrase to find" } },
235
+ required: ["query"],
236
+ additionalProperties: false,
237
+ },
238
+ },
239
+ {
240
+ name: "explain_rule",
241
+ description: "Explain one of Gemmein's seven collection safety rules (private, shared, admin_write, public_read, community, addressed, direct): the exact access contract, what it's right for, and the mistakes to avoid. Call with no rule to get the one-line summary of all seven (rule choice cheat-sheet).",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ rule: {
246
+ type: "string",
247
+ enum: Object.keys(RULES),
248
+ description: "the rule to explain; omit for the all-rules cheat-sheet",
249
+ },
250
+ },
251
+ additionalProperties: false,
252
+ },
253
+ },
254
+ {
255
+ name: "explain_error",
256
+ description: "Explain a GemmeinError code (e.g. conflict, forbidden, unknown_collection, invalid_shape, html_not_allowed): what it means and exactly what to do. Sourced live from the API reference. Call with no code to list every stable code.",
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: { code: { type: "string", description: "the err.code to explain; omit to list all" } },
260
+ additionalProperties: false,
261
+ },
262
+ },
263
+ {
264
+ name: "validate_collection_name",
265
+ description: "Check a collection name against Gemmein's naming law (lowercase letters, numbers, underscores; starts with a letter). A bad name throws synchronously in the SDK and can blank a whole app with no console error — validate names at planning time.",
266
+ inputSchema: {
267
+ type: "object",
268
+ properties: { name: { type: "string" } },
269
+ required: ["name"],
270
+ additionalProperties: false,
271
+ },
272
+ },
273
+ {
274
+ name: "reaffirm_template",
275
+ description: "The ready-to-edit reaffirm.mjs CI harness that proves an app's boundaries against live Gemmein on every deploy (also shipped inside the @gemmein/sdk npm package). Copy it next to the app, set the CONFIG block, run it in CI. For an immediate one-off check, use check_integration instead.",
276
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
277
+ },
278
+ {
279
+ name: "check_integration",
280
+ description: "Run Gemmein's reaffirm boundary checks against the caller's own app, live, and return structured pass/fail results. Tier A (public key only): anonymous access to a private collection is refused, collection names are valid, a public collection reads as intended — safe against any environment. Tier B (add the sk_dev secret key): proves cross-user isolation with two throwaway test sessions in the DEV environment (sk_live is refused by design). The only writes are Tier B's own probe records in dev, deleted afterwards. NEVER pass an sk_live key to any tool.",
281
+ inputSchema: {
282
+ type: "object",
283
+ properties: {
284
+ publicKey: { type: "string", description: "the app's public pk_ key" },
285
+ privateCollection: { type: "string", description: "a collection with the `private` rule" },
286
+ publicCollection: { type: "string", description: "optional: a community/public_read collection to confirm anonymous readability" },
287
+ secretKey: { type: "string", description: "optional: the sk_dev secret key — enables Tier B isolation proof (sk_live is refused)" },
288
+ apiUrl: { type: "string", description: "optional: API base URL override (local/dev API); omit for production Gemmein" },
289
+ testUsers: { type: "array", items: { type: "string" }, minItems: 2, maxItems: 2, description: "optional: the two Tier-B test emails (default reaffirm-a/b@test.dev)" },
290
+ timeoutMs: { type: "number", description: "overall time budget, default 30000" },
291
+ },
292
+ required: ["publicKey", "privateCollection"],
293
+ additionalProperties: false,
294
+ },
295
+ },
296
+ ];
297
+ const server = new Server({ name: "gemmein", version: "0.1.0" }, { capabilities: { tools: {} } });
298
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
299
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
300
+ const errText = (t) => ({ content: [{ type: "text", text: t }], isError: true });
301
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
302
+ const { name, arguments: args = {} } = req.params;
303
+ try {
304
+ switch (name) {
305
+ case "guide":
306
+ return text(sdkFile("llms.txt"));
307
+ case "reference":
308
+ return text(sdkFile("REFERENCE.md"));
309
+ case "search_docs":
310
+ return text(searchDocs(String(args.query ?? "")));
311
+ case "explain_rule": {
312
+ const rule = args.rule;
313
+ if (!rule) {
314
+ const sheet = Object.entries(RULES)
315
+ .map(([r, d]) => `- \`${r}\` — ${d.contract.split(".")[0]}. Right for: ${d.rightFor}`)
316
+ .join("\n");
317
+ return text(`Gemmein's seven collection safety rules (each collection has exactly one):\n\n${sheet}\n\n${RULES_FOOTER}`);
318
+ }
319
+ const d = RULES[rule];
320
+ if (!d)
321
+ return errText(`Unknown rule "${rule}". The seven rules: ${Object.keys(RULES).join(", ")}.`);
322
+ return text(`## \`${rule}\`\n\n**Contract:** ${d.contract}\n\n**Right for:** ${d.rightFor}\n\n**Cautions:** ${d.cautions}\n\n${RULES_FOOTER}`);
323
+ }
324
+ case "explain_error": {
325
+ const table = parseErrorTable();
326
+ const code = args.code;
327
+ if (!code) {
328
+ return text("Stable GemmeinError codes (branch on err.code, render err.message):\n\n" +
329
+ Object.entries(table).map(([c, e]) => `- \`${c}\` — ${e.meaning}`).join("\n"));
330
+ }
331
+ const e = table[code];
332
+ if (!e)
333
+ return errText(`"${code}" is not a stable Gemmein error code. Known codes: ${Object.keys(table).join(", ")}.`);
334
+ return text(`## \`${code}\`\n\n**Meaning:** ${e.meaning}\n\n**What to do:** ${e.fix}`);
335
+ }
336
+ case "validate_collection_name": {
337
+ const n = String(args.name ?? "");
338
+ if (COLLECTION_NAME_RE.test(n))
339
+ return text(`"${n}" is a valid collection name.`);
340
+ const suggestion = n.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^[^a-z]+/, "").slice(0, 63);
341
+ return text(`"${n}" is INVALID — collection names are lowercase letters, numbers, and underscores, starting with a letter (2-63 chars). ` +
342
+ (suggestion && COLLECTION_NAME_RE.test(suggestion) ? `Suggested: "${suggestion}". ` : "") +
343
+ "A bad name throws synchronously from g.collection(name) — at module load it can blank the whole app with no console error.");
344
+ }
345
+ case "reaffirm_template":
346
+ return text(sdkFile("reaffirm.mjs"));
347
+ case "check_integration": {
348
+ const input = args;
349
+ if (typeof input.publicKey !== "string" || !input.publicKey.startsWith("pk_")) {
350
+ return errText("publicKey must be the app's public pk_ key (never an sk_ secret).");
351
+ }
352
+ const timeoutMs = input.timeoutMs ?? 30000;
353
+ const run = runIntegrationChecks(input);
354
+ const timeout = new Promise((_, rej) => setTimeout(() => rej(new Error(`check_integration timed out after ${timeoutMs}ms — is the API reachable?`)), timeoutMs).unref?.());
355
+ const { checks, notes } = await Promise.race([run, timeout]);
356
+ const failed = checks.filter((c) => !c.pass);
357
+ const lines = checks.map((c) => `${c.pass ? "✓" : "✗"} ${c.label}${c.detail ? ` — ${c.detail}` : ""}`);
358
+ const verdict = failed.length
359
+ ? `${failed.length} boundary check(s) FAILED — treat this as drift between the app's assumptions and its rules; fix before shipping.`
360
+ : "All boundaries reaffirmed.";
361
+ return {
362
+ content: [{
363
+ type: "text",
364
+ text: [lines.join("\n"), notes.map((n) => `ℹ ${n}`).join("\n"), verdict].filter(Boolean).join("\n\n"),
365
+ }],
366
+ structuredContent: { checks, notes, failedCount: failed.length, passed: failed.length === 0 },
367
+ ...(failed.length ? { isError: true } : {}),
368
+ };
369
+ }
370
+ default:
371
+ return errText(`Unknown tool "${name}".`);
372
+ }
373
+ }
374
+ catch (e) {
375
+ const err = e;
376
+ return errText(`${name} failed: ${err.code ? `[${err.code}] ` : ""}${err.message ?? String(e)}`);
377
+ }
378
+ });
379
+ await server.connect(new StdioServerTransport());
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@gemmein/mcp",
3
+ "version": "0.2.0",
4
+ "description": "Gemmein MCP server — gives coding agents the Gemmein guide, API reference, rule/error explainers, and a live integration check (reaffirm) as tools. Read-only: it never creates, edits, or deletes anything.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "gemmein-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "model-context-protocol",
19
+ "auth",
20
+ "backend",
21
+ "baas",
22
+ "ai",
23
+ "vibe-coding",
24
+ "gemmein"
25
+ ],
26
+ "homepage": "https://gemmein.com",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "scripts": {
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/index.js",
32
+ "prepack": "npm run build",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "@gemmein/sdk": "^0.2.0",
37
+ "@modelcontextprotocol/sdk": "^1.29.0"
38
+ },
39
+ "author": "Gemmein Limited",
40
+ "bugs": {
41
+ "email": "hello@gemmein.com"
42
+ }
43
+ }