@genlobe/mcp-server 3.6.1 → 3.7.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 +790 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2069,6 +2069,30 @@ const END_USER_ENDPOINTS = {
2069
2069
  },
2070
2070
  notes: "Three modes (#178 + #350): (1) sk_live_* alone -> tenant-scoped record, created_by_id is NULL (catalog/blog/global config). (2) sk_live_* + end-user JWT -> user-scoped record, created_by_id = user.id (server-side caller acting on behalf of a user). (3) pk_live_* + end-user JWT -> user-scoped record, created_by_id = user.id (canonical 'two-phase' frontend pattern: pk in the browser, JWT after login). pk_live_* WITHOUT a JWT is rejected with 401 — a public key alone is anonymous and can't be the creator of a record."
2071
2071
  },
2072
+ {
2073
+ method: "POST",
2074
+ path: "/v1/entity/records/bulk",
2075
+ summary: "Bulk-insert up to 500 records into one schema in one transaction (migration-friendly)",
2076
+ auth: { api_key: "sk_live_* or pk_live_*", jwt: "optional (same modes as POST /v1/entity/records)", headers: "X-Organization-Id required when key is not organization-scoped" },
2077
+ request_body: {
2078
+ schema_id: "uuid (required) - all records in the batch target this schema",
2079
+ records: "array (required) - up to 500 JSON objects. Each is validated against the schema's fields_definition and reference fields (ADR-0009)."
2080
+ },
2081
+ notes: "All-or-nothing transaction: if ANY record fails schema or reference validation, the entire batch is rejected and nothing is persisted. Per-record errors include `record_index` so the agent can fix specific rows and retry. Capped at 500 records per call — for larger migrations, chunk the source dataset. Designed for Supabase → Genlobe table migrations (one source table → one custom_data_schema → one or more bulk chunks). Plan limits are checked once against the batch size; the whole batch fits or the whole batch fails. created_by_id follows the same rule as POST /v1/entity/records (NULL for sk_live_* alone, user.id when an end-user JWT is attached) and is identical for every row in the batch.",
2082
+ example_request: `{
2083
+ "schema_id": "550e8400-e29b-41d4-a716-446655440000",
2084
+ "records": [
2085
+ { "name": "Sugar 1kg", "price": 2.5, "stock": 100 },
2086
+ { "name": "Rice 1kg", "price": 3.0, "stock": 50 },
2087
+ { "name": "Coffee 250g", "price": 6.75, "stock": 30 }
2088
+ ]
2089
+ }`,
2090
+ example_response: `{
2091
+ "schema_id": "550e8400-e29b-41d4-a716-446655440000",
2092
+ "inserted_count": 3,
2093
+ "items": [ { "id": "...", "schema_id": "...", "organization_id": "...", "data": {...} }, ... ]
2094
+ }`
2095
+ },
2072
2096
  {
2073
2097
  method: "GET",
2074
2098
  path: "/v1/entity/records/mine",
@@ -3473,6 +3497,720 @@ has a different shape because of where the API key can safely live.`,
3473
3497
  const body = sections[appType] ?? sections["unsure"];
3474
3498
  return `# Stack recommendation\n\n${keyLine}\n\n${body}\n\n---\n\nRun \`get_security_guide\` for the full security cheat-sheet that matches your key type.`;
3475
3499
  }
