@gemmein/mcp 0.3.0 → 0.5.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.
package/dist/index.js CHANGED
@@ -291,7 +291,7 @@ const TOOLS = [
291
291
  },
292
292
  {
293
293
  name: "explain_relay",
294
- description: "Call while WRITING or FIXING gemmein/relays/<name>.json — before `gemmein sync` carries it to the cloud. A relay is one trigger (receiver: a provider's webhook; schedule: a clock; data_change: a record changing) and up to ten actions in Gemmein's own verbs (write_record, grant_access, revoke_access, email_person, call_url) — route, map, authorise, never compute. Pass the definition JSON; the answer is the English sentence the dashboard shows (\"When gocardless-paid receives an event where event_type is confirmed → grant Pro, email the person, call https://…\") or the ONE refusal sentence the cloud would answer, naming the field and the fix. Offline and read-only: nothing is created. Two checks run only in the cloud and are stated in the answer (the API's own hosts; the address's resolved network at call time).",
294
+ description: "Call while WRITING or FIXING gemmein/relays/<name>.json — before `gemmein sync` carries it to the cloud. A relay is one trigger (receiver: a provider's webhook; schedule: a clock; data_change: a record changing) and up to ten actions in Gemmein's own verbs (write_record, grant_access, revoke_access, grant_credits, email_person, call_url) — route, map, authorise, never compute. Pass the definition JSON; the answer is the English sentence the dashboard shows (\"When gocardless-paid receives an event where event_type is confirmed → grant Pro, email the person, call https://…\") or the ONE refusal sentence the cloud would answer, naming the field and the fix. Offline and read-only: nothing is created. Two checks run only in the cloud and are stated in the answer (the API's own hosts; the address's resolved network at call time).",
295
295
  annotations: { title: "Explain relay (validate offline)", readOnlyHint: true },
296
296
  inputSchema: {
297
297
  type: "object",
package/dist/relays.js CHANGED
@@ -34,9 +34,13 @@ const DURATION_RE = /^(\d{1,4})([hdw])$/;
34
34
  const MAX_MAP_ENTRIES = 20;
35
35
  const MAX_WHEN_ENTRIES = 20;
36
36
  const RECORD_KINDS = ["created", "updated", "deleted"];
37
- const ACTION_TYPES = ["write_record", "grant_access", "revoke_access", "email_person", "call_url"];
37
+ const ACTION_TYPES = ["write_record", "grant_access", "revoke_access", "email_person", "call_url", "grant_credits", "fulfil_product", "refund_product"];
38
+ /** W9.5 — mirrors apps/api/src/relays/schema.ts. */
39
+ const PRODUCT_NAME_RE = /^[a-z0-9][a-z0-9 _.-]{0,39}$/i;
40
+ // W9.3: a relay may grant a person up to 10,000 credits per action.
41
+ const MAX_RELAY_CREDITS = 10000;
38
42
  const SCHEMES = ["hmac_sha256_header", "stripe", "svix", "shared_token"];
39
- const PERSON_ACTIONS = new Set(["grant_access", "revoke_access", "email_person"]);
43
+ const PERSON_ACTIONS = new Set(["grant_access", "grant_credits", "revoke_access", "email_person", "fulfil_product", "refund_product"]);
40
44
  const ENTITLEMENT_KEY = /^access:[a-z0-9][a-z0-9._-]{0,63}$/;
41
45
  const RESERVED_FIELD_NAMES = new Set([
42
46
  "id", "appId", "app_id", "environmentId", "environment_id", "userId", "user_id", "ownerId", "ownerUserId", "owner_user_id",
@@ -296,6 +300,40 @@ function validateAction(raw, index, triggerKind) {
296
300
  const reason = optionalString(a, "reason", what, 200);
297
301
  return { type: "grant_access", entitlement, ...(expiresAt !== undefined ? { expiresAt } : {}), ...(reason !== undefined ? { reason } : {}) };
298
302
  }
303
+ case "grant_credits": {
304
+ // W9.3 — mirrors apps/api/src/relays/schema.ts (the lockstep test
305
+ // holds them equal): a whole number of credits, 1..10,000, and an
306
+ // optional reason the owner reads in the ledger.
307
+ rejectUnknownKeys(a, ["type", "amount", "reason"], what);
308
+ const amount = a.amount;
309
+ if (typeof amount !== "number" || !Number.isInteger(amount) || amount < 1 || amount > MAX_RELAY_CREDITS) {
310
+ refuse(`${what}.amount must be a whole number from 1 to ${MAX_RELAY_CREDITS.toLocaleString("en-GB")} credits`);
311
+ }
312
+ const reason = optionalString(a, "reason", what, 200);
313
+ return { type: "grant_credits", amount, ...(reason !== undefined ? { reason } : {}) };
314
+ }
315
+ case "fulfil_product":
316
+ case "refund_product": {
317
+ // W9.5 — mirrors apps/api/src/relays/schema.ts (the lockstep test holds
318
+ // them equal): the product is a NAME, never a template; the payment
319
+ // reference may be a template, because it comes out of the event.
320
+ rejectUnknownKeys(a, ["type", "product", "ref"], what);
321
+ const product = a.product;
322
+ if (typeof product !== "string" || !PRODUCT_NAME_RE.test(product.trim())) {
323
+ refuse(`${what}.product must be a product name (1-40 letters, numbers, spaces, or -_.)`);
324
+ }
325
+ const rawRef = a.ref;
326
+ // A written ref that is empty is not the absent ref: absent means "make
327
+ // one from the event id", and a blank string would silently take that
328
+ // road instead of the one the founder wrote. Refused here.
329
+ if (rawRef !== undefined &&
330
+ rawRef !== null &&
331
+ (typeof rawRef !== "string" || rawRef.trim() === "" || rawRef.length > 200)) {
332
+ refuse(`${what}.ref must be text of at most 200 characters (templates allowed)`);
333
+ }
334
+ const ref = typeof rawRef === "string" ? rawRef : undefined;
335
+ return { type: a.type, product: product.trim(), ...(ref !== undefined ? { ref } : {}) };
336
+ }
299
337
  case "revoke_access": {
300
338
  rejectUnknownKeys(a, ["type", "entitlement"], what);
301
339
  return { type: "revoke_access", entitlement: validateEntitlementInput(a.entitlement, what) };
@@ -385,6 +423,9 @@ export function describeRelay(def) {
385
423
  switch (a.type) {
386
424
  case "write_record": return `write a ${a.collection} record${a.to === "person" ? " for the person" : ""}`;
387
425
  case "grant_access": return `grant ${entitlementWords(a.entitlement)}`;
426
+ case "grant_credits": return `grant ${a.amount} credit${a.amount === 1 ? "" : "s"}`;
427
+ case "fulfil_product": return `fulfil ${a.product}`;
428
+ case "refund_product": return `refund ${a.product}`;
388
429
  case "revoke_access": return `revoke ${entitlementWords(a.entitlement)}`;
389
430
  case "email_person": return "email the person";
390
431
  case "call_url": return `call ${a.url}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemmein/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "mcpName": "com.gemmein/mcp",
5
5
  "description": "Gemmein MCP server \u2014 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.",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "prepublishOnly": "npm run build"
35
35
  },
36
36
  "dependencies": {
37
- "@gemmein/sdk": "^0.6.0",
37
+ "@gemmein/sdk": "^0.8.0",
38
38
  "@modelcontextprotocol/sdk": "^1.29.0"
39
39
  },
40
40
  "author": "Gemmein Limited",