@genlobe/mcp-server 2.2.0 → 3.1.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/README.md +86 -116
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +2203 -1474
  4. package/package.json +41 -41
package/dist/index.js CHANGED
@@ -13,17 +13,23 @@
13
13
  * the End-user endpoints, NOT the Tenant/Dashboard endpoints.
14
14
  *
15
15
  * Usage:
16
- * npx @multiagent/mcp-server
16
+ * npx @genlobe/mcp-server
17
17
  *
18
18
  * Or configure in VS Code MCP settings with SAAS_API_URL environment variable.
19
19
  */
20
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
21
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
22
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
+ import { createRequire } from "module";
23
24
  // =============================================================================
24
25
  // Configuration
25
26
  // =============================================================================
26
- const API_URL = process.env.SAAS_API_URL || process.env.API_URL || "https://api.example.com";
27
+ // Read version from package.json so every surface (banner, API_OVERVIEW,
28
+ // MCP Server metadata) stays in sync with whatever was last published.
29
+ const require = createRequire(import.meta.url);
30
+ const pkg = require("../package.json");
31
+ const SERVER_VERSION = pkg.version;
32
+ const API_URL = process.env.SAAS_API_URL || process.env.API_URL || "http://localhost:8001";
27
33
  const API_KEY = process.env.SAAS_API_KEY || process.env.API_KEY || "";
28
34
  // Cache for API responses
29
35
  const cache = new Map();
@@ -37,50 +43,65 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
37
43
  */
