@genlobe/mcp-server 3.7.1 → 3.8.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 (2) hide show
  1. package/dist/index.js +370 -23
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3731,18 +3731,44 @@ End-to-end recipe to wire a chatbot agent that answers from a knowledge base
3731
3731
  populated by a snapshot of your custom-entity catalog (typical "store
3732
3732
  assistant" / "product Q&A" / "support deflection" use case).
3733
3733
 
3734
- ## Auth context clarification (frequent confusion)
3734
+ ## Public chatbots: use service-mode (ADR-0013) — the simple path
3735
3735
 
3736
- The end-user chat endpoint \`POST /v1/user/agents/{agent_id}/chat\` requires
3737
- an end-user JWT. **There are two legitimate ways to obtain that JWT**, and a
3738
- public-facing bot needs option (b):
3736
+ For a **public chatbot** (anonymous visitors, no human login), call the
3737
+ service-mode endpoint with your \`sk_live_*\` ONLY no end-user JWT, no bot
3738
+ user, no email verification:
3739
+
3740
+ \`\`\`http
3741
+ POST /v1/server/agents/{agent_id}/chat
3742
+ X-API-Key: <your sk_live_*> ← server-side only
3743
+ X-Organization-Id: <org_id> (or organization_id in the body)
3744
+ \`\`\`
3745
+
3746
+ Body:
3747
+ \`\`\`json
3748
+ { "messages": [{"role":"user","content":"do you have rice?"}], "session_id": "anon-visitor-abc" }
3749
+ \`\`\`
3750
+
3751
+ Usage is billed to the Tenant + Organization; \`user_id\` on the execution is
3752
+ NULL (no human). \`session_id\` (optional) groups one anonymous conversation.
3753
+ This is the **recommended path** for storefront/support bots — it removes the
3754
+ old bot-user + email-verification wall entirely.
3755
+
3756
+ Your Next.js \`/api/chat\` route handler calls this server-side; the browser
3757
+ only talks to \`/api/chat\`, never to Genlobe directly.
3758
+
3759
+ ## Per-user chat (when there IS a logged-in human)
3760
+
3761
+ If the chat belongs to an authenticated end-user (e.g. an in-app assistant),
3762
+ use \`POST /v1/user/agents/{agent_id}/chat\` with that user's JWT instead — it
3763
+ attributes usage per user and applies their plan limits. Two ways to get the
3764
+ JWT:
3739
3765
 
3740
3766
  - **(a) Real human end-user**: customer signs up via \`POST /v1/auth/register\`,
3741
3767
  logs in, and chats with the agent. The JWT is theirs.
3742
- - **(b) Bot service account**: register **one real bot user** for the chat
3743
- surface (use a real email you control, e.g. \`bot@yourshop.com\`), keep its
3744
- credentials in your server env, log in server-side, attach the resulting
3745
- JWT to incoming chat requests. This is what "register a bot user" means.
3768
+ - **(b) Bot service account (legacy fallback)**: register **one real bot user**
3769
+ (real email you control), log in server-side, attach the JWT. Only needed if
3770
+ you specifically want per-"user" attribution for the bot. For public bots,
3771
+ prefer service-mode above it is simpler.
3746
3772
 
3747
3773
  **What is NOT allowed** (and what some agents mis-read as "no LLM bot is
3748
3774
  possible"): inventing/synthesizing fake email addresses to bulk-create
@@ -3773,6 +3799,43 @@ LLM layer. The bot user path is still the canonical way to get a real LLM bot.
3773
3799
 
3774
3800
  ## Step-by-step
3775
3801
 
3802
+ ### 0. Register the bot service-account user (only once per project)
3803
+
3804
+ The chat endpoint requires an end-user JWT. Register **one** real user that
3805
+ the server uses to obtain that JWT — this is not the same as inventing fake
3806
+ emails for bulk signups (which is what the security guide prohibits). Use a
3807
+ mailbox you actually own.
3808
+
3809
+ \`\`\`http
3810
+ POST /v1/auth/register
3811
+ Content-Type: application/json
3812
+ X-API-Key: <your pk_live_* ← register works with the public key>
3813
+ \`\`\`
3814
+
3815
+ Body:
3816
+ \`\`\`json
3817
+ {
3818
+ "email": "bot@yourshop.com",
3819
+ "password": "<32-char random string, store it once and never again in plain text>",
3820
+ "display_name": "Store Bot"
3821
+ }
3822
+ \`\`\`
3823
+
3824
+ Save the email and the password in your server-side env (\`.env.local\`):
3825
+
3826
+ \`\`\`
3827
+ BOT_USER_EMAIL=bot@yourshop.com
3828
+ BOT_USER_PASSWORD=<that random string>
3829
+ \`\`\`
3830
+
3831
+ From then on the server logs in this user on demand and caches the JWT for
3832
+ its lifetime. **Never** ship \`BOT_USER_PASSWORD\` to the browser. See the
3833
+ "Server-side TypeScript snippet" further down for the cached login helper.
3834
+
3835
+ If your project does not allow public chat (the bot is internal-only and
3836
+ every conversation already belongs to a logged-in human), skip this step
3837
+ and pass each user's own JWT to the chat endpoint.
3838
+
3776
3839
  ### 1. Create the KnowledgeBase
3777
3840
 
3778
3841
  \`\`\`http
@@ -3926,11 +3989,28 @@ export async function refreshCatalogKB(productSchemaId: string, kbId: string, ex
3926
3989
  });
3927
3990
  }
3928
3991
 
3992
+ // Cached login for the bot service-account user. The JWT is reused until it
3993
+ // nears expiry; on next call we refresh server-side.
3994
+ let cachedBotJwt: { token: string; expiresAt: number } | null = null;
3995
+
3996
+ export async function getBotJwt(): Promise<string> {
3997
+ if (cachedBotJwt && cachedBotJwt.expiresAt > Date.now() + 60_000) {
3998
+ return cachedBotJwt.token;
3999
+ }
4000
+ const email = process.env.BOT_USER_EMAIL!;
4001
+ const password = process.env.BOT_USER_PASSWORD!;
4002
+ const r = await api('/v1/auth/login', { email, password });
4003
+ const ttl = (r.expires_in ?? 3600) * 1000;
4004
+ cachedBotJwt = { token: r.access_token, expiresAt: Date.now() + ttl };
4005
+ return r.access_token;
4006
+ }
4007
+
3929
4008
  export async function chatWithAgent(agentId: string, message: string, conversationId?: string) {
4009
+ const jwt = await getBotJwt();
3930
4010
  return api(\`/v1/user/agents/\${agentId}/chat\`, {
3931
4011
  message,
3932
4012
  ...(conversationId ? { conversation_id: conversationId } : {}),
3933
- });
4013
+ }, jwt);
3934
4014
  }
3935
4015
  \`\`\`
3936
4016
 
@@ -3938,12 +4018,19 @@ export async function chatWithAgent(agentId: string, message: string, conversati
3938
4018
 
3939
4019
  - \`POST /v1/agents\` is the Tenant Dashboard endpoint — when configuring from
3940
4020
  your scaffolding script, use \`sk_live_*\`. End-user chat (\`/v1/user/agents/{id}/chat\`)
3941
- is reached with \`pk_live_*\` + the end-user's JWT.
4021
+ is reached with \`pk_live_*\` (or \`sk_live_*\` on the server) + the end-user's
4022
+ JWT (or the bot's, if you registered one in Step 0).
3942
4023
  - Tool calling vs RAG: for MVP / low-frequency catalog updates, RAG via KB is
3943
4024
  simpler. Switch to tool calling when the catalog updates faster than the
3944
4025
  refresh cadence you can sustain.
3945
4026
  - The bot does not write to the catalog and does not take orders unless you
3946
- add a separate tool / API path. Keep the system prompt strict.`;
4027
+ add a separate tool / API path. Keep the system prompt strict.
4028
+ - **Fallback for MVP without an LLM** — if the project explicitly cannot
4029
+ register a bot user (no email domain available yet), a deterministic
4030
+ keyword-search bot over the catalog is acceptable. Implement \`/api/chat\` as
4031
+ a server-side \`POST /v1/entity/records/search\` with the user's query as an
4032
+ \`ilike\` filter. Not equivalent to the LLM bot but ships a working surface;
4033
+ swap it for the LLM later.`;
3947
4034
  }