3500
+ const ENTITY_SCHEMA_RECIPES = {
3501
+ product: {
3502
+ slug: "product",
3503
+ name: "Product",
3504
+ description: "Catalog product with price, stock, category, description.",
3505
+ fields_definition: {
3506
+ name: { type: "string", required: true, max_length: 200 },
3507
+ price: { type: "number", required: true, min: 0 },
3508
+ stock: { type: "number", required: true, min: 0, default: 0 },
3509
+ currency: { type: "string", required: false, default: "USD", max_length: 3 },
3510
+ category: { type: "string", required: false, max_length: 100 },
3511
+ description: { type: "string", required: false },
3512
+ is_active: { type: "boolean", required: false, default: true },
3513
+ },
3514
+ bulk_seed_example: [
3515
+ { name: "Sugar 1kg", price: 2.5, stock: 100, category: "groceries" },
3516
+ { name: "Rice 1kg", price: 3.0, stock: 50, category: "groceries" },
3517
+ { name: "Coffee 250g", price: 6.75, stock: 30, category: "groceries" },
3518
+ ],
3519
+ notes: "If you need product variants (size/color), keep `name` for the base product and add a separate `variant` entity with a `product_id` reference (type=reference, target_schema_slug='product').",
3520
+ },
3521
+ customer: {
3522
+ slug: "customer",
3523
+ name: "Customer",
3524
+ description: "Store-side customer record (NOT a Genlobe end-user — those go through /v1/auth/register). Use this when you want to track purchases without forcing email signup.",
3525
+ fields_definition: {
3526
+ name: { type: "string", required: true, max_length: 200 },
3527
+ phone: { type: "string", required: false, max_length: 50 },
3528
+ email: { type: "string", required: false, max_length: 200 },
3529
+ notes: { type: "string", required: false },
3530
+ tags: { type: "array", required: false },
3531
+ },
3532
+ bulk_seed_example: [
3533
+ { name: "Ana Pérez", phone: "+1-809-555-0100" },
3534
+ { name: "Walk-in", notes: "no-data anonymous buyer" },
3535
+ ],
3536
+ notes: "Common mistake: registering each store customer as a Genlobe end-user. Don't — that requires real email + SES delivery. Keep store customers as this custom entity unless they need to log into the storefront.",
3537
+ },
3538
+ order: {
3539
+ slug: "order",
3540
+ name: "Order / Sale",
3541
+ description: "A completed sale with line items (price + name snapshotted at the time of sale, so editing a product later does NOT mutate past orders).",
3542
+ fields_definition: {
3543
+ customer_id: {
3544
+ type: "reference",
3545
+ target_schema_slug: "customer",
3546
+ required: false,
3547
+ on_delete: "set_null",
3548
+ },
3549
+ items: {
3550
+ type: "array",
3551
+ required: true,
3552
+ item_schema: {
3553
+ product_id: {
3554
+ type: "reference",
3555
+ target_schema_slug: "product",
3556
+ required: true,
3557
+ on_delete: "restrict",
3558
+ },
3559
+ name_snapshot: { type: "string", required: true },
3560
+ price_snapshot: { type: "number", required: true, min: 0 },
3561
+ qty: { type: "number", required: true, min: 1 },
3562
+ },
3563
+ },
3564
+ total: { type: "number", required: true, min: 0 },
3565
+ currency: { type: "string", required: false, default: "USD" },
3566
+ paid: { type: "boolean", required: true, default: false },
3567
+ paid_at: { type: "datetime", required: false },
3568
+ notes: { type: "string", required: false },
3569
+ },
3570
+ notes: "Always snapshot `name` and `price` into `items[]` at write time. Reading a 6-month-old order should show the price the customer paid, not the current product price. Cross-schema reads (customer name on the order list) are 1 EXISTS sub-select via POST /v1/entity/records/search with `join` (ADR-0009).",
3571
+ },
3572
+ post: {
3573
+ slug: "post",
3574
+ name: "Blog post / Article",
3575
+ description: "Authored content with title, body, status, tags.",
3576
+ fields_definition: {
3577
+ title: { type: "string", required: true, max_length: 200 },
3578
+ slug: { type: "string", required: true, max_length: 200 },
3579
+ body: { type: "string", required: true },
3580
+ status: {
3581
+ type: "string",
3582
+ required: true,
3583
+ enum: ["draft", "published", "archived"],
3584
+ default: "draft",
3585
+ },
3586
+ tags: { type: "array", required: false },
3587
+ published_at: { type: "datetime", required: false },
3588
+ cover_image_url: { type: "string", required: false },
3589
+ },
3590
+ notes: "`created_by_id` is the author (auto-stamped by the API from the JWT, no need to include in records). For multi-author orgs the elevated-role bypass on /v1/entity/records/search lets editors see everyone's posts.",
3591
+ },
3592
+ comment: {
3593
+ slug: "comment",
3594
+ name: "Comment",
3595
+ description: "User-authored comment that points at another record (post, product, order, anything).",
3596
+ fields_definition: {
3597
+ target_id: {
3598
+ type: "string",
3599
+ required: true,
3600
+ notes: "UUID of the commented record. Not a reference field because the target schema varies — store the target schema slug in `target_schema` if you need polymorphism.",
3601
+ },
3602
+ target_schema: { type: "string", required: true, max_length: 100 },
3603
+ body: { type: "string", required: true },
3604
+ is_deleted: { type: "boolean", required: false, default: false },
3605
+ },
3606
+ notes: "Polymorphic targets defeat the ADR-0009 `reference` validator. If your comments only ever point at ONE schema (e.g. `post`), drop `target_schema` and convert `target_id` to a proper reference for FK integrity.",
3607
+ },
3608
+ message: {
3609
+ slug: "message",
3610
+ name: "Chat / direct message",
3611
+ description: "A single message in a conversation. Pair with the native `conversations` table (Genlobe AI agents) or roll your own thread entity.",
3612
+ fields_definition: {
3613
+ conversation_id: { type: "string", required: true },
3614
+ sender_user_id: { type: "string", required: false },
3615
+ sender_role: {
3616
+ type: "string",
3617
+ required: true,
3618
+ enum: ["user", "assistant", "system"],
3619
+ },
3620
+ body: { type: "string", required: true },
3621
+ read_at: { type: "datetime", required: false },
3622
+ },
3623
+ notes: "If you're building a chatbot powered by a Genlobe agent, you do NOT need this entity — agent conversations are persisted automatically (see `/v1/user/agents/{id}/conversations`). Use this only for human-to-human messaging.",
3624
+ },
3625
+ task: {
3626
+ slug: "task",
3627
+ name: "Task / To-do",
3628
+ description: "Single task with title, status, due date, assignee.",
3629
+ fields_definition: {
3630
+ title: { type: "string", required: true, max_length: 200 },
3631
+ description: { type: "string", required: false },
3632
+ status: {
3633
+ type: "string",
3634
+ required: true,
3635
+ enum: ["pending", "in_progress", "completed", "cancelled"],
3636
+ default: "pending",
3637
+ },
3638
+ priority: {
3639
+ type: "string",
3640
+ required: false,
3641
+ enum: ["low", "medium", "high"],
3642
+ default: "medium",
3643
+ },
3644
+ assigned_to_id: {
3645
+ type: "reference",
3646
+ target_schema_slug: "users",
3647
+ required: false,
3648
+ on_delete: "set_null",
3649
+ },
3650
+ due_date: { type: "datetime", required: false },
3651
+ },
3652
+ notes: "`assigned_to_id` references the native `users` table (Genlobe end-users), not a custom entity. The reference validator enforces `OrganizationMember` active membership server-side.",
3653
+ },
3654
+ note: {
3655
+ slug: "note",
3656
+ name: "Personal note",
3657
+ description: "A free-text note owned by the creator. Row-level scope means each end-user only sees their own notes via /v1/entity/records/mine.",
3658
+ fields_definition: {
3659
+ title: { type: "string", required: false, max_length: 200 },
3660
+ body: { type: "string", required: true },
3661
+ tags: { type: "array", required: false },
3662
+ is_pinned: { type: "boolean", required: false, default: false },
3663
+ },
3664
+ notes: "Perfect for ADR-0007 row-level scope: regular end-users see only their own notes via `GET /v1/entity/records/mine` (no extra filter needed in the frontend).",
3665
+ },
3666
+ };
3667
+ function ENTITY_SCHEMA_RECIPE(entityType) {
3668
+ const recipe = ENTITY_SCHEMA_RECIPES[entityType];
3669
+ if (!recipe) {
3670
+ return `# Unknown entity_type "${entityType}"\n\nAvailable: ${Object.keys(ENTITY_SCHEMA_RECIPES).join(", ")}.`;
3671
+ }
3672
+ const postBody = {
3673
+ name: recipe.name,
3674
+ slug: recipe.slug,
3675
+ description: recipe.description,
3676
+ fields_definition: recipe.fields_definition,
3677
+ };
3678
+ const bulkBody = recipe.bulk_seed_example
3679
+ ? {
3680
+ schema_id: "<paste the id returned by POST /v1/entity/schemas above>",
3681
+ records: recipe.bulk_seed_example,
3682
+ }
3683
+ : null;
3684
+ return `# Entity schema recipe — ${recipe.name}
3685
+
3686
+ ${recipe.description}
3687
+
3688
+ ## Step 1 — Create the schema
3689
+
3690
+ \`\`\`http
3691
+ POST /v1/entity/schemas
3692
+ Content-Type: application/json
3693
+ X-API-Key: <your sk_live_*>
3694
+ X-Organization-Id: <your organization_id>
3695
+ \`\`\`
3696
+
3697
+ Body:
3698
+ \`\`\`json
3699
+ ${JSON.stringify(postBody, null, 2)}
3700
+ \`\`\`
3701
+
3702
+ ${bulkBody
3703
+ ? `## Step 2 — Seed sample rows (bulk)
3704
+
3705
+ \`\`\`http
3706
+ POST /v1/entity/records/bulk
3707
+ Content-Type: application/json
3708
+ X-API-Key: <your sk_live_*>
3709
+ X-Organization-Id: <your organization_id>
3710
+ \`\`\`
3711
+
3712
+ Body:
3713
+ \`\`\`json
3714
+ ${JSON.stringify(bulkBody, null, 2)}
3715
+ \`\`\`
3716
+
3717
+ Bulk insert is capped at 500 records per call; chunk larger migrations.
3718
+ All-or-nothing: any validation failure rolls back the whole batch.
3719
+ `
3720
+ : ""}
3721
+ ${recipe.notes ? `## Notes\n\n${recipe.notes}\n` : ""}
3722
+
3723
+ ---
3724
+
3725
+ **Before posting**, call \`get_reserved_schema_slugs()\` to confirm \`${recipe.slug}\` is not on the reserved list. It is not, today, but slug-collision checking is part of the contract.`;
3726
+ }
3727
+ // =============================================================================
3728
+ // Chatbot setup recipe (v3.7.0) — closes G2.
3729
+ // =============================================================================
3730
+ function CHATBOT_SETUP_RECIPE() {
3731
+ return `# Chatbot setup recipe — agent + KB bound by \`rag_role\`
3732
+
3733
+ End-to-end recipe to wire a chatbot agent that answers from a knowledge base
3734
+ populated by a snapshot of your custom-entity catalog (typical "store
3735
+ assistant" / "product Q&A" / "support deflection" use case).
3736
+
3737
+ ## Architecture in one diagram
3738
+
3739
+ \`\`\`
3740
+ Custom-entity catalog
3741
+ (e.g. products)
3742
+
3743
+ │ 1. snapshot to plain text
3744
+
3745
+ KnowledgeBase (per-Org)
3746
+
3747
+ │ 2. uploaded as a Document
3748
+
3749
+ Agent (rag_role bound to KB) ←─ end-user chats
3750
+ /v1/user/agents/{id}/chat
3751
+ \`\`\`
3752
+
3753
+ ## Step-by-step
3754
+
3755
+ ### 1. Create the KnowledgeBase
3756
+
3757
+ \`\`\`http
3758
+ POST /v1/organization-admin/{org_id}/knowledge-bases
3759
+ \`\`\`
3760
+
3761
+ Body:
3762
+ \`\`\`json
3763
+ {
3764
+ "name": "Vendy catalog",
3765
+ "description": "Snapshot of products, updated on demand",
3766
+ "rag_role": "product_catalog"
3767
+ }
3768
+ \`\`\`
3769
+
3770
+ Save the returned \`kb_id\`. The \`rag_role\` value is the binding key — the
3771
+ agent will reference the same role string to find this KB at chat time.
3772
+
3773
+ ### 2. Snapshot your catalog and upload it as a Document
3774
+
3775
+ Pull every product record:
3776
+
3777
+ \`\`\`http
3778
+ POST /v1/entity/records/search
3779
+ \`\`\`
3780
+
3781
+ Body: \`{ "schema_id": "<your product schema id>", "query": { "is_active": true } }\`
3782
+
3783
+ Format each row as one paragraph the LLM can read:
3784
+
3785
+ \`\`\`text
3786
+ Product: Sugar 1kg
3787
+ Price: $2.50
3788
+ Stock: 100
3789
+ Category: groceries
3790
+ Description: white sugar in 1kg bags
3791
+ ---
3792
+ Product: Rice 1kg
3793
+ ...
3794
+ \`\`\`
3795
+
3796
+ Upload as a document:
3797
+
3798
+ \`\`\`http
3799
+ POST /v1/organization-admin/{org_id}/knowledge-bases/{kb_id}/documents
3800
+ \`\`\`
3801
+
3802
+ Body:
3803
+ \`\`\`json
3804
+ {
3805
+ "title": "Catalog snapshot 2026-05-26",
3806
+ "content": "<the multi-paragraph text from above>",
3807
+ "metadata": { "source": "catalog_snapshot", "snapshotted_at": "<ISO timestamp>" }
3808
+ }
3809
+ \`\`\`
3810
+
3811
+ ### 3. Create the Agent
3812
+
3813
+ \`\`\`http
3814
+ POST /v1/agents
3815
+ \`\`\`
3816
+
3817
+ Body:
3818
+ \`\`\`json
3819
+ {
3820
+ "name": "Vendy Assistant",
3821
+ "description": "Answers customer questions about the store catalog.",
3822
+ "system_prompt": "You are a helpful store assistant for Vendy. Answer ONLY about products in the catalog you have access to. If the customer asks about anything else (delivery, billing, hours), reply that you can only help with product questions. Never invent products or prices.",
3823
+ "is_public": true,
3824
+ "rag_role": "product_catalog"
3825
+ }
3826
+ \`\`\`
3827
+
3828
+ \`is_public: true\` lets every end-user of the Org talk to the same agent (no
3829
+ per-user fork). \`rag_role: "product_catalog"\` is the binding to the KB
3830
+ created in step 1.
3831
+
3832
+ ### 4. End-user chat (frontend)
3833
+
3834
+ \`\`\`http
3835
+ POST /v1/user/agents/{agent_id}/chat
3836
+ \`\`\`
3837
+
3838
+ Body:
3839
+ \`\`\`json
3840
+ {
3841
+ "message": "do you have rice?",
3842
+ "conversation_id": "<optional, persist in sessionStorage to keep context>"
3843
+ }
3844
+ \`\`\`
3845
+
3846
+ Returns the assistant's response plus a \`conversation_id\` you should pass on
3847
+ subsequent messages to keep the same thread.
3848
+
3849
+ ## Refresh policy
3850
+
3851
+ When products change (price update, new SKU), regenerate the snapshot text
3852
+ and either:
3853
+
3854
+ - **Replace the document** (\`DELETE /v1/organization-admin/{org_id}/knowledge-bases/{kb_id}/documents/{doc_id}\` then re-upload) — simplest.
3855
+ - **Append a delta document** (\`POST\` a new document with only the changes) — faster but you need to manage stale entries.
3856
+
3857
+ For an MVP the first option is what we recommend.
3858
+
3859
+ ## Server-side TypeScript snippet
3860
+
3861
+ \`\`\`typescript
3862
+ // lib/genlobe.server.ts
3863
+ import 'server-only';
3864
+
3865
+ const API = process.env.SAAS_API_URL!;
3866
+ const KEY = process.env.SAAS_API_KEY!; // sk_live_*
3867
+ const ORG = process.env.SAAS_ORGANIZATION_ID!;
3868
+
3869
+ async function api(path: string, body: any, jwt?: string) {
3870
+ const res = await fetch(\`\${API}\${path}\`, {
3871
+ method: 'POST',
3872
+ headers: {
3873
+ 'Content-Type': 'application/json',
3874
+ 'X-API-Key': KEY,
3875
+ 'X-Organization-Id': ORG,
3876
+ ...(jwt ? { 'Authorization': \`Bearer \${jwt}\` } : {}),
3877
+ },
3878
+ body: JSON.stringify(body),
3879
+ });
3880
+ if (!res.ok) throw new Error(\`\${res.status} \${await res.text()}\`);
3881
+ return res.json();
3882
+ }
3883
+
3884
+ export async function refreshCatalogKB(productSchemaId: string, kbId: string, existingDocId?: string) {
3885
+ const { records } = await api('/v1/entity/records/search', {
3886
+ schema_id: productSchemaId,
3887
+ query: { is_active: true },
3888
+ });
3889
+ const content = records
3890
+ .map((r: any) =>
3891
+ \`Product: \${r.data.name}\\nPrice: $\${r.data.price}\\nStock: \${r.data.stock}\\nCategory: \${r.data.category ?? 'n/a'}\\nDescription: \${r.data.description ?? ''}\`
3892
+ )
3893
+ .join('\\n---\\n');
3894
+
3895
+ if (existingDocId) {
3896
+ await fetch(\`\${API}/v1/organization-admin/\${ORG}/knowledge-bases/\${kbId}/documents/\${existingDocId}\`, {
3897
+ method: 'DELETE',
3898
+ headers: { 'X-API-Key': KEY, 'X-Organization-Id': ORG },
3899
+ });
3900
+ }
3901
+ return api(\`/v1/organization-admin/\${ORG}/knowledge-bases/\${kbId}/documents\`, {
3902
+ title: \`Catalog snapshot \${new Date().toISOString()}\`,
3903
+ content,
3904
+ metadata: { source: 'catalog_snapshot' },
3905
+ });
3906
+ }
3907
+
3908
+ export async function chatWithAgent(agentId: string, message: string, conversationId?: string) {
3909
+ return api(\`/v1/user/agents/\${agentId}/chat\`, {
3910
+ message,
3911
+ ...(conversationId ? { conversation_id: conversationId } : {}),
3912
+ });
3913
+ }
3914
+ \`\`\`
3915
+
3916
+ ## Notes
3917
+
3918
+ - \`POST /v1/agents\` is the Tenant Dashboard endpoint — when configuring from
3919
+ your scaffolding script, use \`sk_live_*\`. End-user chat (\`/v1/user/agents/{id}/chat\`)
3920
+ is reached with \`pk_live_*\` + the end-user's JWT.
3921
+ - Tool calling vs RAG: for MVP / low-frequency catalog updates, RAG via KB is
3922
+ simpler. Switch to tool calling when the catalog updates faster than the
3923
+ refresh cadence you can sustain.
3924
+ - The bot does not write to the catalog and does not take orders unless you
3925
+ add a separate tool / API path. Keep the system prompt strict.`;
3926
+ }
3927
+ const APP_SCAFFOLDS = {
3928
+ pos: {
3929
+ template: "pos",
3930
+ title: "POS / small-store register",
3931
+ description: "A storefront with a chatbot for product questions and an owner dashboard for products, customers, sales.",
3932
+ entities: [
3933
+ { slug: "product", recipe_key: "product", purpose: "Catalog the owner manages." },
3934
+ {
3935
+ slug: "customer",
3936
+ recipe_key: "customer",
3937
+ purpose: "Store-side customer record (NOT a Genlobe end-user).",
3938
+ },
3939
+ {
3940
+ slug: "order",
3941
+ recipe_key: "order",
3942
+ purpose: "Completed sales with snapshotted line items (FK to product + customer).",
3943
+ },
3944
+ ],
3945
+ agents: [
3946
+ {
3947
+ name: "Store Assistant",
3948
+ rag_role: "product_catalog",
3949
+ purpose: "Answers customer questions about the product catalog. Read-only.",
3950
+ },
3951
+ ],
3952
+ kbs: [{ rag_role: "product_catalog", source: "Snapshot of `product` records" }],
3953
+ kpis: [
3954
+ {
3955
+ name: "Today's revenue",
3956
+ how: "POST /v1/entity/records/search with `query: { paid: true, paid_at: { operator: 'gte', value: <today_iso> } }`, sum `data.total` client-side.",
3957
+ },
3958
+ {
3959
+ name: "Top product this month",
3960
+ how: "Search orders, flatten `items[]`, group by `product_id`, sum `qty`. Cross-schema `join` with `product` to get the name in the same call.",
3961
+ },
3962
+ {
3963
+ name: "New customers this week",
3964
+ how: "Search customers with `created_at` filter; show count + names.",
3965
+ },
3966
+ ],
3967
+ build_order: [
3968
+ "1. Next.js 16 app router + Tailwind + shadcn. Add `lib/genlobe.server.ts` with `import 'server-only'`.",
3969
+ "2. Bootstrap script: create `product`, `customer`, `order` schemas via `get_entity_schema_recipe`. Bulk-seed 5-10 sample products.",
3970
+ "3. Owner login: `/admin/login` → POST /v1/auth/login server-side → cookie httpOnly with JWT.",
3971
+ "4. Owner CRUD products (single end-to-end vertical slice first).",
3972
+ "5. Sales form (`/admin/sales/new`): customer autocomplete, multi-product line items, total auto-calc, POST one `order` record.",
3973
+ "6. Sales list + detail.",
3974
+ "7. Customers list + per-customer purchase history (cross-schema join from order → customer).",
3975
+ "8. Dashboard KPIs (search + sum client-side; switch to server-side aggregates later if data grows).",
3976
+ "9. Public storefront `/` + `/chat`. Bot via `get_chatbot_setup_recipe`.",
3977
+ "10. Catalog refresh button in `/admin` (regenerates the KB document).",
3978
+ ],
3979
+ 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.",
3980
+ },
3981
+ crm: {
3982
+ template: "crm",
3983
+ title: "Lightweight CRM",
3984
+ description: "Track contacts, deals, notes, and a sales pipeline. No marketing automation, no email integration — that's later.",
3985
+ entities: [
3986
+ { slug: "customer", recipe_key: "customer", purpose: "Contact records." },
3987
+ {
3988
+ slug: "deal",
3989
+ recipe_key: "order",
3990
+ purpose: "Reuse the `order` recipe but rename to `deal`; line items become deliverables and `paid` becomes `closed_won`.",
3991
+ },
3992
+ {
3993
+ slug: "note",
3994
+ recipe_key: "note",
3995
+ purpose: "Free-text notes per contact (rename `note` → `interaction` if you want to scope it to the contact).",
3996
+ },
3997
+ ],
3998
+ agents: [
3999
+ {
4000
+ name: "CRM Helper",
4001
+ purpose: "Internal-only agent that drafts follow-up emails based on the most recent notes on a contact. No KB needed; works with conversation context.",
4002
+ },
4003
+ ],
4004
+ kbs: [],
4005
+ kpis: [
4006
+ { name: "Open pipeline value", how: "Search deals where `closed_won: false`, sum `total`." },
4007
+ {
4008
+ name: "Conversion rate",
4009
+ how: "Count deals `closed_won: true` / total deals over a period.",
4010
+ },
4011
+ {
4012
+ name: "Stale contacts",
4013
+ how: "Customers with no `note` records in the last 30 days (cross-schema NOT-EXISTS).",
4014
+ },
4015
+ ],
4016
+ build_order: [
4017
+ "1. Same bootstrap as POS.",
4018
+ "2. Customer CRUD (list + detail + edit).",
4019
+ "3. Notes per customer (reference field to customer).",
4020
+ "4. Deals pipeline view (kanban by `status`).",
4021
+ "5. CRM Helper agent (no KB) for drafting follow-ups.",
4022
+ ],
4023
+ notes: "If you want incoming email parsing, that's a Stage-3 deploy capability — not in MVP.",
4024
+ },
4025
+ blog: {
4026
+ template: "blog",
4027
+ title: "Multi-author blog",
4028
+ description: "Posts, comments, tags. Public read, authenticated write.",
4029
+ entities: [
4030
+ { slug: "post", recipe_key: "post", purpose: "Articles." },
4031
+ { slug: "comment", recipe_key: "comment", purpose: "User comments on posts." },
4032
+ ],
4033
+ agents: [
4034
+ {
4035
+ name: "Blog Recommender",
4036
+ rag_role: "blog_archive",
4037
+ purpose: "Suggests related posts based on what the reader is browsing.",
4038
+ },
4039
+ ],
4040
+ kbs: [{ rag_role: "blog_archive", source: "Snapshot of published `post` records" }],
4041
+ kpis: [
4042
+ { name: "Posts published this month", how: "Search posts where `status: published`, count." },
4043
+ {
4044
+ name: "Most commented post",
4045
+ how: "Search comments, group by `target_id`, count, cross-schema join with `post` for the title.",
4046
+ },
4047
+ ],
4048
+ build_order: [
4049
+ "1. Bootstrap.",
4050
+ "2. Post CRUD (admin only — gate with role check).",
4051
+ "3. Public reader view (`/blog/[slug]`) with comments form.",
4052
+ "4. Comments moderation page.",
4053
+ "5. Recommender bot via KB.",
4054
+ ],
4055
+ },
4056
+ task_manager: {
4057
+ template: "task_manager",
4058
+ title: "Personal / team task manager",
4059
+ description: "To-do list with assignees, due dates, and status. Per-user view by default (ADR-0007 row-level scope).",
4060
+ entities: [{ slug: "task", recipe_key: "task", purpose: "Tasks." }],
4061
+ agents: [
4062
+ {
4063
+ name: "Task Coach",
4064
+ purpose: "Suggests next-best task based on due dates and priorities (no KB; uses context).",
4065
+ },
4066
+ ],
4067
+ kbs: [],
4068
+ kpis: [
4069
+ {
4070
+ name: "My open tasks",
4071
+ how: "GET /v1/entity/records/mine?schema_id=<task> — returns only the caller's tasks via row-level scope, no extra filter.",
4072
+ },
4073
+ {
4074
+ name: "Overdue tasks (team)",
4075
+ how: "Search tasks where `due_date < now` AND `status != completed`. Elevated role only.",
4076
+ },
4077
+ ],
4078
+ build_order: [
4079
+ "1. Bootstrap.",
4080
+ "2. End-user signup + login (`pk_live_*` + JWT flow).",
4081
+ "3. `/tasks` list using `GET /v1/entity/records/mine`.",
4082
+ "4. Create / edit task.",
4083
+ "5. Coach bot.",
4084
+ ],
4085
+ notes: "Use `GET /v1/entity/records/mine` instead of POST /search with `created_by_id` filter — row-level scope is enforced server-side, the frontend MUST NOT be the filter.",
4086
+ },
4087
+ notes: {
4088
+ template: "notes",
4089
+ title: "Personal notes app",
4090
+ description: "Free-text notes per user, scoped to the author. Inspired by Apple Notes / Bear.",
4091
+ entities: [{ slug: "note", recipe_key: "note", purpose: "Notes." }],
4092
+ agents: [
4093
+ {
4094
+ name: "Notes Search",
4095
+ rag_role: "user_notes",
4096
+ purpose: "Semantic search across the user's notes. Per-user KB scoped via row-level scope on the search side.",
4097
+ },
4098
+ ],
4099
+ kbs: [
4100
+ {
4101
+ rag_role: "user_notes",
4102
+ source: "Each user's notes uploaded as one document per note. Could also be one consolidated doc per user, refreshed on each save.",
4103
+ },
4104
+ ],
4105
+ kpis: [
4106
+ { name: "Total notes", how: "GET /v1/entity/records/mine — count." },
4107
+ { name: "Pinned", how: "GET /v1/entity/records/mine with `is_pinned: true` filter." },
4108
+ ],
4109
+ build_order: [
4110
+ "1. Bootstrap + end-user auth.",
4111
+ "2. Note CRUD (`/v1/entity/records/mine` for the list).",
4112
+ "3. Search bot per user.",
4113
+ "4. Pin / tag filters.",
4114
+ ],
4115
+ },
4116
+ support_inbox: {
4117
+ template: "support_inbox",
4118
+ title: "Customer support inbox",
4119
+ description: "Capture support tickets, route to agents, let an AI first-responder handle FAQs before escalation.",
4120
+ entities: [
4121
+ {
4122
+ slug: "ticket",
4123
+ recipe_key: "task",
4124
+ purpose: "Reuse `task` recipe. Rename `task` → `ticket`. `assigned_to_id` becomes the support agent (human end-user).",
4125
+ },
4126
+ {
4127
+ slug: "message",
4128
+ recipe_key: "message",
4129
+ purpose: "Conversation thread per ticket.",
4130
+ },
4131
+ ],
4132
+ agents: [
4133
+ {
4134
+ name: "First Responder",
4135
+ rag_role: "support_kb",
4136
+ purpose: "Answers from the support knowledge base before escalating to a human.",
4137
+ },
4138
+ ],
4139
+ kbs: [
4140
+ {
4141
+ rag_role: "support_kb",
4142
+ source: "Help docs / FAQs. Upload manually or sync from your existing help center.",
4143
+ },
4144
+ ],
4145
+ kpis: [
4146
+ { name: "Open tickets", how: "Search tickets where `status != completed`." },
4147
+ {
4148
+ name: "Avg time to first response",
4149
+ how: "For each ticket, find the first `message` where `sender_role != 'user'`, compute delta. Aggregate client-side.",
4150
+ },
4151
+ ],
4152
+ build_order: [
4153
+ "1. Bootstrap.",
4154
+ "2. Public form (`/contact`) that creates a `ticket` + an initial `message`.",
4155
+ "3. Internal queue view (`/support/inbox`).",
4156
+ "4. First Responder bot, with escalation flag.",
4157
+ "5. SLA dashboard.",
4158
+ ],
4159
+ },
4160
+ };
4161
+ function APP_SCAFFOLD(template) {
4162
+ const s = APP_SCAFFOLDS[template];
4163
+ if (!s) {
4164
+ return `# Unknown template "${template}"\n\nAvailable: ${Object.keys(APP_SCAFFOLDS).join(", ")}.`;
4165
+ }
4166
+ const entitySection = s.entities
4167
+ .map((e) => `- **\`${e.slug}\`** — ${e.purpose} Get the schema recipe with \`get_entity_schema_recipe({ entity_type: "${e.recipe_key}" })\`.`)
4168
+ .join("\n");
4169
+ const agentSection = s.agents
4170
+ .map((a) => `- **${a.name}**${a.rag_role ? ` (\`rag_role: "${a.rag_role}"\`)` : ""} — ${a.purpose}`)
4171
+ .join("\n");
4172
+ const kbSection = s.kbs.length
4173
+ ? s.kbs
4174
+ .map((kb) => `- \`rag_role: "${kb.rag_role}"\` ← ${kb.source}`)
4175
+ .join("\n")
4176
+ : "_(none — agents work from conversation context)_";
4177
+ const kpiSection = s.kpis
4178
+ .map((k) => `- **${k.name}** — ${k.how}`)
4179
+ .join("\n");
4180
+ const orderSection = s.build_order.map((step) => step).join("\n");
4181
+ return `# App scaffold — ${s.title}
4182
+
4183
+ ${s.description}
4184
+
4185
+ ## Custom entities to create
4186
+
4187
+ ${entitySection}
4188
+
4189
+ ## Agents to create
4190
+
4191
+ ${agentSection}
4192
+
4193
+ ## Knowledge bases
4194
+
4195
+ ${kbSection}
4196
+
4197
+ ## Typical KPIs and how to compute them
4198
+
4199
+ ${kpiSection}
4200
+
4201
+ ## Suggested build order
4202
+
4203
+ ${orderSection}
4204
+
4205
+ ${s.notes ? `## Notes\n\n${s.notes}\n` : ""}
4206
+
4207
+ ---
4208
+
4209
+ **Next calls**:
4210
+ - For each entity above, call \`get_entity_schema_recipe({ entity_type: "..." })\` and POST the result.
4211
+ - For the chatbot wiring, call \`get_chatbot_setup_recipe()\`.
4212
+ - For the auth shape (sk_live_* / pk_live_* + cookies), call \`get_authentication_flow()\` and \`get_security_guide()\`.`;
4213
+ }
3476
4214
  // =============================================================================