38
44
  const API_OVERVIEW = {
39
45
  name: "Multi-tenant SaaS API",
40
- version: "2.2.0",
41
- description: `
42
- This is a Backend-as-a-Service API for building SaaS applications.
43
-
44
- ## Architecture Overview
45
-
46
- There are TWO types of authentication contexts:
47
-
48
- ### 1. TENANT CONTEXT (Dashboard)
49
- - Used by: Tenant owners/admins managing their SaaS account
50
- - Authentication: JWT from /v1/tenant-auth/login
51
- - Purpose: Manage API keys, team members, view analytics, configure settings
52
- - These endpoints are NOT for end-user frontends
53
-
54
- ### 2. END-USER CONTEXT (API)
55
- - Used by: End-users of applications built on top of this API
56
- - Authentication: API Key (X-API-Key header) + optional User JWT
57
- - Purpose: User auth, organizations, subscriptions, billing
58
- - These endpoints ARE for building end-user frontends
59
-
60
- ## API Key Types
61
- - \`pk_live_*\`: Public key - safe to expose in frontend code (LIMITED to auth endpoints only)
62
- - \`sk_live_*\`: Secret key - backend only, never expose (FULL API access)
63
-
64
- ## Security Features (for public keys)
65
-
66
- ### Origin Validation
67
- Public keys (pk_live_*) require Origin header validation:
68
- - When creating a public key, the tenant specifies allowed origins
69
- - Requests from other origins are rejected
70
- - This prevents stolen API keys from being used on other domains
71
-
72
- ### Rate Limiting (per IP)
73
- - Register: 3/hour (blocked 30 min after exceeding)
74
- - Login: 10/15 min (blocked 15 min after exceeding)
75
- - Forgot password: 3/hour (blocked 30 min after exceeding)
76
- - Resend verification: 3/hour (blocked 30 min after exceeding)
77
-
78
- ## When Building a Frontend
79
- If you're building a frontend application for end-users, you should:
80
- 1. Use ONLY the End-user endpoints (not Tenant/Dashboard endpoints)
81
- 2. Use the public API key (pk_live_*) for client-side requests
82
- 3. Include X-API-Key header in ALL requests
83
- 4. After login, also include Authorization: Bearer <jwt> for authenticated requests
46
+ version: SERVER_VERSION,
47
+ description: `
48
+ This is a Backend-as-a-Service API for building SaaS applications.
49
+
50
+ ## Architecture Overview
51
+
52
+ There are TWO types of authentication contexts:
53
+
54
+ ### 1. TENANT CONTEXT (Dashboard)
55
+ - Used by: Tenant owners/admins managing their SaaS account
56
+ - Authentication: JWT from /v1/tenant-auth/login
57
+ - Purpose: Manage API keys, team members, view analytics, configure settings
58
+ - These endpoints are NOT for end-user frontends
59
+
60
+ ### 2. END-USER CONTEXT (API)
61
+ - Used by: End-users of applications built on top of this API
62
+ - Authentication: API Key (X-API-Key header) + optional User JWT
63
+ - Purpose: User auth, organizations, subscriptions, billing
64
+ - These endpoints ARE for building end-user frontends
65
+
66
+ ## API Key Types
67
+ - \`pk_live_*\`: Public key - safe to expose in frontend code (LIMITED to auth endpoints only)
68
+ - \`sk_live_*\`: Secret key - backend only, never expose (FULL API access)
69
+
70
+ ## API Key Organization Scope (B2B2C)
71
+
72
+ Every API key is associated with exactly one organization inside a tenant:
73
+
74
+ - **Root-org key** (tenant-wide): the key inherits the tenant's root organization. Callers MUST send \`X-Organization-Id\` to target a non-root customer organization.
75
+ - **Customer-org key** (scoped): the key is bound to a specific customer organization. The organization context is resolved automatically from the key the \`X-Organization-Id\` header is ignored for these keys.
76
+
77
+ **Implication for end-user endpoints that list "X-Organization-Id required":**
78
+ - If the request uses a root-org key → the header IS required.
79
+ - If the request uses a customer-org-scoped key → the header is optional (the key's org wins). Sending a different org_id in the header has no effect; the scope of the key is authoritative.
80
+
81
+ **Implication for \`POST /v1/auth/register\`:** the new end-user joins the API key's organization automatically as role=\`member\`. The platform no longer creates a "<email>'s Organization" per signup — that auto-spawn behavior was removed. Do not try to pass \`organization_id\` in the request body; it isn't part of the schema. The X-API-Key alone determines where the user lands.
82
+
83
+ Tenants assign keys to customer organizations from their dashboard.
84
+
85
+ ## Security Features (for public keys)
86
+
87
+ ### Origin Validation
88
+ Public keys (pk_live_*) require Origin header validation:
89
+ - When creating a public key, the tenant specifies allowed origins
90
+ - Requests from other origins are rejected
91
+ - This prevents stolen API keys from being used on other domains
92
+
93
+ ### Rate Limiting (per IP)
94
+ - Register: 3/hour (blocked 30 min after exceeding)
95
+ - Login: 10/15 min (blocked 15 min after exceeding)
96
+ - Forgot password: 3/hour (blocked 30 min after exceeding)
97
+ - Resend verification: 3/hour (blocked 30 min after exceeding)
98
+
99
+ ## When Building a Frontend
100
+ If you're building a frontend application for end-users, you should:
101
+ 1. Use ONLY the End-user endpoints (not Tenant/Dashboard endpoints)
102
+ 2. Use the public API key (pk_live_*) for client-side requests
103
+ 3. Include X-API-Key header in ALL requests
104
+ 4. After login, also include Authorization: Bearer <jwt> for authenticated requests
84
105
  `,
85
106
  base_url: API_URL,
86
107
  documentation_urls: {
@@ -128,12 +149,12 @@ const END_USER_ENDPOINTS = {
128
149
  message: "Registration successful. Please check your email to verify your account.",
129
150
  verification_required: "true"
130
151
  },
131
- note: "The response depends on whether the tenant has email verification enabled. When verification IS required, NO tokens are returned — the user must verify their email first, then login. Check for 'verification_required: true' in the response to show the appropriate UI.",
132
- example_request: `{
133
- "email": "user@example.com",
134
- "password": "SecurePass123",
135
- "display_name": "John Doe",
136
- "return_to": "https://myapp.com/verify-email"
152
+ note: "Two important things:\n\n1) The response depends on whether the tenant has email verification enabled. When verification IS required, NO tokens are returned — the user must verify their email first, then login. Check for 'verification_required: true' in the response to show the appropriate UI.\n\n2) Organization assignment is automatic from the API key (B2B2C). The new end-user joins the organization the API key is scoped to (root-org or a customer-org) as role=member. NO per-user 'personal' organization is auto-spawned anymore. Do not pass any organization_id in the body — it isn't accepted, and the routing happens server-side from the X-API-Key.",
153
+ example_request: `{
154
+ "email": "user@example.com",
155
+ "password": "SecurePass123",
156
+ "display_name": "John Doe",
157
+ "return_to": "https://myapp.com/verify-email"
137
158
  }`
138
159
  },
139
160
  {
@@ -147,9 +168,9 @@ const END_USER_ENDPOINTS = {
147
168
  password: "string (required)"
148
169
  },
149
170
  response: "Same as register",
150
- example_request: `{
151
- "email": "user@example.com",
152
- "password": "SecurePass123"
171
+ example_request: `{
172
+ "email": "user@example.com",
173
+ "password": "SecurePass123"
153
174
  }`
154
175
  },
155
176
  {
@@ -317,10 +338,10 @@ const END_USER_ENDPOINTS = {
317
338
  slug: "string (required) - URL-friendly identifier",
318
339
  description: "string (optional)"
319
340
  },
320
- example_request: `{
321
- "name": "My Company",
322
- "slug": "my-company",
323
- "description": "Our awesome company"
341
+ example_request: `{
342
+ "name": "My Company",
343
+ "slug": "my-company",
344
+ "description": "Our awesome company"
324
345
  }`
325
346
  },
326
347
  {
@@ -554,26 +575,26 @@ const END_USER_ENDPOINTS = {
554
575
  updated_at: "ISO datetime"
555
576
  }
556
577
  ],
557
- example_response: `[
558
- {
559
- "id": "550e8400-e29b-41d4-a716-446655440000",
560
- "name": "Pro Plan",
561
- "description": "For growing teams",
562
- "price_monthly": 29.99,
563
- "price_yearly": 299.99,
564
- "currency": "usd",
565
- "features": ["API Access", "Priority Support", "Advanced Analytics"],
566
- "is_active": true,
567
- "is_free": false,
568
- "max_cost_usd_per_month": 100.00,
569
- "max_messages_per_month": 1000,
570
- "max_tokens_per_month": null,
571
- "stripe_product_id": "prod_xxxxx",
572
- "stripe_price_id_monthly": "price_xxxxx",
573
- "stripe_price_id_yearly": "price_xxxxx",
574
- "created_at": "2024-01-15T10:30:00Z",
575
- "updated_at": "2024-01-15T10:30:00Z"
576
- }
578
+ example_response: `[
579
+ {
580
+ "id": "550e8400-e29b-41d4-a716-446655440000",
581
+ "name": "Pro Plan",
582
+ "description": "For growing teams",
583
+ "price_monthly": 29.99,
584
+ "price_yearly": 299.99,
585
+ "currency": "usd",
586
+ "features": ["API Access", "Priority Support", "Advanced Analytics"],
587
+ "is_active": true,
588
+ "is_free": false,
589
+ "max_cost_usd_per_month": 100.00,
590
+ "max_messages_per_month": 1000,
591
+ "max_tokens_per_month": null,
592
+ "stripe_product_id": "prod_xxxxx",
593
+ "stripe_price_id_monthly": "price_xxxxx",
594
+ "stripe_price_id_yearly": "price_xxxxx",
595
+ "created_at": "2024-01-15T10:30:00Z",
596
+ "updated_at": "2024-01-15T10:30:00Z"
597
+ }
577
598
  ]`
578
599
  },
579
600
  {
@@ -600,24 +621,24 @@ const END_USER_ENDPOINTS = {
600
621
  created_at: "ISO datetime",
601
622
  updated_at: "ISO datetime"
602
623
  },
603
- example_response: `{
604
- "id": "550e8400-e29b-41d4-a716-446655440000",
605
- "plan_id": "660e8400-e29b-41d4-a716-446655440001",
606
- "organization_id": "770e8400-e29b-41d4-a716-446655440002",
607
- "tenant_id": "880e8400-e29b-41d4-a716-446655440003",
608
- "stripe_customer_id": "cus_xxxxx",
609
- "stripe_subscription_id": "sub_xxxxx",
610
- "billing_cycle": "monthly",
611
- "status": "active",
612
- "current_period_start": "2024-01-01T00:00:00Z",
613
- "current_period_end": "2024-02-01T00:00:00Z",
614
- "cancel_at_period_end": false,
615
- "canceled_at": null,
616
- "trial_start": null,
617
- "trial_end": null,
618
- "subscription_metadata": {},
619
- "created_at": "2024-01-01T00:00:00Z",
620
- "updated_at": "2024-01-01T00:00:00Z"
624
+ example_response: `{
625
+ "id": "550e8400-e29b-41d4-a716-446655440000",
626
+ "plan_id": "660e8400-e29b-41d4-a716-446655440001",
627
+ "organization_id": "770e8400-e29b-41d4-a716-446655440002",
628
+ "tenant_id": "880e8400-e29b-41d4-a716-446655440003",
629
+ "stripe_customer_id": "cus_xxxxx",
630
+ "stripe_subscription_id": "sub_xxxxx",
631
+ "billing_cycle": "monthly",
632
+ "status": "active",
633
+ "current_period_start": "2024-01-01T00:00:00Z",
634
+ "current_period_end": "2024-02-01T00:00:00Z",
635
+ "cancel_at_period_end": false,
636
+ "canceled_at": null,
637
+ "trial_start": null,
638
+ "trial_end": null,
639
+ "subscription_metadata": {},
640
+ "created_at": "2024-01-01T00:00:00Z",
641
+ "updated_at": "2024-01-01T00:00:00Z"
621
642
  }`,
622
643
  note: "Does NOT include plan_name or price. Use plan_id to fetch plan details if needed."
623
644
  },
@@ -797,12 +818,12 @@ const END_USER_ENDPOINTS = {
797
818
  price: "string - Display price",
798
819
  message: "string | null"
799
820
  },
800
- example_request: `{
801
- "user_id": "550e8400-e29b-41d4-a716-446655440000",
802
- "organization_id": "660e8400-e29b-41d4-a716-446655440001",
803
- "plan_id": "770e8400-e29b-41d4-a716-446655440002",
804
- "success_url": "https://myapp.com/billing/success",
805
- "cancel_url": "https://myapp.com/billing/cancel"
821
+ example_request: `{
822
+ "user_id": "550e8400-e29b-41d4-a716-446655440000",
823
+ "organization_id": "660e8400-e29b-41d4-a716-446655440001",
824
+ "plan_id": "770e8400-e29b-41d4-a716-446655440002",
825
+ "success_url": "https://myapp.com/billing/success",
826
+ "cancel_url": "https://myapp.com/billing/cancel"
806
827
  }`
807
828
  },
808
829
  {
@@ -824,9 +845,9 @@ const END_USER_ENDPOINTS = {
824
845
  cancel_at_period_end: "boolean | null"
825
846
  },
826
847
  note: "Upgrades create Stripe prorations. Downgrades cancel at period end and schedule the new plan.",
827
- example_request: `{
828
- "subscription_id": "550e8400-e29b-41d4-a716-446655440000",
829
- "new_plan_id": "660e8400-e29b-41d4-a716-446655440001"
848
+ example_request: `{
849
+ "subscription_id": "550e8400-e29b-41d4-a716-446655440000",
850
+ "new_plan_id": "660e8400-e29b-41d4-a716-446655440001"
830
851
  }`
831
852
  },
832
853
  {
@@ -899,31 +920,31 @@ const END_USER_ENDPOINTS = {
899
920
  },
900
921
  error: "string | null - Error message if any"
901
922
  },
902
- example_response: `{
903
- "period_start": "2024-01-01T00:00:00Z",
904
- "period_end": "2024-02-01T00:00:00Z",
905
- "plan_name": "Pro Plan",
906
- "plan_id": "550e8400-e29b-41d4-a716-446655440000",
907
- "usage": {
908
- "messages": {
909
- "current": 450,
910
- "max": 1000,
911
- "remaining": 550,
912
- "percentage": 45.0
913
- },
914
- "cost_usd": {
915
- "current": 23.50,
916
- "max": 100.0,
917
- "remaining": 76.50,
918
- "percentage": 23.5
919
- },
920
- "tokens": {
921
- "total": 125000,
922
- "input": 75000,
923
- "output": 50000
924
- }
925
- },
926
- "error": null
923
+ example_response: `{
924
+ "period_start": "2024-01-01T00:00:00Z",
925
+ "period_end": "2024-02-01T00:00:00Z",
926
+ "plan_name": "Pro Plan",
927
+ "plan_id": "550e8400-e29b-41d4-a716-446655440000",
928
+ "usage": {
929
+ "messages": {
930
+ "current": 450,
931
+ "max": 1000,
932
+ "remaining": 550,
933
+ "percentage": 45.0
934
+ },
935
+ "cost_usd": {
936
+ "current": 23.50,
937
+ "max": 100.0,
938
+ "remaining": 76.50,
939
+ "percentage": 23.5
940
+ },
941
+ "tokens": {
942
+ "total": 125000,
943
+ "input": 75000,
944
+ "output": 50000
945
+ }
946
+ },
947
+ "error": null
927
948
  }`
928
949
  },
929
950
  {
@@ -1077,26 +1098,26 @@ const END_USER_ENDPOINTS = {
1077
1098
  execution_metadata: "object | null",
1078
1099
  created_at: "ISO datetime"
1079
1100
  },
1080
- example_request: `{
1081
- "messages": [
1082
- {"role": "user", "content": "Hello, how can you help me?"}
1083
- ],
1084
- "temperature": 0.7
1101
+ example_request: `{
1102
+ "messages": [
1103
+ {"role": "user", "content": "Hello, how can you help me?"}
1104
+ ],
1105
+ "temperature": 0.7
1085
1106
  }`,
1086
- example_response: `{
1087
- "id": "550e8400-e29b-41d4-a716-446655440000",
1088
- "agent_id": "660e8400-e29b-41d4-a716-446655440001",
1089
- "tenant_id": "770e8400-e29b-41d4-a716-446655440002",
1090
- "user_id": "880e8400-e29b-41d4-a716-446655440003",
1091
- "organization_id": "990e8400-e29b-41d4-a716-446655440004",
1092
- "input_text": "Hello, how can you help me?",
1093
- "output_text": "I'm an AI assistant designed to help you with...",
1094
- "tokens_used": 150,
1095
- "execution_time_ms": 1234,
1096
- "status": "SUCCESS",
1097
- "error_message": null,
1098
- "execution_metadata": {"model": "gpt-4o", "temperature": 0.7},
1099
- "created_at": "2024-01-30T10:30:00Z"
1107
+ example_response: `{
1108
+ "id": "550e8400-e29b-41d4-a716-446655440000",
1109
+ "agent_id": "660e8400-e29b-41d4-a716-446655440001",
1110
+ "tenant_id": "770e8400-e29b-41d4-a716-446655440002",
1111
+ "user_id": "880e8400-e29b-41d4-a716-446655440003",
1112
+ "organization_id": "990e8400-e29b-41d4-a716-446655440004",
1113
+ "input_text": "Hello, how can you help me?",
1114
+ "output_text": "I'm an AI assistant designed to help you with...",
1115
+ "tokens_used": 150,
1116
+ "execution_time_ms": 1234,
1117
+ "status": "SUCCESS",
1118
+ "error_message": null,
1119
+ "execution_metadata": {"model": "gpt-4o", "temperature": 0.7},
1120
+ "created_at": "2024-01-30T10:30:00Z"
1100
1121
  }`,
1101
1122
  note: "Usage is tracked against your plan limits. Use /v1/api/usage/current to check remaining usage."
1102
1123
  },
@@ -1507,13 +1528,13 @@ const END_USER_ENDPOINTS = {
1507
1528
  order_dir: "'asc' | 'desc' (default 'desc')"
1508
1529
  },
1509
1530
  notes: "Supported operators: eq, ne, gt, gte, lt, lte, like, ilike, in, not_in",
1510
- example_request: `{
1511
- "schema_id": "550e8400-e29b-41d4-a716-446655440000",
1512
- "query": {
1513
- "price": { "operator": "gt", "value": 100 },
1514
- "category": "electronics",
1515
- "status": { "operator": "in", "value": ["active", "pending"] }
1516
- }
1531
+ example_request: `{
1532
+ "schema_id": "550e8400-e29b-41d4-a716-446655440000",
1533
+ "query": {
1534
+ "price": { "operator": "gt", "value": 100 },
1535
+ "category": "electronics",
1536
+ "status": { "operator": "in", "value": ["active", "pending"] }
1537
+ }
1517
1538
  }`
1518
1539
  }
1519
1540
  ]
@@ -1706,7 +1727,7 @@ const END_USER_ENDPOINTS = {
1706
1727
  endpoints: [
1707
1728
  {
1708
1729
  method: "POST",
1709
- path: "/v1/external-agents/{agent_id}/calls",
1730
+ path: "/v1/user/external-agents/{agent_id}/calls",
1710
1731
  summary: "Register an external agent call",
1711
1732
  auth: { api_key: "sk_live_*", jwt: true },
1712
1733
  rate_limit: "120 per minute per IP",
@@ -1721,6 +1742,7 @@ const END_USER_ENDPOINTS = {
1721
1742
  total_tokens: "integer (optional) - Total tokens. If not provided, calculated as input + output",
1722
1743
  cost_usd: "number (optional, default: 0.0) - Cost in USD",
1723
1744
  duration_ms: "integer (optional, default: 0) - Call duration in milliseconds",
1745
+ model_name: "string (optional) - Model used (e.g., openai/gpt-4o). If provided, cost is calculated from provider rates instead of flat rate",
1724
1746
  status: "string (optional, default: 'SUCCESS') - 'SUCCESS' or 'ERROR'",
1725
1747
  error_message: "string (optional) - Error message if status is ERROR",
1726
1748
  call_metadata: "object (optional) - Arbitrary metadata about the call"
@@ -1741,7 +1763,60 @@ const END_USER_ENDPOINTS = {
1741
1763
  call_metadata: "object | null",
1742
1764
  created_at: "ISO datetime"
1743
1765
  },
1744
- note: "Does NOT add to tenant billing the tenant pays the AI provider directly. This endpoint is for tracking/analytics only."
1766
+ note: "Adds a fixed cost (EXTERNAL_AGENT_REQUEST_COST env var, default $0.01) to tenant billing. If model_name is provided with tokens > 0, cost is calculated from provider rates instead."
1767
+ },
1768
+ {
1769
+ method: "POST",
1770
+ path: "/v1/user/external-agents/{agent_id}/usage",
1771
+ summary: "Register usage metrics for an external agent",
1772
+ auth: { api_key: "sk_live_*", jwt: true },
1773
+ rate_limit: "120 per minute per IP",
1774
+ path_params: {
1775
+ agent_id: "uuid (required) - The external agent ID"
1776
+ },
1777
+ request_body: {
1778
+ input_tokens: "integer (optional, default: 0) - Number of input tokens consumed",
1779
+ output_tokens: "integer (optional, default: 0) - Number of output tokens consumed",
1780
+ cost_usd: "number (optional) - Explicit cost in USD. If omitted, calculated from model rates or flat rate",
1781
+ model_name: "string (optional) - Model used (e.g., openai/gpt-4o)",
1782
+ provider: "string (optional) - Provider name (e.g., openai, anthropic). Defaults to 'external_agent'",
1783
+ metadata: "object (optional) - Arbitrary metadata"
1784
+ },
1785
+ response: {
1786
+ tracked: "boolean - Whether usage was tracked successfully",
1787
+ tokens: "integer - Total tokens tracked",
1788
+ cost_usd: "number - Cost tracked in USD",
1789
+ model_name: "string - Model name used for tracking",
1790
+ provider: "string - Provider name used for tracking"
1791
+ },
1792
+ note: "Use this for batch usage reporting or updating token counts without creating a full call record. Tracks tokens and cost in tenant billing."
1793
+ },
1794
+ {
1795
+ method: "POST",
1796
+ path: "/v1/user/external-agents/{agent_id}/execute",
1797
+ summary: "Execute an external agent via its configured webhook",
1798
+ auth: { api_key: "sk_live_*", jwt: true },
1799
+ rate_limit: "60 per minute per IP",
1800
+ path_params: {
1801
+ agent_id: "uuid (required) - The external agent ID"
1802
+ },
1803
+ request_body: {
1804
+ input: "string (required) - Input text forwarded to the external agent",
1805
+ metadata: "object (optional) - Arbitrary context forwarded to the webhook"
1806
+ },
1807
+ response: {
1808
+ output: "string - Agent response text",
1809
+ input_tokens: "integer - Input tokens reported by the webhook",
1810
+ output_tokens: "integer - Output tokens reported by the webhook",
1811
+ total_tokens: "integer - input + output",
1812
+ duration_ms: "integer - Execution time measured by Genlobe",
1813
+ cost_usd: "number - Platform fee registered for this call",
1814
+ model_name: "string | null - Model reported by the webhook",
1815
+ status: "string - SUCCESS or ERROR",
1816
+ error_message: "string | null",
1817
+ call_id: "uuid | null - Registered call record"
1818
+ },
1819
+ note: "Requires the external agent to have webhook_url configured. Genlobe POSTs {agent_id, input, metadata} to the webhook and expects {output, input_tokens?, output_tokens?, model_name?} in the JSON response."
1745
1820
  }
1746
1821
  ]
1747
1822
  }
@@ -1749,619 +1824,619 @@ const END_USER_ENDPOINTS = {
1749
1824
  // =============================================================================
1750
1825
  // TypeScript/JavaScript SDK Template
1751
1826
  // =============================================================================
1752
- const SDK_TEMPLATE_TYPESCRIPT = `/**
1753
- * Multi-tenant SaaS API Client
1754
- *
1755
- * Usage:
1756
- * const api = new ApiClient('pk_live_your_key_here', 'https://api.yoursaas.com');
1757
- *
1758
- * // Login
1759
- * const { access_token, user } = await api.auth.login('email@example.com', 'password');
1760
- *
1761
- * // Set token for authenticated requests
1762
- * api.setAccessToken(access_token);
1763
- *
1764
- * // Get user's organizations
1765
- * const orgs = await api.organizations.getMyOrganizations();
1766
- */
1767
-
1768
- interface TokenResponse {
1769
- access_token: string;
1770
- refresh_token: string;
1771
- token_type: string;
1772
- expires_in: number;
1773
- user: {
1774
- id: string;
1775
- email: string;
1776
- display_name: string | null;
1777
- avatar_url: string | null;
1778
- is_active: boolean;
1779
- created_at: string;
1780
- };
1781
- }
1782
-
1783
- interface Organization {
1784
- id: string;
1785
- name: string;
1786
- slug: string;
1787
- description?: string;
1788
- logo_url?: string;
1789
- role: 'owner' | 'admin' | 'member' | 'developer';
1790
- created_at: string;
1791
- }
1792
-
1793
- interface ApiError {
1794
- detail: string;
1795
- error?: string;
1796
- error_description?: string;
1797
- }
1798
-
1799
- class ApiClient {
1800
- private apiKey: string;
1801
- private baseUrl: string;
1802
- private accessToken: string | null = null;
1803
- private refreshToken: string | null = null;
1804
-
1805
- constructor(apiKey: string, baseUrl: string) {
1806
- if (!apiKey) throw new Error('API key is required');
1807
- if (!baseUrl) throw new Error('Base URL is required');
1808
- this.apiKey = apiKey;
1809
- this.baseUrl = baseUrl.replace(/\\/$/, ''); // Remove trailing slash
1810
- }
1811
-
1812
- setAccessToken(token: string) {
1813
- this.accessToken = token;
1814
- }
1815
-
1816
- setRefreshToken(token: string) {
1817
- this.refreshToken = token;
1818
- }
1819
-
1820
- private organizationId: string | null = null;
1821
-
1822
- setOrganizationId(orgId: string) {
1823
- this.organizationId = orgId;
1824
- }
1825
-
1826
- private async request<T>(
1827
- method: string,
1828
- endpoint: string,
1829
- body?: any,
1830
- requiresAuth = false
1831
- ): Promise<T> {
1832
- const headers: Record<string, string> = {
1833
- 'X-API-Key': this.apiKey,
1834
- 'Content-Type': 'application/json',
1835
- };
1836
-
1837
- if (requiresAuth && this.accessToken) {
1838
- headers['Authorization'] = \`Bearer \${this.accessToken}\`;
1839
- }
1840
-
1841
- if (this.organizationId) {
1842
- headers['X-Organization-Id'] = this.organizationId;
1843
- }
1844
-
1845
- const response = await fetch(\`\${this.baseUrl}\${endpoint}\`, {
1846
- method,
1847
- headers,
1848
- body: body ? JSON.stringify(body) : undefined,
1849
- });
1850
-
1851
- if (!response.ok) {
1852
- const error: ApiError = await response.json();
1853
- throw new Error(error.detail || \`Request failed: \${response.status}\`);
1854
- }
1855
-
1856
- return response.json();
1857
- }
1858
-
1859
- // Auth endpoints
1860
- auth = {
1861
- register: async (email: string, password: string, displayName?: string) => {
1862
- const result = await this.request<TokenResponse>('POST', '/v1/auth/register', {
1863
- email,
1864
- password,
1865
- display_name: displayName,
1866
- });
1867
- this.accessToken = result.access_token;
1868
- this.refreshToken = result.refresh_token;
1869
- return result;
1870
- },
1871
-
1872
- login: async (email: string, password: string) => {
1873
- const result = await this.request<TokenResponse>('POST', '/v1/auth/login', {
1874
- email,
1875
- password,
1876
- });
1877
- this.accessToken = result.access_token;
1878
- this.refreshToken = result.refresh_token;
1879
- return result;
1880
- },
1881
-
1882
- logout: async () => {
1883
- await this.request('POST', '/v1/auth/logout', undefined, true);
1884
- this.accessToken = null;
1885
- this.refreshToken = null;
1886
- },
1887
-
1888
- me: async () => {
1889
- return this.request<TokenResponse['user']>('GET', '/v1/auth/me', undefined, true);
1890
- },
1891
-
1892
- refresh: async () => {
1893
- if (!this.refreshToken) throw new Error('No refresh token available');
1894
- const result = await this.request<TokenResponse>('POST', '/v1/auth/refresh', {
1895
- refresh_token: this.refreshToken,
1896
- });
1897
- this.accessToken = result.access_token;
1898
- return result;
1899
- },
1900
-
1901
- forgotPassword: async (email: string) => {
1902
- return this.request('POST', '/v1/auth/forgot-password', { email });
1903
- },
1904
-
1905
- resetPassword: async (token: string, newPassword: string) => {
1906
- return this.request('POST', '/v1/auth/reset-password', {
1907
- token,
1908
- new_password: newPassword,
1909
- });
1910
- },
1911
-
1912
- changePassword: async (currentPassword: string, newPassword: string) => {
1913
- return this.request('POST', '/v1/auth/change-password', {
1914
- current_password: currentPassword,
1915
- new_password: newPassword,
1916
- });
1917
- },
1918
-
1919
- verifyEmail: async (token: string) => {
1920
- return this.request('POST', '/v1/auth/verify-email', { token });
1921
- },
1922
-
1923
- resendVerification: async (email: string) => {
1924
- return this.request('POST', '/v1/auth/resend-verification', { email });
1925
- },
1926
-
1927
- getProviders: async () => {
1928
- return this.request<{ providers: Array<{ provider: string; name: string }> }>(
1929
- 'GET', '/v1/auth/providers'
1930
- );
1931
- },
1932
-
1933
- getGoogleAuthUrl: async (redirectUri: string) => {
1934
- return this.request<{ authorization_url: string }>(
1935
- 'GET', \`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`
1936
- );
1937
- },
1938
- };
1939
-
1940
- // Agent endpoints
1941
- agents = {
1942
- getAvailable: async (skip = 0, limit = 50) => {
1943
- return this.request(
1944
- 'GET',
1945
- \`/v1/user/agents/available?skip=\${skip}&limit=\${limit}\`,
1946
- undefined,
1947
- true
1948
- );
1949
- },
1950
-
1951
- get: async (agentId: string) => {
1952
- return this.request('GET', \`/v1/user/agents/\${agentId}\`, undefined, true);
1953
- },
1954
-
1955
- chat: async (agentId: string, messages: Array<{ role: string; content: string }>, options?: { temperature?: number; model?: string; conversation_id?: string }) => {
1956
- return this.request('POST', \`/v1/user/agents/\${agentId}/chat\`, {
1957
- messages,
1958
- ...options,
1959
- }, true);
1960
- },
1961
-
1962
- getHistory: async (agentId: string, skip = 0, limit = 20) => {
1963
- return this.request(
1964
- 'GET',
1965
- \`/v1/user/agents/\${agentId}/history?skip=\${skip}&limit=\${limit}\`,
1966
- undefined,
1967
- true
1968
- );
1969
- },
1970
-
1971
- createConversation: async (agentId: string, title?: string) => {
1972
- return this.request('POST', \`/v1/user/agents/\${agentId}/conversations\`, {
1973
- title,
1974
- }, true);
1975
- },
1976
-
1977
- listConversations: async (agentId: string, skip = 0, limit = 20) => {
1978
- return this.request(
1979
- 'GET',
1980
- \`/v1/user/agents/\${agentId}/conversations?skip=\${skip}&limit=\${limit}\`,
1981
- undefined,
1982
- true
1983
- );
1984
- },
1985
-
1986
- getConversation: async (agentId: string, conversationId: string) => {
1987
- return this.request(
1988
- 'GET',
1989
- \`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
1990
- undefined,
1991
- true
1992
- );
1993
- },
1994
-
1995
- endConversation: async (agentId: string, conversationId: string) => {
1996
- return this.request(
1997
- 'DELETE',
1998
- \`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
1999
- undefined,
2000
- true
2001
- );
2002
- },
2003
- };
2004
-
2005
- // Organization endpoints
2006
- organizations = {
2007
- getMyOrganizations: async () => {
2008
- return this.request<{ organizations: Organization[] }>(
2009
- 'GET',
2010
- '/v1/organizations/my-organizations',
2011
- undefined,
2012
- true
2013
- );
2014
- },
2015
-
2016
- create: async (name: string, slug: string, description?: string) => {
2017
- return this.request<Organization>(
2018
- 'POST',
2019
- '/v1/organizations',
2020
- { name, slug, description },
2021
- true
2022
- );
2023
- },
2024
-
2025
- get: async (id: string) => {
2026
- return this.request<Organization>(
2027
- 'GET',
2028
- \`/v1/organizations/\${id}\`,
2029
- undefined,
2030
- true
2031
- );
2032
- },
2033
-
2034
- update: async (id: string, data: { name?: string; description?: string }) => {
2035
- return this.request<Organization>(
2036
- 'PUT',
2037
- \`/v1/organizations/\${id}\`,
2038
- data,
2039
- true
2040
- );
2041
- },
2042
-
2043
- getMembers: async (id: string) => {
2044
- return this.request(
2045
- 'GET',
2046
- \`/v1/organizations/\${id}/members\`,
2047
- undefined,
2048
- true
2049
- );
2050
- },
2051
-
2052
- invite: async (id: string, email: string, role: 'admin' | 'member' | 'developer') => {
2053
- return this.request(
2054
- 'POST',
2055
- \`/v1/organizations/\${id}/invite\`,
2056
- { email, role },
2057
- true
2058
- );
2059
- },
2060
-
2061
- acceptInvitation: async (token: string) => {
2062
- return this.request(
2063
- 'POST',
2064
- '/v1/organizations/accept-invitation',
2065
- { token },
2066
- true
2067
- );
2068
- },
2069
-
2070
- rejectInvitation: async (token: string) => {
2071
- return this.request(
2072
- 'POST',
2073
- '/v1/organizations/reject-invitation',
2074
- { token }
2075
- );
2076
- },
2077
-
2078
- updateMemberRole: async (orgId: string, userId: string, role: 'admin' | 'member' | 'developer') => {
2079
- return this.request(
2080
- 'PUT',
2081
- \`/v1/organizations/\${orgId}/members/\${userId}\`,
2082
- { role },
2083
- true
2084
- );
2085
- },
2086
-
2087
- removeMember: async (orgId: string, userId: string) => {
2088
- return this.request(
2089
- 'DELETE',
2090
- \`/v1/organizations/\${orgId}/members/\${userId}\`,
2091
- undefined,
2092
- true
2093
- );
2094
- },
2095
-
2096
- deactivateMember: async (orgId: string, userId: string) => {
2097
- return this.request(
2098
- 'POST',
2099
- \`/v1/organizations/\${orgId}/members/\${userId}/deactivate\`,
2100
- undefined,
2101
- true
2102
- );
2103
- },
2104
-
2105
- setDefault: async (orgId: string) => {
2106
- return this.request(
2107
- 'POST',
2108
- \`/v1/organizations/\${orgId}/set-default\`,
2109
- undefined,
2110
- true
2111
- );
2112
- },
2113
-
2114
- getSubscription: async (orgId: string) => {
2115
- return this.request('GET', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
2116
- },
2117
-
2118
- subscribe: async (orgId: string, planId: string) => {
2119
- return this.request('POST', \`/v1/organizations/\${orgId}/subscription/\${planId}\`, undefined, true);
2120
- },
2121
-
2122
- cancelSubscription: async (orgId: string) => {
2123
- return this.request('DELETE', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
2124
- },
2125
-
2126
- getUsage: async (orgId: string) => {
2127
- return this.request('GET', \`/v1/organizations/\${orgId}/usage\`, undefined, true);
2128
- },
2129
- };
2130
-
2131
- // Subscription endpoints
2132
- subscriptions = {
2133
- getPlans: async () => {
2134
- return this.request('GET', '/v1/subscriptions/plans', undefined, true);
2135
- },
2136
-
2137
- getCurrent: async () => {
2138
- return this.request('GET', '/v1/subscriptions/current', undefined, true);
2139
- },
2140
-
2141
- checkout: async (planId: string, successUrl: string, cancelUrl: string) => {
2142
- return this.request<{ checkout_url: string; session_id: string }>(
2143
- 'POST',
2144
- '/v1/subscriptions/checkout',
2145
- {
2146
- plan_id: planId,
2147
- success_url: successUrl,
2148
- cancel_url: cancelUrl,
2149
- },
2150
- true
2151
- );
2152
- },
2153
-
2154
- getById: async (subscriptionId: string) => {
2155
- return this.request('GET', \`/v1/subscriptions/subscriptions/\${subscriptionId}\`, undefined, true);
2156
- },
2157
-
2158
- cancel: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
2159
- return this.request(
2160
- 'DELETE',
2161
- \`/v1/subscriptions/subscriptions/\${subscriptionId}?cancel_at_period_end=\${cancelAtPeriodEnd}\`,
2162
- undefined,
2163
- true
2164
- );
2165
- },
2166
- };
2167
-
2168
- // Billing endpoints (sk_live_* only)
2169
- billing = {
2170
- getAvailablePlans: async () => {
2171
- return this.request('GET', '/v1/billing/available-plans', undefined, true);
2172
- },
2173
-
2174
- createCheckoutSession: async (data: { user_id: string; organization_id: string; plan_id: string; success_url: string; cancel_url: string }) => {
2175
- return this.request<{ checkout_url: string; session_id: string; plan_name: string; price: string }>('POST', '/v1/billing/create-checkout-session', data, true);
2176
- },
2177
-
2178
- changePlan: async (data: { subscription_id: string; new_plan_id: string }) => {
2179
- return this.request('POST', '/v1/billing/change-plan', data, true);
2180
- },
2181
-
2182
- cancelSubscription: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
2183
- return this.request('POST', \`/v1/billing/cancel-subscription?subscription_id=\${subscriptionId}&cancel_at_period_end=\${cancelAtPeriodEnd}\`, undefined, true);
2184
- },
2185
-
2186
- getBillingHistory: async (organizationId: string, limit = 20) => {
2187
- return this.request('GET', \`/v1/billing/billing-history-org/\${organizationId}?limit=\${limit}\`, undefined, true);
2188
- },
2189
- };
2190
-
2191
- // Plans endpoints (JWT required)
2192
- plans = {
2193
- list: async (skip = 0, limit = 100, includeInactive = false) => {
2194
- return this.request('GET', \`/v1/plans?skip=\${skip}&limit=\${limit}&include_inactive=\${includeInactive}\`, undefined, true);
2195
- },
2196
-
2197
- getActive: async () => {
2198
- return this.request('GET', '/v1/plans/active', undefined, true);
2199
- },
2200
-
2201
- getFree: async () => {
2202
- return this.request('GET', '/v1/plans/free', undefined, true);
2203
- },
2204
-
2205
- getById: async (planId: string) => {
2206
- return this.request('GET', \`/v1/plans/\${planId}\`, undefined, true);
2207
- },
2208
- };
2209
-
2210
- // User management endpoints
2211
- users = {
2212
- list: async (page = 1, perPage = 20, includeInactive = false) => {
2213
- return this.request('GET', \`/v1/users?page=\${page}&per_page=\${perPage}&include_inactive=\${includeInactive}\`, undefined, true);
2214
- },
2215
-
2216
- stats: async () => {
2217
- return this.request('GET', '/v1/users/stats', undefined, true);
2218
- },
2219
-
2220
- get: async (userId: string, includeSettings = false) => {
2221
- return this.request('GET', \`/v1/users/\${userId}?include_settings=\${includeSettings}\`, undefined, true);
2222
- },
2223
-
2224
- update: async (userId: string, data: { display_name?: string; avatar_url?: string; locale?: string }) => {
2225
- return this.request('PUT', \`/v1/users/\${userId}\`, data, true);
2226
- },
2227
-
2228
- updateSettings: async (userId: string, settings: any) => {
2229
- return this.request('PUT', \`/v1/users/\${userId}/settings\`, settings, true);
2230
- },
2231
-
2232
- deactivate: async (userId: string) => {
2233
- return this.request('POST', \`/v1/users/\${userId}/deactivate\`, undefined, true);
2234
- },
2235
-
2236
- activate: async (userId: string) => {
2237
- return this.request('POST', \`/v1/users/\${userId}/activate\`, undefined, true);
2238
- },
2239
-
2240
- delete: async (userId: string) => {
2241
- return this.request('DELETE', \`/v1/users/\${userId}\`, undefined, true);
2242
- },
2243
- };
2244
-
2245
- // Usage endpoints
2246
- usage = {
2247
- getCurrent: async (organizationId: string) => {
2248
- return this.request('GET', \`/v1/api/usage/current?organization_id=\${organizationId}\`, undefined, true);
2249
- },
2250
-
2251
- getHistory: async (organizationId: string, limit = 20, offset = 0) => {
2252
- return this.request(
2253
- 'GET',
2254
- \`/v1/api/usage/history?organization_id=\${organizationId}&limit=\${limit}&offset=\${offset}\`,
2255
- undefined,
2256
- true
2257
- );
2258
- },
2259
-
2260
- getEntityUsage: async (organizationId: string) => {
2261
- return this.request('GET', \`/v1/api/usage/entity-usage?organization_id=\${organizationId}\`, undefined, true);
2262
- },
2263
-
2264
- getStats: async (organizationId: string) => {
2265
- return this.request('GET', \`/v1/usage/stats?organization_id=\${organizationId}\`, undefined, true);
2266
- },
2267
-
2268
- check: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
2269
- return this.request('POST', '/v1/usage/check', data, true);
2270
- },
2271
-
2272
- consume: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
2273
- return this.request('POST', '/v1/usage/consume', data, true);
2274
- },
2275
- };
2276
-
2277
- // Entity Schema endpoints
2278
- entitySchemas = {
2279
- list: async (page = 1, size = 20) => {
2280
- return this.request('GET', \`/v1/entity/schemas?page=\${page}&size=\${size}\`, undefined, true);
2281
- },
2282
-
2283
- create: async (data: { name: string; slug: string; description?: string; fields_definition: any }) => {
2284
- return this.request('POST', '/v1/entity/schemas', data, true);
2285
- },
2286
-
2287
- get: async (schemaId: string) => {
2288
- return this.request('GET', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
2289
- },
2290
-
2291
- update: async (schemaId: string, data: { name?: string; description?: string; fields_definition?: any; is_active?: boolean }) => {
2292
- return this.request('PUT', \`/v1/entity/schemas/\${schemaId}\`, data, true);
2293
- },
2294
-
2295
- delete: async (schemaId: string) => {
2296
- return this.request('DELETE', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
2297
- },
2298
- };
2299
-
2300
- // Entity Record endpoints
2301
- entityRecords = {
2302
- create: async (schemaId: string, data: any) => {
2303
- return this.request('POST', '/v1/entity/records', { schema_id: schemaId, data }, true);
2304
- },
2305
-
2306
- get: async (recordId: string) => {
2307
- return this.request('GET', \`/v1/entity/records/\${recordId}\`, undefined, true);
2308
- },
2309
-
2310
- update: async (recordId: string, data: any) => {
2311
- return this.request('PUT', \`/v1/entity/records/\${recordId}\`, { data }, true);
2312
- },
2313
-
2314
- delete: async (recordId: string) => {
2315
- return this.request('DELETE', \`/v1/entity/records/\${recordId}\`, undefined, true);
2316
- },
2317
-
2318
- search: async (searchReq: { schema_id?: string; query?: any; page?: number; size?: number; order_by?: string; order_dir?: string }) => {
2319
- const params = new URLSearchParams();
2320
- if (searchReq.page) params.set('page', String(searchReq.page));
2321
- if (searchReq.size) params.set('size', String(searchReq.size));
2322
- if (searchReq.order_by) params.set('order_by', searchReq.order_by);
2323
- if (searchReq.order_dir) params.set('order_dir', searchReq.order_dir);
2324
- return this.request('POST', \`/v1/entity/records/search?\${params}\`, { schema_id: searchReq.schema_id, query: searchReq.query }, true);
2325
- },
2326
- };
2327
-
2328
- // Department endpoints
2329
- departments = {
2330
- get: async (departmentId: string, userId: string) => {
2331
- return this.request('GET', \`/v1/departments/\${departmentId}?user_id=\${userId}\`, undefined, true);
2332
- },
2333
- };
2334
- }
2335
-
2336
- export { ApiClient, TokenResponse, Organization, ApiError };
1827
+ const SDK_TEMPLATE_TYPESCRIPT = `/**
1828
+ * Multi-tenant SaaS API Client
1829
+ *
1830
+ * Usage:
1831
+ * const api = new ApiClient('pk_live_your_key_here', 'https://api.yoursaas.com');
1832
+ *
1833
+ * // Login
1834
+ * const { access_token, user } = await api.auth.login('email@example.com', 'password');
1835
+ *
1836
+ * // Set token for authenticated requests
1837
+ * api.setAccessToken(access_token);
1838
+ *
1839
+ * // Get user's organizations
1840
+ * const orgs = await api.organizations.getMyOrganizations();
1841
+ */
1842
+
1843
+ interface TokenResponse {
1844
+ access_token: string;
1845
+ refresh_token: string;
1846
+ token_type: string;
1847
+ expires_in: number;
1848
+ user: {
1849
+ id: string;
1850
+ email: string;
1851
+ display_name: string | null;
1852
+ avatar_url: string | null;
1853
+ is_active: boolean;
1854
+ created_at: string;
1855
+ };
1856
+ }
1857
+
1858
+ interface Organization {
1859
+ id: string;
1860
+ name: string;
1861
+ slug: string;
1862
+ description?: string;
1863
+ logo_url?: string;
1864
+ role: 'owner' | 'admin' | 'member' | 'developer';
1865
+ created_at: string;
1866
+ }
1867
+
1868
+ interface ApiError {
1869
+ detail: string;
1870
+ error?: string;
1871
+ error_description?: string;
1872
+ }
1873
+
1874
+ class ApiClient {
1875
+ private apiKey: string;
1876
+ private baseUrl: string;
1877
+ private accessToken: string | null = null;
1878
+ private refreshToken: string | null = null;
1879
+
1880
+ constructor(apiKey: string, baseUrl: string) {
1881
+ if (!apiKey) throw new Error('API key is required');
1882
+ if (!baseUrl) throw new Error('Base URL is required');
1883
+ this.apiKey = apiKey;
1884
+ this.baseUrl = baseUrl.replace(/\\/$/, ''); // Remove trailing slash
1885
+ }
1886
+
1887
+ setAccessToken(token: string) {
1888
+ this.accessToken = token;
1889
+ }
1890
+
1891
+ setRefreshToken(token: string) {
1892
+ this.refreshToken = token;
1893
+ }
1894
+
1895
+ private organizationId: string | null = null;
1896
+
1897
+ setOrganizationId(orgId: string) {
1898
+ this.organizationId = orgId;
1899
+ }
1900
+
1901
+ private async request<T>(
1902
+ method: string,
1903
+ endpoint: string,
1904
+ body?: any,
1905
+ requiresAuth = false
1906
+ ): Promise<T> {
1907
+ const headers: Record<string, string> = {
1908
+ 'X-API-Key': this.apiKey,
1909
+ 'Content-Type': 'application/json',
1910
+ };
1911
+
1912
+ if (requiresAuth && this.accessToken) {
1913
+ headers['Authorization'] = \`Bearer \${this.accessToken}\`;
1914
+ }
1915
+
1916
+ if (this.organizationId) {
1917
+ headers['X-Organization-Id'] = this.organizationId;
1918
+ }
1919
+
1920
+ const response = await fetch(\`\${this.baseUrl}\${endpoint}\`, {
1921
+ method,
1922
+ headers,
1923
+ body: body ? JSON.stringify(body) : undefined,
1924
+ });
1925
+
1926
+ if (!response.ok) {
1927
+ const error: ApiError = await response.json();
1928
+ throw new Error(error.detail || \`Request failed: \${response.status}\`);
1929
+ }
1930
+
1931
+ return response.json();
1932
+ }
1933
+
1934
+ // Auth endpoints
1935
+ auth = {
1936
+ register: async (email: string, password: string, displayName?: string) => {
1937
+ const result = await this.request<TokenResponse>('POST', '/v1/auth/register', {
1938
+ email,
1939
+ password,
1940
+ display_name: displayName,
1941
+ });
1942
+ this.accessToken = result.access_token;
1943
+ this.refreshToken = result.refresh_token;
1944
+ return result;
1945
+ },
1946
+
1947
+ login: async (email: string, password: string) => {
1948
+ const result = await this.request<TokenResponse>('POST', '/v1/auth/login', {
1949
+ email,
1950
+ password,
1951
+ });
1952
+ this.accessToken = result.access_token;
1953
+ this.refreshToken = result.refresh_token;
1954
+ return result;
1955
+ },
1956
+
1957
+ logout: async () => {
1958
+ await this.request('POST', '/v1/auth/logout', undefined, true);
1959
+ this.accessToken = null;
1960
+ this.refreshToken = null;
1961
+ },
1962
+
1963
+ me: async () => {
1964
+ return this.request<TokenResponse['user']>('GET', '/v1/auth/me', undefined, true);
1965
+ },
1966
+
1967
+ refresh: async () => {
1968
+ if (!this.refreshToken) throw new Error('No refresh token available');
1969
+ const result = await this.request<TokenResponse>('POST', '/v1/auth/refresh', {
1970
+ refresh_token: this.refreshToken,
1971
+ });
1972
+ this.accessToken = result.access_token;
1973
+ return result;
1974
+ },
1975
+
1976
+ forgotPassword: async (email: string) => {
1977
+ return this.request('POST', '/v1/auth/forgot-password', { email });
1978
+ },
1979
+
1980
+ resetPassword: async (token: string, newPassword: string) => {
1981
+ return this.request('POST', '/v1/auth/reset-password', {
1982
+ token,
1983
+ new_password: newPassword,
1984
+ });
1985
+ },
1986
+
1987
+ changePassword: async (currentPassword: string, newPassword: string) => {
1988
+ return this.request('POST', '/v1/auth/change-password', {
1989
+ current_password: currentPassword,
1990
+ new_password: newPassword,
1991
+ });
1992
+ },
1993
+
1994
+ verifyEmail: async (token: string) => {
1995
+ return this.request('POST', '/v1/auth/verify-email', { token });
1996
+ },
1997
+
1998
+ resendVerification: async (email: string) => {
1999
+ return this.request('POST', '/v1/auth/resend-verification', { email });
2000
+ },
2001
+
2002
+ getProviders: async () => {
2003
+ return this.request<{ providers: Array<{ provider: string; name: string }> }>(
2004
+ 'GET', '/v1/auth/providers'
2005
+ );
2006
+ },
2007
+
2008
+ getGoogleAuthUrl: async (redirectUri: string) => {
2009
+ return this.request<{ authorization_url: string }>(
2010
+ 'GET', \`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`
2011
+ );
2012
+ },
2013
+ };
2014
+
2015
+ // Agent endpoints
2016
+ agents = {
2017
+ getAvailable: async (skip = 0, limit = 50) => {
2018
+ return this.request(
2019
+ 'GET',
2020
+ \`/v1/user/agents/available?skip=\${skip}&limit=\${limit}\`,
2021
+ undefined,
2022
+ true
2023
+ );
2024
+ },
2025
+
2026
+ get: async (agentId: string) => {
2027
+ return this.request('GET', \`/v1/user/agents/\${agentId}\`, undefined, true);
2028
+ },
2029
+
2030
+ chat: async (agentId: string, messages: Array<{ role: string; content: string }>, options?: { temperature?: number; model?: string; conversation_id?: string }) => {
2031
+ return this.request('POST', \`/v1/user/agents/\${agentId}/chat\`, {
2032
+ messages,
2033
+ ...options,
2034
+ }, true);
2035
+ },
2036
+
2037
+ getHistory: async (agentId: string, skip = 0, limit = 20) => {
2038
+ return this.request(
2039
+ 'GET',
2040
+ \`/v1/user/agents/\${agentId}/history?skip=\${skip}&limit=\${limit}\`,
2041
+ undefined,
2042
+ true
2043
+ );
2044
+ },
2045
+
2046
+ createConversation: async (agentId: string, title?: string) => {
2047
+ return this.request('POST', \`/v1/user/agents/\${agentId}/conversations\`, {
2048
+ title,
2049
+ }, true);
2050
+ },
2051
+
2052
+ listConversations: async (agentId: string, skip = 0, limit = 20) => {
2053
+ return this.request(
2054
+ 'GET',
2055
+ \`/v1/user/agents/\${agentId}/conversations?skip=\${skip}&limit=\${limit}\`,
2056
+ undefined,
2057
+ true
2058
+ );
2059
+ },
2060
+
2061
+ getConversation: async (agentId: string, conversationId: string) => {
2062
+ return this.request(
2063
+ 'GET',
2064
+ \`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
2065
+ undefined,
2066
+ true
2067
+ );
2068
+ },
2069
+
2070
+ endConversation: async (agentId: string, conversationId: string) => {
2071
+ return this.request(
2072
+ 'DELETE',
2073
+ \`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
2074
+ undefined,
2075
+ true
2076
+ );
2077
+ },
2078
+ };
2079
+
2080
+ // Organization endpoints
2081
+ organizations = {
2082
+ getMyOrganizations: async () => {
2083
+ return this.request<{ organizations: Organization[] }>(
2084
+ 'GET',
2085
+ '/v1/organizations/my-organizations',
2086
+ undefined,
2087
+ true
2088
+ );
2089
+ },
2090
+
2091
+ create: async (name: string, slug: string, description?: string) => {
2092
+ return this.request<Organization>(
2093
+ 'POST',
2094
+ '/v1/organizations',
2095
+ { name, slug, description },
2096
+ true
2097
+ );
2098
+ },
2099
+
2100
+ get: async (id: string) => {
2101
+ return this.request<Organization>(
2102
+ 'GET',
2103
+ \`/v1/organizations/\${id}\`,
2104
+ undefined,
2105
+ true
2106
+ );
2107
+ },
2108
+
2109
+ update: async (id: string, data: { name?: string; description?: string }) => {
2110
+ return this.request<Organization>(
2111
+ 'PUT',
2112
+ \`/v1/organizations/\${id}\`,
2113
+ data,
2114
+ true
2115
+ );
2116
+ },
2117
+
2118
+ getMembers: async (id: string) => {
2119
+ return this.request(
2120
+ 'GET',
2121
+ \`/v1/organizations/\${id}/members\`,
2122
+ undefined,
2123
+ true
2124
+ );
2125
+ },
2126
+
2127
+ invite: async (id: string, email: string, role: 'admin' | 'member' | 'developer') => {
2128
+ return this.request(
2129
+ 'POST',
2130
+ \`/v1/organizations/\${id}/invite\`,
2131
+ { email, role },
2132
+ true
2133
+ );
2134
+ },
2135
+
2136
+ acceptInvitation: async (token: string) => {
2137
+ return this.request(
2138
+ 'POST',
2139
+ '/v1/organizations/accept-invitation',
2140
+ { token },
2141
+ true
2142
+ );
2143
+ },
2144
+
2145
+ rejectInvitation: async (token: string) => {
2146
+ return this.request(
2147
+ 'POST',
2148
+ '/v1/organizations/reject-invitation',
2149
+ { token }
2150
+ );
2151
+ },
2152
+
2153
+ updateMemberRole: async (orgId: string, userId: string, role: 'admin' | 'member' | 'developer') => {
2154
+ return this.request(
2155
+ 'PUT',
2156
+ \`/v1/organizations/\${orgId}/members/\${userId}\`,
2157
+ { role },
2158
+ true
2159
+ );
2160
+ },
2161
+
2162
+ removeMember: async (orgId: string, userId: string) => {
2163
+ return this.request(
2164
+ 'DELETE',
2165
+ \`/v1/organizations/\${orgId}/members/\${userId}\`,
2166
+ undefined,
2167
+ true
2168
+ );
2169
+ },
2170
+
2171
+ deactivateMember: async (orgId: string, userId: string) => {
2172
+ return this.request(
2173
+ 'POST',
2174
+ \`/v1/organizations/\${orgId}/members/\${userId}/deactivate\`,
2175
+ undefined,
2176
+ true
2177
+ );
2178
+ },
2179
+
2180
+ setDefault: async (orgId: string) => {
2181
+ return this.request(
2182
+ 'POST',
2183
+ \`/v1/organizations/\${orgId}/set-default\`,
2184
+ undefined,
2185
+ true
2186
+ );
2187
+ },
2188
+
2189
+ getSubscription: async (orgId: string) => {
2190
+ return this.request('GET', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
2191
+ },
2192
+
2193
+ subscribe: async (orgId: string, planId: string) => {
2194
+ return this.request('POST', \`/v1/organizations/\${orgId}/subscription/\${planId}\`, undefined, true);
2195
+ },
2196
+
2197
+ cancelSubscription: async (orgId: string) => {
2198
+ return this.request('DELETE', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
2199
+ },
2200
+
2201
+ getUsage: async (orgId: string) => {
2202
+ return this.request('GET', \`/v1/organizations/\${orgId}/usage\`, undefined, true);
2203
+ },
2204
+ };
2205
+
2206
+ // Subscription endpoints
2207
+ subscriptions = {
2208
+ getPlans: async () => {
2209
+ return this.request('GET', '/v1/subscriptions/plans', undefined, true);
2210
+ },
2211
+
2212
+ getCurrent: async () => {
2213
+ return this.request('GET', '/v1/subscriptions/current', undefined, true);
2214
+ },
2215
+
2216
+ checkout: async (planId: string, successUrl: string, cancelUrl: string) => {
2217
+ return this.request<{ checkout_url: string; session_id: string }>(
2218
+ 'POST',
2219
+ '/v1/subscriptions/checkout',
2220
+ {
2221
+ plan_id: planId,
2222
+ success_url: successUrl,
2223
+ cancel_url: cancelUrl,
2224
+ },
2225
+ true
2226
+ );
2227
+ },
2228
+
2229
+ getById: async (subscriptionId: string) => {
2230
+ return this.request('GET', \`/v1/subscriptions/subscriptions/\${subscriptionId}\`, undefined, true);
2231
+ },
2232
+
2233
+ cancel: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
2234
+ return this.request(
2235
+ 'DELETE',
2236
+ \`/v1/subscriptions/subscriptions/\${subscriptionId}?cancel_at_period_end=\${cancelAtPeriodEnd}\`,
2237
+ undefined,
2238
+ true
2239
+ );
2240
+ },
2241
+ };
2242
+
2243
+ // Billing endpoints (sk_live_* only)
2244
+ billing = {
2245
+ getAvailablePlans: async () => {
2246
+ return this.request('GET', '/v1/billing/available-plans', undefined, true);
2247
+ },
2248
+
2249
+ createCheckoutSession: async (data: { user_id: string; organization_id: string; plan_id: string; success_url: string; cancel_url: string }) => {
2250
+ return this.request<{ checkout_url: string; session_id: string; plan_name: string; price: string }>('POST', '/v1/billing/create-checkout-session', data, true);
2251
+ },
2252
+
2253
+ changePlan: async (data: { subscription_id: string; new_plan_id: string }) => {
2254
+ return this.request('POST', '/v1/billing/change-plan', data, true);
2255
+ },
2256
+
2257
+ cancelSubscription: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
2258
+ return this.request('POST', \`/v1/billing/cancel-subscription?subscription_id=\${subscriptionId}&cancel_at_period_end=\${cancelAtPeriodEnd}\`, undefined, true);
2259
+ },
2260
+
2261
+ getBillingHistory: async (organizationId: string, limit = 20) => {
2262
+ return this.request('GET', \`/v1/billing/billing-history-org/\${organizationId}?limit=\${limit}\`, undefined, true);
2263
+ },
2264
+ };
2265
+
2266
+ // Plans endpoints (JWT required)
2267
+ plans = {
2268
+ list: async (skip = 0, limit = 100, includeInactive = false) => {
2269
+ return this.request('GET', \`/v1/plans?skip=\${skip}&limit=\${limit}&include_inactive=\${includeInactive}\`, undefined, true);
2270
+ },
2271
+
2272
+ getActive: async () => {
2273
+ return this.request('GET', '/v1/plans/active', undefined, true);
2274
+ },
2275
+
2276
+ getFree: async () => {
2277
+ return this.request('GET', '/v1/plans/free', undefined, true);
2278
+ },
2279
+
2280
+ getById: async (planId: string) => {
2281
+ return this.request('GET', \`/v1/plans/\${planId}\`, undefined, true);
2282
+ },
2283
+ };
2284
+
2285
+ // User management endpoints
2286
+ users = {
2287
+ list: async (page = 1, perPage = 20, includeInactive = false) => {
2288
+ return this.request('GET', \`/v1/users?page=\${page}&per_page=\${perPage}&include_inactive=\${includeInactive}\`, undefined, true);
2289
+ },
2290
+
2291
+ stats: async () => {
2292
+ return this.request('GET', '/v1/users/stats', undefined, true);
2293
+ },
2294
+
2295
+ get: async (userId: string, includeSettings = false) => {
2296
+ return this.request('GET', \`/v1/users/\${userId}?include_settings=\${includeSettings}\`, undefined, true);
2297
+ },
2298
+
2299
+ update: async (userId: string, data: { display_name?: string; avatar_url?: string; locale?: string }) => {
2300
+ return this.request('PUT', \`/v1/users/\${userId}\`, data, true);
2301
+ },
2302
+
2303
+ updateSettings: async (userId: string, settings: any) => {
2304
+ return this.request('PUT', \`/v1/users/\${userId}/settings\`, settings, true);
2305
+ },
2306
+
2307
+ deactivate: async (userId: string) => {
2308
+ return this.request('POST', \`/v1/users/\${userId}/deactivate\`, undefined, true);
2309
+ },
2310
+
2311
+ activate: async (userId: string) => {
2312
+ return this.request('POST', \`/v1/users/\${userId}/activate\`, undefined, true);
2313
+ },
2314
+
2315
+ delete: async (userId: string) => {
2316
+ return this.request('DELETE', \`/v1/users/\${userId}\`, undefined, true);
2317
+ },
2318
+ };
2319
+
2320
+ // Usage endpoints
2321
+ usage = {
2322
+ getCurrent: async (organizationId: string) => {
2323
+ return this.request('GET', \`/v1/api/usage/current?organization_id=\${organizationId}\`, undefined, true);
2324
+ },
2325
+
2326
+ getHistory: async (organizationId: string, limit = 20, offset = 0) => {
2327
+ return this.request(
2328
+ 'GET',
2329
+ \`/v1/api/usage/history?organization_id=\${organizationId}&limit=\${limit}&offset=\${offset}\`,
2330
+ undefined,
2331
+ true
2332
+ );
2333
+ },
2334
+
2335
+ getEntityUsage: async (organizationId: string) => {
2336
+ return this.request('GET', \`/v1/api/usage/entity-usage?organization_id=\${organizationId}\`, undefined, true);
2337
+ },
2338
+
2339
+ getStats: async (organizationId: string) => {
2340
+ return this.request('GET', \`/v1/usage/stats?organization_id=\${organizationId}\`, undefined, true);
2341
+ },
2342
+
2343
+ check: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
2344
+ return this.request('POST', '/v1/usage/check', data, true);
2345
+ },
2346
+
2347
+ consume: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
2348
+ return this.request('POST', '/v1/usage/consume', data, true);
2349
+ },
2350
+ };
2351
+
2352
+ // Entity Schema endpoints
2353
+ entitySchemas = {
2354
+ list: async (page = 1, size = 20) => {
2355
+ return this.request('GET', \`/v1/entity/schemas?page=\${page}&size=\${size}\`, undefined, true);
2356
+ },
2357
+
2358
+ create: async (data: { name: string; slug: string; description?: string; fields_definition: any }) => {
2359
+ return this.request('POST', '/v1/entity/schemas', data, true);
2360
+ },
2361
+
2362
+ get: async (schemaId: string) => {
2363
+ return this.request('GET', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
2364
+ },
2365
+
2366
+ update: async (schemaId: string, data: { name?: string; description?: string; fields_definition?: any; is_active?: boolean }) => {
2367
+ return this.request('PUT', \`/v1/entity/schemas/\${schemaId}\`, data, true);
2368
+ },
2369
+
2370
+ delete: async (schemaId: string) => {
2371
+ return this.request('DELETE', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
2372
+ },
2373
+ };
2374
+
2375
+ // Entity Record endpoints
2376
+ entityRecords = {
2377
+ create: async (schemaId: string, data: any) => {
2378
+ return this.request('POST', '/v1/entity/records', { schema_id: schemaId, data }, true);
2379
+ },
2380
+
2381
+ get: async (recordId: string) => {
2382
+ return this.request('GET', \`/v1/entity/records/\${recordId}\`, undefined, true);
2383
+ },
2384
+
2385
+ update: async (recordId: string, data: any) => {
2386
+ return this.request('PUT', \`/v1/entity/records/\${recordId}\`, { data }, true);
2387
+ },
2388
+
2389
+ delete: async (recordId: string) => {
2390
+ return this.request('DELETE', \`/v1/entity/records/\${recordId}\`, undefined, true);
2391
+ },
2392
+
2393
+ search: async (searchReq: { schema_id?: string; query?: any; page?: number; size?: number; order_by?: string; order_dir?: string }) => {
2394
+ const params = new URLSearchParams();
2395
+ if (searchReq.page) params.set('page', String(searchReq.page));
2396
+ if (searchReq.size) params.set('size', String(searchReq.size));
2397
+ if (searchReq.order_by) params.set('order_by', searchReq.order_by);
2398
+ if (searchReq.order_dir) params.set('order_dir', searchReq.order_dir);
2399
+ return this.request('POST', \`/v1/entity/records/search?\${params}\`, { schema_id: searchReq.schema_id, query: searchReq.query }, true);
2400
+ },
2401
+ };
2402
+
2403
+ // Department endpoints
2404
+ departments = {
2405
+ get: async (departmentId: string, userId: string) => {
2406
+ return this.request('GET', \`/v1/departments/\${departmentId}?user_id=\${userId}\`, undefined, true);
2407
+ },
2408
+ };
2409
+ }
2410
+
2411
+ export { ApiClient, TokenResponse, Organization, ApiError };
2337
2412
  `;
2338
2413
  // =============================================================================
2339
2414
  // Request Headers Helper
2340
2415
  // =============================================================================
2341
- const REQUEST_HEADERS = `
2342
- ## Required Headers for ALL Requests
2343
-
2344
- \`\`\`
2345
- X-API-Key: pk_live_xxxxx
2346
- Content-Type: application/json
2347
- \`\`\`
2348
-
2349
- ## For Authenticated Requests (after login)
2350
-
2351
- \`\`\`
2352
- X-API-Key: pk_live_xxxxx
2353
- Content-Type: application/json
2354
- Authorization: Bearer <access_token>
2355
- \`\`\`
2356
-
2357
- ## Header Details
2358
-
2359
- | Header | Required | Description |
2360
- |--------|----------|-------------|
2361
- | X-API-Key | Always | Your API key (pk_live_* for frontend) |
2362
- | Content-Type | For POST/PUT/PATCH | Always use application/json |
2363
- | Authorization | After login | Bearer token from login response |
2364
- | X-Organization-Id | For org-scoped endpoints | Organization UUID (entities, usage) |
2416
+ const REQUEST_HEADERS = `
2417
+ ## Required Headers for ALL Requests
2418
+
2419
+ \`\`\`
2420
+ X-API-Key: pk_live_xxxxx
2421
+ Content-Type: application/json
2422
+ \`\`\`
2423
+
2424
+ ## For Authenticated Requests (after login)
2425
+
2426
+ \`\`\`
2427
+ X-API-Key: pk_live_xxxxx
2428
+ Content-Type: application/json
2429
+ Authorization: Bearer <access_token>
2430
+ \`\`\`
2431
+
2432
+ ## Header Details
2433
+
2434
+ | Header | Required | Description |
2435
+ |--------|----------|-------------|
2436
+ | X-API-Key | Always | Your API key (pk_live_* for frontend) |
2437
+ | Content-Type | For POST/PUT/PATCH | Always use application/json |
2438
+ | Authorization | After login | Bearer token from login response |
2439
+ | X-Organization-Id | For org-scoped endpoints | Organization UUID (entities, usage) |
2365
2440
  `;
2366
2441
  // =============================================================================
2367
2442
  // API Client
@@ -2395,12 +2470,390 @@ async function fetchFromApi(endpoint) {
2395
2470
  throw error;
2396
2471
  }
2397
2472
  }
2473
+ function detectKeyType(key) {
2474
+ if (!key)
2475
+ return { type: null, prefix: "" };
2476
+ // Mask everything past the prefix so logs don't leak.
2477
+ const prefix = key.slice(0, 12) + "…";
2478
+ if (key.startsWith("pk_"))
2479
+ return { type: "public", prefix };
2480
+ if (key.startsWith("sk_"))
2481
+ return { type: "secret", prefix };
2482
+ return { type: null, prefix };
2483
+ }
2484
+ function SECURITY_GUIDE(detected) {
2485
+ if (detected === null) {
2486
+ return `# API key security guide
2487
+
2488
+ No \`SAAS_API_KEY\` is configured in the MCP env, so this guide is generic.
2489
+ Configure a key first (\`validate_credentials\` will tell you which one
2490
+ you have) and rerun this tool to get the targeted guide.
2491
+
2492
+ ## Two key types — pick the right one BEFORE writing code
2493
+
2494
+ ### Public key (\`pk_live_*\`)
2495
+
2496
+ - **Surface:** safe to ship in browser / mobile bundles, **only when
2497
+ paired with allowed-origins restrictions configured at key creation**.
2498
+ - **Allowed endpoints:** auth-only (\`/v1/auth/register\`, \`login\`,
2499
+ \`refresh\`, \`logout\`, \`me\`, \`forgot-password\`, \`reset-password\`).
2500
+ - **Forbidden:** any admin / data-listing endpoint. The backend rejects
2501
+ these calls at the middleware level — no surprises.
2502
+
2503
+ ### Secret key (\`sk_live_*\`)
2504
+
2505
+ - **Surface:** server-side ONLY. Never put in a browser bundle, mobile
2506
+ app, public repo, or any environment a user can inspect.
2507
+ - **Allowed endpoints:** every end-user endpoint, including admin
2508
+ listings (\`/v1/auth/users\`, \`/v1/dashboard/*\` patterns through
2509
+ appropriate flows).
2510
+ - **Forbidden:** including the literal string in client-side code.
2511
+
2512
+ ## Common leak vectors to watch out for
2513
+
2514
+ - Hardcoding into Git history (rotate the key if it slipped — \`git\`
2515
+ history is forever).
2516
+ - \`NEXT_PUBLIC_*\` / \`VITE_*\` / \`PUBLIC_*\` env vars: these get baked
2517
+ into the client bundle. Only public keys belong here.
2518
+ - CI logs that echo env vars.
2519
+ - Browser DevTools "Network" tab: every request leaks its headers to
2520
+ whoever has the device. With public keys + allowed-origins this is
2521
+ fine; with secret keys it is a breach.`;
2522
+ }
2523
+ if (detected === "public") {
2524
+ return `# Security guide for your **public** key (\`pk_live_*\`)
2525
+
2526
+ Your configured key is a public key. The platform allows it in browser
2527
+ bundles and mobile apps **only if** you set its \`allowed_origins\` at
2528
+ creation. Verify that with the dashboard or with \`get_endpoint_details\`
2529
+ for \`POST /v1/dashboard/api-keys\`.
2530
+
2531
+ ## ✅ Where it's safe to put this key
2532
+
2533
+ - A React/Vue/Svelte SPA bundle, served from one of the allowed origins.
2534
+ - A Next.js / Remix / SvelteKit app, in either client or server code.
2535
+ - A mobile app (React Native / Flutter / Swift / Kotlin).
2536
+ - An \`.env\` file committed to a private repo, prefixed
2537
+ \`NEXT_PUBLIC_*\` / \`VITE_*\` / \`PUBLIC_*\` so the bundler exposes it.
2538
+
2539
+ ## ❌ Endpoints this key CAN'T call
2540
+
2541
+ The backend will reject calls to admin / management endpoints with this
2542
+ key. Trying to do \`/v1/auth/users\` (the user list) or any \`/v1/dashboard/*\`
2543
+ endpoint with a public key returns 401/403. Use a secret key on a server
2544
+ for those.
2545
+
2546
+ ## Allowed endpoints (full list)
2547
+
2548
+ - \`POST /v1/auth/register\`
2549
+ - \`POST /v1/auth/login\`
2550
+ - \`POST /v1/auth/refresh\`
2551
+ - \`POST /v1/auth/logout\`
2552
+ - \`GET /v1/auth/me\`
2553
+ - \`POST /v1/auth/forgot-password\`
2554
+ - \`POST /v1/auth/reset-password\`
2555
+ - \`POST /v1/auth/verify-email\`
2556
+ - \`POST /v1/auth/resend-verification\`
2557
+ - \`GET /v1/auth/providers\`
2558
+ - \`GET /v1/auth/google/url\` (and equivalents per provider)
2559
+
2560
+ ## Allowed-origins rule of thumb
2561
+
2562
+ - Always include every domain you serve the app from, including
2563
+ \`http://localhost:3000\` for dev. Origins that aren't on the list
2564
+ produce a 403 even if the key is correct.
2565
+ - Wildcards are not allowed. Listing the exact origins is the point.
2566
+
2567
+ ## Things that are still secret even with a public key
2568
+
2569
+ - The end-user's **JWT** issued after \`/v1/auth/login\`. Treat it like a
2570
+ session token: keep it in memory or HTTP-only cookies, never in
2571
+ localStorage if XSS is a real threat for your app.
2572
+
2573
+ ## Recommended next step
2574
+
2575
+ Run \`recommend_stack\` with the kind of app you want to build to get a
2576
+ matching framework + architecture. For a public key the answer is
2577
+ usually \`react / vue / svelte\` for SPA, or \`next / remix / sveltekit\`
2578
+ for an app that also needs SSR.`;
2579
+ }
2580
+ // secret
2581
+ return `# Security guide for your **secret** key (\`sk_live_*\`)
2582
+
2583
+ Your configured key has full end-user API access on behalf of the
2584
+ tenant. **It must never reach a browser, a mobile bundle, a public
2585
+ repo, or anywhere a user can inspect.**
2586
+
2587
+ ## ✅ Where it's safe to put this key
2588
+
2589
+ - A server you run: Node.js / Python / Go / Rust / Ruby — anywhere the
2590
+ source isn't shipped to clients.
2591
+ - A Next.js / Remix / SvelteKit app, **only inside server-only files**
2592
+ (\`app/api/**\`, server components, route handlers, server actions,
2593
+ \`+page.server.ts\`, route loaders). Pair with environment variables
2594
+ read **without** a \`NEXT_PUBLIC_\` / \`VITE_\` / \`PUBLIC_\` prefix so
2595
+ the bundler does not embed it.
2596
+ - Edge functions (Vercel Edge / Cloudflare Workers) where the secret
2597
+ lives in the platform's secret store, not in code.
2598
+ - CI/CD secrets stores: GitHub Actions encrypted secrets, Doppler,
2599
+ AWS Secrets Manager, etc.
2600
+
2601
+ ## ❌ Where it MUST NOT go
2602
+
2603
+ - React/Vue/Svelte client components that the bundler ships to the browser.
2604
+ - \`NEXT_PUBLIC_*\`, \`VITE_*\`, \`PUBLIC_*\` env vars (they end up in the
2605
+ bundle).
2606
+ - Mobile apps — the binary can be reverse-engineered, the key extracted.
2607
+ - Git history, including \`.env\` accidentally committed before
2608
+ \`.gitignore\` was set up.
2609
+ - Browser DevTools / sw caches (don't forward it through service workers).
2610
+ - Logs that aren't access-controlled.
2611
+
2612
+ ## What this key can do
2613
+
2614
+ - Every end-user-facing endpoint, including listings that public keys
2615
+ can't touch (\`GET /v1/auth/users\`, \`GET /v1/auth/providers\`,
2616
+ user-by-id lookups, billing operations on behalf of end-users, etc.).
2617
+ - Acts on behalf of the tenant — every call is attributed to the
2618
+ organization the key is scoped to.
2619
+
2620
+ ## Architecture pattern that keeps it safe
2621
+
2622
+ Don't expose this key to the client. Put a thin server in front and
2623
+ proxy the requests:
2624
+
2625
+ \`\`\`
2626
+ [Browser app] → POST /api/login → [Your server] → POST /v1/auth/login → [Genlobe]
2627
+ (no key) (X-API-Key: sk_live_…)
2628
+ \`\`\`
2629
+
2630
+ Your server holds the key in env. The browser never sees it. The
2631
+ end-user JWT that comes back from \`/v1/auth/login\` is what your
2632
+ browser app holds for that session.
2633
+
2634
+ ## Common leak vectors (each one is a real incident pattern)
2635
+
2636
+ - "Just for testing" hardcoded into a React component → committed →
2637
+ pushed → key on GitHub forever (rotate immediately).
2638
+ - A \`fetch\` call inside \`useEffect\` that sends the secret in the
2639
+ Authorization header — it shows in the user's Network tab. Move that
2640
+ call to your server.
2641
+ - An LLM coding agent pasting the key into a comment to "remember" it.
2642
+ Refuse — the key belongs in env.
2643
+ - Logging \`console.log(headers)\` in middleware on a public-facing app.
2644
+
2645
+ ## Recommended next step
2646
+
2647
+ Run \`recommend_stack\` with your app_type to get a server-aware framework
2648
+ recommendation. For a secret key that almost always means **Next.js /
2649
+ Remix / SvelteKit / Nuxt**, all of which give you a clear server boundary
2650
+ where this key can live.`;
2651
+ }
2652
+ function STACK_RECOMMENDATION(appType, detected) {
2653
+ const keyLine = detected === "secret"
2654
+ ? "🔴 You have a **secret key** (\`sk_live_*\`). It must stay server-side."
2655
+ : detected === "public"
2656
+ ? "🟢 You have a **public key** (\`pk_live_*\`). It can ship to clients with allowed-origins set."
2657
+ : "⚪ No key configured yet. Pick one first (`validate_credentials`).";
2658
+ const sections = {
2659
+ fullstack: detected === "secret" ? `## Recommendation: **Next.js (App Router)**
2660
+
2661
+ A fullstack project with a secret key needs a clear server boundary.
2662
+ Next.js App Router is the simplest framework that gives you:
2663
+
2664
+ - **Server Components** + **Server Actions** + **Route Handlers** where
2665
+ the key lives in \`process.env.SAAS_API_KEY\` and never reaches the
2666
+ client.
2667
+ - File-based routing for the user-facing pages.
2668
+ - Built-in support for env-based config without leaking it to the bundle.
2669
+
2670
+ ### Project layout
2671
+
2672
+ \`\`\`
2673
+ app/
2674
+ ├─ layout.tsx
2675
+ ├─ page.tsx (RSC, talks to SaaS via server action)
2676
+ ├─ (auth)/
2677
+ │ ├─ login/page.tsx (client form → posts to server action)
2678
+ │ └─ register/page.tsx
2679
+ ├─ api/
2680
+ │ ├─ auth/login/route.ts (Route handler, sk_live_* used here)
2681
+ │ ├─ auth/register/route.ts
2682
+ │ └─ proxy/[...path]/route.ts (catch-all server proxy if you need it)
2683
+ └─ lib/saas-client.ts ('server-only', wraps fetch + sk_live)
2684
+ \`\`\`
2685
+
2686
+ ### Three rules that keep the key safe
2687
+
2688
+ 1. \`SAAS_API_KEY\` env var is **never** prefixed with \`NEXT_PUBLIC_\`.
2689
+ 2. The wrapper file imports \`'server-only'\` at the top. If a client
2690
+ component ever imports it, the build fails — that's the contract.
2691
+ 3. The browser only ever talks to *your* \`/api/*\` routes, never to
2692
+ \`api.your-saas.com\` directly.
2693
+
2694
+ ### Equivalents in other frameworks
2695
+
2696
+ - **Remix** — \`loader\` and \`action\` functions are server-only by
2697
+ design; same shape works.
2698
+ - **SvelteKit** — \`+page.server.ts\` / \`+server.ts\` files.
2699
+ - **Nuxt** — \`server/api/*.ts\` route files, \`useNitroApp\`.
2700
+ - **Astro** — server-rendered routes (\`output: 'server'\`).
2701
+
2702
+ Pick the one your team is most comfortable with. Next.js is the most
2703
+ documented for this exact pattern.` : `## Recommendation: lighter SPA + your own backend
2704
+
2705
+ A fullstack project with a public key is unusual — public keys are
2706
+ restricted to auth endpoints. If you want to read or write data beyond
2707
+ register/login/me, you'll need a secret key on the server.
2708
+
2709
+ For now you have two paths:
2710
+
2711
+ **A. Keep it client-only.** Use Vite + React/Vue/Svelte. Allowed origins
2712
+ on the public key restrict who can call. Limited to auth, but works for
2713
+ the "user signs up, signs in" surface.
2714
+
2715
+ **B. Add a thin server.** Generate a secret key from the dashboard,
2716
+ stash it in a Next.js/Remix server, run \`recommend_stack\` again with
2717
+ the secret key configured. That moves you to the server-side pattern
2718
+ with full API access.`,
2719
+ frontend_only: detected === "public" ? `## Recommendation: **Vite + React** (or Vue / Svelte / SolidJS)
2720
+
2721
+ A frontend-only app with a public key is the textbook case. Use any
2722
+ SPA-style framework that lets you embed the key in the bundle.
2723
+
2724
+ \`\`\`
2725
+ src/
2726
+ ├─ App.tsx
2727
+ ├─ pages/
2728
+ │ ├─ Login.tsx
2729
+ │ ├─ Register.tsx
2730
+ │ └─ Dashboard.tsx
2731
+ ├─ lib/
2732
+ │ └─ api.ts (fetch wrapper that adds X-API-Key)
2733
+ └─ main.tsx
2734
+
2735
+ .env
2736
+ └─ VITE_API_KEY=pk_live_…
2737
+ └─ VITE_API_URL=https://api.your-saas.com
2738
+ \`\`\`
2739
+
2740
+ ### Three rules
2741
+
2742
+ 1. The key only goes into \`import.meta.env.VITE_API_KEY\` (Vite) or
2743
+ \`process.env.NEXT_PUBLIC_API_KEY\` (Next.js client). Never paste the
2744
+ raw string in a component.
2745
+ 2. The deployed origin must be on the key's allowed-origins list, or
2746
+ every request returns 403.
2747
+ 3. The end-user JWT (the result of \`/v1/auth/login\`) is what your app
2748
+ holds in memory or HTTP-only cookies — *not* the public key. JWTs
2749
+ are scoped to the user; the public key is shared by everyone.
2750
+
2751
+ If you ever need to call admin-style endpoints (list users, etc.), you
2752
+ need to upgrade to a secret key on a server (option B above).` : `## Recommendation: don't ship the secret key to the client
2753
+
2754
+ Your key is secret. A frontend-only architecture cannot use it without
2755
+ leaking it. Two paths:
2756
+
2757
+ **A. Generate a public key from the dashboard** for the frontend, keep
2758
+ the secret key for any server you might need later. This is the
2759
+ canonical setup. Re-run \`recommend_stack\` with \`app_type=frontend_only\`
2760
+ once you swap.
2761
+
2762
+ **B. Add a small server** (Vercel Functions, Cloudflare Workers, a Node
2763
+ service) and proxy. Then call it "fullstack" and use the Next.js
2764
+ recommendation above.`,
2765
+ backend_only: detected === "secret" ? `## Recommendation: any server runtime, secret in env
2766
+
2767
+ A backend-only consumer (a worker, a webhook handler, a cron) is the
2768
+ simplest case. Any language works:
2769
+
2770
+ - **Node** (\`fastify\`, \`express\`, \`hono\`)
2771
+ - **Python** (\`fastapi\`, \`flask\`)
2772
+ - **Go** (\`net/http\`, \`echo\`)
2773
+ - **Rust** (\`axum\`, \`actix-web\`)
2774
+
2775
+ \`\`\`bash
2776
+ # .env (loaded via dotenv-cli, doppler, or your platform's secret store)
2777
+ SAAS_API_URL=https://api.your-saas.com
2778
+ SAAS_API_KEY=sk_live_…
2779
+ \`\`\`
2780
+
2781
+ \`\`\`ts
2782
+ // minimal Node example
2783
+ const r = await fetch(\`\${process.env.SAAS_API_URL}/v1/auth/login\`, {
2784
+ method: 'POST',
2785
+ headers: {
2786
+ 'Content-Type': 'application/json',
2787
+ 'X-API-Key': process.env.SAAS_API_KEY!,
2788
+ },
2789
+ body: JSON.stringify({ email, password }),
2790
+ });
2791
+ \`\`\`
2792
+
2793
+ The secret never leaves the host process; rotate via the dashboard if
2794
+ the host or env is ever compromised.` : `A backend with a public key works only for auth endpoints. If your
2795
+ backend needs admin operations (list users, billing on behalf of a
2796
+ tenant), generate a secret key from the dashboard and re-run this tool.`,
2797
+ mobile: detected === "public" ? `## Recommendation: native or React Native + public key in app config
2798
+
2799
+ Mobile apps with a public key behave like SPAs. Bundle the key, set
2800
+ allowed-origins to whatever your auth callback host is.
2801
+
2802
+ - **React Native / Expo** — \`react-native-config\` reads
2803
+ \`SAAS_API_KEY\` from \`.env\` and exposes it to JS.
2804
+ - **Flutter** — \`flutter_dotenv\` or \`--dart-define\` flags.
2805
+ - **Swift / Kotlin** — \`xcconfig\` / \`local.properties\` + \`BuildConfig\`.
2806
+
2807
+ Even though the key is in the binary, public keys are *designed* for
2808
+ this. The risk is identical to a website's public key.` : `## Recommendation: don't ship a secret key in a mobile binary
2809
+
2810
+ Mobile apps cannot keep a secret. Reverse-engineering the binary is
2811
+ trivial. Two paths:
2812
+
2813
+ **A. Switch to a public key** for the mobile client and keep the
2814
+ secret key on a server you control. Your server proxies the calls
2815
+ that need elevated permissions.
2816
+
2817
+ **B. Architecture A is non-negotiable for production.** The only way
2818
+ to use a secret key safely from a mobile app is *not to use it from
2819
+ the mobile app*.`,
2820
+ cli: detected === "secret" ? `## Recommendation: any language, secret in user's local env
2821
+
2822
+ A CLI is essentially a backend running on the user's machine. Standard
2823
+ pattern:
2824
+
2825
+ - Read \`SAAS_API_KEY\` from \`process.env\` (Node), \`os.environ\`
2826
+ (Python), etc.
2827
+ - Print a clear error if missing: \`"Set SAAS_API_KEY (get one from
2828
+ https://app.your-saas.com/api-keys)"\`.
2829
+ - For multi-tenant CLIs, store the key in OS keychain (\`keytar\` on
2830
+ Node, \`keyring\` on Python) instead of plain \`.env\`.
2831
+
2832
+ Frameworks: \`commander\` / \`oclif\` (Node), \`typer\` / \`click\`
2833
+ (Python), \`cobra\` (Go).` : `A CLI with a public key works only for auth flows. For most useful CLI
2834
+ behaviors (managing data, running admin commands) you want a secret key.`,
2835
+ unsure: `## Pick the audience first, the stack second
2836
+
2837
+ Tell me one of:
2838
+
2839
+ - **fullstack** — one app that needs both UI and server logic
2840
+ - **frontend_only** — only the browser, no server you own
2841
+ - **backend_only** — server, worker, cron, webhook
2842
+ - **mobile** — iOS / Android app
2843
+ - **cli** — a command-line tool
2844
+
2845
+ …then call \`recommend_stack\` again with \`app_type=<picked>\`. Each path
2846
+ has a different shape because of where the API key can safely live.`,
2847
+ };
2848
+ const body = sections[appType] ?? sections["unsure"];
2849
+ 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.`;
2850
+ }
2398
2851
  // =============================================================================
2399
2852
  // MCP Server Implementation
2400
2853
  // =============================================================================
2401
2854
  const server = new Server({
2402
2855
  name: "saas-multi-agent-api",
2403
- version: "2.2.0",
2856
+ version: SERVER_VERSION,
2404
2857
  }, {
2405
2858
  capabilities: {
2406
2859
  tools: {},
@@ -2412,20 +2865,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
2412
2865
  tools: [
2413
2866
  {
2414
2867
  name: "get_api_overview",
2415
- description: `Get a high-level overview of the Multi-tenant SaaS API.
2416
-
2417
- IMPORTANT: This API has TWO types of endpoints:
2418
- 1. TENANT/DASHBOARD endpoints - For tenant admins (NOT for end-user frontends)
2419
- 2. END-USER endpoints - For building frontend applications
2420
-
2421
- KEY FEATURES:
2422
- - User authentication (login, register, password reset)
2423
- - Organizations and team management
2424
- - Subscriptions and billing (Stripe)
2425
- - AI Agents: Chat with AI agents created by the tenant
2426
- - External Agents: Track usage from external agent integrations (e.g., N8N)
2427
- - Usage tracking
2428
-
2868
+ description: `Get a high-level overview of the Multi-tenant SaaS API.
2869
+
2870
+ IMPORTANT: This API has TWO types of endpoints:
2871
+ 1. TENANT/DASHBOARD endpoints - For tenant admins (NOT for end-user frontends)
2872
+ 2. END-USER endpoints - For building frontend applications
2873
+
2874
+ KEY FEATURES:
2875
+ - User authentication (login, register, password reset)
2876
+ - Organizations and team management
2877
+ - Subscriptions and billing (Stripe)
2878
+ - AI Agents: Chat with AI agents created by the tenant
2879
+ - External Agents: Track usage from external agent integrations (e.g., N8N)
2880
+ - Usage tracking
2881
+
2429
2882
  Call this first to understand the API architecture and authentication model.`,
2430
2883
  inputSchema: {
2431
2884
  type: "object",
@@ -2435,12 +2888,12 @@ Call this first to understand the API architecture and authentication model.`,
2435
2888
  },
2436
2889
  {
2437
2890
  name: "get_end_user_endpoints",
2438
- description: `Get detailed documentation for END-USER API endpoints.
2439
-
2440
- Use this when building a frontend for end-users. These endpoints use:
2441
- - API Key (X-API-Key header) - required for ALL requests
2442
- - JWT token (Authorization: Bearer) - required AFTER login
2443
-
2891
+ description: `Get detailed documentation for END-USER API endpoints.
2892
+
2893
+ Use this when building a frontend for end-users. These endpoints use:
2894
+ - API Key (X-API-Key header) - required for ALL requests
2895
+ - JWT token (Authorization: Bearer) - required AFTER login
2896
+
2444
2897
  Categories: authentication, organizations, subscriptions, billing, usage, agents, users, plans, ai, entities, departments, external_agents`,
2445
2898
  inputSchema: {
2446
2899
  type: "object",
@@ -2465,15 +2918,15 @@ Categories: authentication, organizations, subscriptions, billing, usage, agents
2465
2918
  },
2466
2919
  {
2467
2920
  name: "get_sdk_template",
2468
- description: `Get a complete TypeScript/JavaScript SDK template for the API.
2469
-
2470
- This template includes:
2471
- - Typed interfaces for all responses
2472
- - Auth methods (login, register, refresh, logout)
2473
- - Organization methods
2474
- - Subscription methods
2475
- - Usage methods
2476
-
2921
+ description: `Get a complete TypeScript/JavaScript SDK template for the API.
2922
+
2923
+ This template includes:
2924
+ - Typed interfaces for all responses
2925
+ - Auth methods (login, register, refresh, logout)
2926
+ - Organization methods
2927
+ - Subscription methods
2928
+ - Usage methods
2929
+
2477
2930
  You can use this as a starting point for your frontend.`,
2478
2931
  inputSchema: {
2479
2932
  type: "object",
@@ -2525,15 +2978,15 @@ You can use this as a starting point for your frontend.`,
2525
2978
  },
2526
2979
  {
2527
2980
  name: "get_authentication_flow",
2528
- description: `Get a step-by-step guide for implementing authentication in your frontend.
2529
-
2530
- Covers:
2531
- - Registration flow (with conditional email verification)
2532
- - Login flow
2533
- - Token refresh
2534
- - Password reset
2535
- - Email verification flow
2536
- - Google OAuth flow
2981
+ description: `Get a step-by-step guide for implementing authentication in your frontend.
2982
+
2983
+ Covers:
2984
+ - Registration flow (with conditional email verification)
2985
+ - Login flow
2986
+ - Token refresh
2987
+ - Password reset
2988
+ - Email verification flow
2989
+ - Google OAuth flow
2537
2990
  - Logout`,
2538
2991
  inputSchema: {
2539
2992
  type: "object",
@@ -2543,14 +2996,14 @@ Covers:
2543
2996
  },
2544
2997
  {
2545
2998
  name: "get_common_patterns",
2546
- description: `Get common implementation patterns for frontend development.
2547
-
2548
- Includes:
2549
- - Token storage best practices
2550
- - Auto token refresh
2551
- - Error handling
2552
- - API request wrapper
2553
- - OAuth callback handling
2999
+ description: `Get common implementation patterns for frontend development.
3000
+
3001
+ Includes:
3002
+ - Token storage best practices
3003
+ - Auto token refresh
3004
+ - Error handling
3005
+ - API request wrapper
3006
+ - OAuth callback handling
2554
3007
  - Environment variables`,
2555
3008
  inputSchema: {
2556
3009
  type: "object",
@@ -2558,6 +3011,66 @@ Includes:
2558
3011
  required: [],
2559
3012
  },
2560
3013
  },
3014
+ {
3015
+ name: "validate_credentials",
3016
+ description: `Verify the SAAS_API_KEY currently configured by hitting the live API.
3017
+ Returns the detected key type (public pk_live_* or secret sk_live_*),
3018
+ whether the key authenticates correctly, the API URL it points at, and
3019
+ the kind of operations the key is allowed to perform. Run this first
3020
+ when starting a new project so the agent knows what's safe to build.`,
3021
+ inputSchema: { type: "object", properties: {}, required: [] },
3022
+ },
3023
+ {
3024
+ name: "list_end_users",
3025
+ description: `List the tenant's end-users via GET /v1/auth/users. Requires a
3026
+ secret API key (sk_live_*). Useful when scaffolding admin views or
3027
+ debugging a signup flow. Returns an array of {id, email, display_name,
3028
+ is_active, created_at}.`,
3029
+ inputSchema: {
3030
+ type: "object",
3031
+ properties: {
3032
+ skip: { type: "number", description: "Pagination offset (default 0)" },
3033
+ limit: { type: "number", description: "Page size (default 20, max 100)" },
3034
+ },
3035
+ required: [],
3036
+ },
3037
+ },
3038
+ {
3039
+ name: "list_oauth_providers",
3040
+ description: `List the OAuth providers (Google, GitHub, etc.) the tenant has
3041
+ configured for end-user social login. Useful when wiring up a "Sign in
3042
+ with Google" button — if the provider isn't in the list, the button
3043
+ won't work. Calls GET /v1/auth/providers.`,
3044
+ inputSchema: { type: "object", properties: {}, required: [] },
3045
+ },
3046
+ {
3047
+ name: "get_security_guide",
3048
+ description: `Get key-type-specific security best practices. Reads SAAS_API_KEY
3049
+ to detect public (pk_live_*) vs secret (sk_live_*) and returns the
3050
+ matching guide: where it's safe to put the key, common leak vectors,
3051
+ allowed-origins setup, and what NOT to do. Always consult this before
3052
+ generating production code.`,
3053
+ inputSchema: { type: "object", properties: {}, required: [] },
3054
+ },
3055
+ {
3056
+ name: "recommend_stack",
3057
+ description: `Recommend a stack and high-level architecture given the configured
3058
+ API key type and the kind of app the developer wants to build. Picks
3059
+ between Next.js / Remix / SvelteKit (server-side) for secret keys vs
3060
+ React/Vue SPA for public keys, and explains why. Use this whenever the
3061
+ developer says "I want to build X" before writing code.`,
3062
+ inputSchema: {
3063
+ type: "object",
3064
+ properties: {
3065
+ app_type: {
3066
+ type: "string",
3067
+ enum: ["fullstack", "frontend_only", "backend_only", "mobile", "cli", "unsure"],
3068
+ description: "What kind of app are you building?",
3069
+ },
3070
+ },
3071
+ required: ["app_type"],
3072
+ },
3073
+ },
2561
3074
  ],
2562
3075
  };
2563
3076
  });
@@ -2571,30 +3084,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2571
3084
  content: [
2572
3085
  {
2573
3086
  type: "text",
2574
- text: `# Multi-tenant SaaS API Overview
2575
-
2576
- **API URL:** ${API_URL}
2577
- **Version:** 2.1.0
2578
-
2579
- ${API_OVERVIEW.description}
2580
-
2581
- ## Quick Links
2582
- - Swagger UI: ${API_URL}/docs
2583
- - ReDoc: ${API_URL}/redoc
2584
- - OpenAPI JSON: ${API_URL}/v1/developer/openapi.json
2585
-
2586
- ## Getting Started
2587
-
2588
- 1. Get your API key from the tenant dashboard
2589
- 2. Use \`pk_live_*\` key for frontend development
2590
- 3. Include \`X-API-Key\` header in ALL requests
2591
- 4. After login, include \`Authorization: Bearer <token>\` for authenticated requests
2592
-
2593
- ## Important Notes
2594
-
2595
- ⚠️ **When building a frontend for end-users:**
2596
- - ONLY use endpoints from the "End-user" category
2597
- - DO NOT use /v1/tenant-auth/* or /v1/dashboard/* endpoints
3087
+ text: `# Multi-tenant SaaS API Overview
3088
+
3089
+ **API URL:** ${API_URL}
3090
+ **Version:** ${SERVER_VERSION}
3091
+
3092
+ ${API_OVERVIEW.description}
3093
+
3094
+ ## Quick Links
3095
+ - Swagger UI: ${API_URL}/docs
3096
+ - ReDoc: ${API_URL}/redoc
3097
+ - OpenAPI JSON: ${API_URL}/v1/developer/openapi.json
3098
+
3099
+ ## Recommended first calls (vibecoding flow)
3100
+
3101
+ Before generating any code for the user, run these in order:
3102
+
3103
+ 1. **\`validate_credentials\`** — confirms the configured API key works and tells you whether it's a public (\`pk_live_*\`) or secret (\`sk_live_*\`) key. The rest of your decisions depend on the answer.
3104
+ 2. **\`get_security_guide\`** returns the safe-usage rules for the *detected* key type (where it can run, what it can call, leak vectors).
3105
+ 3. **\`recommend_stack\`** — given the kind of app the user wants (frontend_only, fullstack, backend_service, etc.), returns the framework + project layout that won't leak the key.
3106
+
3107
+ Skipping these is how secret keys end up shipped to a browser bundle. Don't skip them.
3108
+
3109
+ ## Getting Started (manual)
3110
+
3111
+ 1. Get your API key from the tenant dashboard
3112
+ 2. Use \`pk_live_*\` key for frontend-only apps; use \`sk_live_*\` only on a server (Next.js route handler, backend, etc.)
3113
+ 3. Include \`X-API-Key\` header in ALL requests
3114
+ 4. After login, include \`Authorization: Bearer <token>\` for authenticated requests
3115
+
3116
+ ## Important Notes
3117
+
3118
+ ⚠️ **When building a frontend for end-users:**
3119
+ - ONLY use endpoints from the "End-user" category
3120
+ - DO NOT use /v1/tenant-auth/* or /v1/dashboard/* endpoints
2598
3121
  - Those are for tenant admin dashboards, not end-user apps`,
2599
3122
  },
2600
3123
  ],
@@ -2610,115 +3133,115 @@ ${API_OVERVIEW.description}
2610
3133
  content: [
2611
3134
  {
2612
3135
  type: "text",
2613
- text: `# End-User API Endpoints${category ? ` - ${category}` : ''}
2614
-
2615
- These endpoints are for building frontend applications for end-users.
2616
-
2617
- ${JSON.stringify(endpoints, null, 2)}
2618
-
2619
- ## Quick Reference
2620
-
2621
- ### Authentication (No JWT required)
2622
- - POST /v1/auth/register - Register new user
2623
- - POST /v1/auth/login - Login user
2624
- - POST /v1/auth/refresh - Refresh access token
2625
- - POST /v1/auth/forgot-password - Request password reset
2626
- - POST /v1/auth/reset-password - Reset password
2627
- - POST /v1/auth/verify-email - Verify email (with API Key)
2628
- - POST /v1/auth/verify-email-with-token - Verify email with token only (no API Key)
2629
- - POST /v1/auth/resend-verification - Resend verification email
2630
- - GET /v1/auth/providers - List enabled OAuth providers (e.g. Google)
2631
- - GET /v1/auth/google/url?redirect_uri=... - Get Google OAuth URL
2632
- - GET /v1/auth/google/callback - OAuth callback (redirects with tokens in URL fragment #)
2633
-
2634
- ### Authenticated Endpoints (JWT required)
2635
- - GET /v1/auth/me - Get current user
2636
- - POST /v1/auth/logout - Logout
2637
- - POST /v1/auth/change-password - Change password (authenticated)
2638
- - GET /v1/organizations/my-organizations - Get user's organizations
2639
- - POST /v1/organizations - Create organization
2640
- - GET /v1/organizations/{id}/departments - List departments
2641
- - GET /v1/organizations/{id}/members - List members
2642
- - PUT /v1/organizations/{id}/members/{user_id} - Update member role
2643
- - DELETE /v1/organizations/{id}/members/{user_id} - Remove member
2644
- - POST /v1/organizations/{id}/invite - Invite member
2645
- - POST /v1/organizations/accept-invitation - Accept invitation
2646
- - POST /v1/organizations/reject-invitation - Reject invitation
2647
- - GET /v1/organizations/invitations/pending?email= - Check pending invitations
2648
- - GET /v1/organizations/invitations/{token} - Get invitation details
2649
- - POST /v1/organizations/{id}/set-default - Set default org
2650
- - GET /v1/organizations/{id}/subscription - Get org subscription
2651
- - POST /v1/organizations/{id}/subscription/{plan_id} - Subscribe to plan
2652
- - DELETE /v1/organizations/{id}/subscription - Cancel subscription
2653
- - GET /v1/organizations/{id}/usage - Get org usage
2654
-
2655
- ### Plans (JWT required)
2656
- - GET /v1/plans - List all plans (paginated)
2657
- - GET /v1/plans/active - List active plans
2658
- - GET /v1/plans/free - List free plans
2659
- - GET /v1/plans/{plan_id} - Get plan details
2660
-
2661
- ### Subscriptions (JWT required)
2662
- - GET /v1/subscriptions/plans - List plans
2663
- - GET /v1/subscriptions/plans/{plan_id} - Get plan by ID
2664
- - GET /v1/subscriptions/current - Get current subscription
2665
- - POST /v1/subscriptions/subscriptions - Create subscription
2666
- - GET /v1/subscriptions/subscriptions/{id} - Get subscription by ID
2667
- - PUT /v1/subscriptions/subscriptions/{id} - Update subscription
2668
- - DELETE /v1/subscriptions/subscriptions/{id} - Cancel subscription
2669
- - POST /v1/subscriptions/checkout - Start checkout
2670
- - GET /v1/subscriptions/users/{user_id}/usage - Get user plan usage
2671
- - GET /v1/subscriptions/users/{user_id}/usage/limits - Check user limits
2672
- - PUT /v1/subscriptions/users/{user_id}/plan/{plan_id} - Assign plan
2673
- - POST /v1/subscriptions/organizations/{org_id}/resume - Resume org subscription (sk_live_*)
2674
- - GET /v1/subscriptions/organizations/{org_id}/invoices - Get org invoices (sk_live_*)
2675
- - DELETE /v1/subscriptions/organizations/{org_id}/subscription - Cancel org subscription (sk_live_*)
2676
-
2677
- ### Billing (sk_live_* ONLY)
2678
- - GET /v1/billing/available-plans - List available plans
2679
- - POST /v1/billing/create-checkout-session - Create checkout session
2680
- - POST /v1/billing/change-plan - Change plan (upgrade/downgrade)
2681
- - POST /v1/billing/cancel-subscription - Cancel subscription
2682
- - GET /v1/billing/billing-history-org/{id} - Billing history
2683
-
2684
- ### Users
2685
- - POST /v1/users - Create user (sk_live_* + JWT)
2686
- - GET /v1/users - List users (sk_live_* only, paginated)
2687
- - GET /v1/users/stats - User stats (sk_live_* only)
2688
- - GET /v1/users/{id} - Get user (self or sk_live_*)
2689
- - PUT /v1/users/{id} - Update user
2690
- - PUT /v1/users/{id}/settings - Update settings
2691
- - POST /v1/users/{id}/deactivate - Deactivate user (soft delete)
2692
- - POST /v1/users/{id}/activate - Activate user (sk_live_* only)
2693
- - DELETE /v1/users/{id} - Delete user (sk_live_* only, soft delete)
2694
-
2695
- ### Usage (JWT required)
2696
- - GET /v1/api/usage/current - Current usage stats
2697
- - GET /v1/api/usage/history - Usage history (limit/offset pagination)
2698
- - GET /v1/api/usage/entity-usage - Entity usage stats
2699
- - GET /v1/usage/stats - Comprehensive usage statistics
2700
- - POST /v1/usage/check - Check if usage allowed (without consuming)
2701
- - POST /v1/usage/consume - Consume usage tokens/messages
2702
-
2703
- ### AI Agents (JWT required)
2704
- - GET /v1/user/agents/available - List available agents
2705
- - GET /v1/user/agents/{agent_id} - Get agent details
2706
- - POST /v1/user/agents/{agent_id}/chat - Chat with an agent
2707
- - GET /v1/user/agents/{agent_id}/history - Get conversation history
2708
-
2709
- ### Custom Entities (API Key + JWT + X-Organization-Id)
2710
- - GET /v1/entity/schemas - List schemas
2711
- - POST /v1/entity/schemas - Create schema
2712
- - GET /v1/entity/schemas/{id} - Get schema
2713
- - PUT /v1/entity/schemas/{id} - Update schema
2714
- - DELETE /v1/entity/schemas/{id} - Delete schema
2715
- - POST /v1/entity/records - Create record
2716
- - GET /v1/entity/records/{id} - Get record
2717
- - PUT /v1/entity/records/{id} - Update record
2718
- - DELETE /v1/entity/records/{id} - Delete record
2719
- - POST /v1/entity/records/search - Search records with filters
2720
-
2721
- ### Departments
3136
+ text: `# End-User API Endpoints${category ? ` - ${category}` : ''}
3137
+
3138
+ These endpoints are for building frontend applications for end-users.
3139
+
3140
+ ${JSON.stringify(endpoints, null, 2)}
3141
+
3142
+ ## Quick Reference
3143
+
3144
+ ### Authentication (No JWT required)
3145
+ - POST /v1/auth/register - Register new user
3146
+ - POST /v1/auth/login - Login user
3147
+ - POST /v1/auth/refresh - Refresh access token
3148
+ - POST /v1/auth/forgot-password - Request password reset
3149
+ - POST /v1/auth/reset-password - Reset password
3150
+ - POST /v1/auth/verify-email - Verify email (with API Key)
3151
+ - POST /v1/auth/verify-email-with-token - Verify email with token only (no API Key)
3152
+ - POST /v1/auth/resend-verification - Resend verification email
3153
+ - GET /v1/auth/providers - List enabled OAuth providers (e.g. Google)
3154
+ - GET /v1/auth/google/url?redirect_uri=... - Get Google OAuth URL
3155
+ - GET /v1/auth/google/callback - OAuth callback (redirects with tokens in URL fragment #)
3156
+
3157
+ ### Authenticated Endpoints (JWT required)
3158
+ - GET /v1/auth/me - Get current user
3159
+ - POST /v1/auth/logout - Logout
3160
+ - POST /v1/auth/change-password - Change password (authenticated)
3161
+ - GET /v1/organizations/my-organizations - Get user's organizations
3162
+ - POST /v1/organizations - Create organization
3163
+ - GET /v1/organizations/{id}/departments - List departments
3164
+ - GET /v1/organizations/{id}/members - List members
3165
+ - PUT /v1/organizations/{id}/members/{user_id} - Update member role
3166
+ - DELETE /v1/organizations/{id}/members/{user_id} - Remove member
3167
+ - POST /v1/organizations/{id}/invite - Invite member
3168
+ - POST /v1/organizations/accept-invitation - Accept invitation
3169
+ - POST /v1/organizations/reject-invitation - Reject invitation
3170
+ - GET /v1/organizations/invitations/pending?email= - Check pending invitations
3171
+ - GET /v1/organizations/invitations/{token} - Get invitation details
3172
+ - POST /v1/organizations/{id}/set-default - Set default org
3173
+ - GET /v1/organizations/{id}/subscription - Get org subscription
3174
+ - POST /v1/organizations/{id}/subscription/{plan_id} - Subscribe to plan
3175
+ - DELETE /v1/organizations/{id}/subscription - Cancel subscription
3176
+ - GET /v1/organizations/{id}/usage - Get org usage
3177
+
3178
+ ### Plans (JWT required)
3179
+ - GET /v1/plans - List all plans (paginated)
3180
+ - GET /v1/plans/active - List active plans
3181
+ - GET /v1/plans/free - List free plans
3182
+ - GET /v1/plans/{plan_id} - Get plan details
3183
+
3184
+ ### Subscriptions (JWT required)
3185
+ - GET /v1/subscriptions/plans - List plans
3186
+ - GET /v1/subscriptions/plans/{plan_id} - Get plan by ID
3187
+ - GET /v1/subscriptions/current - Get current subscription
3188
+ - POST /v1/subscriptions/subscriptions - Create subscription
3189
+ - GET /v1/subscriptions/subscriptions/{id} - Get subscription by ID
3190
+ - PUT /v1/subscriptions/subscriptions/{id} - Update subscription
3191
+ - DELETE /v1/subscriptions/subscriptions/{id} - Cancel subscription
3192
+ - POST /v1/subscriptions/checkout - Start checkout
3193
+ - GET /v1/subscriptions/users/{user_id}/usage - Get user plan usage
3194
+ - GET /v1/subscriptions/users/{user_id}/usage/limits - Check user limits
3195
+ - PUT /v1/subscriptions/users/{user_id}/plan/{plan_id} - Assign plan
3196
+ - POST /v1/subscriptions/organizations/{org_id}/resume - Resume org subscription (sk_live_*)
3197
+ - GET /v1/subscriptions/organizations/{org_id}/invoices - Get org invoices (sk_live_*)
3198
+ - DELETE /v1/subscriptions/organizations/{org_id}/subscription - Cancel org subscription (sk_live_*)
3199
+
3200
+ ### Billing (sk_live_* ONLY)
3201
+ - GET /v1/billing/available-plans - List available plans
3202
+ - POST /v1/billing/create-checkout-session - Create checkout session
3203
+ - POST /v1/billing/change-plan - Change plan (upgrade/downgrade)
3204
+ - POST /v1/billing/cancel-subscription - Cancel subscription
3205
+ - GET /v1/billing/billing-history-org/{id} - Billing history
3206
+
3207
+ ### Users
3208
+ - POST /v1/users - Create user (sk_live_* + JWT)
3209
+ - GET /v1/users - List users (sk_live_* only, paginated)
3210
+ - GET /v1/users/stats - User stats (sk_live_* only)
3211
+ - GET /v1/users/{id} - Get user (self or sk_live_*)
3212
+ - PUT /v1/users/{id} - Update user
3213
+ - PUT /v1/users/{id}/settings - Update settings
3214
+ - POST /v1/users/{id}/deactivate - Deactivate user (soft delete)
3215
+ - POST /v1/users/{id}/activate - Activate user (sk_live_* only)
3216
+ - DELETE /v1/users/{id} - Delete user (sk_live_* only, soft delete)
3217
+
3218
+ ### Usage (JWT required)
3219
+ - GET /v1/api/usage/current - Current usage stats
3220
+ - GET /v1/api/usage/history - Usage history (limit/offset pagination)
3221
+ - GET /v1/api/usage/entity-usage - Entity usage stats
3222
+ - GET /v1/usage/stats - Comprehensive usage statistics
3223
+ - POST /v1/usage/check - Check if usage allowed (without consuming)
3224
+ - POST /v1/usage/consume - Consume usage tokens/messages
3225
+
3226
+ ### AI Agents (JWT required)
3227
+ - GET /v1/user/agents/available - List available agents
3228
+ - GET /v1/user/agents/{agent_id} - Get agent details
3229
+ - POST /v1/user/agents/{agent_id}/chat - Chat with an agent
3230
+ - GET /v1/user/agents/{agent_id}/history - Get conversation history
3231
+
3232
+ ### Custom Entities (API Key + JWT + X-Organization-Id)
3233
+ - GET /v1/entity/schemas - List schemas
3234
+ - POST /v1/entity/schemas - Create schema
3235
+ - GET /v1/entity/schemas/{id} - Get schema
3236
+ - PUT /v1/entity/schemas/{id} - Update schema
3237
+ - DELETE /v1/entity/schemas/{id} - Delete schema
3238
+ - POST /v1/entity/records - Create record
3239
+ - GET /v1/entity/records/{id} - Get record
3240
+ - PUT /v1/entity/records/{id} - Update record
3241
+ - DELETE /v1/entity/records/{id} - Delete record
3242
+ - POST /v1/entity/records/search - Search records with filters
3243
+
3244
+ ### Departments
2722
3245
  - GET /v1/departments/{department_id} - Get department details`,
2723
3246
  },
2724
3247
  ],
@@ -2739,33 +3262,33 @@ ${JSON.stringify(endpoints, null, 2)}
2739
3262
  content: [
2740
3263
  {
2741
3264
  type: "text",
2742
- text: `# TypeScript/JavaScript SDK Template
2743
-
2744
- \`\`\`typescript
2745
- ${SDK_TEMPLATE_TYPESCRIPT}
2746
- \`\`\`
2747
-
2748
- ## Usage Example
2749
-
2750
- \`\`\`typescript
2751
- import { ApiClient } from './api-client';
2752
-
2753
- const api = new ApiClient('pk_live_your_key', 'https://api.yoursaas.com');
2754
-
2755
- // Register
2756
- const { user } = await api.auth.register('email@example.com', 'Password123', 'John');
2757
-
2758
- // Login
2759
- const { access_token, user } = await api.auth.login('email@example.com', 'Password123');
2760
-
2761
- // Get organizations
2762
- const { organizations } = await api.organizations.getMyOrganizations();
2763
-
2764
- // Create organization
2765
- const org = await api.organizations.create('My Org', 'my-org');
2766
-
2767
- // Get subscription plans
2768
- const plans = await api.subscriptions.getPlans();
3265
+ text: `# TypeScript/JavaScript SDK Template
3266
+
3267
+ \`\`\`typescript
3268
+ ${SDK_TEMPLATE_TYPESCRIPT}
3269
+ \`\`\`
3270
+
3271
+ ## Usage Example
3272
+
3273
+ \`\`\`typescript
3274
+ import { ApiClient } from './api-client';
3275
+
3276
+ const api = new ApiClient('pk_live_your_key', 'https://api.yoursaas.com');
3277
+
3278
+ // Register
3279
+ const { user } = await api.auth.register('email@example.com', 'Password123', 'John');
3280
+
3281
+ // Login
3282
+ const { access_token, user } = await api.auth.login('email@example.com', 'Password123');
3283
+
3284
+ // Get organizations
3285
+ const { organizations } = await api.organizations.getMyOrganizations();
3286
+
3287
+ // Create organization
3288
+ const org = await api.organizations.create('My Org', 'my-org');
3289
+
3290
+ // Get subscription plans
3291
+ const plans = await api.subscriptions.getPlans();
2769
3292
  \`\`\``,
2770
3293
  },
2771
3294
  ],
@@ -2788,10 +3311,10 @@ const plans = await api.subscriptions.getPlans();
2788
3311
  content: [
2789
3312
  {
2790
3313
  type: "text",
2791
- text: `Error fetching OpenAPI spec: ${error}
2792
-
2793
- The API might not be accessible. Try accessing ${API_URL}/docs directly.
2794
-
3314
+ text: `Error fetching OpenAPI spec: ${error}
3315
+
3316
+ The API might not be accessible. Try accessing ${API_URL}/docs directly.
3317
+
2795
3318
  In the meantime, use the get_end_user_endpoints tool for endpoint documentation.`,
2796
3319
  },
2797
3320
  ],
@@ -2821,12 +3344,12 @@ In the meantime, use the get_end_user_endpoints tool for endpoint documentation.
2821
3344
  content: [
2822
3345
  {
2823
3346
  type: "text",
2824
- text: `No endpoints found matching "${query}".
2825
-
2826
- Try searching for:
2827
- - auth, login, register, password
2828
- - organization, member, invite
2829
- - subscription, plan, checkout
3347
+ text: `No endpoints found matching "${query}".
3348
+
3349
+ Try searching for:
3350
+ - auth, login, register, password
3351
+ - organization, member, invite
3352
+ - subscription, plan, checkout
2830
3353
  - billing, usage`,
2831
3354
  },
2832
3355
  ],
@@ -2836,12 +3359,12 @@ Try searching for:
2836
3359
  content: [
2837
3360
  {
2838
3361
  type: "text",
2839
- text: `# Endpoints matching "${query}"
2840
-
2841
- ${results.map(r => `## ${r.method} ${r.path}
2842
- **Category:** ${r.category}
2843
- **Summary:** ${r.summary}
2844
- **Auth:** API Key: ${r.auth.api_key}, JWT: ${r.auth.jwt ? 'Required' : 'Not required'}
3362
+ text: `# Endpoints matching "${query}"
3363
+
3364
+ ${results.map(r => `## ${r.method} ${r.path}
3365
+ **Category:** ${r.category}
3366
+ **Summary:** ${r.summary}
3367
+ **Auth:** API Key: ${r.auth.api_key}, JWT: ${r.auth.jwt ? 'Required' : 'Not required'}
2845
3368
  `).join('\n')}`,
2846
3369
  },
2847
3370
  ],
@@ -2849,55 +3372,67 @@ ${results.map(r => `## ${r.method} ${r.path}
2849
3372
  }
2850
3373
  case "get_endpoint_details": {
2851
3374
  const path = args.path;
2852
- const method = (args.method || "GET").toUpperCase();
2853
- // Search in embedded endpoints
3375
+ const methodArg = args.method;
3376
+ const method = methodArg ? methodArg.toUpperCase() : null;
3377
+ const matches = [];
2854
3378
  for (const [category, data] of Object.entries(END_USER_ENDPOINTS)) {
2855
3379
  for (const endpoint of data.endpoints || []) {
2856
- if (endpoint.path === path && endpoint.method === method) {
2857
- return {
2858
- content: [
2859
- {
2860
- type: "text",
2861
- text: `# ${method} ${path}
2862
-
2863
- **Category:** ${category}
2864
- **Summary:** ${endpoint.summary}
2865
-
2866
- ## Authentication
2867
- - API Key: ${endpoint.auth.api_key}
2868
- - JWT Token: ${endpoint.auth.jwt ? 'Required' : 'Not required'}
2869
-
2870
- ${endpoint.request_body ? `## Request Body
2871
- \`\`\`json
2872
- ${JSON.stringify(endpoint.request_body, null, 2)}
2873
- \`\`\`` : ''}
2874
-
2875
- ${endpoint.example_request ? `## Example Request
2876
- \`\`\`json
2877
- ${endpoint.example_request}
2878
- \`\`\`` : ''}
2879
-
2880
- ${endpoint.response ? `## Response
2881
- \`\`\`json
2882
- ${JSON.stringify(endpoint.response, null, 2)}
2883
- \`\`\`` : ''}
2884
-
2885
- ${endpoint.note ? `## Note
2886
- ${endpoint.note}` : ''}`,
2887
- },
2888
- ],
2889
- };
3380
+ if (endpoint.path === path && (method === null || endpoint.method === method)) {
3381
+ matches.push({ category, endpoint });
2890
3382
  }
2891
3383
  }
2892
3384
  }
3385
+ const renderEndpoint = (m) => {
3386
+ const ep = m.endpoint;
3387
+ return `# ${ep.method} ${ep.path}
3388
+
3389
+ **Category:** ${m.category}
3390
+ **Summary:** ${ep.summary}
3391
+
3392
+ ## Authentication
3393
+ - API Key: ${ep.auth.api_key}
3394
+ - JWT Token: ${ep.auth.jwt ? 'Required' : 'Not required'}
3395
+
3396
+ ${ep.request_body ? `## Request Body
3397
+ \`\`\`json
3398
+ ${JSON.stringify(ep.request_body, null, 2)}
3399
+ \`\`\`` : ''}
3400
+
3401
+ ${ep.example_request ? `## Example Request
3402
+ \`\`\`json
3403
+ ${ep.example_request}
3404
+ \`\`\`` : ''}
3405
+
3406
+ ${ep.response ? `## Response
3407
+ \`\`\`json
3408
+ ${JSON.stringify(ep.response, null, 2)}
3409
+ \`\`\`` : ''}
3410
+
3411
+ ${ep.note ? `## Note
3412
+ ${ep.note}` : ''}`;
3413
+ };
3414
+ if (matches.length === 1) {
3415
+ return { content: [{ type: "text", text: renderEndpoint(matches[0]) }] };
3416
+ }
3417
+ if (matches.length > 1) {
3418
+ // When several methods share the path, render them all.
3419
+ return {
3420
+ content: [
3421
+ {
3422
+ type: "text",
3423
+ text: matches.map(renderEndpoint).join("\n\n---\n\n"),
3424
+ },
3425
+ ],
3426
+ };
3427
+ }
2893
3428
  return {
2894
3429
  content: [
2895
3430
  {
2896
3431
  type: "text",
2897
- text: `Endpoint "${method} ${path}" not found in end-user endpoints.
2898
-
2899
- Make sure you're using the correct path format (e.g., /v1/auth/login).
2900
-
3432
+ text: `Endpoint "${method ? method + " " : ""}${path}" not found in end-user endpoints.
3433
+
3434
+ Make sure you're using the correct path format (e.g., /v1/auth/login).
3435
+
2901
3436
  Use search_endpoints tool to find available endpoints.`,
2902
3437
  },
2903
3438
  ],
@@ -2908,232 +3443,232 @@ Use search_endpoints tool to find available endpoints.`,
2908
3443
  content: [
2909
3444
  {
2910
3445
  type: "text",
2911
- text: `# Authentication Flow Guide
2912
-
2913
- ## 1. Registration Flow
2914
-
2915
- \`\`\`typescript
2916
- // 1. Register user
2917
- const response = await fetch('/v1/auth/register', {
2918
- method: 'POST',
2919
- headers: {
2920
- 'X-API-Key': 'pk_live_your_key',
2921
- 'Content-Type': 'application/json',
2922
- },
2923
- body: JSON.stringify({
2924
- email: 'user@example.com',
2925
- password: 'SecurePass123', // min 8 chars, 1 upper, 1 lower, 1 digit
2926
- display_name: 'John Doe', // optional
2927
- }),
2928
- });
2929
-
2930
- const { access_token, refresh_token, user } = await response.json();
2931
-
2932
- // 2. Store tokens securely
2933
- localStorage.setItem('access_token', access_token);
2934
- localStorage.setItem('refresh_token', refresh_token);
2935
-
2936
- // User is now logged in
2937
- \`\`\`
2938
-
2939
- ## 2. Login Flow
2940
-
2941
- \`\`\`typescript
2942
- const response = await fetch('/v1/auth/login', {
2943
- method: 'POST',
2944
- headers: {
2945
- 'X-API-Key': 'pk_live_your_key',
2946
- 'Content-Type': 'application/json',
2947
- },
2948
- body: JSON.stringify({
2949
- email: 'user@example.com',
2950
- password: 'SecurePass123',
2951
- }),
2952
- });
2953
-
2954
- if (!response.ok) {
2955
- const error = await response.json();
2956
- // Handle error: error.detail contains the message
2957
- throw new Error(error.detail);
2958
- }
2959
-
2960
- const { access_token, refresh_token, user } = await response.json();
2961
- \`\`\`
2962
-
2963
- ## 3. Token Refresh Flow
2964
-
2965
- \`\`\`typescript
2966
- // Call this when access_token expires (expires_in seconds from login)
2967
- const response = await fetch('/v1/auth/refresh', {
2968
- method: 'POST',
2969
- headers: {
2970
- 'X-API-Key': 'pk_live_your_key',
2971
- 'Content-Type': 'application/json',
2972
- },
2973
- body: JSON.stringify({
2974
- refresh_token: localStorage.getItem('refresh_token'),
2975
- }),
2976
- });
2977
-
2978
- const { access_token } = await response.json();
2979
- localStorage.setItem('access_token', access_token);
2980
- \`\`\`
2981
-
2982
- ## 4. Password Reset Flow
2983
-
2984
- \`\`\`typescript
2985
- // Step 1: Request reset email
2986
- await fetch('/v1/auth/forgot-password', {
2987
- method: 'POST',
2988
- headers: {
2989
- 'X-API-Key': 'pk_live_your_key',
2990
- 'Content-Type': 'application/json',
2991
- },
2992
- body: JSON.stringify({ email: 'user@example.com' }),
2993
- });
2994
- // Always succeeds (security)
2995
-
2996
- // Step 2: User clicks link in email with token
2997
-
2998
- // Step 3: Reset password with token
2999
- await fetch('/v1/auth/reset-password', {
3000
- method: 'POST',
3001
- headers: {
3002
- 'X-API-Key': 'pk_live_your_key',
3003
- 'Content-Type': 'application/json',
3004
- },
3005
- body: JSON.stringify({
3006
- token: 'token-from-email-link',
3007
- new_password: 'NewSecurePass123',
3008
- }),
3009
- });
3010
- \`\`\`
3011
-
3012
- ## 5. Change Password Flow (Authenticated)
3013
-
3014
- \`\`\`typescript
3015
- await fetch('/v1/auth/change-password', {
3016
- method: 'POST',
3017
- headers: {
3018
- 'X-API-Key': 'pk_live_your_key',
3019
- 'Authorization': 'Bearer your_jwt_token',
3020
- 'Content-Type': 'application/json',
3021
- },
3022
- body: JSON.stringify({
3023
- current_password: 'OldPass123!',
3024
- new_password: 'NewPass456!',
3025
- }),
3026
- });
3027
- \`\`\`
3028
-
3029
- ## 6. Email Verification Flow
3030
-
3031
- \`\`\`typescript
3032
- // Registration may require email verification depending on tenant config.
3033
- // Check the register response for verification_required: true
3034
-
3035
- const registerResult = await response.json();
3036
-
3037
- if (registerResult.verification_required) {
3038
- // 1. Show "Check your email" message — NO tokens returned
3039
- // 2. User clicks link in email → opens /verify-email?token=xxx
3040
-
3041
- // 3. Verify the email token
3042
- const verifyResponse = await fetch('/v1/auth/verify-email', {
3043
- method: 'POST',
3044
- headers: {
3045
- 'X-API-Key': 'pk_live_your_key',
3046
- 'Content-Type': 'application/json',
3047
- },
3048
- body: JSON.stringify({ token: tokenFromUrl }),
3049
- });
3050
- // Returns: { message: "Email verified successfully" }
3051
-
3052
- // 4. If token expired, user can request a new one
3053
- await fetch('/v1/auth/resend-verification', {
3054
- method: 'POST',
3055
- headers: {
3056
- 'X-API-Key': 'pk_live_your_key',
3057
- 'Content-Type': 'application/json',
3058
- },
3059
- body: JSON.stringify({ email: 'user@example.com' }),
3060
- });
3061
- // Rate limited: 3/hour
3062
-
3063
- // 5. After verification, user can login normally via /v1/auth/login
3064
- } else {
3065
- // No verification required — tokens returned directly from register
3066
- localStorage.setItem('access_token', registerResult.access_token);
3067
- localStorage.setItem('refresh_token', registerResult.refresh_token);
3068
- }
3069
- \`\`\`
3070
-
3071
- ## 7. Google OAuth Flow
3072
-
3073
- \`\`\`typescript
3074
- // 1. Check if Google OAuth is enabled for this tenant
3075
- const providersRes = await fetch('/v1/auth/providers', {
3076
- headers: { 'X-API-Key': 'pk_live_your_key' },
3077
- });
3078
- const { providers } = await providersRes.json();
3079
- const googleEnabled = providers.some((p: any) => p.provider === 'google');
3080
-
3081
- // 2. Get Google authorization URL
3082
- const redirectUri = window.location.origin + '/oauth/callback';
3083
- const urlRes = await fetch(
3084
- \`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`,
3085
- { headers: { 'X-API-Key': 'pk_live_your_key' } }
3086
- );
3087
- const { authorization_url } = await urlRes.json();
3088
-
3089
- // 3. Redirect user to Google
3090
- window.location.href = authorization_url;
3091
-
3092
- // 4. Google redirects to /v1/auth/google/callback (backend handles this)
3093
- // 5. Backend redirects to YOUR redirect_uri with tokens in URL FRAGMENT (#)
3094
-
3095
- // 6. In your /oauth/callback page, read tokens from hash (NOT query params)
3096
- const hash = window.location.hash.substring(1);
3097
- const params = new URLSearchParams(hash);
3098
- const accessToken = params.get('access_token');
3099
- const refreshToken = params.get('refresh_token');
3100
- const expiresIn = params.get('expires_in');
3101
-
3102
- // Also check query params for errors
3103
- const urlParams = new URLSearchParams(window.location.search);
3104
- const error = urlParams.get('error');
3105
- if (error) {
3106
- console.error('OAuth failed:', urlParams.get('error_description'));
3107
- }
3108
- \`\`\`
3109
-
3110
- ## 8. Authenticated Requests
3111
-
3112
- \`\`\`typescript
3113
- // After login, include Authorization header
3114
- const response = await fetch('/v1/auth/me', {
3115
- headers: {
3116
- 'X-API-Key': 'pk_live_your_key',
3117
- 'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
3118
- },
3119
- });
3120
-
3121
- const user = await response.json();
3122
- \`\`\`
3123
-
3124
- ## 9. Logout
3125
-
3126
- \`\`\`typescript
3127
- await fetch('/v1/auth/logout', {
3128
- method: 'POST',
3129
- headers: {
3130
- 'X-API-Key': 'pk_live_your_key',
3131
- 'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
3132
- },
3133
- });
3134
-
3135
- localStorage.removeItem('access_token');
3136
- localStorage.removeItem('refresh_token');
3446
+ text: `# Authentication Flow Guide
3447
+
3448
+ ## 1. Registration Flow
3449
+
3450
+ \`\`\`typescript
3451
+ // 1. Register user
3452
+ const response = await fetch('/v1/auth/register', {
3453
+ method: 'POST',
3454
+ headers: {
3455
+ 'X-API-Key': 'pk_live_your_key',
3456
+ 'Content-Type': 'application/json',
3457
+ },
3458
+ body: JSON.stringify({
3459
+ email: 'user@example.com',
3460
+ password: 'SecurePass123', // min 8 chars, 1 upper, 1 lower, 1 digit
3461
+ display_name: 'John Doe', // optional
3462
+ }),
3463
+ });
3464
+
3465
+ const { access_token, refresh_token, user } = await response.json();
3466
+
3467
+ // 2. Store tokens securely
3468
+ localStorage.setItem('access_token', access_token);
3469
+ localStorage.setItem('refresh_token', refresh_token);
3470
+
3471
+ // User is now logged in
3472
+ \`\`\`
3473
+
3474
+ ## 2. Login Flow
3475
+
3476
+ \`\`\`typescript
3477
+ const response = await fetch('/v1/auth/login', {
3478
+ method: 'POST',
3479
+ headers: {
3480
+ 'X-API-Key': 'pk_live_your_key',
3481
+ 'Content-Type': 'application/json',
3482
+ },
3483
+ body: JSON.stringify({
3484
+ email: 'user@example.com',
3485
+ password: 'SecurePass123',
3486
+ }),
3487
+ });
3488
+
3489
+ if (!response.ok) {
3490
+ const error = await response.json();
3491
+ // Handle error: error.detail contains the message
3492
+ throw new Error(error.detail);
3493
+ }
3494
+
3495
+ const { access_token, refresh_token, user } = await response.json();
3496
+ \`\`\`
3497
+
3498
+ ## 3. Token Refresh Flow
3499
+
3500
+ \`\`\`typescript
3501
+ // Call this when access_token expires (expires_in seconds from login)
3502
+ const response = await fetch('/v1/auth/refresh', {
3503
+ method: 'POST',
3504
+ headers: {
3505
+ 'X-API-Key': 'pk_live_your_key',
3506
+ 'Content-Type': 'application/json',
3507
+ },
3508
+ body: JSON.stringify({
3509
+ refresh_token: localStorage.getItem('refresh_token'),
3510
+ }),
3511
+ });
3512
+
3513
+ const { access_token } = await response.json();
3514
+ localStorage.setItem('access_token', access_token);
3515
+ \`\`\`
3516
+
3517
+ ## 4. Password Reset Flow
3518
+
3519
+ \`\`\`typescript
3520
+ // Step 1: Request reset email
3521
+ await fetch('/v1/auth/forgot-password', {
3522
+ method: 'POST',
3523
+ headers: {
3524
+ 'X-API-Key': 'pk_live_your_key',
3525
+ 'Content-Type': 'application/json',
3526
+ },
3527
+ body: JSON.stringify({ email: 'user@example.com' }),
3528
+ });
3529
+ // Always succeeds (security)
3530
+
3531
+ // Step 2: User clicks link in email with token
3532
+
3533
+ // Step 3: Reset password with token
3534
+ await fetch('/v1/auth/reset-password', {
3535
+ method: 'POST',
3536
+ headers: {
3537
+ 'X-API-Key': 'pk_live_your_key',
3538
+ 'Content-Type': 'application/json',
3539
+ },
3540
+ body: JSON.stringify({
3541
+ token: 'token-from-email-link',
3542
+ new_password: 'NewSecurePass123',
3543
+ }),
3544
+ });
3545
+ \`\`\`
3546
+
3547
+ ## 5. Change Password Flow (Authenticated)
3548
+
3549
+ \`\`\`typescript
3550
+ await fetch('/v1/auth/change-password', {
3551
+ method: 'POST',
3552
+ headers: {
3553
+ 'X-API-Key': 'pk_live_your_key',
3554
+ 'Authorization': 'Bearer your_jwt_token',
3555
+ 'Content-Type': 'application/json',
3556
+ },
3557
+ body: JSON.stringify({
3558
+ current_password: 'OldPass123!',
3559
+ new_password: 'NewPass456!',
3560
+ }),
3561
+ });
3562
+ \`\`\`
3563
+
3564
+ ## 6. Email Verification Flow
3565
+
3566
+ \`\`\`typescript
3567
+ // Registration may require email verification depending on tenant config.
3568
+ // Check the register response for verification_required: true
3569
+
3570
+ const registerResult = await response.json();
3571
+
3572
+ if (registerResult.verification_required) {
3573
+ // 1. Show "Check your email" message — NO tokens returned
3574
+ // 2. User clicks link in email → opens /verify-email?token=xxx
3575
+
3576
+ // 3. Verify the email token
3577
+ const verifyResponse = await fetch('/v1/auth/verify-email', {
3578
+ method: 'POST',
3579
+ headers: {
3580
+ 'X-API-Key': 'pk_live_your_key',
3581
+ 'Content-Type': 'application/json',
3582
+ },
3583
+ body: JSON.stringify({ token: tokenFromUrl }),
3584
+ });
3585
+ // Returns: { message: "Email verified successfully" }
3586
+
3587
+ // 4. If token expired, user can request a new one
3588
+ await fetch('/v1/auth/resend-verification', {
3589
+ method: 'POST',
3590
+ headers: {
3591
+ 'X-API-Key': 'pk_live_your_key',
3592
+ 'Content-Type': 'application/json',
3593
+ },
3594
+ body: JSON.stringify({ email: 'user@example.com' }),
3595
+ });
3596
+ // Rate limited: 3/hour
3597
+
3598
+ // 5. After verification, user can login normally via /v1/auth/login
3599
+ } else {
3600
+ // No verification required — tokens returned directly from register
3601
+ localStorage.setItem('access_token', registerResult.access_token);
3602
+ localStorage.setItem('refresh_token', registerResult.refresh_token);
3603
+ }
3604
+ \`\`\`
3605
+
3606
+ ## 7. Google OAuth Flow
3607
+
3608
+ \`\`\`typescript
3609
+ // 1. Check if Google OAuth is enabled for this tenant
3610
+ const providersRes = await fetch('/v1/auth/providers', {
3611
+ headers: { 'X-API-Key': 'pk_live_your_key' },
3612
+ });
3613
+ const { providers } = await providersRes.json();
3614
+ const googleEnabled = providers.some((p: any) => p.provider === 'google');
3615
+
3616
+ // 2. Get Google authorization URL
3617
+ const redirectUri = window.location.origin + '/oauth/callback';
3618
+ const urlRes = await fetch(
3619
+ \`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`,
3620
+ { headers: { 'X-API-Key': 'pk_live_your_key' } }
3621
+ );
3622
+ const { authorization_url } = await urlRes.json();
3623
+
3624
+ // 3. Redirect user to Google
3625
+ window.location.href = authorization_url;
3626
+
3627
+ // 4. Google redirects to /v1/auth/google/callback (backend handles this)
3628
+ // 5. Backend redirects to YOUR redirect_uri with tokens in URL FRAGMENT (#)
3629
+
3630
+ // 6. In your /oauth/callback page, read tokens from hash (NOT query params)
3631
+ const hash = window.location.hash.substring(1);
3632
+ const params = new URLSearchParams(hash);
3633
+ const accessToken = params.get('access_token');
3634
+ const refreshToken = params.get('refresh_token');
3635
+ const expiresIn = params.get('expires_in');
3636
+
3637
+ // Also check query params for errors
3638
+ const urlParams = new URLSearchParams(window.location.search);
3639
+ const error = urlParams.get('error');
3640
+ if (error) {
3641
+ console.error('OAuth failed:', urlParams.get('error_description'));
3642
+ }
3643
+ \`\`\`
3644
+
3645
+ ## 8. Authenticated Requests
3646
+
3647
+ \`\`\`typescript
3648
+ // After login, include Authorization header
3649
+ const response = await fetch('/v1/auth/me', {
3650
+ headers: {
3651
+ 'X-API-Key': 'pk_live_your_key',
3652
+ 'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
3653
+ },
3654
+ });
3655
+
3656
+ const user = await response.json();
3657
+ \`\`\`
3658
+
3659
+ ## 9. Logout
3660
+
3661
+ \`\`\`typescript
3662
+ await fetch('/v1/auth/logout', {
3663
+ method: 'POST',
3664
+ headers: {
3665
+ 'X-API-Key': 'pk_live_your_key',
3666
+ 'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
3667
+ },
3668
+ });
3669
+
3670
+ localStorage.removeItem('access_token');
3671
+ localStorage.removeItem('refresh_token');
3137
3672
  \`\`\``,
3138
3673
  },
3139
3674
  ],
@@ -3144,225 +3679,412 @@ localStorage.removeItem('refresh_token');
3144
3679
  content: [
3145
3680
  {
3146
3681
  type: "text",
3147
- text: `# Common Implementation Patterns
3148
-
3149
- ## 1. API Request Wrapper
3150
-
3151
- \`\`\`typescript
3152
- const API_URL = process.env.NEXT_PUBLIC_API_URL;
3153
- const API_KEY = process.env.NEXT_PUBLIC_API_KEY; // pk_live_*
3154
-
3155
- interface RequestOptions {
3156
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
3157
- body?: any;
3158
- requiresAuth?: boolean;
3159
- }
3160
-
3161
- async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
3162
- const { method = 'GET', body, requiresAuth = false } = options;
3163
-
3164
- const headers: Record<string, string> = {
3165
- 'X-API-Key': API_KEY,
3166
- 'Content-Type': 'application/json',
3167
- };
3168
-
3169
- if (requiresAuth) {
3170
- const token = localStorage.getItem('access_token');
3171
- if (token) {
3172
- headers['Authorization'] = \`Bearer \${token}\`;
3173
- }
3174
- }
3175
-
3176
- const response = await fetch(\`\${API_URL}\${endpoint}\`, {
3177
- method,
3178
- headers,
3179
- body: body ? JSON.stringify(body) : undefined,
3180
- });
3181
-
3182
- if (response.status === 401 && requiresAuth) {
3183
- // Try to refresh token
3184
- const refreshed = await refreshToken();
3185
- if (refreshed) {
3186
- // Retry request with new token
3187
- headers['Authorization'] = \`Bearer \${localStorage.getItem('access_token')}\`;
3188
- const retryResponse = await fetch(\`\${API_URL}\${endpoint}\`, {
3189
- method,
3190
- headers,
3191
- body: body ? JSON.stringify(body) : undefined,
3192
- });
3193
- if (!retryResponse.ok) throw new Error('Request failed after refresh');
3194
- return retryResponse.json();
3195
- }
3196
- // Redirect to login
3197
- window.location.href = '/login';
3198
- throw new Error('Session expired');
3199
- }
3200
-
3201
- if (!response.ok) {
3202
- const error = await response.json();
3203
- throw new Error(error.detail || 'Request failed');
3204
- }
3205
-
3206
- return response.json();
3207
- }
3208
- \`\`\`
3209
-
3210
- ## 2. Auto Token Refresh
3211
-
3212
- \`\`\`typescript
3213
- async function refreshToken(): Promise<boolean> {
3214
- const refreshToken = localStorage.getItem('refresh_token');
3215
- if (!refreshToken) return false;
3216
-
3217
- try {
3218
- const response = await fetch(\`\${API_URL}/v1/auth/refresh\`, {
3219
- method: 'POST',
3220
- headers: {
3221
- 'X-API-Key': API_KEY,
3222
- 'Content-Type': 'application/json',
3223
- },
3224
- body: JSON.stringify({ refresh_token: refreshToken }),
3225
- });
3226
-
3227
- if (!response.ok) return false;
3228
-
3229
- const data = await response.json();
3230
- localStorage.setItem('access_token', data.access_token);
3231
- return true;
3232
- } catch {
3233
- return false;
3234
- }
3235
- }
3236
- \`\`\`
3237
-
3238
- ## 3. React Hook Example
3239
-
3240
- \`\`\`typescript
3241
- import { useState, useEffect } from 'react';
3242
-
3243
- function useAuth() {
3244
- const [user, setUser] = useState(null);
3245
- const [loading, setLoading] = useState(true);
3246
-
3247
- useEffect(() => {
3248
- const token = localStorage.getItem('access_token');
3249
- if (!token) {
3250
- setLoading(false);
3251
- return;
3252
- }
3253
-
3254
- apiRequest('/v1/auth/me', { requiresAuth: true })
3255
- .then(setUser)
3256
- .catch(() => {
3257
- localStorage.removeItem('access_token');
3258
- localStorage.removeItem('refresh_token');
3259
- })
3260
- .finally(() => setLoading(false));
3261
- }, []);
3262
-
3263
- const login = async (email: string, password: string) => {
3264
- const data = await apiRequest('/v1/auth/login', {
3265
- method: 'POST',
3266
- body: { email, password },
3267
- });
3268
- localStorage.setItem('access_token', data.access_token);
3269
- localStorage.setItem('refresh_token', data.refresh_token);
3270
- setUser(data.user);
3271
- return data;
3272
- };
3273
-
3274
- const logout = async () => {
3275
- await apiRequest('/v1/auth/logout', { method: 'POST', requiresAuth: true });
3276
- localStorage.removeItem('access_token');
3277
- localStorage.removeItem('refresh_token');
3278
- setUser(null);
3279
- };
3280
-
3281
- return { user, loading, login, logout };
3282
- }
3283
- \`\`\`
3284
-
3285
- ## 4. Error Handling
3286
-
3287
- \`\`\`typescript
3288
- interface ApiError {
3289
- detail: string;
3290
- error?: string;
3291
- error_description?: string;
3292
- }
3293
-
3294
- function handleApiError(error: ApiError): string {
3295
- // Common error messages
3296
- const errorMessages: Record<string, string> = {
3297
- 'Invalid email or password': 'The email or password you entered is incorrect.',
3298
- 'User already exists': 'An account with this email already exists.',
3299
- 'Invalid token': 'Your session has expired. Please log in again.',
3300
- 'Rate limit exceeded': 'Too many requests. Please try again later.',
3301
- };
3302
-
3303
- return errorMessages[error.detail] || error.detail || 'An error occurred';
3304
- }
3305
- \`\`\`
3306
-
3307
- ## 5. OAuth Callback Handling
3308
-
3309
- \`\`\`typescript
3310
- // After Google OAuth, tokens arrive in the URL FRAGMENT (#), not query params (?)
3311
- // This is because fragments are never sent to the server — they stay client-side only
3312
-
3313
- function parseOAuthCallback(): { access_token: string; refresh_token: string; expires_in: string } | null {
3314
- // Check for errors in query params first
3315
- const urlParams = new URLSearchParams(window.location.search);
3316
- const error = urlParams.get('error');
3317
- if (error) {
3318
- console.error('OAuth error:', urlParams.get('error_description'));
3319
- return null;
3320
- }
3321
-
3322
- // Parse tokens from URL fragment
3323
- const hash = window.location.hash.substring(1); // Remove leading #
3324
- const params = new URLSearchParams(hash);
3325
-
3326
- const access_token = params.get('access_token');
3327
- const refresh_token = params.get('refresh_token');
3328
- const expires_in = params.get('expires_in');
3329
-
3330
- if (!access_token || !refresh_token) return null;
3331
-
3332
- // Clean up the URL (remove fragment)
3333
- window.history.replaceState({}, '', window.location.pathname);
3334
-
3335
- return { access_token, refresh_token, expires_in: expires_in || '3600' };
3336
- }
3337
-
3338
- // Usage in your OAuth callback page:
3339
- // const tokens = parseOAuthCallback();
3340
- // if (tokens) {
3341
- // localStorage.setItem('access_token', tokens.access_token);
3342
- // localStorage.setItem('refresh_token', tokens.refresh_token);
3343
- // navigate('/dashboard');
3344
- // }
3345
- \`\`\`
3346
-
3347
- ## 6. Environment Variables (Vite / React)
3348
-
3349
- \`\`\`env
3350
- # .env
3351
- VITE_API_URL=https://api.yoursaas.com
3352
- VITE_API_KEY=pk_live_your_public_key_here
3353
- \`\`\`
3354
-
3355
- ## 7. Environment Variables (Next.js)
3356
-
3357
- \`\`\`env
3358
- # .env.local
3359
- NEXT_PUBLIC_API_URL=https://api.yoursaas.com
3360
- NEXT_PUBLIC_API_KEY=pk_live_your_public_key_here
3682
+ text: `# Common Implementation Patterns
3683
+
3684
+ ## 1. API Request Wrapper
3685
+
3686
+ \`\`\`typescript
3687
+ const API_URL = process.env.NEXT_PUBLIC_API_URL;
3688
+ const API_KEY = process.env.NEXT_PUBLIC_API_KEY; // pk_live_*
3689
+
3690
+ interface RequestOptions {
3691
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
3692
+ body?: any;
3693
+ requiresAuth?: boolean;
3694
+ }
3695
+
3696
+ async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
3697
+ const { method = 'GET', body, requiresAuth = false } = options;
3698
+
3699
+ const headers: Record<string, string> = {
3700
+ 'X-API-Key': API_KEY,
3701
+ 'Content-Type': 'application/json',
3702
+ };
3703
+
3704
+ if (requiresAuth) {
3705
+ const token = localStorage.getItem('access_token');
3706
+ if (token) {
3707
+ headers['Authorization'] = \`Bearer \${token}\`;
3708
+ }
3709
+ }
3710
+
3711
+ const response = await fetch(\`\${API_URL}\${endpoint}\`, {
3712
+ method,
3713
+ headers,
3714
+ body: body ? JSON.stringify(body) : undefined,
3715
+ });
3716
+
3717
+ if (response.status === 401 && requiresAuth) {
3718
+ // Try to refresh token
3719
+ const refreshed = await refreshToken();
3720
+ if (refreshed) {
3721
+ // Retry request with new token
3722
+ headers['Authorization'] = \`Bearer \${localStorage.getItem('access_token')}\`;
3723
+ const retryResponse = await fetch(\`\${API_URL}\${endpoint}\`, {
3724
+ method,
3725
+ headers,
3726
+ body: body ? JSON.stringify(body) : undefined,
3727
+ });
3728
+ if (!retryResponse.ok) throw new Error('Request failed after refresh');
3729
+ return retryResponse.json();
3730
+ }
3731
+ // Redirect to login
3732
+ window.location.href = '/login';
3733
+ throw new Error('Session expired');
3734
+ }
3735
+
3736
+ if (!response.ok) {
3737
+ const error = await response.json();
3738
+ throw new Error(error.detail || 'Request failed');
3739
+ }
3740
+
3741
+ return response.json();
3742
+ }
3743
+ \`\`\`
3744
+
3745
+ ## 2. Auto Token Refresh
3746
+
3747
+ \`\`\`typescript
3748
+ async function refreshToken(): Promise<boolean> {
3749
+ const refreshToken = localStorage.getItem('refresh_token');
3750
+ if (!refreshToken) return false;
3751
+
3752
+ try {
3753
+ const response = await fetch(\`\${API_URL}/v1/auth/refresh\`, {
3754
+ method: 'POST',
3755
+ headers: {
3756
+ 'X-API-Key': API_KEY,
3757
+ 'Content-Type': 'application/json',
3758
+ },
3759
+ body: JSON.stringify({ refresh_token: refreshToken }),
3760
+ });
3761
+
3762
+ if (!response.ok) return false;
3763
+
3764
+ const data = await response.json();
3765
+ localStorage.setItem('access_token', data.access_token);
3766
+ return true;
3767
+ } catch {
3768
+ return false;
3769
+ }
3770
+ }
3771
+ \`\`\`
3772
+
3773
+ ## 3. React Hook Example
3774
+
3775
+ \`\`\`typescript
3776
+ import { useState, useEffect } from 'react';
3777
+
3778
+ function useAuth() {
3779
+ const [user, setUser] = useState(null);
3780
+ const [loading, setLoading] = useState(true);
3781
+
3782
+ useEffect(() => {
3783
+ const token = localStorage.getItem('access_token');
3784
+ if (!token) {
3785
+ setLoading(false);
3786
+ return;
3787
+ }
3788
+
3789
+ apiRequest('/v1/auth/me', { requiresAuth: true })
3790
+ .then(setUser)
3791
+ .catch(() => {
3792
+ localStorage.removeItem('access_token');
3793
+ localStorage.removeItem('refresh_token');
3794
+ })
3795
+ .finally(() => setLoading(false));
3796
+ }, []);
3797
+
3798
+ const login = async (email: string, password: string) => {
3799
+ const data = await apiRequest('/v1/auth/login', {
3800
+ method: 'POST',
3801
+ body: { email, password },
3802
+ });
3803
+ localStorage.setItem('access_token', data.access_token);
3804
+ localStorage.setItem('refresh_token', data.refresh_token);
3805
+ setUser(data.user);
3806
+ return data;
3807
+ };
3808
+
3809
+ const logout = async () => {
3810
+ await apiRequest('/v1/auth/logout', { method: 'POST', requiresAuth: true });
3811
+ localStorage.removeItem('access_token');
3812
+ localStorage.removeItem('refresh_token');
3813
+ setUser(null);
3814
+ };
3815
+
3816
+ return { user, loading, login, logout };
3817
+ }
3818
+ \`\`\`
3819
+
3820
+ ## 4. Error Handling
3821
+
3822
+ \`\`\`typescript
3823
+ interface ApiError {
3824
+ detail: string;
3825
+ error?: string;
3826
+ error_description?: string;
3827
+ }
3828
+
3829
+ function handleApiError(error: ApiError): string {
3830
+ // Common error messages
3831
+ const errorMessages: Record<string, string> = {
3832
+ 'Invalid email or password': 'The email or password you entered is incorrect.',
3833
+ 'User already exists': 'An account with this email already exists.',
3834
+ 'Invalid token': 'Your session has expired. Please log in again.',
3835
+ 'Rate limit exceeded': 'Too many requests. Please try again later.',
3836
+ };
3837
+
3838
+ return errorMessages[error.detail] || error.detail || 'An error occurred';
3839
+ }
3840
+ \`\`\`
3841
+
3842
+ ## 5. OAuth Callback Handling
3843
+
3844
+ \`\`\`typescript
3845
+ // After Google OAuth, tokens arrive in the URL FRAGMENT (#), not query params (?)
3846
+ // This is because fragments are never sent to the server — they stay client-side only
3847
+
3848
+ function parseOAuthCallback(): { access_token: string; refresh_token: string; expires_in: string } | null {
3849
+ // Check for errors in query params first
3850
+ const urlParams = new URLSearchParams(window.location.search);
3851
+ const error = urlParams.get('error');
3852
+ if (error) {
3853
+ console.error('OAuth error:', urlParams.get('error_description'));
3854
+ return null;
3855
+ }
3856
+
3857
+ // Parse tokens from URL fragment
3858
+ const hash = window.location.hash.substring(1); // Remove leading #
3859
+ const params = new URLSearchParams(hash);
3860
+
3861
+ const access_token = params.get('access_token');
3862
+ const refresh_token = params.get('refresh_token');
3863
+ const expires_in = params.get('expires_in');
3864
+
3865
+ if (!access_token || !refresh_token) return null;
3866
+
3867
+ // Clean up the URL (remove fragment)
3868
+ window.history.replaceState({}, '', window.location.pathname);
3869
+
3870
+ return { access_token, refresh_token, expires_in: expires_in || '3600' };
3871
+ }
3872
+
3873
+ // Usage in your OAuth callback page:
3874
+ // const tokens = parseOAuthCallback();
3875
+ // if (tokens) {
3876
+ // localStorage.setItem('access_token', tokens.access_token);
3877
+ // localStorage.setItem('refresh_token', tokens.refresh_token);
3878
+ // navigate('/dashboard');
3879
+ // }
3880
+ \`\`\`
3881
+
3882
+ ## 6. Environment Variables (Vite / React)
3883
+
3884
+ \`\`\`env
3885
+ # .env
3886
+ VITE_API_URL=https://api.yoursaas.com
3887
+ VITE_API_KEY=pk_live_your_public_key_here
3888
+ \`\`\`
3889
+
3890
+ ## 7. Environment Variables (Next.js)
3891
+
3892
+ \`\`\`env
3893
+ # .env.local
3894
+ NEXT_PUBLIC_API_URL=https://api.yoursaas.com
3895
+ NEXT_PUBLIC_API_KEY=pk_live_your_public_key_here
3361
3896
  \`\`\``,
3362
3897
  },
3363
3898
  ],
3364
3899
  };
3365
3900
  }
3901
+ case "validate_credentials": {
3902
+ const detected = detectKeyType(API_KEY);
3903
+ if (!detected.type) {
3904
+ return {
3905
+ content: [
3906
+ {
3907
+ type: "text",
3908
+ text: `# Credentials check
3909
+
3910
+ ❌ **No SAAS_API_KEY configured.**
3911
+
3912
+ Most authenticated tools (\`list_end_users\`, \`list_oauth_providers\`, etc.)
3913
+ will fail. Add the key to your MCP client config:
3914
+
3915
+ \`\`\`json
3916
+ {
3917
+ "mcpServers": {
3918
+ "genlobe": {
3919
+ "command": "node",
3920
+ "args": ["…/dist/index.js"],
3921
+ "env": {
3922
+ "SAAS_API_URL": "${API_URL}",
3923
+ "SAAS_API_KEY": "sk_live_…"
3924
+ }
3925
+ }
3926
+ }
3927
+ }
3928
+ \`\`\`
3929
+
3930
+ Then restart the MCP. Run \`get_security_guide\` for guidance on which key
3931
+ type to pick.`,
3932
+ },
3933
+ ],
3934
+ };
3935
+ }
3936
+ // Probe with a known sk_live-only endpoint. /v1/auth/users is a safe
3937
+ // smoke test for secret keys; for public keys this will 401, which
3938
+ // is itself useful information.
3939
+ let probeResult;
3940
+ try {
3941
+ const r = await fetch(`${API_URL}/v1/auth/users?limit=1`, {
3942
+ headers: { "X-API-Key": API_KEY },
3943
+ });
3944
+ probeResult = { ok: r.ok, status: r.status };
3945
+ if (!r.ok) {
3946
+ try {
3947
+ probeResult.detail = (await r.json()).detail;
3948
+ }
3949
+ catch { }
3950
+ }
3951
+ }
3952
+ catch (err) {
3953
+ probeResult = { ok: false, status: 0, detail: String(err) };
3954
+ }
3955
+ const lines = [
3956
+ `# Credentials check`,
3957
+ ``,
3958
+ `**Configured API URL:** \`${API_URL}\``,
3959
+ `**Detected key type:** ${detected.type === "public" ? "🟢 Public (`pk_live_*`)" : "🔴 Secret (`sk_live_*`)"}`,
3960
+ `**Key prefix:** \`${detected.prefix}\``,
3961
+ ``,
3962
+ ];
3963
+ if (detected.type === "secret") {
3964
+ if (probeResult.ok) {
3965
+ lines.push(`✅ Probe call \`GET /v1/auth/users\` succeeded — the key authenticates.`, ``, `**You can use this key for:** every end-user endpoint that accepts API key auth, including admin-style listings (users, orgs) and end-user signup/login on behalf of users.`, ``, `**You CANNOT use this key for:** anything that runs in a browser. Run \`get_security_guide\` before writing frontend code.`);
3966
+ }
3967
+ else {
3968
+ lines.push(`❌ Probe call \`GET /v1/auth/users\` failed: HTTP ${probeResult.status}${probeResult.detail ? ` — ${probeResult.detail}` : ""}.`, ``, `Possible causes: key revoked, wrong tenant, wrong API URL, network blocked, backend down.`);
3969
+ }
3970
+ }
3971
+ else {
3972
+ if (probeResult.status === 401 || probeResult.status === 403) {
3973
+ lines.push(`✅ Behavior matches a public key: \`/v1/auth/users\` returned HTTP ${probeResult.status} as expected. Public keys can only call auth endpoints (\`/v1/auth/register\`, \`/v1/auth/login\`, etc.).`, ``, `Run \`get_security_guide\` for the full list of allowed endpoints.`);
3974
+ }
3975
+ else if (probeResult.ok) {
3976
+ lines.push(`⚠️ Probe unexpectedly succeeded with what looks like a public key. Verify the prefix; if it really is a secret key the prefix detection is wrong.`);
3977
+ }
3978
+ else {
3979
+ lines.push(`❌ Probe failed unexpectedly: HTTP ${probeResult.status}${probeResult.detail ? ` — ${probeResult.detail}` : ""}.`);
3980
+ }
3981
+ }
3982
+ return { content: [{ type: "text", text: lines.join("\n") }] };
3983
+ }
3984
+ case "list_end_users": {
3985
+ if (!API_KEY) {
3986
+ return {
3987
+ content: [
3988
+ {
3989
+ type: "text",
3990
+ text: `❌ Cannot list end-users: SAAS_API_KEY is not configured. Add a secret key (\`sk_live_*\`) to the MCP env. Use \`validate_credentials\` to verify.`,
3991
+ },
3992
+ ],
3993
+ };
3994
+ }
3995
+ const skip = args.skip ?? 0;
3996
+ const limit = args.limit ?? 20;
3997
+ try {
3998
+ const r = await fetch(`${API_URL}/v1/auth/users?skip=${skip}&limit=${Math.min(limit, 100)}`, { headers: { "X-API-Key": API_KEY } });
3999
+ const body = await r.json();
4000
+ if (!r.ok) {
4001
+ return {
4002
+ content: [
4003
+ {
4004
+ type: "text",
4005
+ text: `HTTP ${r.status} from /v1/auth/users — ${body.detail ?? JSON.stringify(body)}.\n\nIf the key is \`pk_live_*\`, this endpoint is restricted to secret keys. Run \`get_security_guide\` for details.`,
4006
+ },
4007
+ ],
4008
+ };
4009
+ }
4010
+ return {
4011
+ content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
4012
+ };
4013
+ }
4014
+ catch (err) {
4015
+ return {
4016
+ content: [{ type: "text", text: `Network error: ${err}` }],
4017
+ };
4018
+ }
4019
+ }
4020
+ case "list_oauth_providers": {
4021
+ if (!API_KEY) {
4022
+ return {
4023
+ content: [
4024
+ {
4025
+ type: "text",
4026
+ text: `❌ Cannot list providers: SAAS_API_KEY not configured.`,
4027
+ },
4028
+ ],
4029
+ };
4030
+ }
4031
+ try {
4032
+ const r = await fetch(`${API_URL}/v1/auth/providers`, {
4033
+ headers: { "X-API-Key": API_KEY },
4034
+ });
4035
+ const body = await r.json();
4036
+ if (!r.ok) {
4037
+ return {
4038
+ content: [
4039
+ {
4040
+ type: "text",
4041
+ text: `HTTP ${r.status} from /v1/auth/providers — ${body.detail ?? JSON.stringify(body)}.`,
4042
+ },
4043
+ ],
4044
+ };
4045
+ }
4046
+ const providers = body.providers ?? [];
4047
+ if (providers.length === 0) {
4048
+ return {
4049
+ content: [
4050
+ {
4051
+ type: "text",
4052
+ text: `No OAuth providers configured for this tenant. The "Sign in with Google" / GitHub / etc. buttons would not work — only email/password login is wired up. Configure providers from the tenant dashboard before generating social-login UIs.`,
4053
+ },
4054
+ ],
4055
+ };
4056
+ }
4057
+ const list = providers.map(p => `- **${p.name}** (\`${p.provider}\`)`).join("\n");
4058
+ return {
4059
+ content: [
4060
+ {
4061
+ type: "text",
4062
+ text: `# OAuth providers configured for this tenant\n\n${list}\n\nWire each one with \`GET /v1/auth/{provider}/url\` to get the authorization URL.`,
4063
+ },
4064
+ ],
4065
+ };
4066
+ }
4067
+ catch (err) {
4068
+ return {
4069
+ content: [{ type: "text", text: `Network error: ${err}` }],
4070
+ };
4071
+ }
4072
+ }
4073
+ case "get_security_guide": {
4074
+ const detected = detectKeyType(API_KEY);
4075
+ return {
4076
+ content: [{ type: "text", text: SECURITY_GUIDE(detected.type) }],
4077
+ };
4078
+ }
4079
+ case "recommend_stack": {
4080
+ const appType = args.app_type;
4081
+ const detected = detectKeyType(API_KEY);
4082
+ return {
4083
+ content: [
4084
+ { type: "text", text: STACK_RECOMMENDATION(appType, detected.type) },
4085
+ ],
4086
+ };
4087
+ }
3366
4088
  default:
3367
4089
  throw new Error(`Unknown tool: ${name}`);
3368
4090
  }
@@ -3381,10 +4103,14 @@ NEXT_PUBLIC_API_KEY=pk_live_your_public_key_here
3381
4103
  });
3382
4104
  // Start the server
3383
4105
  async function main() {
3384
- console.error(`🚀 Multi-tenant SaaS API MCP Server v2.1.0`);
4106
+ console.error(`🚀 Multi-tenant SaaS API MCP Server v${SERVER_VERSION}`);
3385
4107
  console.error(`📡 API URL: ${API_URL}`);
3386
- console.error(`🔑 API Key: ${API_KEY ? "Configured" : "Not configured"}`);
3387
- console.error(`\nAvailable tools:`);
4108
+ console.error(`🔑 API Key: ${API_KEY ? "Configured" : "Not configured (set SAAS_API_KEY to enable authenticated calls)"}`);
4109
+ console.error(`\n👉 Recommended first calls (vibecoding flow):`);
4110
+ console.error(` 1. validate_credentials — verify key + detect type (pk_live_* vs sk_live_*)`);
4111
+ console.error(` 2. get_security_guide — safe-usage rules for the detected key type`);
4112
+ console.error(` 3. recommend_stack — pick a framework that won't leak your key`);
4113
+ console.error(`\nReference tools:`);
3388
4114
  console.error(` - get_api_overview: Understand the API architecture`);
3389
4115
  console.error(` - get_end_user_endpoints: Get endpoint documentation`);
3390
4116
  console.error(` - get_sdk_template: Get TypeScript SDK template`);
@@ -3393,6 +4119,9 @@ async function main() {
3393
4119
  console.error(` - search_endpoints: Search for endpoints`);
3394
4120
  console.error(` - get_endpoint_details: Get specific endpoint info`);
3395
4121
  console.error(` - get_openapi_spec: Get full OpenAPI spec`);
4122
+ console.error(`\nAuthenticated helpers:`);
4123
+ console.error(` - list_end_users (sk_live_* required)`);
4124
+ console.error(` - list_oauth_providers (any key)`);
3396
4125
  const transport = new StdioServerTransport();
3397
4126
  await server.connect(transport);
3398
4127
  }