3948
4035
  const APP_SCAFFOLDS = {
3949
4036
  pos: {
@@ -3986,18 +4073,23 @@ const APP_SCAFFOLDS = {
3986
4073
  },
3987
4074
  ],
3988
4075
  build_order: [
3989
- "1. Next.js 16 app router + Tailwind + shadcn. Add `lib/genlobe.server.ts` with `import 'server-only'`.",
3990
- "2. Bootstrap script: create `product`, `customer`, `order` schemas via `get_entity_schema_recipe`. Bulk-seed 5-10 sample products.",
3991
- "3. Owner login: `/admin/login` POST /v1/auth/login server-side cookie httpOnly with JWT.",
3992
- "4. Owner CRUD products (single end-to-end vertical slice first).",
3993
- "5. Sales form (`/admin/sales/new`): customer autocomplete, multi-product line items, total auto-calc, POST one `order` record.",
3994
- "6. Sales list + detail.",
3995
- "7. Customers list + per-customer purchase history (cross-schema join from order customer).",
3996
- "8. Dashboard KPIs (search + sum client-side; switch to server-side aggregates later if data grows).",
3997
- "9. Public storefront `/` + `/chat`. Bot via `get_chatbot_setup_recipe`.",
3998
- "10. Catalog refresh button in `/admin` (regenerates the KB document).",
4076
+ "**0. Pre-flight.** Call `validate_credentials` to confirm sk_live_* and the org context. Restart any long-running uvicorn on the Genlobe backend if it predates today — the bulk endpoint is only available on backends started after the v3.6.2 merge.",
4077
+ "**1. Scaffold a Next.js 16 app router project under `vendy-app/` (or similar).** Tailwind + shadcn. `pnpm dev -p 3100` so it doesn't collide with the Genlobe Tenant UI on 3000. Verify `.gitignore` lists `.env*.local`, `.env`, `node_modules`, `.next`.",
4078
+ "**2. Create `lib/genlobe.server.ts` with `import 'server-only';` at the top.** Wrap fetch with X-API-Key + X-Organization-Id + optional JWT — see Section 10 of `get_authentication_flow`.",
4079
+ "**3. `.env.local`**: SAAS_API_URL, SAAS_API_KEY (sk_live_*), SAAS_ORGANIZATION_ID, BOT_USER_EMAIL, BOT_USER_PASSWORD, STORE_NAME, STORE_CURRENCY. Once schemas are created in step 5, append PRODUCT_SCHEMA_ID, CUSTOMER_SCHEMA_ID, ORDER_SCHEMA_ID.",
4080
+ "**4. Bot service-account user (Step 0 of `get_chatbot_setup_recipe`).** Register `bot@<your-shop>` via `POST /v1/auth/register`. Save credentials in `.env.local`. The server logs in this user on demand to obtain the chat JWT — never the human's.",
4081
+ "**5. Bootstrap script (`scripts/bootstrap.ts`)**: idempotent. Reads each schema slug from `.env.local`; if missing, calls `get_entity_schema_recipe({entity_type:'product|customer|order'})` shape and `POST /v1/entity/schemas`; appends the returned id to `.env.local`. Then `POST /v1/entity/records/bulk` with 5-10 sample products in ONE call (this is the saboreo demo of bulk insert).",
4082
+ "**6. Owner login**: `/admin/login` → server-side `POST /v1/auth/login` set `genlobe_jwt` cookie httpOnly. Use the email the human gave you (NOT the bot user).",
4083
+ "**7. Admin layout gate**: `app/(admin)/layout.tsx` reads `genlobe_jwt` cookie, redirects to `/admin/login` if missing.",
4084
+ "**8. Products vertical slice end-to-end first**: list, create, edit, delete. Confirms the full stack works before fanning out to other entities.",
4085
+ "**9. Sales (`/admin/sales/new`)**: customer autocomplete (search customers with `ilike`), product picker (paginated), line items with `name_snapshot` + `price_snapshot` per item, total auto-calc. POST one `order` record. Stock decrement is best-effort (continue if it fails — the sale is recorded).",
4086
+ "**10. Sales list (`/admin/sales`) + per-customer history (`/admin/customers/{id}`)**: cross-schema join (`POST /v1/entity/records/search` with `join` clause on `customer_id`/`product_id`) to pull customer / product names in the same call.",
4087
+ "**11. Dashboard KPIs (`/admin`)**: today's revenue, top 3 products, new customers this week. Client-side sums for MVP; flag any KPI taking >300ms as a candidate for server-side aggregation later.",
4088
+ "**12. KB + bot setup (`scripts/sync-catalog-kb.ts`)**: snapshot the catalog to plain text, create KB with `rag_role: 'product_catalog'`, upload document, create the Store Assistant agent with the same `rag_role`. Add a button in `/admin` that re-runs this script when the catalog changes.",
4089
+ "**13. Public storefront (`/`) + `/chat`**: storefront uses the catalog directly via a server route (no auth on the customer); `/chat` UI posts to `/api/chat` which calls `chatWithAgent(agentId, message, conversationId)` — JWT handled server-side by the cached bot login.",
4090
+ "**14. Smoke**: open `http://localhost:3100`, place one test order, confirm it shows up in `/admin/sales`, ask the bot \"¿tienen X?\", verify it answers from the KB.",
3999
4091
  ],
4000
- notes: "Stock decrement on sale is intentionally NOT automatic — for MVP let the owner adjust stock manually after a sale, to avoid double-decrement bugs.",
4092
+ notes: "Stock decrement on sale is intentionally NOT automatic to the level of a DB transaction — for MVP it is best-effort. If the stock UPDATE fails, the order is still saved. The owner reconciles manually. This avoids the double-decrement and partial-failure bugs that a naive transactional implementation hits without a row-lock primitive.\n\nReal-life pitfall (Vendy build, 2026-05-26): an agent built this 95% successfully but skipped the LLM bot setup because it mis-read the security guidance as 'no bot users allowed'. The Step 0 in `get_chatbot_setup_recipe` and the bot service-account clarification in `get_authentication_flow` exist to prevent that exact regression. If you find yourself building a keyword-search fallback because LLM 'isn't possible', re-read both — the LLM bot IS possible with one registered bot user, and the recipes spell out the ceremony.",
4001
4093
  },