3477
4215
  // MCP Server Implementation
3478
4216
  // =============================================================================
@@ -3776,6 +4514,41 @@ developer says "I want to build X" before writing code.`,
3776
4514
  required: ["app_type"],
3777
4515
  },
3778
4516
  },
4517
+ {
4518
+ name: "get_entity_schema_recipe",
4519
+ description: `Return a ready-to-POST custom entity schema for a common entity type. Removes the guesswork of building \`fields_definition\` JSON by hand and ensures the agent picks the right field types (\`string\`, \`number\`, \`boolean\`, \`reference\` per ADR-0009, \`datetime\`, ...). Pure data, no network call. Pair with \`get_reserved_schema_slugs\` to avoid native-table collisions, and with \`POST /v1/entity/records/bulk\` for a fast seed of sample rows. Available entity types: product, customer, order, post, comment, message, task, note. Each recipe ships with the matching POST body, a sample \`fields_definition\`, and \`bulk_seed_example\` if applicable.`,
4520
+ inputSchema: {
4521
+ type: "object",
4522
+ properties: {
4523
+ entity_type: {
4524
+ type: "string",
4525
+ enum: ["product", "customer", "order", "post", "comment", "message", "task", "note"],
4526
+ description: "Which common entity pattern do you want a recipe for?",
4527
+ },
4528
+ },
4529
+ required: ["entity_type"],
4530
+ },
4531
+ },
4532
+ {
4533
+ name: "get_chatbot_setup_recipe",
4534
+ description: `End-to-end recipe for wiring a chatbot agent to a knowledge base populated from your custom-entity catalog. Returns the ordered list of API calls (create agent → create KB → upload doc → bind via rag_role) plus a TypeScript snippet (server-side, sk_live_* required). Aimed at the canonical "store assistant" / "product Q&A bot" / "support deflection" use case. Pure data; no network call.`,
4535
+ inputSchema: { type: "object", properties: {}, required: [] },
4536
+ },
4537
+ {
4538
+ name: "get_app_scaffold",
4539
+ description: `Return a complete app scaffold for a common product template (\`pos\`, \`crm\`, \`blog\`, \`task_manager\`, \`notes\`, \`support_inbox\`). Each scaffold emits: the custom-entity schemas to create (with field types pre-decided), the agent(s) + KBs to set up, the typical KPIs and how to compute them (cross-schema search with \`join\` per ADR-0009), and the suggested build order. Designed to short-circuit the agent's "what should I build first" deliberation when the prompt is vibecoder-style ("armame un POS para una tiendita"). Pure data; no network call.`,
4540
+ inputSchema: {
4541
+ type: "object",
4542
+ properties: {
4543
+ template: {
4544
+ type: "string",
4545
+ enum: ["pos", "crm", "blog", "task_manager", "notes", "support_inbox"],
4546
+ description: "Which app template do you want to scaffold?",
4547
+ },
4548
+ },
4549
+ required: ["template"],
4550
+ },
4551
+ },
3779
4552
  ],
3780
4553
  };
3781
4554
  });
@@ -4829,6 +5602,23 @@ type to pick.`,
4829
5602
  ],
4830
5603
  };
4831
5604
  }
5605
+ case "get_entity_schema_recipe": {
5606
+ const entityType = args.entity_type;
5607
+ return {
5608
+ content: [{ type: "text", text: ENTITY_SCHEMA_RECIPE(entityType) }],
5609
+ };
5610
+ }
5611
+ case "get_chatbot_setup_recipe": {
5612
+ return {
5613
+ content: [{ type: "text", text: CHATBOT_SETUP_RECIPE() }],
5614
+ };
5615
+ }
5616
+ case "get_app_scaffold": {
5617
+ const template = args.template;
5618
+ return {
5619
+ content: [{ type: "text", text: APP_SCAFFOLD(template) }],
5620
+ };
5621
+ }
4832
5622
  default:
4833
5623
  throw new Error(`Unknown tool: ${name}`);
4834
5624
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genlobe/mcp-server",
3
- "version": "3.6.1",
3
+ "version": "3.7.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": {