@genlobe/mcp-server 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3402 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Server for Multi-tenant SaaS API
4
+ *
5
+ * This server provides AI assistants with comprehensive API documentation
6
+ * and tools to understand how to build frontends using this API.
7
+ *
8
+ * The API has TWO types of endpoints:
9
+ * 1. TENANT/DASHBOARD endpoints - For tenant admins (JWT auth from dashboard login)
10
+ * 2. END-USER endpoints - For end-users of tenants (API Key + optional JWT)
11
+ *
12
+ * When building a frontend for END-USERS of a tenant, you should ONLY use
13
+ * the End-user endpoints, NOT the Tenant/Dashboard endpoints.
14
+ *
15
+ * Usage:
16
+ * npx @multiagent/mcp-server
17
+ *
18
+ * Or configure in VS Code MCP settings with SAAS_API_URL environment variable.
19
+ */
20
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
21
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
22
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
+ // =============================================================================
24
+ // Configuration
25
+ // =============================================================================
26
+ const API_URL = process.env.SAAS_API_URL || process.env.API_URL || "https://api.example.com";
27
+ const API_KEY = process.env.SAAS_API_KEY || process.env.API_KEY || "";
28
+ // Cache for API responses
29
+ const cache = new Map();
30
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
31
+ // =============================================================================
32
+ // API Documentation - Embedded for reliability
33
+ // =============================================================================
34
+ /**
35
+ * CRITICAL: This embedded documentation ensures AI assistants have accurate
36
+ * information even if the API is not accessible.
37
+ */
38
+ const API_OVERVIEW = {
39
+ 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
84
+ `,
85
+ base_url: API_URL,
86
+ documentation_urls: {
87
+ swagger: `${API_URL}/docs`,
88
+ redoc: `${API_URL}/redoc`,
89
+ openapi: `${API_URL}/v1/developer/openapi.json`
90
+ }
91
+ };
92
+ // =============================================================================
93
+ // End-User Endpoints (for building frontends)
94
+ // =============================================================================
95
+ const END_USER_ENDPOINTS = {
96
+ authentication: {
97
+ description: "User authentication endpoints - login, register, password reset. Protected by rate limiting.",
98
+ endpoints: [
99
+ {
100
+ method: "POST",
101
+ path: "/v1/auth/register",
102
+ summary: "Register a new user",
103
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
104
+ rate_limit: "3 per hour per IP (blocked 30 min after exceeding)",
105
+ request_body: {
106
+ email: "string (required) - valid email",
107
+ password: "string (required) - min 8 chars, 1 uppercase, 1 lowercase, 1 digit",
108
+ display_name: "string (optional)",
109
+ locale: "string (optional, default: 'en')",
110
+ return_to: "string (optional) - frontend URL for email verification redirect (e.g. https://app.example.com/verify-email)"
111
+ },
112
+ response: "CONDITIONAL - depends on tenant email verification setting",
113
+ response_when_no_verification: {
114
+ access_token: "string - JWT access token (expires in 1 hour)",
115
+ refresh_token: "string - JWT refresh token",
116
+ token_type: "Bearer",
117
+ expires_in: "number - seconds until expiration",
118
+ user: {
119
+ id: "uuid",
120
+ email: "string",
121
+ display_name: "string | null",
122
+ avatar_url: "string | null",
123
+ is_active: "boolean",
124
+ created_at: "ISO datetime"
125
+ }
126
+ },
127
+ response_when_verification_required: {
128
+ message: "Registration successful. Please check your email to verify your account.",
129
+ verification_required: "true"
130
+ },
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"
137
+ }`
138
+ },
139
+ {
140
+ method: "POST",
141
+ path: "/v1/auth/login",
142
+ summary: "Login with email and password",
143
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
144
+ rate_limit: "10 per 15 min per IP (blocked 15 min after exceeding)",
145
+ request_body: {
146
+ email: "string (required)",
147
+ password: "string (required)"
148
+ },
149
+ response: "Same as register",
150
+ example_request: `{
151
+ "email": "user@example.com",
152
+ "password": "SecurePass123"
153
+ }`
154
+ },
155
+ {
156
+ method: "POST",
157
+ path: "/v1/auth/refresh",
158
+ summary: "Refresh access token using refresh token",
159
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
160
+ request_body: {
161
+ refresh_token: "string (required)"
162
+ },
163
+ response: "Same as login (with same refresh_token returned)"
164
+ },
165
+ {
166
+ method: "GET",
167
+ path: "/v1/auth/me",
168
+ summary: "Get current authenticated user info",
169
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
170
+ response: {
171
+ id: "uuid",
172
+ email: "string",
173
+ display_name: "string | null",
174
+ avatar_url: "string | null",
175
+ is_active: "boolean",
176
+ created_at: "ISO datetime"
177
+ }
178
+ },
179
+ {
180
+ method: "POST",
181
+ path: "/v1/auth/logout",
182
+ summary: "Logout (invalidates token on client side)",
183
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
184
+ response: { message: "Successfully logged out" }
185
+ },
186
+ {
187
+ method: "POST",
188
+ path: "/v1/auth/forgot-password",
189
+ summary: "Request password reset email",
190
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
191
+ rate_limit: "3 per hour per IP (blocked 30 min after exceeding)",
192
+ request_body: {
193
+ email: "string (required)"
194
+ },
195
+ response: { message: "Password reset email sent" },
196
+ note: "Always returns success even if email doesn't exist (security)"
197
+ },
198
+ {
199
+ method: "POST",
200
+ path: "/v1/auth/reset-password",
201
+ summary: "Reset password using token from email",
202
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
203
+ request_body: {
204
+ token: "string (required) - from email link",
205
+ new_password: "string (required) - same requirements as register"
206
+ }
207
+ },
208
+ {
209
+ method: "POST",
210
+ path: "/v1/auth/change-password",
211
+ summary: "Change password for authenticated user",
212
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
213
+ rate_limit: "5 per hour per user (blocked 30 min after exceeding)",
214
+ request_body: {
215
+ current_password: "string (required)",
216
+ new_password: "string (required) - same requirements as register"
217
+ },
218
+ response: { message: "Password changed successfully" }
219
+ },
220
+ {
221
+ method: "POST",
222
+ path: "/v1/auth/verify-email",
223
+ summary: "Verify end-user email with token (requires API Key)",
224
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
225
+ request_body: {
226
+ token: "string (required) - from verification email link"
227
+ },
228
+ response: { message: "Email verified successfully" },
229
+ note: "Use when the verification link is opened in a context that has the tenant API key (e.g. end-user app)."
230
+ },
231
+ {
232
+ method: "POST",
233
+ path: "/v1/auth/verify-email-with-token",
234
+ summary: "Verify end-user email with token only (no API Key)",
235
+ auth: { api_key: "none", jwt: false },
236
+ rate_limit: "20 per hour per IP",
237
+ request_body: {
238
+ token: "string (required) - from verification email link"
239
+ },
240
+ response: { message: "Email verified successfully" },
241
+ note: "Use when the verification link is opened from a page without API key (e.g. dashboard domain). Rate limited by IP."
242
+ },
243
+ {
244
+ method: "POST",
245
+ path: "/v1/auth/resend-verification",
246
+ summary: "Resend verification email for end-user",
247
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
248
+ rate_limit: "3 per hour per IP (blocked 30 min after exceeding)",
249
+ request_body: {
250
+ email: "string (required) - valid email address"
251
+ },
252
+ response: { message: "Verification email sent" },
253
+ note: "Always returns success even if email doesn't exist (security). Rate limited to prevent abuse."
254
+ },
255
+ {
256
+ method: "GET",
257
+ path: "/v1/auth/providers",
258
+ summary: "List enabled OAuth providers for this tenant",
259
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
260
+ response: {
261
+ providers: "[ { provider: 'google', name: 'Google' }, ... ]"
262
+ },
263
+ note: "Use on login page to show/hide 'Login with Google' button. Only returns providers with is_enabled in tenant config."
264
+ },
265
+ {
266
+ method: "GET",
267
+ path: "/v1/auth/google/url",
268
+ summary: "Get Google OAuth authorization URL",
269
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
270
+ query_params: {
271
+ redirect_uri: "string (required) - frontend URL where Google redirects after auth (must match tenant config)"
272
+ },
273
+ response: { authorization_url: "string - redirect user to this URL" }
274
+ },
275
+ {
276
+ method: "GET",
277
+ path: "/v1/auth/google/callback",
278
+ summary: "Google OAuth callback (called by Google, not by your frontend)",
279
+ auth: { api_key: "none", jwt: false },
280
+ query_params: {
281
+ code: "from Google",
282
+ state: "from Google (HMAC-signed, contains tenant_id + redirect_uri)"
283
+ },
284
+ response: "Redirects to frontend redirect_uri with tokens in URL fragment (#): access_token, refresh_token, expires_in. Errors redirect with ?error=oauth_failed&error_description=..."
285
+ }
286
+ ]
287
+ },
288
+ organizations: {
289
+ description: "Organization management - create orgs, manage members, invitations, subscriptions, usage",
290
+ endpoints: [
291
+ {
292
+ method: "GET",
293
+ path: "/v1/organizations/my-organizations",
294
+ summary: "Get all organizations the current user belongs to",
295
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
296
+ response: {
297
+ organizations: [
298
+ {
299
+ id: "uuid",
300
+ name: "string",
301
+ slug: "string",
302
+ description: "string | null",
303
+ logo_url: "string | null",
304
+ role: "owner | admin | member | developer",
305
+ created_at: "ISO datetime"
306
+ }
307
+ ]
308
+ }
309
+ },
310
+ {
311
+ method: "POST",
312
+ path: "/v1/organizations",
313
+ summary: "Create a new organization (user becomes owner)",
314
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
315
+ request_body: {
316
+ name: "string (required)",
317
+ slug: "string (required) - URL-friendly identifier",
318
+ description: "string (optional)"
319
+ },
320
+ example_request: `{
321
+ "name": "My Company",
322
+ "slug": "my-company",
323
+ "description": "Our awesome company"
324
+ }`
325
+ },
326
+ {
327
+ method: "GET",
328
+ path: "/v1/organizations/{organization_id}",
329
+ summary: "Get organization details (must be a member)",
330
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true }
331
+ },
332
+ {
333
+ method: "PUT",
334
+ path: "/v1/organizations/{organization_id}",
335
+ summary: "Update organization (owner/admin only)",
336
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
337
+ request_body: {
338
+ name: "string (optional)",
339
+ description: "string (optional)",
340
+ logo_url: "string (optional)"
341
+ }
342
+ },
343
+ {
344
+ method: "GET",
345
+ path: "/v1/organizations/{organization_id}/departments",
346
+ summary: "List organization departments",
347
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true }
348
+ },
349
+ {
350
+ method: "GET",
351
+ path: "/v1/organizations/{organization_id}/members",
352
+ summary: "List all members of the organization",
353
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
354
+ response: [
355
+ {
356
+ user_id: "uuid",
357
+ email: "string",
358
+ display_name: "string | null",
359
+ role: "owner | admin | member | developer",
360
+ joined_at: "ISO datetime"
361
+ }
362
+ ]
363
+ },
364
+ {
365
+ method: "PUT",
366
+ path: "/v1/organizations/{organization_id}/members/{user_id}",
367
+ summary: "Update member role (owner/admin only)",
368
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
369
+ request_body: {
370
+ role: "admin | member | developer (required)"
371
+ }
372
+ },
373
+ {
374
+ method: "DELETE",
375
+ path: "/v1/organizations/{organization_id}/members/{user_id}",
376
+ summary: "Remove a member from the organization (owner/admin only)",
377
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true }
378
+ },
379
+ {
380
+ method: "POST",
381
+ path: "/v1/organizations/{organization_id}/members/{user_id}/deactivate",
382
+ summary: "Deactivate an organization member",
383
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true }
384
+ },
385
+ {
386
+ method: "POST",
387
+ path: "/v1/organizations/{organization_id}/invite",
388
+ summary: "Invite a user to the organization (owner/admin only)",
389
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
390
+ request_body: {
391
+ email: "string (required)",
392
+ role: "admin | member | developer (required)"
393
+ }
394
+ },
395
+ {
396
+ method: "POST",
397
+ path: "/v1/organizations/{organization_id}/invitations/{invitation_id}/resend",
398
+ summary: "Resend an organization invitation",
399
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true }
400
+ },
401
+ {
402
+ method: "DELETE",
403
+ path: "/v1/organizations/{organization_id}/invitations/{invitation_id}",
404
+ summary: "Cancel/delete an organization invitation",
405
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true }
406
+ },
407
+ {
408
+ method: "POST",
409
+ path: "/v1/organizations/accept-invitation",
410
+ summary: "Accept an organization invitation",
411
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
412
+ request_body: {
413
+ token: "string (required) - from invitation email"
414
+ }
415
+ },
416
+ {
417
+ method: "POST",
418
+ path: "/v1/organizations/reject-invitation",
419
+ summary: "Reject an organization invitation (public, no auth required)",
420
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
421
+ request_body: {
422
+ token: "string (required) - from invitation email"
423
+ }
424
+ },
425
+ {
426
+ method: "GET",
427
+ path: "/v1/organizations/invitations/pending",
428
+ summary: "Get pending invitations for an email",
429
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
430
+ query_params: {
431
+ email: "string (required) - email to check"
432
+ }
433
+ },
434
+ {
435
+ method: "GET",
436
+ path: "/v1/organizations/invitations/{invitation_token}",
437
+ summary: "Get invitation details by token (public)",
438
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false }
439
+ },
440
+ {
441
+ method: "POST",
442
+ path: "/v1/organizations/{organization_id}/set-default",
443
+ summary: "Set default organization for the current user",
444
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true }
445
+ },
446
+ // Organization subscription management
447
+ {
448
+ method: "GET",
449
+ path: "/v1/organizations/{organization_id}/subscription",
450
+ summary: "Get organization's current subscription with plan details",
451
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
452
+ response: "SubscriptionWithPlanResponse - includes subscription + embedded plan object",
453
+ note: "Requires BILLING_READ permission. Returns null if no active subscription."
454
+ },
455
+ {
456
+ method: "POST",
457
+ path: "/v1/organizations/{organization_id}/subscription/{plan_id}",
458
+ summary: "Subscribe organization to a plan",
459
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
460
+ response: "SubscriptionResponse",
461
+ note: "Requires BILLING_MANAGE permission (owner/admin)."
462
+ },
463
+ {
464
+ method: "PUT",
465
+ path: "/v1/organizations/{organization_id}/subscription",
466
+ summary: "Update organization subscription",
467
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
468
+ request_body: {
469
+ plan_id: "uuid (optional)",
470
+ billing_cycle: "string (optional) - 'monthly' or 'yearly'",
471
+ subscription_metadata: "object (optional)"
472
+ },
473
+ response: "SubscriptionResponse",
474
+ note: "Requires BILLING_MANAGE permission."
475
+ },
476
+ {
477
+ method: "DELETE",
478
+ path: "/v1/organizations/{organization_id}/subscription",
479
+ summary: "Cancel organization subscription",
480
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
481
+ query_params: {
482
+ cancel_at_period_end: "boolean (default: true) - If true, access continues until period end"
483
+ },
484
+ response: {
485
+ message: "string",
486
+ subscription: "SubscriptionResponse"
487
+ },
488
+ note: "Requires BILLING_MANAGE permission."
489
+ },
490
+ // Organization usage
491
+ {
492
+ method: "GET",
493
+ path: "/v1/organizations/{organization_id}/usage",
494
+ summary: "Get organization usage statistics",
495
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
496
+ response: {
497
+ usage_stats: "object - Aggregated usage for the organization"
498
+ },
499
+ note: "Requires ORG_READ permission. Must be a member of the organization."
500
+ },
501
+ {
502
+ method: "GET",
503
+ path: "/v1/organizations/{organization_id}/usage/user/{target_user_id}",
504
+ summary: "Get usage statistics for a specific user in the organization",
505
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
506
+ response: {
507
+ usage: "object - User's usage within this organization"
508
+ },
509
+ note: "Requires MEMBERS_READ permission."
510
+ },
511
+ {
512
+ method: "POST",
513
+ path: "/v1/organizations/{organization_id}/usage/check",
514
+ summary: "Check if organization would exceed usage limits",
515
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
516
+ query_params: {
517
+ additional_messages: "number (default: 0) - Additional messages to check",
518
+ additional_tokens: "number (default: 0) - Additional tokens to check"
519
+ },
520
+ response: {
521
+ would_exceed: "boolean - Whether limits would be exceeded",
522
+ limits: "object - Current limit details"
523
+ },
524
+ note: "Pre-validate before making API calls. Requires ORG_READ permission."
525
+ }
526
+ ]
527
+ },
528
+ subscriptions: {
529
+ description: "Subscription and billing management with Stripe",
530
+ endpoints: [
531
+ {
532
+ method: "GET",
533
+ path: "/v1/subscriptions/plans",
534
+ summary: "List all available subscription plans",
535
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
536
+ response: [
537
+ {
538
+ id: "uuid",
539
+ name: "string",
540
+ description: "string",
541
+ price_monthly: "number",
542
+ price_yearly: "number",
543
+ currency: "string (e.g., 'usd')",
544
+ features: "array of strings",
545
+ is_active: "boolean",
546
+ is_free: "boolean",
547
+ max_cost_usd_per_month: "number | null - Maximum AI usage cost per month",
548
+ max_messages_per_month: "number | null - Maximum AI requests per month",
549
+ max_tokens_per_month: "number | null - DEPRECATED: Use max_cost_usd_per_month",
550
+ stripe_product_id: "string",
551
+ stripe_price_id_monthly: "string",
552
+ stripe_price_id_yearly: "string",
553
+ created_at: "ISO datetime",
554
+ updated_at: "ISO datetime"
555
+ }
556
+ ],
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
+ }
577
+ ]`
578
+ },
579
+ {
580
+ method: "GET",
581
+ path: "/v1/subscriptions/current",
582
+ summary: "Get current user's active subscription",
583
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
584
+ response: {
585
+ id: "uuid",
586
+ plan_id: "uuid",
587
+ organization_id: "uuid",
588
+ tenant_id: "uuid",
589
+ stripe_customer_id: "string",
590
+ stripe_subscription_id: "string",
591
+ billing_cycle: "monthly | yearly",
592
+ status: "active | cancelled | past_due | incomplete | trialing | unpaid",
593
+ current_period_start: "ISO datetime",
594
+ current_period_end: "ISO datetime",
595
+ cancel_at_period_end: "boolean",
596
+ canceled_at: "ISO datetime | null",
597
+ trial_start: "ISO datetime | null",
598
+ trial_end: "ISO datetime | null",
599
+ subscription_metadata: "object - custom metadata",
600
+ created_at: "ISO datetime",
601
+ updated_at: "ISO datetime"
602
+ },
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"
621
+ }`,
622
+ note: "Does NOT include plan_name or price. Use plan_id to fetch plan details if needed."
623
+ },
624
+ {
625
+ method: "POST",
626
+ path: "/v1/subscriptions/checkout",
627
+ summary: "Create Stripe checkout session for subscription",
628
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
629
+ request_body: {
630
+ plan_id: "uuid (required)",
631
+ success_url: "string (required) - URL to redirect after success",
632
+ cancel_url: "string (required) - URL to redirect on cancel",
633
+ organization_id: "uuid (optional) - for org subscriptions"
634
+ },
635
+ response: {
636
+ checkout_url: "string - Stripe checkout URL",
637
+ session_id: "string"
638
+ }
639
+ },
640
+ {
641
+ method: "GET",
642
+ path: "/v1/subscriptions/plans/{plan_id}",
643
+ summary: "Get plan details by ID",
644
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
645
+ response: "PlanResponse (same as /v1/subscriptions/plans list item)",
646
+ note: "Tenant-isolated: plan must belong to tenant or be a global plan."
647
+ },
648
+ {
649
+ method: "POST",
650
+ path: "/v1/subscriptions/subscriptions",
651
+ summary: "Create a new subscription",
652
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
653
+ request_body: {
654
+ plan_id: "uuid (required)",
655
+ organization_id: "uuid (required)",
656
+ billing_cycle: "string (default: 'monthly') - 'monthly' or 'yearly'",
657
+ subscription_metadata: "object (optional) - custom metadata"
658
+ },
659
+ response: "SubscriptionResponse (same as /v1/subscriptions/current)"
660
+ },
661
+ {
662
+ method: "GET",
663
+ path: "/v1/subscriptions/subscriptions/{subscription_id}",
664
+ summary: "Get subscription details by ID",
665
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
666
+ response: "SubscriptionResponse",
667
+ note: "User must own the subscription."
668
+ },
669
+ {
670
+ method: "PUT",
671
+ path: "/v1/subscriptions/subscriptions/{subscription_id}",
672
+ summary: "Update a subscription",
673
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
674
+ request_body: {
675
+ plan_id: "uuid (optional)",
676
+ billing_cycle: "string (optional) - 'monthly' or 'yearly'",
677
+ subscription_metadata: "object (optional)"
678
+ },
679
+ response: "SubscriptionResponse",
680
+ note: "User must own the subscription."
681
+ },
682
+ {
683
+ method: "DELETE",
684
+ path: "/v1/subscriptions/subscriptions/{subscription_id}",
685
+ summary: "Cancel/delete a subscription",
686
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
687
+ query_params: {
688
+ cancel_at_period_end: "boolean (default: true) - if true, access continues until period end"
689
+ },
690
+ response: "SubscriptionResponse",
691
+ note: "Returns 403 if user doesn't own the subscription."
692
+ },
693
+ {
694
+ method: "GET",
695
+ path: "/v1/subscriptions/users/{user_id}/usage",
696
+ summary: "Get user plan usage statistics",
697
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
698
+ response: {
699
+ id: "uuid",
700
+ organization_id: "uuid",
701
+ user_id: "uuid",
702
+ plan_id: "uuid | null",
703
+ period_start: "ISO datetime",
704
+ period_end: "ISO datetime",
705
+ messages_count: "number",
706
+ tokens_count: "number",
707
+ usage_metadata: "object",
708
+ created_at: "ISO datetime",
709
+ updated_at: "ISO datetime"
710
+ },
711
+ note: "User can only access own usage. Returns 404 if no active plan."
712
+ },
713
+ {
714
+ method: "GET",
715
+ path: "/v1/subscriptions/users/{user_id}/usage/limits",
716
+ summary: "Check user usage limits",
717
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
718
+ response: {
719
+ limits: "object with plan limit details"
720
+ },
721
+ note: "User can only check own limits."
722
+ },
723
+ {
724
+ method: "PUT",
725
+ path: "/v1/subscriptions/users/{user_id}/plan/{plan_id}",
726
+ summary: "Assign a plan to a user",
727
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
728
+ response: {
729
+ success: "boolean",
730
+ user_id: "uuid",
731
+ plan_id: "uuid"
732
+ },
733
+ note: "User can only assign plans to themselves. Typically done via subscriptions."
734
+ },
735
+ {
736
+ method: "POST",
737
+ path: "/v1/subscriptions/organizations/{organization_id}/resume",
738
+ summary: "Resume a cancelled organization subscription",
739
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
740
+ query_params: {
741
+ user_id: "uuid (required) - User requesting the resume"
742
+ },
743
+ response: "SubscriptionResponse"
744
+ },
745
+ {
746
+ method: "GET",
747
+ path: "/v1/subscriptions/organizations/{organization_id}/invoices",
748
+ summary: "Get organization invoices from Stripe",
749
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
750
+ query_params: {
751
+ user_id: "uuid (required) - User requesting invoices"
752
+ },
753
+ response: "array of Stripe invoice objects"
754
+ },
755
+ {
756
+ method: "DELETE",
757
+ path: "/v1/subscriptions/organizations/{organization_id}/subscription",
758
+ summary: "Cancel organization subscription",
759
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
760
+ query_params: {
761
+ user_id: "uuid (required)",
762
+ cancel_at_period_end: "boolean (default: true)"
763
+ },
764
+ response: "SubscriptionResponse"
765
+ }
766
+ ]
767
+ },
768
+ billing: {
769
+ description: "Billing operations - checkout, plan changes, cancellation. All endpoints require SECRET key (sk_live_*) only.",
770
+ endpoints: [
771
+ {
772
+ method: "GET",
773
+ path: "/v1/billing/available-plans",
774
+ summary: "List available billing plans for upgrade/downgrade",
775
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
776
+ response: {
777
+ success: "boolean",
778
+ plans: "array of plan objects with: id (uuid), name (string), price_cents (int), price_display (string), currency (string), features (array), is_free (boolean), stripe_price_id (string | null)"
779
+ }
780
+ },
781
+ {
782
+ method: "POST",
783
+ path: "/v1/billing/create-checkout-session",
784
+ summary: "Create Stripe checkout session for billing",
785
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
786
+ request_body: {
787
+ user_id: "uuid (required) - End-user ID",
788
+ organization_id: "uuid (required) - Organization to subscribe",
789
+ plan_id: "uuid (required) - Target plan",
790
+ success_url: "string (required) - Redirect URL after successful checkout",
791
+ cancel_url: "string (required) - Redirect URL on cancel"
792
+ },
793
+ response: {
794
+ checkout_url: "string - Stripe hosted checkout URL",
795
+ session_id: "string - Stripe session ID",
796
+ plan_name: "string - Name of selected plan",
797
+ price: "string - Display price",
798
+ message: "string | null"
799
+ },
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"
806
+ }`
807
+ },
808
+ {
809
+ method: "POST",
810
+ path: "/v1/billing/change-plan",
811
+ summary: "Change current subscription plan (upgrade/downgrade)",
812
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
813
+ request_body: {
814
+ subscription_id: "uuid (required) - Current subscription ID",
815
+ new_plan_id: "uuid (required) - Target plan ID"
816
+ },
817
+ response: {
818
+ success: "boolean",
819
+ message: "string",
820
+ plan_name: "string",
821
+ subscription_id: "uuid | null",
822
+ stripe_subscription_id: "string | null",
823
+ status: "string | null",
824
+ cancel_at_period_end: "boolean | null"
825
+ },
826
+ 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"
830
+ }`
831
+ },
832
+ {
833
+ method: "POST",
834
+ path: "/v1/billing/cancel-subscription",
835
+ summary: "Cancel current subscription",
836
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
837
+ query_params: {
838
+ subscription_id: "uuid (required) - Subscription to cancel",
839
+ cancel_at_period_end: "boolean (default: true) - If true, access continues until period end"
840
+ },
841
+ response: {
842
+ success: "boolean",
843
+ message: "string",
844
+ cancel_at_period_end: "boolean"
845
+ }
846
+ },
847
+ {
848
+ method: "GET",
849
+ path: "/v1/billing/billing-history-org/{organization_id}",
850
+ summary: "Get billing history (Stripe invoices) for organization",
851
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
852
+ query_params: {
853
+ limit: "number (default: 20) - Number of invoices to return"
854
+ },
855
+ response: {
856
+ success: "boolean",
857
+ organization_id: "string",
858
+ invoices: "array of Stripe invoice objects",
859
+ total_invoices: "number"
860
+ },
861
+ note: "Returns 404 if organization not found or doesn't belong to tenant."
862
+ }
863
+ ]
864
+ },
865
+ usage: {
866
+ description: "Usage tracking and limits. Endpoints map to /v1/api/usage/* (current/history) and /v1/usage/* (stats/check/consume).",
867
+ endpoints: [
868
+ {
869
+ method: "GET",
870
+ path: "/v1/api/usage/current",
871
+ summary: "Get current usage statistics for authenticated user",
872
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
873
+ query_params: {
874
+ organization_id: "uuid (required) - Organization ID to check usage for"
875
+ },
876
+ response: {
877
+ period_start: "ISO datetime",
878
+ period_end: "ISO datetime",
879
+ plan_name: "string",
880
+ plan_id: "uuid",
881
+ usage: {
882
+ messages: {
883
+ current: "number - Messages used in current period",
884
+ max: "number | 'unlimited' - Maximum allowed messages",
885
+ remaining: "number | 'unlimited' - Remaining messages",
886
+ percentage: "number - Usage percentage (0-100)"
887
+ },
888
+ cost_usd: {
889
+ current: "number - Current AI cost in USD",
890
+ max: "number | 'unlimited' - Maximum allowed cost",
891
+ remaining: "number | 'unlimited' - Remaining budget",
892
+ percentage: "number - Usage percentage (0-100)"
893
+ },
894
+ tokens: {
895
+ total: "number - Total tokens used",
896
+ input: "number - Input tokens",
897
+ output: "number - Output tokens"
898
+ }
899
+ },
900
+ error: "string | null - Error message if any"
901
+ },
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
927
+ }`
928
+ },
929
+ {
930
+ method: "GET",
931
+ path: "/v1/api/usage/history",
932
+ summary: "Get usage history",
933
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
934
+ query_params: {
935
+ organization_id: "uuid (required) - Organization ID",
936
+ limit: "number (default: 20) - Number of records to return",
937
+ offset: "number (default: 0) - Pagination offset"
938
+ },
939
+ response: "array of usage records with period dates, usage counts, and metadata"
940
+ },
941
+ {
942
+ method: "GET",
943
+ path: "/v1/api/usage/entity-usage",
944
+ summary: "Get custom entity usage statistics",
945
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
946
+ query_params: {
947
+ organization_id: "uuid (required) - Organization ID"
948
+ },
949
+ response: "object with entity usage statistics (schemas count, records count, etc.)"
950
+ },
951
+ {
952
+ method: "GET",
953
+ path: "/v1/usage/stats",
954
+ summary: "Get comprehensive usage statistics for user and organization",
955
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
956
+ query_params: {
957
+ organization_id: "uuid (required) - Organization ID to check usage for"
958
+ },
959
+ response: {
960
+ period_start: "string | null",
961
+ period_end: "string | null",
962
+ plan_name: "string | null",
963
+ plan_id: "string | null",
964
+ usage: "object - Usage stats by type",
965
+ error: "string | null"
966
+ }
967
+ },
968
+ {
969
+ method: "POST",
970
+ path: "/v1/usage/check",
971
+ summary: "Check if usage is allowed without consuming",
972
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
973
+ request_body: {
974
+ tokens: "number (required, gt 0) - Amount to check",
975
+ usage_type: "string (default: 'tokens') - 'tokens' or 'messages'",
976
+ user_id: "uuid (required) - Must match authenticated user",
977
+ organization_id: "uuid (required)"
978
+ },
979
+ response: {
980
+ allowed: "boolean",
981
+ current_usage: "number",
982
+ max_limit: "number | 'unlimited'",
983
+ remaining: "number | 'unlimited'",
984
+ requested: "number | null",
985
+ error: "string | null"
986
+ }
987
+ },
988
+ {
989
+ method: "POST",
990
+ path: "/v1/usage/consume",
991
+ summary: "Consume usage tokens/messages",
992
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
993
+ request_body: {
994
+ tokens: "number (required, gt 0) - Amount to consume",
995
+ usage_type: "string (default: 'tokens') - 'tokens' or 'messages'",
996
+ user_id: "uuid (required) - Must match authenticated user",
997
+ organization_id: "uuid (required)"
998
+ },
999
+ response: {
1000
+ success: "boolean",
1001
+ consumed: "number | object | null",
1002
+ usage_type: "string | null",
1003
+ allowed: "boolean",
1004
+ limits: "object - Current limits state",
1005
+ error: "string | null"
1006
+ },
1007
+ note: "Returns 402 Payment Required if usage limits exceeded."
1008
+ }
1009
+ ]
1010
+ },
1011
+ agents: {
1012
+ description: "AI Agents - Interact with AI agents created by the tenant",
1013
+ endpoints: [
1014
+ {
1015
+ method: "GET",
1016
+ path: "/v1/user/agents/available",
1017
+ summary: "List available agents for end-users",
1018
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1019
+ query_params: {
1020
+ skip: "number (default: 0)",
1021
+ limit: "number (default: 50, max: 100)"
1022
+ },
1023
+ response: [
1024
+ {
1025
+ id: "uuid",
1026
+ tenant_id: "uuid",
1027
+ name: "string",
1028
+ description: "string | null",
1029
+ system_prompt: "string",
1030
+ response_type: "text | audio | video",
1031
+ rag_provider: "NONE | GOOGLE_FILE_SEARCH",
1032
+ is_active: "boolean",
1033
+ rag_config: "object | null",
1034
+ mcp_servers: "array of MCP server configs",
1035
+ created_by: "uuid",
1036
+ created_at: "ISO datetime",
1037
+ updated_at: "ISO datetime"
1038
+ }
1039
+ ],
1040
+ note: "Returns only active agents that the tenant has made available"
1041
+ },
1042
+ {
1043
+ method: "GET",
1044
+ path: "/v1/user/agents/{agent_id}",
1045
+ summary: "Get agent details by ID",
1046
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1047
+ response: "AgentResponse (same as list)"
1048
+ },
1049
+ {
1050
+ method: "POST",
1051
+ path: "/v1/user/agents/{agent_id}/chat",
1052
+ summary: "Chat with an agent (execute)",
1053
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1054
+ rate_limit: "60 per minute per user",
1055
+ request_body: {
1056
+ messages: "array (optional) - Chat messages [{'role': 'user', 'content': '...'}]",
1057
+ input: "string (optional) - Alternative to messages for single input",
1058
+ conversation_id: "uuid (optional) - Conversation ID for persistent history. If provided, previous messages are auto-injected and new messages are saved.",
1059
+ model: "string (optional) - LLM model override",
1060
+ temperature: "number (optional)",
1061
+ max_tokens: "number (optional)",
1062
+ use_tools: "boolean (default: true) - Enable agent tools",
1063
+ use_rag: "boolean (default: true) - Enable RAG retrieval"
1064
+ },
1065
+ response: {
1066
+ id: "uuid - Execution ID",
1067
+ agent_id: "uuid",
1068
+ tenant_id: "uuid",
1069
+ user_id: "uuid - Your user ID",
1070
+ organization_id: "uuid - Your organization ID",
1071
+ input_text: "string - User input",
1072
+ output_text: "string - Agent response",
1073
+ tokens_used: "number",
1074
+ execution_time_ms: "number",
1075
+ status: "SUCCESS | FAILED | PENDING",
1076
+ error_message: "string | null",
1077
+ execution_metadata: "object | null",
1078
+ created_at: "ISO datetime"
1079
+ },
1080
+ example_request: `{
1081
+ "messages": [
1082
+ {"role": "user", "content": "Hello, how can you help me?"}
1083
+ ],
1084
+ "temperature": 0.7
1085
+ }`,
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"
1100
+ }`,
1101
+ note: "Usage is tracked against your plan limits. Use /v1/api/usage/current to check remaining usage."
1102
+ },
1103
+ {
1104
+ method: "GET",
1105
+ path: "/v1/user/agents/{agent_id}/history",
1106
+ summary: "Get your conversation history with an agent",
1107
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1108
+ query_params: {
1109
+ skip: "number (default: 0)",
1110
+ limit: "number (default: 20, max: 50)"
1111
+ },
1112
+ response: "Array of AgentExecutionResponse (same structure as chat response)",
1113
+ note: "Returns only YOUR past interactions with this agent"
1114
+ },
1115
+ {
1116
+ method: "POST",
1117
+ path: "/v1/user/agents/{agent_id}/conversations",
1118
+ summary: "Create a new conversation session with an agent",
1119
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1120
+ request_body: {
1121
+ title: "string (optional) - Conversation title"
1122
+ },
1123
+ response: {
1124
+ id: "uuid",
1125
+ tenant_id: "uuid",
1126
+ user_id: "uuid",
1127
+ agent_id: "uuid",
1128
+ title: "string | null",
1129
+ status: "ACTIVE | ENDED",
1130
+ created_at: "ISO datetime",
1131
+ updated_at: "ISO datetime"
1132
+ },
1133
+ note: "Use the returned conversation ID in the chat endpoint's conversation_id field to persist messages"
1134
+ },
1135
+ {
1136
+ method: "GET",
1137
+ path: "/v1/user/agents/{agent_id}/conversations",
1138
+ summary: "List your conversations with an agent",
1139
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1140
+ query_params: {
1141
+ skip: "number (default: 0)",
1142
+ limit: "number (default: 20, max: 50)"
1143
+ },
1144
+ response: "Array of ConversationResponse (same as create response)"
1145
+ },
1146
+ {
1147
+ method: "GET",
1148
+ path: "/v1/user/agents/{agent_id}/conversations/{conversation_id}",
1149
+ summary: "Get a conversation with all messages",
1150
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1151
+ response: {
1152
+ id: "uuid",
1153
+ tenant_id: "uuid",
1154
+ user_id: "uuid",
1155
+ agent_id: "uuid",
1156
+ title: "string | null",
1157
+ status: "ACTIVE | ENDED",
1158
+ created_at: "ISO datetime",
1159
+ updated_at: "ISO datetime",
1160
+ messages: "Array of { id, role, content, tokens_used, message_metadata, created_at }"
1161
+ }
1162
+ },
1163
+ {
1164
+ method: "DELETE",
1165
+ path: "/v1/user/agents/{agent_id}/conversations/{conversation_id}",
1166
+ summary: "End a conversation (soft close)",
1167
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1168
+ response: "204 No Content",
1169
+ note: "Marks the conversation as ENDED. No new messages can be added."
1170
+ }
1171
+ ],
1172
+ important_notes: [
1173
+ "All agent endpoints require end-user authentication (API Key + JWT)",
1174
+ "Usage is tracked for billing against your subscription plan",
1175
+ "Agents with tools may take longer to respond as tools are executed",
1176
+ "RAG-enabled agents include context from knowledge bases in responses",
1177
+ "Use conversations to maintain chat history across multiple requests - create a conversation, then pass its ID to the chat endpoint"
1178
+ ]
1179
+ },
1180
+ users: {
1181
+ description: "User management - CRUD, settings, activation/deactivation. Dual auth: sk_live_* for admin ops, pk_live_* + JWT for self-service.",
1182
+ endpoints: [
1183
+ {
1184
+ method: "POST",
1185
+ path: "/v1/users",
1186
+ summary: "Create a new user (admin, JWT required)",
1187
+ auth: { api_key: "sk_live_* ONLY", jwt: true },
1188
+ request_body: {
1189
+ sub: "string (required) - Unique user identifier",
1190
+ email: "string (optional) - valid email",
1191
+ display_name: "string (optional, max 100)",
1192
+ avatar_url: "string (optional)",
1193
+ locale: "string (optional, default: 'en')",
1194
+ tenant_id: "uuid (optional)",
1195
+ is_email_verified: "boolean (default: false)",
1196
+ status: "string (default: 'pending')",
1197
+ is_active: "boolean (default: true)"
1198
+ },
1199
+ response: {
1200
+ id: "uuid",
1201
+ email: "string",
1202
+ display_name: "string | null",
1203
+ avatar_url: "string | null",
1204
+ locale: "string",
1205
+ is_active: "boolean",
1206
+ tenant_id: "uuid",
1207
+ sub: "string",
1208
+ is_email_verified: "boolean",
1209
+ status: "string",
1210
+ current_plan_id: "uuid | null",
1211
+ default_organization_id: "uuid | null",
1212
+ created_at: "ISO datetime",
1213
+ updated_at: "ISO datetime",
1214
+ settings: "UserSettingsResponse | null"
1215
+ }
1216
+ },
1217
+ {
1218
+ method: "GET",
1219
+ path: "/v1/users",
1220
+ summary: "List all users (SECRET KEY ONLY)",
1221
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
1222
+ query_params: {
1223
+ page: "number (default: 1)",
1224
+ per_page: "number (default: 20, max: 100)",
1225
+ include_inactive: "boolean (default: false)"
1226
+ },
1227
+ response: {
1228
+ users: "array of UserResponse",
1229
+ total: "number",
1230
+ page: "number",
1231
+ per_page: "number",
1232
+ has_next: "boolean",
1233
+ has_prev: "boolean"
1234
+ },
1235
+ note: "Returns only users belonging to the secret key's tenant."
1236
+ },
1237
+ {
1238
+ method: "GET",
1239
+ path: "/v1/users/stats",
1240
+ summary: "Get user statistics (SECRET KEY ONLY)",
1241
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
1242
+ response: {
1243
+ total_users: "number",
1244
+ active_users: "number",
1245
+ inactive_users: "number"
1246
+ }
1247
+ },
1248
+ {
1249
+ method: "GET",
1250
+ path: "/v1/users/{user_id}",
1251
+ summary: "Get user by ID (sk_live_* or self with pk_live_*)",
1252
+ auth: { api_key: "pk_live_* (self) or sk_live_*", jwt: "optional" },
1253
+ query_params: {
1254
+ include_settings: "boolean (default: false) - Include user settings in response"
1255
+ },
1256
+ response: {
1257
+ id: "uuid",
1258
+ email: "string",
1259
+ display_name: "string | null",
1260
+ avatar_url: "string | null",
1261
+ locale: "string",
1262
+ is_active: "boolean",
1263
+ tenant_id: "uuid",
1264
+ sub: "string",
1265
+ is_email_verified: "boolean",
1266
+ status: "string",
1267
+ current_plan_id: "uuid | null",
1268
+ default_organization_id: "uuid | null",
1269
+ created_at: "ISO datetime",
1270
+ updated_at: "ISO datetime",
1271
+ settings: "UserSettingsResponse | null (only if include_settings=true)"
1272
+ },
1273
+ note: "With sk_live_*: can view any user in tenant. With pk_live_* + JWT: can only view own profile (returns 404 for other users)."
1274
+ },
1275
+ {
1276
+ method: "PUT",
1277
+ path: "/v1/users/{user_id}",
1278
+ summary: "Update user profile",
1279
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1280
+ request_body: {
1281
+ display_name: "string (optional, max 100)",
1282
+ avatar_url: "string (optional)",
1283
+ locale: "string (optional)"
1284
+ },
1285
+ response: "UserResponse (same fields as GET)",
1286
+ note: "With sk_live_*: can update any user. With pk_live_* + JWT: can only update own profile."
1287
+ },
1288
+ {
1289
+ method: "PUT",
1290
+ path: "/v1/users/{user_id}/settings",
1291
+ summary: "Update user settings",
1292
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1293
+ request_body: "UserSettingsUpdate object (extensible, currently empty placeholder for future settings)",
1294
+ response: "UserWithSettingsResponse (UserResponse + settings object)",
1295
+ note: "With sk_live_*: can update any user's settings. With pk_live_* + JWT: can only update own settings."
1296
+ },
1297
+ {
1298
+ method: "POST",
1299
+ path: "/v1/users/{user_id}/deactivate",
1300
+ summary: "Deactivate a user (soft delete)",
1301
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1302
+ response: "UserResponse",
1303
+ note: "Sets is_active=false. With sk_live_*: can deactivate any user. With pk_live_* + JWT: can deactivate own account."
1304
+ },
1305
+ {
1306
+ method: "POST",
1307
+ path: "/v1/users/{user_id}/activate",
1308
+ summary: "Activate a user (SECRET KEY ONLY)",
1309
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
1310
+ response: "UserResponse",
1311
+ note: "Sets is_active=true. Admin operation only."
1312
+ },
1313
+ {
1314
+ method: "DELETE",
1315
+ path: "/v1/users/{user_id}",
1316
+ summary: "Delete a user (SECRET KEY ONLY, soft delete)",
1317
+ auth: { api_key: "sk_live_* ONLY", jwt: false },
1318
+ response: "204 No Content",
1319
+ note: "Performs soft delete (deactivation). Returns 404 if user not found."
1320
+ }
1321
+ ]
1322
+ },
1323
+ plans: {
1324
+ description: "Plan listing - browse available subscription plans. All endpoints require JWT authentication.",
1325
+ endpoints: [
1326
+ {
1327
+ method: "GET",
1328
+ path: "/v1/plans",
1329
+ summary: "List all plans (paginated)",
1330
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1331
+ query_params: {
1332
+ skip: "number (default: 0) - Pagination offset",
1333
+ limit: "number (default: 100, max: 1000) - Items per page",
1334
+ include_inactive: "boolean (default: false) - Include deactivated plans"
1335
+ },
1336
+ response: {
1337
+ plans: "array of PlanResponse",
1338
+ total: "number",
1339
+ page: "number",
1340
+ per_page: "number",
1341
+ has_next: "boolean",
1342
+ has_prev: "boolean"
1343
+ }
1344
+ },
1345
+ {
1346
+ method: "GET",
1347
+ path: "/v1/plans/active",
1348
+ summary: "List active plans only",
1349
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1350
+ response: "array of PlanResponse",
1351
+ note: "Useful for displaying plan options on pricing pages."
1352
+ },
1353
+ {
1354
+ method: "GET",
1355
+ path: "/v1/plans/free",
1356
+ summary: "List free plans only",
1357
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1358
+ response: "array of PlanResponse",
1359
+ note: "Useful for signup flows — auto-assign a free plan."
1360
+ },
1361
+ {
1362
+ method: "GET",
1363
+ path: "/v1/plans/{plan_id}",
1364
+ summary: "Get plan details by ID",
1365
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1366
+ response: {
1367
+ id: "uuid",
1368
+ name: "string",
1369
+ description: "string | null",
1370
+ price_monthly: "number",
1371
+ price_yearly: "number",
1372
+ currency: "string (default: 'USD')",
1373
+ max_messages_per_month: "number | null",
1374
+ max_cost_usd_per_month: "number | null",
1375
+ features: "array of strings",
1376
+ is_free: "boolean",
1377
+ is_active: "boolean",
1378
+ stripe_product_id: "string | null",
1379
+ stripe_price_id_monthly: "string | null",
1380
+ stripe_price_id_yearly: "string | null",
1381
+ created_at: "ISO datetime",
1382
+ updated_at: "ISO datetime"
1383
+ },
1384
+ note: "Returns 404 if plan not found."
1385
+ }
1386
+ ]
1387
+ },
1388
+ ai: {
1389
+ description: "AI services (LLM completions)",
1390
+ endpoints: [
1391
+ {
1392
+ method: "POST",
1393
+ path: "/v1/ai/completions",
1394
+ summary: "Generate AI completion (streaming supported)",
1395
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1396
+ request_body: {
1397
+ model: "string (e.g., 'gpt-4', 'claude-3-opus')",
1398
+ messages: [
1399
+ { role: "system | user | assistant", content: "string" }
1400
+ ],
1401
+ max_tokens: "number (optional)",
1402
+ temperature: "number (optional, 0-2)",
1403
+ stream: "boolean (optional)"
1404
+ }
1405
+ }
1406
+ ]
1407
+ },
1408
+ entities: {
1409
+ description: "Entity management (Custom Data) - define schemas and manage records. Two contexts: End-User API (API Key + JWT + X-Organization-Id) and Dashboard (Tenant JWT).",
1410
+ endpoints: [
1411
+ // ── End-User Schema Endpoints ──
1412
+ {
1413
+ method: "GET",
1414
+ path: "/v1/entity/schemas",
1415
+ summary: "List all entity schemas in the organization",
1416
+ auth: { api_key: "sk_live_*", jwt: true, headers: "X-Organization-Id required" },
1417
+ query_params: {
1418
+ page: "number (default 1)",
1419
+ size: "number (default 20, max 100)"
1420
+ }
1421
+ },
1422
+ {
1423
+ method: "POST",
1424
+ path: "/v1/entity/schemas",
1425
+ summary: "Create a new entity schema",
1426
+ auth: { api_key: "sk_live_*", jwt: true, headers: "X-Organization-Id required" },
1427
+ request_body: {
1428
+ name: "string (required) - unique per organization",
1429
+ slug: "string (required) - unique per organization, lowercase/hyphens only",
1430
+ description: "string (optional)",
1431
+ fields_definition: "object (required) - field definitions with type, required, constraints"
1432
+ },
1433
+ notes: "Supported field types: string, integer, float, boolean, datetime, text, enum, email, url, phone. Returns 409 if name or slug already exists."
1434
+ },
1435
+ {
1436
+ method: "GET",
1437
+ path: "/v1/entity/schemas/{schema_id}",
1438
+ summary: "Get a specific schema by ID",
1439
+ auth: { api_key: "sk_live_*", jwt: true, headers: "X-Organization-Id required" }
1440
+ },
1441
+ {
1442
+ method: "PUT",
1443
+ path: "/v1/entity/schemas/{schema_id}",
1444
+ summary: "Update an existing schema",
1445
+ auth: { api_key: "sk_live_*", jwt: true, headers: "X-Organization-Id required" },
1446
+ request_body: {
1447
+ name: "string (optional)",
1448
+ slug: "string (optional)",
1449
+ description: "string (optional)",
1450
+ fields_definition: "object (optional) - version auto-increments if changed",
1451
+ is_active: "boolean (optional)"
1452
+ }
1453
+ },
1454
+ {
1455
+ method: "DELETE",
1456
+ path: "/v1/entity/schemas/{schema_id}",
1457
+ summary: "Delete a schema (may fail if records exist)",
1458
+ auth: { api_key: "sk_live_*", jwt: true, headers: "X-Organization-Id required" }
1459
+ },
1460
+ // ── End-User Record Endpoints ──
1461
+ {
1462
+ method: "POST",
1463
+ path: "/v1/entity/records",
1464
+ summary: "Create a new entity record",
1465
+ auth: { api_key: "sk_live_* or pk_live_*", jwt: true, headers: "X-Organization-Id required" },
1466
+ request_body: {
1467
+ schema_id: "uuid (required)",
1468
+ data: "object (required) - validated against schema fields_definition"
1469
+ },
1470
+ notes: "Sets created_by_id to the authenticated end-user."
1471
+ },
1472
+ {
1473
+ method: "GET",
1474
+ path: "/v1/entity/records/{record_id}",
1475
+ summary: "Get a specific record by ID",
1476
+ auth: { api_key: "sk_live_* or pk_live_*", jwt: true, headers: "X-Organization-Id required" }
1477
+ },
1478
+ {
1479
+ method: "PUT",
1480
+ path: "/v1/entity/records/{record_id}",
1481
+ summary: "Update a record",
1482
+ auth: { api_key: "sk_live_* or pk_live_*", jwt: true, headers: "X-Organization-Id required" },
1483
+ request_body: {
1484
+ data: "object (required) - validated against schema fields_definition"
1485
+ },
1486
+ notes: "Sets updated_by_id to the authenticated end-user."
1487
+ },
1488
+ {
1489
+ method: "DELETE",
1490
+ path: "/v1/entity/records/{record_id}",
1491
+ summary: "Delete a record",
1492
+ auth: { api_key: "sk_live_* or pk_live_*", jwt: true, headers: "X-Organization-Id required" }
1493
+ },
1494
+ {
1495
+ method: "POST",
1496
+ path: "/v1/entity/records/search",
1497
+ summary: "Advanced search for entity records with filters and ordering",
1498
+ auth: { api_key: "sk_live_*", jwt: true, headers: "X-Organization-Id required" },
1499
+ request_body: {
1500
+ schema_id: "uuid (optional) - filter by schema",
1501
+ query: "object (optional) - filters: {field: value} for equality, {field: {operator: 'gt', value: 100}} for operators"
1502
+ },
1503
+ query_params: {
1504
+ page: "number (default 1)",
1505
+ size: "number (default 20, max 100)",
1506
+ order_by: "string (default 'created_at')",
1507
+ order_dir: "'asc' | 'desc' (default 'desc')"
1508
+ },
1509
+ 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
+ }
1517
+ }`
1518
+ }
1519
+ ]
1520
+ },
1521
+ departments: {
1522
+ description: "Department management within organizations",
1523
+ endpoints: [
1524
+ {
1525
+ method: "GET",
1526
+ path: "/v1/departments/{department_id}",
1527
+ summary: "Get department details by ID",
1528
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: false },
1529
+ query_params: {
1530
+ user_id: "uuid (required) - User ID for membership verification"
1531
+ },
1532
+ response: {
1533
+ id: "uuid",
1534
+ organization_id: "uuid",
1535
+ name: "string",
1536
+ description: "string | null",
1537
+ is_default: "boolean",
1538
+ is_active: "boolean",
1539
+ settings: "object",
1540
+ created_at: "ISO datetime",
1541
+ updated_at: "ISO datetime"
1542
+ },
1543
+ note: "User must be a member of the organization this department belongs to."
1544
+ }
1545
+ ]
1546
+ },
1547
+ files: {
1548
+ description: "End-user Drive - private file storage with folders. Users can upload/manage their own files and access shared tenant files (read-only). Shared files are controlled by the tenant's is_shared flag — only files explicitly marked as shared are visible to end-users.",
1549
+ endpoints: [
1550
+ {
1551
+ method: "POST",
1552
+ path: "/v1/user/files",
1553
+ summary: "Upload a file to user's Drive",
1554
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1555
+ query_params: {
1556
+ description: "string (optional) - File description",
1557
+ folder_id: "uuid (optional) - Target folder ID (null = root)"
1558
+ },
1559
+ request_body: "multipart/form-data with 'file' field",
1560
+ response: {
1561
+ message: "string",
1562
+ file: {
1563
+ id: "uuid",
1564
+ user_id: "uuid",
1565
+ tenant_id: "uuid",
1566
+ folder_id: "uuid | null",
1567
+ filename: "string",
1568
+ original_filename: "string",
1569
+ file_size: "integer",
1570
+ content_type: "string",
1571
+ storage_provider: "string",
1572
+ description: "string | null",
1573
+ extra_metadata: "object | null",
1574
+ created_at: "ISO datetime",
1575
+ updated_at: "ISO datetime"
1576
+ }
1577
+ }
1578
+ },
1579
+ {
1580
+ method: "GET",
1581
+ path: "/v1/user/files",
1582
+ summary: "List user's files with optional folder filter",
1583
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1584
+ query_params: {
1585
+ folder_id: "uuid (optional) - Filter by folder (null = root files)",
1586
+ skip: "integer (default: 0)",
1587
+ limit: "integer (default: 20, max: 100)"
1588
+ },
1589
+ response: {
1590
+ files: "UserFileResponse[]",
1591
+ total: "integer",
1592
+ skip: "integer",
1593
+ limit: "integer"
1594
+ }
1595
+ },
1596
+ {
1597
+ method: "GET",
1598
+ path: "/v1/user/files/shared",
1599
+ summary: "List shared tenant Drive files (read-only)",
1600
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1601
+ query_params: {
1602
+ skip: "integer (default: 0)",
1603
+ limit: "integer (default: 20, max: 100)"
1604
+ },
1605
+ response: {
1606
+ files: "TenantFileResponse[]",
1607
+ total: "integer",
1608
+ skip: "integer",
1609
+ limit: "integer"
1610
+ },
1611
+ note: "Only returns files explicitly marked as shared by the tenant (is_shared=true). Files are private by default."
1612
+ },
1613
+ {
1614
+ method: "GET",
1615
+ path: "/v1/user/files/shared/{file_id}/download",
1616
+ summary: "Download a shared tenant file",
1617
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1618
+ response: "Binary file content with Content-Disposition header",
1619
+ note: "Returns 404 if the file is not marked as shared by the tenant."
1620
+ },
1621
+ {
1622
+ method: "GET",
1623
+ path: "/v1/user/files/{file_id}",
1624
+ summary: "Get own file metadata",
1625
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1626
+ response: "UserFileResponse"
1627
+ },
1628
+ {
1629
+ method: "GET",
1630
+ path: "/v1/user/files/{file_id}/download",
1631
+ summary: "Download own file",
1632
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1633
+ response: "Binary file content with Content-Disposition header"
1634
+ },
1635
+ {
1636
+ method: "DELETE",
1637
+ path: "/v1/user/files/{file_id}",
1638
+ summary: "Delete own file",
1639
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1640
+ response: "204 No Content"
1641
+ },
1642
+ {
1643
+ method: "PATCH",
1644
+ path: "/v1/user/files/{file_id}/move",
1645
+ summary: "Move file to folder (or root)",
1646
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1647
+ request_body: {
1648
+ folder_id: "uuid | null - Target folder (null = move to root)"
1649
+ },
1650
+ response: "UserFileResponse"
1651
+ },
1652
+ {
1653
+ method: "POST",
1654
+ path: "/v1/user/files/folders",
1655
+ summary: "Create a folder in user's Drive",
1656
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1657
+ request_body: {
1658
+ name: "string (required, max 255 chars)",
1659
+ parent_id: "uuid (optional) - Parent folder for nesting"
1660
+ },
1661
+ response: {
1662
+ id: "uuid",
1663
+ tenant_id: "uuid",
1664
+ user_id: "uuid",
1665
+ parent_id: "uuid | null",
1666
+ name: "string",
1667
+ path: "string - materialized path e.g. /Documents/Reports",
1668
+ created_at: "ISO datetime",
1669
+ updated_at: "ISO datetime"
1670
+ }
1671
+ },
1672
+ {
1673
+ method: "GET",
1674
+ path: "/v1/user/files/folders",
1675
+ summary: "List folders (root or children of parent)",
1676
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1677
+ query_params: {
1678
+ parent_id: "uuid (optional) - List children of this folder (null = root)"
1679
+ },
1680
+ response: {
1681
+ folders: "FolderResponse[]"
1682
+ }
1683
+ },
1684
+ {
1685
+ method: "PATCH",
1686
+ path: "/v1/user/files/folders/{folder_id}",
1687
+ summary: "Rename a folder",
1688
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1689
+ request_body: {
1690
+ name: "string (required, max 255 chars)"
1691
+ },
1692
+ response: "FolderResponse"
1693
+ },
1694
+ {
1695
+ method: "DELETE",
1696
+ path: "/v1/user/files/folders/{folder_id}",
1697
+ summary: "Delete an empty folder (409 if non-empty)",
1698
+ auth: { api_key: "pk_live_* or sk_live_*", jwt: true },
1699
+ response: "204 No Content",
1700
+ note: "Folder must be empty (no files or subfolders). Move or delete contents first."
1701
+ }
1702
+ ]
1703
+ },
1704
+ external_agents: {
1705
+ description: "External agent call tracking. Used by external agents (e.g., N8N workflows) to report their usage back to the platform for analytics and cost tracking.",
1706
+ endpoints: [
1707
+ {
1708
+ method: "POST",
1709
+ path: "/v1/external-agents/{agent_id}/calls",
1710
+ summary: "Register an external agent call",
1711
+ auth: { api_key: "sk_live_*", jwt: true },
1712
+ rate_limit: "120 per minute per IP",
1713
+ path_params: {
1714
+ agent_id: "uuid (required) - The external agent ID"
1715
+ },
1716
+ request_body: {
1717
+ user_id: "uuid (optional) - End-user who triggered the call. Defaults to authenticated user",
1718
+ organization_id: "uuid (optional) - Organization context. Defaults to user's default organization",
1719
+ input_tokens: "integer (optional, default: 0) - Number of input tokens consumed",
1720
+ output_tokens: "integer (optional, default: 0) - Number of output tokens consumed",
1721
+ total_tokens: "integer (optional) - Total tokens. If not provided, calculated as input + output",
1722
+ cost_usd: "number (optional, default: 0.0) - Cost in USD",
1723
+ duration_ms: "integer (optional, default: 0) - Call duration in milliseconds",
1724
+ status: "string (optional, default: 'SUCCESS') - 'SUCCESS' or 'ERROR'",
1725
+ error_message: "string (optional) - Error message if status is ERROR",
1726
+ call_metadata: "object (optional) - Arbitrary metadata about the call"
1727
+ },
1728
+ response: {
1729
+ id: "uuid",
1730
+ external_agent_id: "uuid",
1731
+ tenant_id: "uuid",
1732
+ user_id: "uuid | null",
1733
+ organization_id: "uuid | null",
1734
+ input_tokens: "integer",
1735
+ output_tokens: "integer",
1736
+ total_tokens: "integer",
1737
+ cost_usd: "number",
1738
+ duration_ms: "integer",
1739
+ status: "string - SUCCESS or ERROR",
1740
+ error_message: "string | null",
1741
+ call_metadata: "object | null",
1742
+ created_at: "ISO datetime"
1743
+ },
1744
+ note: "Does NOT add to tenant billing — the tenant pays the AI provider directly. This endpoint is for tracking/analytics only."
1745
+ }
1746
+ ]
1747
+ }
1748
+ };
1749
+ // =============================================================================
1750
+ // TypeScript/JavaScript SDK Template
1751
+ // =============================================================================
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 };
2337
+ `;
2338
+ // =============================================================================
2339
+ // Request Headers Helper
2340
+ // =============================================================================
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) |
2365
+ `;
2366
+ // =============================================================================
2367
+ // API Client
2368
+ // =============================================================================
2369
+ async function fetchFromApi(endpoint) {
2370
+ const cacheKey = endpoint;
2371
+ const cached = cache.get(cacheKey);
2372
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
2373
+ return cached.data;
2374
+ }
2375
+ const headers = {
2376
+ "Content-Type": "application/json",
2377
+ };
2378
+ if (API_KEY) {
2379
+ headers["X-API-Key"] = API_KEY;
2380
+ }
2381
+ try {
2382
+ const response = await fetch(`${API_URL}${endpoint}`, { headers });
2383
+ if (!response.ok) {
2384
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
2385
+ }
2386
+ const data = await response.json();
2387
+ cache.set(cacheKey, { data, timestamp: Date.now() });
2388
+ return data;
2389
+ }
2390
+ catch (error) {
2391
+ if (cached) {
2392
+ console.error(`API fetch failed, using cached data: ${error}`);
2393
+ return cached.data;
2394
+ }
2395
+ throw error;
2396
+ }
2397
+ }
2398
+ // =============================================================================
2399
+ // MCP Server Implementation
2400
+ // =============================================================================
2401
+ const server = new Server({
2402
+ name: "saas-multi-agent-api",
2403
+ version: "2.2.0",
2404
+ }, {
2405
+ capabilities: {
2406
+ tools: {},
2407
+ },
2408
+ });
2409
+ // List available tools
2410
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
2411
+ return {
2412
+ tools: [
2413
+ {
2414
+ 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
+
2429
+ Call this first to understand the API architecture and authentication model.`,
2430
+ inputSchema: {
2431
+ type: "object",
2432
+ properties: {},
2433
+ required: [],
2434
+ },
2435
+ },
2436
+ {
2437
+ 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
+
2444
+ Categories: authentication, organizations, subscriptions, billing, usage, agents, users, plans, ai, entities, departments, external_agents`,
2445
+ inputSchema: {
2446
+ type: "object",
2447
+ properties: {
2448
+ category: {
2449
+ type: "string",
2450
+ description: "Filter by category. Leave empty for all endpoints.",
2451
+ enum: ["authentication", "organizations", "subscriptions", "billing", "usage", "agents", "users", "plans", "ai", "entities", "departments", "files", "external_agents"],
2452
+ },
2453
+ },
2454
+ required: [],
2455
+ },
2456
+ },
2457
+ {
2458
+ name: "get_request_headers",
2459
+ description: "Get detailed information about required HTTP headers for API requests.",
2460
+ inputSchema: {
2461
+ type: "object",
2462
+ properties: {},
2463
+ required: [],
2464
+ },
2465
+ },
2466
+ {
2467
+ 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
+
2477
+ You can use this as a starting point for your frontend.`,
2478
+ inputSchema: {
2479
+ type: "object",
2480
+ properties: {},
2481
+ required: [],
2482
+ },
2483
+ },
2484
+ {
2485
+ name: "get_openapi_spec",
2486
+ description: "Get the complete OpenAPI specification (JSON) from the live API. Use this for detailed schema information.",
2487
+ inputSchema: {
2488
+ type: "object",
2489
+ properties: {},
2490
+ required: [],
2491
+ },
2492
+ },
2493
+ {
2494
+ name: "search_endpoints",
2495
+ description: "Search for endpoints by keyword. Searches path, summary, and description.",
2496
+ inputSchema: {
2497
+ type: "object",
2498
+ properties: {
2499
+ query: {
2500
+ type: "string",
2501
+ description: "Search keyword (e.g., 'login', 'organization', 'checkout')",
2502
+ },
2503
+ },
2504
+ required: ["query"],
2505
+ },
2506
+ },
2507
+ {
2508
+ name: "get_endpoint_details",
2509
+ description: "Get detailed information about a specific endpoint including request/response schemas.",
2510
+ inputSchema: {
2511
+ type: "object",
2512
+ properties: {
2513
+ path: {
2514
+ type: "string",
2515
+ description: "The endpoint path (e.g., '/v1/auth/login')",
2516
+ },
2517
+ method: {
2518
+ type: "string",
2519
+ description: "HTTP method",
2520
+ enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
2521
+ },
2522
+ },
2523
+ required: ["path"],
2524
+ },
2525
+ },
2526
+ {
2527
+ 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
2537
+ - Logout`,
2538
+ inputSchema: {
2539
+ type: "object",
2540
+ properties: {},
2541
+ required: [],
2542
+ },
2543
+ },
2544
+ {
2545
+ 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
2554
+ - Environment variables`,
2555
+ inputSchema: {
2556
+ type: "object",
2557
+ properties: {},
2558
+ required: [],
2559
+ },
2560
+ },
2561
+ ],
2562
+ };
2563
+ });
2564
+ // Handle tool calls
2565
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2566
+ const { name, arguments: args } = request.params;
2567
+ try {
2568
+ switch (name) {
2569
+ case "get_api_overview": {
2570
+ return {
2571
+ content: [
2572
+ {
2573
+ 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
2598
+ - Those are for tenant admin dashboards, not end-user apps`,
2599
+ },
2600
+ ],
2601
+ };
2602
+ }
2603
+ case "get_end_user_endpoints": {
2604
+ const category = args.category;
2605
+ let endpoints = END_USER_ENDPOINTS;
2606
+ if (category && category in END_USER_ENDPOINTS) {
2607
+ endpoints = { [category]: END_USER_ENDPOINTS[category] };
2608
+ }
2609
+ return {
2610
+ content: [
2611
+ {
2612
+ 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
2722
+ - GET /v1/departments/{department_id} - Get department details`,
2723
+ },
2724
+ ],
2725
+ };
2726
+ }
2727
+ case "get_request_headers": {
2728
+ return {
2729
+ content: [
2730
+ {
2731
+ type: "text",
2732
+ text: REQUEST_HEADERS,
2733
+ },
2734
+ ],
2735
+ };
2736
+ }
2737
+ case "get_sdk_template": {
2738
+ return {
2739
+ content: [
2740
+ {
2741
+ 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();
2769
+ \`\`\``,
2770
+ },
2771
+ ],
2772
+ };
2773
+ }
2774
+ case "get_openapi_spec": {
2775
+ try {
2776
+ const spec = await fetchFromApi("/v1/developer/openapi.json");
2777
+ return {
2778
+ content: [
2779
+ {
2780
+ type: "text",
2781
+ text: JSON.stringify(spec, null, 2),
2782
+ },
2783
+ ],
2784
+ };
2785
+ }
2786
+ catch (error) {
2787
+ return {
2788
+ content: [
2789
+ {
2790
+ type: "text",
2791
+ text: `Error fetching OpenAPI spec: ${error}
2792
+
2793
+ The API might not be accessible. Try accessing ${API_URL}/docs directly.
2794
+
2795
+ In the meantime, use the get_end_user_endpoints tool for endpoint documentation.`,
2796
+ },
2797
+ ],
2798
+ };
2799
+ }
2800
+ }
2801
+ case "search_endpoints": {
2802
+ const query = (args.query || "").toLowerCase();
2803
+ const results = [];
2804
+ // Search in embedded endpoints
2805
+ for (const [category, data] of Object.entries(END_USER_ENDPOINTS)) {
2806
+ for (const endpoint of data.endpoints || []) {
2807
+ const searchText = `${endpoint.path} ${endpoint.summary} ${endpoint.method}`.toLowerCase();
2808
+ if (searchText.includes(query)) {
2809
+ results.push({
2810
+ category,
2811
+ method: endpoint.method,
2812
+ path: endpoint.path,
2813
+ summary: endpoint.summary,
2814
+ auth: endpoint.auth,
2815
+ });
2816
+ }
2817
+ }
2818
+ }
2819
+ if (results.length === 0) {
2820
+ return {
2821
+ content: [
2822
+ {
2823
+ 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
2830
+ - billing, usage`,
2831
+ },
2832
+ ],
2833
+ };
2834
+ }
2835
+ return {
2836
+ content: [
2837
+ {
2838
+ 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'}
2845
+ `).join('\n')}`,
2846
+ },
2847
+ ],
2848
+ };
2849
+ }
2850
+ case "get_endpoint_details": {
2851
+ const path = args.path;
2852
+ const method = (args.method || "GET").toUpperCase();
2853
+ // Search in embedded endpoints
2854
+ for (const [category, data] of Object.entries(END_USER_ENDPOINTS)) {
2855
+ 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
+ };
2890
+ }
2891
+ }
2892
+ }
2893
+ return {
2894
+ content: [
2895
+ {
2896
+ 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
+
2901
+ Use search_endpoints tool to find available endpoints.`,
2902
+ },
2903
+ ],
2904
+ };
2905
+ }
2906
+ case "get_authentication_flow": {
2907
+ return {
2908
+ content: [
2909
+ {
2910
+ 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');
3137
+ \`\`\``,
3138
+ },
3139
+ ],
3140
+ };
3141
+ }
3142
+ case "get_common_patterns": {
3143
+ return {
3144
+ content: [
3145
+ {
3146
+ 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
3361
+ \`\`\``,
3362
+ },
3363
+ ],
3364
+ };
3365
+ }
3366
+ default:
3367
+ throw new Error(`Unknown tool: ${name}`);
3368
+ }
3369
+ }
3370
+ catch (error) {
3371
+ return {
3372
+ content: [
3373
+ {
3374
+ type: "text",
3375
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
3376
+ },
3377
+ ],
3378
+ isError: true,
3379
+ };
3380
+ }
3381
+ });
3382
+ // Start the server
3383
+ async function main() {
3384
+ console.error(`🚀 Multi-tenant SaaS API MCP Server v2.1.0`);
3385
+ console.error(`📡 API URL: ${API_URL}`);
3386
+ console.error(`🔑 API Key: ${API_KEY ? "Configured" : "Not configured"}`);
3387
+ console.error(`\nAvailable tools:`);
3388
+ console.error(` - get_api_overview: Understand the API architecture`);
3389
+ console.error(` - get_end_user_endpoints: Get endpoint documentation`);
3390
+ console.error(` - get_sdk_template: Get TypeScript SDK template`);
3391
+ console.error(` - get_authentication_flow: Auth implementation guide`);
3392
+ console.error(` - get_common_patterns: Best practices and patterns`);
3393
+ console.error(` - search_endpoints: Search for endpoints`);
3394
+ console.error(` - get_endpoint_details: Get specific endpoint info`);
3395
+ console.error(` - get_openapi_spec: Get full OpenAPI spec`);
3396
+ const transport = new StdioServerTransport();
3397
+ await server.connect(transport);
3398
+ }
3399
+ main().catch((error) => {
3400
+ console.error("Fatal error:", error);
3401
+ process.exit(1);
3402
+ });