4002
4094
  crm: {
4003
4095
  template: "crm",
@@ -4233,6 +4325,87 @@ ${s.notes ? `## Notes\n\n${s.notes}\n` : ""}
4233
4325
  - For the auth shape (sk_live_* / pk_live_* + cookies), call \`get_authentication_flow()\` and \`get_security_guide()\`.`;
4234
4326
  }
4235
4327
  // =============================================================================
4328
+ // Supabase → Genlobe migration recipe (G10 from the DX audit).
4329
+ // =============================================================================
4330
+ function SUPABASE_MIGRATION_RECIPE() {
4331
+ return `# Supabase → Genlobe migration recipe
4332
+
4333
+ Move a Supabase project's data into Genlobe. The tricky part is **users +
4334
+ foreign keys**; data tables are the easy part (bulk insert). Do it in this
4335
+ order so foreign keys never dangle.
4336
+
4337
+ ## Mental model (ADR-0012)
4338
+
4339
+ - Genlobe \`users\` (native, auth) ← Supabase \`auth.users\`. Auth stays in the
4340
+ native table; do NOT model users as a custom entity.
4341
+ - Genlobe custom entities ← Supabase \`public.*\` data tables.
4342
+ - One Genlobe **Organization = one namespace** (≈ a Supabase project). Your
4343
+ app's *customers* are rows in a custom entity, NOT Organizations.
4344
+
4345
+ ## Step 1 — Import users FIRST (preserve UUIDs)
4346
+
4347
+ \`\`\`http
4348
+ POST /v1/server/users/import (sk_live_* — server/agent callable)
4349
+ \`\`\`
4350
+ \`\`\`json
4351
+ {
4352
+ "organization_id": "<target org>",
4353
+ "users": [
4354
+ {
4355
+ "id": "<the Supabase auth.users.id — PRESERVE IT>",
4356
+ "email": "ada@shop.com",
4357
+ "password_hash": "<Supabase encrypted_password — bcrypt, imported as-is>",
4358
+ "profile_data": { "...": "from raw_user_meta_data" },
4359
+ "is_email_verified": true,
4360
+ "status": "active"
4361
+ }
4362
+ ]
4363
+ }
4364
+ \`\`\`
4365
+
4366
+ - **Preserve \`id\`**: pass the Supabase UUID so every foreign key that pointed
4367
+ at it stays valid. This is the single most important step.
4368
+ - **\`password_hash\`**: Supabase and Genlobe both use bcrypt, so the hash is
4369
+ imported as-is — users keep their password, NO reset.
4370
+ - Capped at 500 per call, all-or-nothing. Chunk larger sets.
4371
+
4372
+ ## Step 2 — Create the entity schemas for the data tables
4373
+
4374
+ For each Supabase \`public.<table>\`, define a custom entity schema. Use
4375
+ \`get_entity_schema_recipe\` for common shapes. Map a column that was a FK to
4376
+ \`auth.users\` as a \`reference\` field with \`target_schema_slug: "users"\`
4377
+ (ADR-0009) — it validates against the native users table.
4378
+
4379
+ \`POST /v1/entity/schemas\` (one per table). Valid field types: string, text,
4380
+ integer, float, boolean, datetime, enum, email, url, phone, reference.
4381
+ (No \`array\`, no \`number\` — see get_entity_schema_recipe.)
4382
+
4383
+ ## Step 3 — Bulk-insert the rows
4384
+
4385
+ \`\`\`http
4386
+ POST /v1/entity/records/bulk
4387
+ \`\`\`
4388
+ Because you preserved user UUIDs in Step 1, any \`reference→users\` field in
4389
+ these rows already points at a valid user. For FKs between data tables,
4390
+ preserve those source ids too (store them in a field) or rewrite them with a
4391
+ mapping you keep while importing.
4392
+
4393
+ ## Step 4 — Verify
4394
+
4395
+ - Spot-check: a migrated user can log in with their old password.
4396
+ - A migrated row's \`reference→users\` resolves (the user exists).
4397
+ - Counts match the Supabase source.
4398
+
4399
+ ## Auth for the import endpoints
4400
+
4401
+ All three steps accept \`sk_live_*\` from a server/agent:
4402
+ - Users: \`POST /v1/server/users/import\` (server-scoped, sk_live_*). The
4403
+ dashboard variant \`POST /v1/dashboard/organizations/users/import\` (TenantMember
4404
+ auth) does the same thing for a human in the dashboard.
4405
+ - Schemas: \`POST /v1/entity/schemas\`.
4406
+ - Rows: \`POST /v1/entity/records/bulk\`.`;
4407
+ }
4408
+ // =============================================================================
4236
4409
  // MCP Server Implementation
4237
4410
  // =============================================================================
4238
4411
  const server = new Server({
@@ -4570,6 +4743,24 @@ developer says "I want to build X" before writing code.`,
4570
4743
  required: ["template"],
4571
4744
  },
4572
4745
  },
4746
+ {
4747
+ name: "get_supabase_migration_recipe",
4748
+ description: `Step-by-step recipe to migrate a Supabase project into Genlobe (G10). Covers the tricky part — users + foreign keys — in the correct order: import users FIRST preserving their UUIDs (so FKs stay valid) and their bcrypt password hash (Supabase + Genlobe both use bcrypt — no password reset), then create entity schemas mapping FK columns to reference fields (target_schema_slug: "users"), then bulk-insert rows. Explains the ADR-0012 mental model (Organization = namespace; your app's customers are rows, not Organizations). Pure data; no network call.`,
4749
+ inputSchema: { type: "object", properties: {}, required: [] },
4750
+ },
4751
+ {
4752
+ name: "invite_organization_owner",
4753
+ description: `Invite the owner of an Organization (F2/F4). Creates the owner end-user and EMAILS them their sign-in credentials. The agent NEVER sees the password — it is delivered by email only (MCP "no raw secrets" invariant). Ask the human for the owner's REAL email; never invent one (a bounced address gets SES-suppressed — incident #168). Requires sk_live_*. Returns { user_id, email, email_sent }. If email_sent is false, tell the human to grab the temporary credential from the Tenant Dashboard. Calls POST /v1/server/organizations/invite-owner.`,
4754
+ inputSchema: {
4755
+ type: "object",
4756
+ properties: {
4757
+ organization_id: { type: "string", description: "UUID of the Organization the owner will administer." },
4758
+ email: { type: "string", description: "The owner's REAL email address. Ask the human — never fabricate it." },
4759
+ display_name: { type: "string", description: "Optional display name for the owner." },
4760
+ },
4761
+ required: ["organization_id", "email"],
4762
+ },
4763
+ },
4573
4764
  ],
4574
4765
  };
4575
4766
  });
@@ -4980,6 +5171,12 @@ Use search_endpoints tool to find available endpoints.`,
4980
5171
  > 2. **Do NOT register a new end-user with an email you generated.** Inventing an email (e.g. \`admin@<their-domain>\`, \`seed@<their-domain>\`) leads to bounces. AWS SES then suppresses the address at the account level and locks the tenant out of email delivery for that destination — recovery requires manual operator action. This is exactly how production incident #168 happened.
4981
5172
  > 3. **Schemas don't need a JWT.** \`/v1/entity/schemas\` is a tenant-admin operation: \`sk_live_*\` alone is enough (see issue #177). Read \`get_endpoint_details\` for the schemas endpoint before assuming you need to register a user.
4982
5173
  > 4. If the human says \"register a user with this email: ...\" — fine, that's an explicit instruction. Inventing one is not.
5174
+ >
5175
+ > ### Bot service-account user (LLM chat surface)
5176
+ >
5177
+ > **Public chatbot surfaces are an exception that is fully permitted**: you can — and should — register **one real bot service-account user** to obtain the JWT the agent chat endpoint requires. The human is implicitly authorizing this when they ask for "an LLM chatbot the customers can use". Use a real email you control (e.g. \`bot@<their-shop-domain>\` or a personal mailbox). Save the credentials in the server-side env (\`.env.local\` with \`BOT_USER_EMAIL\` + \`BOT_USER_PASSWORD\`), log in server-side, attach the JWT to incoming chat requests. See \`get_chatbot_setup_recipe()\` Step 0 for the full ceremony.
5178
+ >
5179
+ > Registering **one** bot account is not the same thing as "inventing emails in bulk" — that distinction matters.
4983
5180
 
4984
5181
  ---
4985
5182
 
@@ -5207,7 +5404,122 @@ await fetch('/v1/auth/logout', {
5207
5404
 
5208
5405
  localStorage.removeItem('access_token');
5209
5406
  localStorage.removeItem('refresh_token');
5210
- \`\`\``,
5407
+ \`\`\`
5408
+
5409
+ ---
5410
+
5411
+ ## 10. Server-side auth pattern (sk_live_* + cookies httpOnly) — Next.js App Router
5412
+
5413
+ When the app uses a **secret key** (\`sk_live_*\`), the browser must never see the key OR the API base URL. All Genlobe calls go through your own \`/api/*\` routes on the Next.js server. End-user sessions are kept in **httpOnly cookies**, not \`localStorage\`.
5414
+
5415
+ ### Project layout (recommended)
5416
+
5417
+ \`\`\`
5418
+ app/
5419
+ ├─ (admin)/ Owner dashboard, gated
5420
+ │ ├─ layout.tsx Reads cookie, redirects to /admin/login if missing
5421
+ │ ├─ admin/login/page.tsx
5422
+ │ └─ admin/page.tsx
5423
+ ├─ (public)/ No auth, anonymous customer surface
5424
+ │ ├─ page.tsx Storefront / POS / catalog
5425
+ │ └─ chat/page.tsx Public chatbot UI
5426
+ ├─ api/
5427
+ │ ├─ admin/login/route.ts POST → server-side call to /v1/auth/login, sets cookie
5428
+ │ ├─ admin/logout/route.ts Clears cookie
5429
+ │ └─ chat/route.ts POST → server-side call to /v1/user/agents/{id}/chat with bot JWT
5430
+ └─ middleware.ts Gates (admin)/*; refreshes bot JWT in background
5431
+ lib/
5432
+ └─ genlobe.server.ts 'server-only', wraps the API
5433
+ .env.local SAAS_API_URL, SAAS_API_KEY, SAAS_ORGANIZATION_ID, BOT_USER_EMAIL, BOT_USER_PASSWORD
5434
+ \`\`\`
5435
+
5436
+ ### lib/genlobe.server.ts
5437
+
5438
+ \`\`\`typescript
5439
+ import 'server-only';
5440
+
5441
+ const API = process.env.SAAS_API_URL!; // e.g. http://localhost:8001
5442
+ const KEY = process.env.SAAS_API_KEY!; // sk_live_* — server only
5443
+ const ORG = process.env.SAAS_ORGANIZATION_ID!; // organization scope
5444
+
5445
+ export async function genlobeApi<T = any>(
5446
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
5447
+ path: string,
5448
+ body?: any,
5449
+ jwt?: string
5450
+ ): Promise<T> {
5451
+ const res = await fetch(\`\${API}\${path}\`, {
5452
+ method,
5453
+ headers: {
5454
+ 'Content-Type': 'application/json',
5455
+ 'X-API-Key': KEY,
5456
+ 'X-Organization-Id': ORG,
5457
+ ...(jwt ? { Authorization: \`Bearer \${jwt}\` } : {}),
5458
+ },
5459
+ body: body ? JSON.stringify(body) : undefined,
5460
+ cache: 'no-store',
5461
+ });
5462
+ if (!res.ok) throw new Error(\`Genlobe \${res.status}: \${await res.text()}\`);
5463
+ return res.json();
5464
+ }
5465
+ \`\`\`
5466
+
5467
+ ### app/api/admin/login/route.ts
5468
+
5469
+ \`\`\`typescript
5470
+ import { NextResponse } from 'next/server';
5471
+ import { cookies } from 'next/headers';
5472
+ import { genlobeApi } from '@/lib/genlobe.server';
5473
+
5474
+ export async function POST(req: Request) {
5475
+ const { email, password } = await req.json();
5476
+ const { access_token, refresh_token, user } = await genlobeApi(
5477
+ 'POST',
5478
+ '/v1/auth/login',
5479
+ { email, password }
5480
+ );
5481
+
5482
+ const jar = cookies();
5483
+ jar.set('genlobe_jwt', access_token, {
5484
+ httpOnly: true, secure: process.env.NODE_ENV === 'production',
5485
+ sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24,
5486
+ });
5487
+ jar.set('genlobe_refresh', refresh_token, {
5488
+ httpOnly: true, secure: process.env.NODE_ENV === 'production',
5489
+ sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 30,
5490
+ });
5491
+
5492
+ return NextResponse.json({ user });
5493
+ }
5494
+ \`\`\`
5495
+
5496
+ ### app/(admin)/layout.tsx (gate)
5497
+
5498
+ \`\`\`typescript
5499
+ import { cookies } from 'next/headers';
5500
+ import { redirect } from 'next/navigation';
5501
+
5502
+ export default async function AdminLayout({ children }: { children: React.ReactNode }) {
5503
+ const jwt = cookies().get('genlobe_jwt')?.value;
5504
+ if (!jwt) redirect('/admin/login');
5505
+ return <>{children}</>;
5506
+ }
5507
+ \`\`\`
5508
+
5509
+ ### .gitignore (critical)
5510
+
5511
+ \`\`\`
5512
+ .env*.local
5513
+ .env
5514
+ \`\`\`
5515
+
5516
+ Next.js boilerplate puts this in by default if you used \`create-next-app\`. **Always verify it before the first commit** — the \`sk_live_*\` and \`BOT_USER_PASSWORD\` live in \`.env.local\` and must never leave your machine.
5517
+
5518
+ ### Three rules that keep this safe
5519
+
5520
+ 1. \`SAAS_API_KEY\` env var is **never** prefixed with \`NEXT_PUBLIC_\`. Same for \`BOT_USER_PASSWORD\` and \`SAAS_ORGANIZATION_ID\`.
5521
+ 2. The wrapper file (\`lib/genlobe.server.ts\`) starts with \`import 'server-only'\`. If a client component ever imports it, the Next.js build fails with a clear message — that's the contract.
5522
+ 3. The browser only talks to *your* \`/api/*\` routes, never to the Genlobe host directly.`,
5211
5523
  },
5212
5524
  ],
5213
5525
  };
@@ -5640,6 +5952,41 @@ type to pick.`,
5640
5952
  content: [{ type: "text", text: APP_SCAFFOLD(template) }],
5641
5953
  };
5642
5954
  }
5955
+ case "get_supabase_migration_recipe": {
5956
+ return {
5957
+ content: [{ type: "text", text: SUPABASE_MIGRATION_RECIPE() }],
5958
+ };
5959
+ }
5960
+ case "invite_organization_owner": {
5961
+ if (!API_KEY) {
5962
+ return {
5963
+ content: [{ type: "text", text: `❌ Cannot invite owner: SAAS_API_KEY is not configured. A secret key (sk_live_*) is required.` }],
5964
+ };
5965
+ }
5966
+ const { organization_id, email, display_name } = args;
5967
+ try {
5968
+ const r = await fetch(`${API_URL}/v1/server/organizations/invite-owner`, {
5969
+ method: "POST",
5970
+ headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
5971
+ body: JSON.stringify({ organization_id, email, display_name }),
5972
+ });
5973
+ const body = await r.json();
5974
+ if (!r.ok) {
5975
+ return {
5976
+ content: [{ type: "text", text: `HTTP ${r.status} from /v1/server/organizations/invite-owner — ${body.detail ?? JSON.stringify(body)}.\n\nNeeds a secret key (sk_live_*). The org must belong to this key's tenant.` }],
5977
+ };
5978
+ }
5979
+ const note = body.email_sent
5980
+ ? "✅ The owner has been emailed their sign-in credentials. They can now sign in at the Org Owner Dashboard and change their password."
5981
+ : "⚠️ Owner created, but the credentials email could NOT be sent (SMTP not configured). Ask the human to retrieve the temporary credential from the Tenant Dashboard → Organizations → invite owner. The password is never exposed here.";
5982
+ return {
5983
+ content: [{ type: "text", text: `Owner invited.\n${JSON.stringify(body, null, 2)}\n\n${note}` }],
5984
+ };
5985
+ }
5986
+ catch (err) {
5987
+ return { content: [{ type: "text", text: `Network error: ${err}` }] };
5988
+ }
5989
+ }
5643
5990
  default:
5644
5991
  throw new Error(`Unknown tool: ${name}`);
5645
5992
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genlobe/mcp-server",
3
- "version": "3.7.1",
3
+ "version": "3.8.0",
4
4
  "description": "MCP Server for GenLobe SaaS API - Provides AI assistants with comprehensive API documentation for building frontend applications",
5
5
  "main": "dist/index.js",
6
6
  "bin": {